Merge branch 'port-e'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 18:11:24 -04:00
commit a004db172b
28 files changed
+1775 -175

No files matched your search

+27 -18
View File
@@ -79,25 +79,34 @@ bool CameraRawCurveSettings::adjusts() const
|| !isLinear(green) || !isLinear(blue);
}
double CameraRawCurveSettings::parametric(double tone) const
// Bends tones below `lower` by `low`, above `upper` by `high`.
double CameraRawCurveSettings::bend(double tone, double lower, double low, double upper, double high)
{
const double shadow = shadowSplit / 100, dark = darkSplit / 100, light = lightSplit / 100;
double amount, lo, hi;
if (tone < shadow)
amount = shadows, lo = 0, hi = shadow;
else if (tone < dark)
amount = darks, lo = shadow, hi = dark;
else if (tone < light)
amount = lights, lo = dark, hi = light;
else
amount = highlights, lo = light, hi = 1;
const double span = std::max(0.02, hi - lo);
const double weight = 1 - std::abs(tone - (lo + hi) / 2) / (span / 2);
// A tone in its region weighs 0 to 1, unfloored.
return std::min(1.0, std::max(0.0, tone + (amount / 100) * weight * 0.22));
// Fitted to Photoshop: Darks −51 dips 0.1 there.
const double strength = 1.66;
if (tone < lower)
return lower * std::pow(tone / lower, std::pow(2, -low / 100 * strength));
if (tone > upper) {
const double rest = 1 - upper;
return 1 - rest * std::pow((1 - tone) / rest, std::pow(2, high / 100 * strength));
}
return tone;
}
std::vector<float> CameraRawCurveSettings::lumaTable() const
double CameraRawCurveSettings::parametric(double tone) const
{
if (shadows == 0 && darks == 0 && lights == 0 && highlights == 0)
return tone;
// Gamma bends, smoothed as Image › Curves smooths.
std::vector<CurvePoint> anchors;
for (int index = 0; index <= 32; ++index) {
const double x = index / 32.0;
anchors.push_back({x, bend(bend(x, shadowSplit / 100, shadows, lightSplit / 100, highlights), darkSplit / 100, darks, darkSplit / 100, lights)});
}
return point(tone, anchors);
}
std::vector<float> CameraRawCurveSettings::toneTable() const
{
std::vector<float> table(256);
for (int index = 0; index < 256; ++index)
@@ -262,10 +271,10 @@ void CameraRawSettings::applyCurveColor(uchar *pixels, int width, int height, qs
const CameraRawCurveSettings shape = curve.normalized();
const CameraRawMixerSettings mixing = mixer.normalized();
const CameraRawGradingSettings grade = grading.normalized();
const std::vector<float> luma = shape.lumaTable(), redTable = shape.channelTable(shape.red), greenTable = shape.channelTable(shape.green),
const std::vector<float> tone = shape.toneTable(), redTable = shape.channelTable(shape.red), greenTable = shape.channelTable(shape.green),
blueTable = shape.channelTable(shape.blue), mixerFloats = mixing.mixerFloats(), pointFloats = mixing.pointFloats(),
gradeFloats = grade.gradeFloats();
adjust_camera_raw_curve_color(pixels, size_t(width), size_t(height), size_t(stride), luma.data(), redTable.data(), greenTable.data(),
adjust_camera_raw_curve_color(pixels, size_t(width), size_t(height), size_t(stride), tone.data(), redTable.data(), greenTable.data(),
blueTable.data(), shape.refineSaturation / 100, mixerFloats.data(), int(mixing.points.size()), pointFloats.data(),
gradeFloats.data(), grade.blending / 100, grade.balance / 100, visualize);
}
+6 -2
View File
@@ -33,15 +33,19 @@ struct CameraRawCurveSettings {
static std::vector<CurvePoint> strongContrast();
static bool isLinear(const std::vector<CurvePoint> &points);
bool adjusts() const;
// Lifts or lowers the region a tone belongs to.
// Photoshop's parametric curve: gamma bends, smoothed as Curves.
double parametric(double tone) const;
std::vector<float> lumaTable() const;
// Applied to red, green and blue alike.
std::vector<float> toneTable() const;
std::vector<float> channelTable(const std::vector<CurvePoint> &points) const;
CameraRawCurveSettings nudged(CameraRawPointChannel channel, double tone, double delta) const;
// Swift's region(for:): the parametric amount a tone moves.
double &region(double tone);
CameraRawCurveSettings normalized() const;
friend bool operator==(const CameraRawCurveSettings &, const CameraRawCurveSettings &) = default;
private:
static double bend(double tone, double lower, double low, double upper, double high);
};
// One picked colour and how far its adjustment reaches.
+62 -5
View File
@@ -1,17 +1,28 @@
#include "Document/Dither.h"
#include "Document/BrushStroke.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Rendering/PoolMap.h"
#include <QFontMetricsF>
#include <QPainter>
#include <QPainterPath>
#include <QTextBoundaryFinder>
#include <algorithm>
#include <cmath>
#include <numeric>
#include <set>
extern "C" {
#include "DitherPixels.h"
}
// The kernel's bands, where Swift's kernel calls dispatch_apply.
extern "C" void dither_bands(size_t bands, void (*band)(void *context, size_t index), void *context)
{
std::vector<size_t> indices(bands);
std::iota(indices.begin(), indices.end(), size_t(0));
PoolMap::blocking(indices, [band, context](size_t index) { band(context, index); });
}
// Swift's Dither.swift: the settings, the chunky pixels, the glyphs.
namespace {
// Swift's Characters: the text's graphemes.
@@ -53,6 +64,7 @@ QString rawValue(DitherStyle style)
case DitherStyle::diamonds: return QStringLiteral("Halftone Diamonds");
case DitherStyle::patterns: return QStringLiteral("Mac Patterns");
case DitherStyle::ascii: return QStringLiteral("ASCII");
case DitherStyle::scanlines: return QStringLiteral("Scanlines (CRT)");
}
throw std::logic_error("unknown dither style");
}
@@ -80,7 +92,12 @@ bool isHalftone(DitherStyle style)
bool drawsMarks(DitherStyle style)
{
return !hasTones(style);
return !hasTones(style) && style != DitherStyle::scanlines;
}
bool usesPixelSize(DitherStyle style)
{
return style != DitherStyle::ascii && style != DitherStyle::scanlines;
}
QString rawValue(DitherPixelShape shape)
@@ -105,6 +122,10 @@ DitherSettings DitherSettings::normalized() const
result.pixelSize = std::round(clamp(pixelSize, pixelSizeLow, pixelSizeHigh, 2));
result.cellSize = std::round(clamp(cellSize, cellSizeLow, cellSizeHigh, 8));
result.textSize = std::round(clamp(textSize, textSizeLow, textSizeHigh, 14));
result.lineSpacing = std::round(clamp(lineSpacing, lineSpacingLow, lineSpacingHigh, 4));
result.glow = clamp(glow, 0, 100, 35);
result.dots = clamp(dots, 0, 100, 0);
result.wobble = clamp(wobble, wobbleLow, wobbleHigh, 0);
result.angle = clamp(angle, -90, 90, 45);
result.levels = std::round(clamp(levels, levelsLow, levelsHigh, 2));
result.diffusion = clamp(diffusion, 0, 100, 100);
@@ -125,8 +146,8 @@ DitherSettings DitherSettings::normalized() const
QImage DitherSettings::apply(const QImage &image) const
{
const DitherSettings settings = normalized();
// ASCII draws at full resolution: chunks would blur its letters.
const int block = settings.style == DitherStyle::ascii ? 1 : int(settings.pixelSize);
// ASCII and Scanlines draw full size: chunks would blur them.
const int block = usesPixelSize(settings.style) ? int(settings.pixelSize) : 1;
const QImage source = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
if (source.isNull())
throw ExportError(ExportError::Kind::render);
@@ -153,6 +174,8 @@ QImage DitherSettings::apply(const QImage &image) const
}
}
const QImage dithered = settings.dither(working);
if (settings.style == DitherStyle::scanlines && settings.glow > 0)
return settings.glowing(dithered);
if (block <= 1)
return dithered;
// Blown back up without smoothing.
@@ -171,9 +194,42 @@ QImage DitherSettings::apply(const QImage &image) const
return full;
}
// The lines' light blurred wide and added back: phosphor bloom.
QImage DitherSettings::glowing(const QImage &image) const
{
// Blurred small, scaled back up: same light, less work.
const double sigma = lineSpacing * 3 + 3, shrink = std::max(1.0, std::floor(sigma / 4));
const int width = image.width(), height = image.height();
// Swift clamps first: edges repeat outward before shrinking.
const int step = int(shrink), pad = int(std::ceil(sigma * 3 / shrink)) * step;
QImage padded = BrushRaster::context(width + 2 * pad, height + 2 * pad, false);
for (int y = 0; y < padded.height(); ++y) {
const uchar *source = image.constScanLine(std::clamp(y - pad, 0, height - 1));
uchar *out = padded.scanLine(y);
for (int x = 0; x < padded.width(); ++x)
std::copy_n(source + std::clamp(x - pad, 0, width - 1) * 4, 4, out + x * 4);
}
const int smallWidth = (padded.width() + step - 1) / step, smallHeight = (padded.height() + step - 1) / step;
const QImage small = padded.scaled(smallWidth, smallHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
if (small.isNull())
throw ExportError(ExportError::Kind::render);
const QImage blurred = PixelAdjust::gaussianBlur(small, sigma / shrink, true)
.scaled(smallWidth * step, smallHeight * step, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)
.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
QImage result = image.copy();
if (blurred.isNull() || result.isNull())
throw ExportError(ExportError::Kind::render);
// The scaled light's own rows, cropped to the image.
QImage bloom = BrushRaster::context(width, height, false);
for (int y = 0; y < height; ++y)
std::copy_n(blurred.constScanLine(y + pad) + pad * 4, width * 4, bloom.scanLine(y));
dither_glow(result.bits(), bloom.constBits(), size_t(width), size_t(height), size_t(result.bytesPerLine()), float(glow / 100 * 2.5));
return result;
}
QImage DitherSettings::dither(const QImage &image) const
{
const int cell = int(cellSize);
const int cell = int(style == DitherStyle::scanlines ? lineSpacing : cellSize);
const Glyphs glyphs = style == DitherStyle::ascii ? DitherSettings::glyphs(characters.isEmpty() ? defaultCharacters() : characters, int(textSize)) : Glyphs();
const bool two = colors == DitherColors::twoColors;
const std::array<uint8_t, 3> darkColor = two ? bytes(dark) : std::array<uint8_t, 3>{0, 0, 0};
@@ -184,7 +240,8 @@ QImage DitherSettings::dither(const QImage &image) const
.contrast = float(contrast / 100), .cell = cell, .angle = float(angle * M_PI / 180), .lightOnDark = lightOnDark ? 1 : 0,
.originalColors = colors == DitherColors::original ? 1 : 0, .dark = {darkColor[0], darkColor[1], darkColor[2]},
.light = {lightColor[0], lightColor[1], lightColor[2]}, .glyphWidth = glyphs.width, .glyphHeight = glyphs.height, .glyphs = glyphs.maps.data(),
.glyphCoverage = glyphs.coverage.data(), .glyphCount = int(glyphs.coverage.size())};
.glyphCoverage = glyphs.coverage.data(), .glyphCount = int(glyphs.coverage.size()), .dots = float(dots / 100),
.wobble = float(wobble)};
failed = dither_apply(pixels, size_t(width), size_t(height), size_t(stride), &params) == 0;
});
if (failed)
+15 -4
View File
@@ -6,10 +6,10 @@
#include <vector>
// Filter › Dither's looks, in the panel's order and DitherPixels.h's.
enum class DitherStyle { atkinson, floydSteinberg, bayer2, bayer4, bayer8, dots, lines, diamonds, patterns, ascii };
inline constexpr std::array allDitherStyles{DitherStyle::atkinson, DitherStyle::floydSteinberg, DitherStyle::bayer2, DitherStyle::bayer4,
DitherStyle::bayer8, DitherStyle::dots, DitherStyle::lines, DitherStyle::diamonds,
DitherStyle::patterns, DitherStyle::ascii};
enum class DitherStyle { atkinson, floydSteinberg, bayer2, bayer4, bayer8, dots, lines, diamonds, patterns, ascii, scanlines };
inline constexpr std::array allDitherStyles{DitherStyle::atkinson, DitherStyle::floydSteinberg, DitherStyle::bayer2, DitherStyle::bayer4,
DitherStyle::bayer8, DitherStyle::dots, DitherStyle::lines, DitherStyle::diamonds,
DitherStyle::patterns, DitherStyle::ascii, DitherStyle::scanlines};
QString rawValue(DitherStyle style);
// Swift's groups: diffusion, ordered, halftone, marks.
int ditherGroup(DitherStyle style);
@@ -19,6 +19,8 @@ bool diffuses(DitherStyle style);
bool hasTones(DitherStyle style);
bool isHalftone(DitherStyle style);
bool drawsMarks(DitherStyle style);
// ASCII and Scanlines draw at full resolution, not chunky.
bool usesPixelSize(DitherStyle style);
// A chunky pixel: a square, or a dot on dark.
enum class DitherPixelShape { square, dot };
@@ -35,6 +37,8 @@ struct DitherSettings {
static constexpr double cellSizeLow = 4, cellSizeHigh = 64;
static constexpr double textSizeLow = 6, textSizeHigh = 64;
static constexpr double levelsLow = 2, levelsHigh = 8;
static constexpr double lineSpacingLow = 2, lineSpacingHigh = 32;
static constexpr double wobbleLow = 0, wobbleHigh = 64;
static QString defaultCharacters() { return QStringLiteral(" .:-=+*#%@"); }
DitherStyle style = DitherStyle::atkinson;
// Each dithered pixel covers this many layer pixels a side.
@@ -44,6 +48,12 @@ struct DitherSettings {
double cellSize = 8;
// ASCII's line height in layer pixels; letters about 0.6 wide.
double textSize = 14;
// Scanlines: the lines' distance apart, in layer pixels.
double lineSpacing = 4;
// Glow and dots 0–100%; wobble sideways in pixels.
double glow = 35;
double dots = 0;
double wobble = 0;
// The halftone screen's angle, in degrees.
double angle = 45;
// Tones per channel; 2 is 1-bit.
@@ -76,4 +86,5 @@ struct DitherSettings {
private:
QImage dither(const QImage &image) const;
QImage glowing(const QImage &image) const;
};
+29 -3
View File
@@ -235,14 +235,40 @@ bool ProjectWorkspace::finishTextEditing()
return true;
}
void ProjectWorkspace::settlePendingEdits(std::function<void()> done)
{
// Canvas edits are applied; dialogs cancelled, as their Cancel.
const std::shared_ptr<ProjectTab> settled = tab(current().id);
settled->session.commitGradient([self = QPointer<ProjectWorkspace>(this), settled, done] {
settled->session.finishPixelMove([self, settled, done] {
EditorSession &session = settled->session;
session.cancelFilter();
session.cancelHueSaturation();
session.cancelLevels();
session.cancelColorRange();
session.setSelectionAmountOperation(std::nullopt);
session.closeColorPicker(false);
if (self)
done();
});
});
}
void ProjectWorkspace::confirmQuit(std::function<void(bool)> done)
{
if (!finishTextEditing() || !canSwitch()) {
if (m_isManaging || !finishTextEditing()) {
finish(done, false);
return;
}
setManaging(true);
askNext(quitOrder(), 0, std::move(done));
settlePendingEdits([this, done] {
if (!canSwitch()) {
qCWarning(lcApp) << "Quit refused: the project is still busy";
finish(done, false);
return;
}
setManaging(true);
askNext(quitOrder(), 0, done);
});
}
void ProjectWorkspace::askNext(std::vector<std::shared_ptr<ProjectTab>> order, size_t index, std::function<void(bool)> done)
+2
View File
@@ -44,6 +44,8 @@ public:
void close(QUuid id, std::function<void()> done = {});
void removeTab(QUuid id);
std::vector<std::shared_ptr<ProjectTab>> quitOrder() const;
// Applies canvas edits and cancels dialogs before a quit.
void settlePendingEdits(std::function<void()> done);
void confirmQuit(std::function<void(bool)> done);
void closeWindow(QWidget *closing);
void receive(const QList<QUrl> &urls, std::optional<QUuid> destination = std::nullopt,
+114
View File
@@ -0,0 +1,114 @@
#include "UI/CameraRawColorControls.h"
#include <algorithm>
#include <array>
#include <cmath>
// Swift's curve graph drags, picked once at the press.
std::optional<CameraRawCurveControls::Drag> CameraRawCurveControls::parametricDrag(QPointF at, QSizeF size) const
{
const CameraRawCurveSettings curve = raw().curve;
const double tone = at.x() / std::max(size.width(), 1.0) * 100;
const std::array<double, 3> splits{curve.shadowSplit, curve.darkSplit, curve.lightSplit};
// Along the bottom, the nearest divider, the first of equals.
if (at.y() > size.height() - 18) {
size_t nearest = 0;
for (size_t index = 1; index < splits.size(); ++index)
if (std::abs(splits[index] - tone) < std::abs(splits[nearest] - tone))
nearest = index;
return Drag{.kind = Drag::Kind::divider, .index = nearest, .from = at};
}
double CameraRawCurveSettings::*key = tone < splits[0] ? &CameraRawCurveSettings::shadows
: tone < splits[1] ? &CameraRawCurveSettings::darks
: tone < splits[2] ? &CameraRawCurveSettings::lights
: &CameraRawCurveSettings::highlights;
return Drag{.kind = Drag::Kind::region, .key = key, .start = curve.*key, .from = at};
}
std::optional<CameraRawCurveControls::Drag> CameraRawCurveControls::pointDrag(QPointF at, QSizeF size)
{
const double x = at.x() / std::max(size.width(), 1.0), y = 1 - at.y() / std::max(size.height(), 1.0);
std::vector<CurvePoint> points = currentPoints();
// On a point, that point; the first of equals.
std::optional<size_t> near;
for (size_t index = 0; index < points.size(); ++index)
if (!near || std::hypot(points[index].x - x, points[index].y - y) < std::hypot(points[*near].x - x, points[*near].y - y))
near = index;
if (near && std::hypot(points[*near].x - x, points[*near].y - y) < 0.055) {
m_selectedPoint = near;
return Drag{.kind = Drag::Kind::point, .index = *near, .from = at};
}
// Else a new point; the ends bound its x.
if (points.size() >= 16 || !std::all_of(points.begin(), points.end(), [x](const CurvePoint &each) { return std::abs(each.x - x) > 0.01; }))
return std::nullopt;
const CurvePoint point{x, y};
points.push_back(point);
std::stable_sort(points.begin(), points.end(), [](const CurvePoint &left, const CurvePoint &right) { return left.x < right.x; });
store(points);
const size_t index = size_t(std::find(points.begin(), points.end(), point) - points.begin());
m_selectedPoint = index;
return Drag{.kind = Drag::Kind::point, .index = index, .from = at};
}
void CameraRawCurveControls::beginDrag(QPointF at, QSizeF size)
{
// Qt may lose a release: every press picks afresh.
const bool parametric = !m_session.filterEdit() || m_session.filterEdit()->rawPanel.curvePage == CameraRawCurvePage::parametric;
m_drag = parametric ? parametricDrag(at, size) : pointDrag(at, size);
continueDrag(at, size);
}
void CameraRawCurveControls::continueDrag(QPointF at, QSizeF size)
{
if (!m_drag)
return;
const double x = at.x() / std::max(size.width(), 1.0), y = 1 - at.y() / std::max(size.height(), 1.0);
const Drag drag = *m_drag;
switch (drag.kind) {
case Drag::Kind::point: {
std::vector<CurvePoint> points = currentPoints();
if (drag.index >= points.size())
return;
points[drag.index].y = y;
// Ends stay ends; the rest keep their order.
if (drag.index > 0 && drag.index < points.size() - 1)
points[drag.index].x = std::min(points[drag.index + 1].x - 0.01, std::max(points[drag.index - 1].x + 0.01, x));
store(points);
return;
}
case Drag::Kind::divider: {
const double value = x * 100;
update([&drag, value](CameraRawSettings &settings) {
// Below the next divider; normalizing does the rest.
CameraRawCurveSettings &curve = settings.curve;
if (drag.index == 0)
curve.shadowSplit = std::min(value, curve.darkSplit - 2);
else if (drag.index == 1)
curve.darkSplit = std::min(curve.lightSplit - 2, value);
else
curve.lightSplit = value;
});
return;
}
case Drag::Kind::region: {
const double amount = std::min(100.0, std::max(-100.0, drag.start - (at.y() - drag.from.y()) / std::max(size.height(), 1.0) * 200));
update([&drag, amount](CameraRawSettings &settings) { settings.curve.*drag.key = std::round(amount); });
return;
}
}
}
void CameraRawCurveControls::removePoint(QPointF at, QSizeF size)
{
std::vector<CurvePoint> points = currentPoints();
const double x = at.x() / size.width();
// The nearest inner point by x, within 0.04; ends stay.
std::optional<size_t> nearest;
for (size_t index = 1; index + 1 < points.size(); ++index)
if (!nearest || std::abs(points[index].x - x) < std::abs(points[*nearest].x - x))
nearest = index;
if (!nearest || !(std::abs(points[*nearest].x - x) < 0.04))
return;
points.erase(points.begin() + std::ptrdiff_t(*nearest));
m_selectedPoint.reset();
store(points);
}
+35 -82
View File
@@ -87,103 +87,45 @@ protected:
return;
}
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::white);
for (const CurvePoint &point : m_owner.currentPoints())
painter.drawEllipse(QRectF(point.x * w - 4, (1 - point.y) * h - 4, 8, 8));
const std::vector<CurvePoint> points = m_owner.currentPoints();
for (size_t index = 0; index < points.size(); ++index) {
painter.setBrush(m_owner.m_selectedPoint == index ? palette().color(QPalette::Highlight) : QColor(Qt::white));
painter.drawEllipse(QRectF(points[index].x * w - 4, (1 - points[index].y) * h - 4, 8, 8));
}
}
// Swift's two drags: still, a press adds; moved, it drags.
// Swift's one drag: picked at the press, kept until release.
void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton)
m_press = event->position();
m_owner.beginDrag(event->position(), size());
}
void mouseMoveEvent(QMouseEvent *event) override
{
if (!m_press || !event->buttons().testFlag(Qt::LeftButton))
return;
if (!m_moved && QLineF(*m_press, event->position()).length() < 2)
return;
m_moved = true;
if (parametric())
moveDivider(event->position().x());
else
movePoint(event->position());
if (event->buttons().testFlag(Qt::LeftButton))
m_owner.continueDrag(event->position(), size());
}
void mouseReleaseEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton && m_press && !m_moved && event->position() == *m_press && !parametric())
addPoint(event->position());
m_press.reset();
m_moved = false;
if (event->button() == Qt::LeftButton)
m_owner.m_drag.reset();
}
// The double click's own release must add nothing back.
// The second press drags too; then the point goes.
void mouseDoubleClickEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton && !parametric())
removePoint(event->position());
if (event->button() != Qt::LeftButton)
return;
m_owner.beginDrag(event->position(), size());
if (!parametric())
m_owner.removePoint(event->position(), size());
m_owner.m_drag.reset();
}
private:
void moveDivider(double x)
{
const double value = std::min(98.0, std::max(2.0, x / std::max(double(width()), 1.0) * 100));
const CameraRawCurveSettings curve = m_owner.raw().curve;
const std::array<double, 3> splits{curve.shadowSplit, curve.darkSplit, curve.lightSplit};
size_t nearest = 0;
for (size_t index = 1; index < 3; ++index)
if (std::abs(splits[index] - value) < std::abs(splits[nearest] - value))
nearest = index;
m_owner.update([nearest, value](CameraRawSettings &settings) {
(nearest == 0 ? settings.curve.shadowSplit : nearest == 1 ? settings.curve.darkSplit : settings.curve.lightSplit) = value;
});
}
void addPoint(QPointF at)
{
std::vector<CurvePoint> points = m_owner.currentPoints();
points.push_back({std::min(0.99, std::max(0.01, at.x() / width())), std::min(1.0, std::max(0.0, 1 - at.y() / height()))});
m_owner.store(points);
}
// The nearest inner point by x; ends stay.
std::optional<size_t> nearestInner(const std::vector<CurvePoint> &points, double x) const
{
std::optional<size_t> nearest;
for (size_t index = 1; index + 1 < points.size(); ++index)
if (!nearest || std::abs(points[index].x - x) < std::abs(points[*nearest].x - x))
nearest = index;
return nearest;
}
void movePoint(QPointF at)
{
std::vector<CurvePoint> points = m_owner.currentPoints();
const double x = at.x() / width();
const std::optional<size_t> index = nearestInner(points, x);
if (!index)
return;
points[*index] = {std::min(0.98, std::max(0.02, x)), std::min(1.0, std::max(0.0, 1 - at.y() / height()))};
m_owner.store(points);
}
void removePoint(QPointF at)
{
std::vector<CurvePoint> points = m_owner.currentPoints();
const double x = at.x() / width();
const std::optional<size_t> index = nearestInner(points, x);
if (!index || !(std::abs(points[*index].x - x) < 0.04))
return;
points.erase(points.begin() + std::ptrdiff_t(*index));
m_owner.store(points);
}
CameraRawCurveControls &m_owner;
std::optional<QPointF> m_press;
bool m_moved = false;
};
namespace {
@@ -250,6 +192,9 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
presetRow->addStretch(1);
points->addLayout(presetRow);
connect(m_preset, &QComboBox::activated, this, [this](int preset) {
// A fix beyond Swift: a preset's points drop the choice.
if (preset != 0)
m_selectedPoint.reset();
if (preset == 1)
store(CameraRawCurveSettings::linear());
else if (preset == 2)
@@ -258,7 +203,7 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
store(CameraRawCurveSettings::strongContrast());
});
m_refine = new CameraRawRow({.name = QStringLiteral("refineSaturation"), .title = QStringLiteral("Refine Saturation"),
.help = QStringLiteral("How much the RGB curve also changes color strength. Zero keeps it to brightness."), .titleWidth = 88,
.help = QStringLiteral("How much the curve also changes color strength. Zero matches Photoshop; lower keeps it to brightness, higher adds more color."), .titleWidth = 88,
.fixedTitle = true, .fieldWidth = 48, .scrub = 1},
[this] { return raw().curve.refineSaturation; },
[this](double value) { update([value](CameraRawSettings &settings) { settings.curve.refineSaturation = value; }); },
@@ -334,15 +279,23 @@ void CameraRawCurveControls::synchronize()
m_channelBox->setVisible(point);
const CameraRawPointChannel channel = edit ? edit->rawPanel.pointChannel : CameraRawPointChannel::rgb;
m_channel->button(int(channel))->setChecked(true);
m_graph->setToolTip(point ? QStringLiteral("Drag a point. Click the curve to add one. Double-click a point to remove it.")
: QStringLiteral("Drag a divider to change which tones the neighboring sliders affect."));
// Swift's onChange: another channel drops the choice and drag.
if (channel != m_shownChannel) {
m_shownChannel = channel;
m_selectedPoint.reset();
m_drag.reset();
}
m_graph->setToolTip(point ? QStringLiteral("Drag a point. Click to add one. Double-click a point to remove it.")
: QStringLiteral("Drag up or down to lift or lower those tones. Drag a divider along the bottom to change which tones each region covers."));
m_graph->update();
m_amounts->setVisible(!point);
m_points->setVisible(point);
const std::vector<CurvePoint> points = currentPoints();
// Swift's selectedPoint: the last inner point, else the last.
const CurvePoint selected = points.size() > 2 ? points[points.size() - 2] : points.back();
m_selected->setText(QStringLiteral("In %1 Out %2").arg(std::lround(selected.x * 255)).arg(std::lround(selected.y * 255)));
// Swift's selectedPoint: the chosen index, while it exists.
const bool chosen = m_selectedPoint && *m_selectedPoint < points.size();
m_selected->setVisible(chosen);
if (chosen)
m_selected->setText(QStringLiteral("In %1 Out %2").arg(std::lround(points[*m_selectedPoint].x * 255)).arg(std::lround(points[*m_selectedPoint].y * 255)));
const QSignalBlocker quiet(m_preset);
m_preset->setCurrentIndex(points == CameraRawCurveSettings::linear() ? 1
: points == CameraRawCurveSettings::mediumContrast() ? 2
+16
View File
@@ -28,9 +28,25 @@ private:
void update(const std::function<void(CameraRawSettings &)> &change);
void store(const std::vector<CurvePoint> &points);
void panel(const std::function<void(CameraRawPanel &)> &change);
// Swift's Drag: a point, divider, or region.
struct Drag {
enum class Kind { point, divider, region } kind;
size_t index = 0;
double CameraRawCurveSettings::*key = nullptr;
double start = 0;
QPointF from;
};
std::optional<Drag> parametricDrag(QPointF at, QSizeF size) const;
std::optional<Drag> pointDrag(QPointF at, QSizeF size);
void beginDrag(QPointF at, QSizeF size);
void continueDrag(QPointF at, QSizeF size);
void removePoint(QPointF at, QSizeF size);
friend class CameraRawCurveGraph;
EditorSession &m_session;
std::optional<Drag> m_drag;
std::optional<size_t> m_selectedPoint;
CameraRawPointChannel m_shownChannel = CameraRawPointChannel::rgb;
QButtonGroup *m_page;
QWidget *m_channelBox;
QButtonGroup *m_channel;
+17 -2
View File
@@ -48,10 +48,25 @@ void FilterSheet::dither()
});
control(QStringLiteral("Pixel Size"), key(&DitherSettings::pixelSize), DitherSettings::pixelSizeLow, DitherSettings::pixelSizeHigh, QStringLiteral("px"), 0, false);
box()->setToolTip(QStringLiteral("Make each dithered pixel this many pixels across, for a chunky old-screen look"));
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return settings.dither.style != DitherStyle::ascii; });
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return usesPixelSize(settings.dither.style); });
control(QStringLiteral("Text Size"), key(&DitherSettings::textSize), DitherSettings::textSizeLow, DitherSettings::textSizeHigh, QStringLiteral("px"), 0, false);
box()->setToolTip(QStringLiteral("The height of each line of characters"));
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return settings.dither.style == DitherStyle::ascii; });
// Scanlines' four rows.
const auto scanlines = [](const FilterSettings &settings) { return settings.dither.style == DitherStyle::scanlines; };
control(QStringLiteral("Line Spacing"), key(&DitherSettings::lineSpacing), DitherSettings::lineSpacingLow, DitherSettings::lineSpacingHigh, QStringLiteral("px"), 0,
false);
box()->setToolTip(QStringLiteral("How far apart the screen's lines are"));
m_shownWhen.emplace_back(box(), scanlines);
control(QStringLiteral("Glow"), key(&DitherSettings::glow), 0, 100, QStringLiteral("%"), 0, false);
box()->setToolTip(QStringLiteral("Light blooming around the lines, like a CRT's phosphors"));
m_shownWhen.emplace_back(box(), scanlines);
control(QStringLiteral("Dots"), key(&DitherSettings::dots), 0, 100, QStringLiteral("%"), 0, false);
box()->setToolTip(QStringLiteral("Break the lines into glowing beads"));
m_shownWhen.emplace_back(box(), scanlines);
control(QStringLiteral("Wobble"), key(&DitherSettings::wobble), DitherSettings::wobbleLow, DitherSettings::wobbleHigh, QStringLiteral("px"), 0, false);
box()->setToolTip(QStringLiteral("Make the lines waver sideways down the screen, like a CRT losing sync"));
m_shownWhen.emplace_back(box(), scanlines);
control(QStringLiteral("Cell Size"), key(&DitherSettings::cellSize), DitherSettings::cellSizeLow, DitherSettings::cellSizeHigh, QStringLiteral("px"), 0, false);
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return isHalftone(settings.dither.style); });
control(QStringLiteral("Angle"), key(&DitherSettings::angle), -90, 90, QStringLiteral("°"), 0, false);
@@ -133,7 +148,7 @@ void FilterSheet::dither()
const QSignalBlocker quiet(shape);
shape->setCurrentIndex(int(settings.dither.pixelShape));
});
m_shownWhen.emplace_back(shape->parentWidget(), [](const FilterSettings &settings) { return settings.dither.pixelSize > 1 && settings.dither.style != DitherStyle::ascii; });
m_shownWhen.emplace_back(shape->parentWidget(), [](const FilterSettings &settings) { return settings.dither.pixelSize > 1 && usesPixelSize(settings.dither.style); });
flag(QStringLiteral("Light on Dark"), QStringLiteral("Draw the marks for the light tones on the dark color, like a glowing screen"),
[](FilterSettings &settings) -> bool & { return settings.dither.lightOnDark; });
m_shownWhen.emplace_back(m_flags.back().first, [](const FilterSettings &settings) { return drawsMarks(settings.dither.style); });