Quit settles a pending gradient, a pixel move and open dialogs; Dither › Scanlines (CRT) on all cores (13.17)
This commit is contained in:
1 parent
765e071c0c
commit
5bfbc6cd5d
12 files changed
+913
-32
No files matched your search
+62
-5
@@ -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), ¶ms) == 0;
|
||||
});
|
||||
if (failed)
|
||||
|
||||
+15
-4
@@ -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;
|
||||
};
|
||||
@@ -220,14 +220,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)
|
||||
|
||||
@@ -42,6 +42,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,
|
||||
|
||||
@@ -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); });
|
||||
|
||||
Reference in new issue
Block a user