14.19: Filter › Scanlines, its own filter, on Swift's cdde327 kernel

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 10:46:16 -04:00
1 parent d06fb55bc4
commit 1a129b9a23
26 files changed
+905 -479

No files matched your search

+317 -254
View File
@@ -1,9 +1,10 @@
#include "Document/BrushStroke.h"
#include "Document/Dither.h"
#include "Document/EditorSession.h"
#include "Document/PixelAdjust.h"
#include "Document/Scanlines.h"
#include "UI/ColorPaletteControls.h"
#include "UI/FilterSheet.h"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
@@ -11,8 +12,11 @@
#include <QSlider>
#include <QtTest>
#include <numeric>
extern "C" {
#include "DitherPixels.h"
}
// Swift 1.4.4's Dither › Scanlines (CRT), on every core.
// Swift 1.4.9's Filter › Scanlines, its own filter.
namespace {
QImage filled(int width, int height, QColor colour)
{
@@ -21,16 +25,6 @@ QImage filled(int width, int height, QColor colour)
return image;
}
DitherSettings scanlines(double spacing, double glow = 0)
{
DitherSettings settings;
settings.style = DitherStyle::scanlines;
settings.lineSpacing = spacing;
settings.glow = glow;
return settings;
}
// One channel of one column, top to bottom.
std::vector<int> column(const QImage &image, int x, int channel = 0)
{
std::vector<int> values;
@@ -47,35 +41,65 @@ std::vector<int> row(const QImage &image, int y, int channel = 0)
return values;
}
// Swift's test: one column of a flat gray's render.
// Swift's render: no glow unless asked.
QImage render(const QImage &image, const std::function<void(ScanlinesSettings &)> &adjust = {})
{
ScanlinesSettings settings;
if (adjust)
adjust(settings);
return settings.apply(image);
}
std::vector<int> gray(int level, double spacing, double glow = 0)
{
return column(scanlines(spacing, glow).apply(filled(16, 32, QColor(level, level, level))), 5);
return column(render(filled(16, 32, QColor(level, level, level)), [&](ScanlinesSettings &s) { s.lineSpacing = spacing, s.glow = glow; }), 5);
}
// Worst difference from Swift's first glow, blurred whole.
int glowError(const QImage &image, double glow)
// The oracle's picture: premultiplied, some clear, some soft.
QImage pattern(int width, int height)
{
const QImage lines = scanlines(8).apply(image), result = scanlines(8, glow).apply(image);
const QImage blurred = PixelAdjust::gaussianBlur(lines, 8 * 3 + 3, true);
const float amount = float(glow / 100 * 2.5);
int worst = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
const int plain = lines.constScanLine(y)[x * 4], light = blurred.constScanLine(y)[x * 4];
const int expected = int(std::lround(std::min(255.0f, float(plain) + float(light) * amount)));
worst = std::max(worst, std::abs(result.constScanLine(y)[x * 4] - expected));
QImage image = BrushRaster::context(width, height, false);
for (int y = 0; y < height; ++y) {
uchar *out = image.scanLine(y);
for (int x = 0; x < width; ++x) {
const unsigned alpha = (x * 13 + y * 7) % 4 == 0 ? 0 : 128 + (x * 29 + y * 17) % 128;
for (int c = 0; c < 3; ++c)
out[x * 4 + c] = uchar(((x * 37 + y * 11 + c * 53) % 256) * alpha / 255);
out[x * 4 + 3] = uchar(alpha);
}
}
return worst;
return image;
}
std::vector<int> repeated(const std::vector<int> &band, int times)
quint64 fnv(const QImage &image)
{
std::vector<int> all;
for (int each = 0; each < times; ++each)
all.insert(all.end(), band.begin(), band.end());
return all;
quint64 hash = 1469598103934665603ULL;
for (int y = 0; y < image.height(); ++y)
for (int x = 0; x < image.width() * 4; ++x)
hash = (hash ^ image.constScanLine(y)[x]) * 1099511628211ULL;
return hash;
}
// Swift's params: unset fields zero, white lines unless set.
ScanlinesParams made(const std::function<void(ScanlinesParams &)> &set)
{
ScanlinesParams params{};
params.light[0] = params.light[1] = params.light[2] = 255;
set(params);
return params;
}
void rgb(uint8_t *to, uint8_t red, uint8_t green, uint8_t blue)
{
to[0] = red, to[1] = green, to[2] = blue;
}
QImage kernel(int width, int height, const ScanlinesParams &params)
{
QImage image = pattern(width, height);
if (scanlines_apply(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), &params) == 0)
throw std::runtime_error("no memory");
return image;
}
struct Shown {
@@ -86,15 +110,11 @@ struct Shown {
session.createDocument(40, 30);
const QImage image = filled(40, 30, QColor(200, 120, 40));
session.insert(ImportedImage(image, image, QStringLiteral("Colour")));
session.beginFilter(FilterKind::dither);
FilterSettings settings = session.filterEdit().value().settings;
settings.dither.style = DitherStyle::scanlines;
session.updateFilter(settings, true);
session.beginFilter(FilterKind::scanlines);
sheet = std::make_unique<FilterSheet>(session);
sheet->show();
}
const DitherSettings &dither() const { return session.filterEdit().value().settings.dither; }
// The shown rows in the column's order, with their help.
const ScanlinesSettings &lines() const { return session.filterEdit().value().settings.scanlines; }
QStringList rows() const
{
QStringList titles;
@@ -103,8 +123,6 @@ struct Shown {
QWidget *each = column->itemAt(index)->widget();
if (!each || each->isHidden())
continue;
if (auto *box = qobject_cast<QCheckBox *>(each); box && box->text() != QLatin1String("Preview"))
titles << box->text();
for (QLabel *label : each->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly)) {
if (label->buddy())
titles << label->text() + u'|' + each->toolTip();
@@ -119,21 +137,22 @@ class ScanlinesTests : public QObject {
Q_OBJECT
private slots:
void scanlinesAreLinesOfLightThatBloomWithBrightness();
void linesShowOnSolidWhiteAndBlack();
void glowLightsBetweenTheLines();
void dotsBreakTheLinesIntoBeads();
void dotsBeadTheLinesBelowTheirLevel();
void wobbleMovesLinesSideways();
void theStyleDrawsAtFullResolutionWithoutMarks();
void normalizingClampsTheFourSettings();
void eachLineIsTheAverageOfItsRows();
void theScreenIsTheDarkColourOrBlack();
void displaceLiftsLinesWhereThePictureIsBright();
void aLiftedLineHidesTheLinesBehindIt();
void thresholdDarkensWhatsBelowIt();
void theKernelDrawsSwiftsOwnBytes();
void theSettingsReachTheKernel();
void normalizingClampsEverySetting();
void clearPixelsStayClear();
void everyBandOfATallImageIsDrawn();
void bandsDrawAsOneBandDoes();
void theGlowIsTheBlurredLightAtItsAmount();
void theGlowScalesBackExactlyOffTheStep();
void theGlowNeverPassesAlpha();
void theSheetShowsScanlinesRows();
void theGlowIsTheBlurredLightEasedNearWhite();
void theFilterIsListedAndPreviewsWhole();
void theSheetShowsSwiftsRows();
void theRowsWriteTheirSettings();
void twoColorsPickTheScreenAndTheLines();
};
void ScanlinesTests::scanlinesAreLinesOfLightThatBloomWithBrightness()
@@ -146,9 +165,15 @@ void ScanlinesTests::scanlinesAreLinesOfLightThatBloomWithBrightness()
}
QVERIFY(std::accumulate(dim.begin(), dim.end(), 0) * 2 < std::accumulate(white.begin(), white.end(), 0));
QVERIFY(std::all_of(black.begin(), black.end(), [](int value) { return value == 0; }));
// The beam's half height, 2.8 white, about 1.98 at 0.35.
QCOMPARE(white, repeated({0, 255, 255, 255, 255, 255, 255, 0}, 4));
QCOMPARE(dim, repeated({0, 0, 118, 120, 120, 118, 0, 0}, 4));
}
void ScanlinesTests::linesShowOnSolidWhiteAndBlack()
{
const std::vector<int> white = gray(255, 8, 100);
QVERIFY(*std::max_element(white.begin(), white.end()) - *std::min_element(white.begin(), white.end()) > 40);
const std::vector<int> black = column(render(filled(16, 32, Qt::black), [](ScanlinesSettings &s) { s.lineSpacing = 8, s.blackLevel = 40; }), 8);
const std::vector<int> band(black.begin(), black.begin() + 8);
QVERIFY(*std::max_element(band.begin(), band.end()) > 60 && *std::min_element(band.begin(), band.end()) < 10);
}
void ScanlinesTests::glowLightsBetweenTheLines()
@@ -157,19 +182,14 @@ void ScanlinesTests::glowLightsBetweenTheLines()
QVERIFY2(glowing[0] > plain[0] + 40, qPrintable(QString::number(glowing[0])));
}
void ScanlinesTests::dotsBreakTheLinesIntoBeads()
void ScanlinesTests::dotsBeadTheLinesBelowTheirLevel()
{
const QImage white = filled(64, 16, Qt::white);
DitherSettings settings = scanlines(8);
QCOMPARE(row(settings.apply(white), 4), std::vector<int>(64, 255));
settings.dots = 100;
// Each bead lit at its middle, dark at its ends.
const std::vector<int> lit = row(settings.apply(white), 4);
QCOMPARE(std::vector<int>(lit.begin(), lit.begin() + 8), (std::vector<int>{0, 255, 255, 255, 255, 255, 255, 0}));
QCOMPARE(std::vector<int>(lit.begin() + 8, lit.begin() + 16), (std::vector<int>{0, 255, 255, 255, 255, 255, 255, 0}));
// Half dots: the bead ends dim off the middle.
settings.dots = 50;
QCOMPARE(row(settings.apply(white), 1)[0], 85);
const auto middle = [](int level) {
return row(render(filled(64, 16, QColor(level, level, level)), [](ScanlinesSettings &s) { s.lineSpacing = 8, s.dots = 70; }), 4);
};
QCOMPARE(middle(255), std::vector<int>(64, 255));
const std::vector<int> grey = middle(102);
QVERIFY(grey[3] > 100 && grey[4] > 100 && grey[0] < 40 && grey[8] < 40);
}
void ScanlinesTests::wobbleMovesLinesSideways()
@@ -177,106 +197,129 @@ void ScanlinesTests::wobbleMovesLinesSideways()
QImage edge = filled(64, 64, Qt::black);
QPainter(&edge).fillRect(QRect(32, 0, 32, 64), Qt::white);
const auto edges = [&edge](double wobble) {
DitherSettings settings = scanlines(8);
settings.wobble = wobble;
const QImage result = settings.apply(edge);
std::vector<int> found;
const QImage result = render(edge, [wobble](ScanlinesSettings &s) { s.lineSpacing = 8, s.wobble = wobble; });
std::set<int> found;
for (int line = 0; line < 8; ++line) {
const std::vector<int> middle = row(result, line * 8 + 4);
found.push_back(int(std::find_if(middle.begin(), middle.end(), [](int value) { return value > 128; }) - middle.begin()));
found.insert(int(std::find_if(middle.begin(), middle.end(), [](int value) { return value > 128; }) - middle.begin()));
}
return found;
};
QCOMPARE(edges(0), std::vector<int>(8, 32));
// Each line's shift: 12 times the two waves, rounded.
QCOMPARE(edges(12), (std::vector<int>{35, 36, 35, 41, 44, 37, 32, 34}));
QCOMPARE(edges(0), std::set<int>{32});
QVERIFY(edges(12).size() >= 3);
}
void ScanlinesTests::theStyleDrawsAtFullResolutionWithoutMarks()
void ScanlinesTests::displaceLiftsLinesWhereThePictureIsBright()
{
QVERIFY(!usesPixelSize(DitherStyle::scanlines) && !usesPixelSize(DitherStyle::ascii) && usesPixelSize(DitherStyle::patterns));
QVERIFY(!drawsMarks(DitherStyle::scanlines) && drawsMarks(DitherStyle::ascii));
QVERIFY(!hasTones(DitherStyle::scanlines) && !isHalftone(DitherStyle::scanlines) && ditherGroup(DitherStyle::scanlines) == 3);
// A pixel size changes nothing: no chunks, no dots.
const QImage image = filled(20, 24, QColor(180, 90, 30));
DitherSettings settings = scanlines(4);
const QImage plain = settings.apply(image);
settings.pixelSize = 5;
settings.pixelShape = DitherPixelShape::dot;
QCOMPARE(settings.apply(image), plain);
// The cell is the line spacing, not the cell size.
settings.cellSize = 32;
QCOMPARE(settings.apply(image), plain);
QImage half = filled(64, 64, Qt::black);
QPainter(&half).fillRect(QRect(32, 0, 32, 64), Qt::white);
const QImage result = render(half, [](ScanlinesSettings &s) { s.lineSpacing = 16, s.displace = 6, s.colors = DitherColors::original, s.blackLevel = 50; });
const auto brightest = [&result](int x) {
const std::vector<int> values = column(result, x);
return int(std::max_element(values.begin() + 16, values.begin() + 32) - values.begin());
};
QVERIFY(brightest(48) < 24);
// Over black the line keeps its middle, row 24.
const std::vector<int> still = column(result, 8);
QVERIFY(still[24] > 0 && still[24] == *std::max_element(still.begin() + 16, still.begin() + 32) && still[16] < still[24]);
}
void ScanlinesTests::normalizingClampsTheFourSettings()
void ScanlinesTests::aLiftedLineHidesTheLinesBehindIt()
{
DitherSettings settings;
QCOMPARE(settings.lineSpacing, 4.0);
QCOMPARE(settings.glow, 35.0);
QCOMPARE(settings.dots, 0.0);
QCOMPARE(settings.wobble, 0.0);
settings.lineSpacing = 1.4;
settings.glow = 150;
settings.dots = -5;
settings.wobble = 3.5;
DitherSettings result = settings.normalized();
QCOMPARE(result.lineSpacing, 2.0);
QCOMPARE(result.glow, 100.0);
QCOMPARE(result.dots, 0.0);
QCOMPARE(result.wobble, 3.5);
settings.lineSpacing = 40;
settings.glow = -1;
settings.dots = 120;
settings.wobble = 70;
result = settings.normalized();
QCOMPARE(result.lineSpacing, 32.0);
QCOMPARE(result.glow, 0.0);
QCOMPARE(result.dots, 100.0);
QCOMPARE(result.wobble, 64.0);
// Rounded within; not a number takes the default.
settings.lineSpacing = 5.5;
settings.glow = std::nan("");
settings.dots = std::nan("");
settings.wobble = std::nan("");
result = settings.normalized();
QCOMPARE(result.lineSpacing, 6.0);
QCOMPARE(result.glow, 35.0);
QCOMPARE(result.dots, 0.0);
QCOMPARE(result.wobble, 0.0);
settings.lineSpacing = std::nan("");
QCOMPARE(settings.normalized().lineSpacing, 4.0);
// Glow and dots keep their fractions.
settings.glow = 12.5;
settings.dots = 33.3;
QCOMPARE(settings.normalized().glow, 12.5);
QCOMPARE(settings.normalized().dots, 33.3);
QImage band = filled(64, 80, QColor(64, 64, 64));
QPainter(&band).fillRect(QRect(16, 32, 32, 16), Qt::white);
const QImage result = render(band, [](ScanlinesSettings &s) { s.lineSpacing = 16, s.displace = 40, s.smoothness = 0; });
QVERIFY(result.constScanLine(14)[4 * 4] > 20);
QVERIFY(result.constScanLine(14)[32 * 4] < 8);
QVERIFY(result.constScanLine(2)[32 * 4] > 200);
}
void ScanlinesTests::eachLineIsTheAverageOfItsRows()
void ScanlinesTests::thresholdDarkensWhatsBelowIt()
{
// Rows of white and black: each line lights at half.
QImage stripes = filled(8, 16, Qt::black);
for (int y = 0; y < 16; y += 2)
QPainter(&stripes).fillRect(QRect(0, y, 8, 1), Qt::white);
QCOMPARE(column(scanlines(8).apply(stripes), 3), repeated({0, 37, 172, 172, 172, 172, 37, 0}, 2));
const QImage grey = filled(32, 32, QColor(77, 77, 77));
const std::vector<int> shown = column(render(grey, [](ScanlinesSettings &s) { s.lineSpacing = 8; }), 16);
const std::vector<int> hidden = column(render(grey, [](ScanlinesSettings &s) { s.lineSpacing = 8, s.threshold = 60; }), 16);
QVERIFY(std::any_of(shown.begin(), shown.end(), [](int value) { return value > 40; }));
QVERIFY(std::none_of(hidden.begin(), hidden.end(), [](int value) { return value > 10; }));
}
void ScanlinesTests::theScreenIsTheDarkColourOrBlack()
// Swift's GCD kernel, built with clang's blocks, gave these.
void ScanlinesTests::theKernelDrawsSwiftsOwnBytes()
{
const QImage image = filled(8, 8, QColor(255, 128, 0));
DitherSettings settings = scanlines(8);
settings.colors = DitherColors::twoColors;
settings.dark = AdjustmentColor(0, 0, 1);
settings.light = AdjustmentColor(1, 1, 0);
const QImage two = settings.apply(image);
// Off the line the dark colour; on it, the mix.
QCOMPARE(two.pixelColor(2, 0), QColor(0, 0, 255));
QCOMPARE(two.pixelColor(2, 4), QColor(197, 197, 147));
settings.colors = DitherColors::original;
const QImage original = settings.apply(image);
QCOMPARE(original.pixelColor(2, 0), QColor(0, 0, 0));
QCOMPARE(original.pixelColor(2, 4), QColor(255, 173, 0));
const QImage a = kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 4; p.thickness = 0.7f; p.displace = 20; p.smoothness = 0.5f; p.originalColors = 1; }));
QCOMPARE(fnv(a), 14473728681183016108ULL);
QCOMPARE(QByteArray(reinterpret_cast<const char *>(a.constScanLine(2)) + 4, 4), QByteArray("\xbd\xbf\x25\xbf", 4));
const QImage b = kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 6; p.thickness = 0.9f; p.dots = 0.6f; p.wobble = 6; p.displace = -15; p.threshold = 0.3f; p.split = 3; p.density = 0.2f; p.contrast = -0.3f; p.blackLevel = 0.25f; p.smoothness = 0.8f; }));
QCOMPARE(fnv(b), 3381563246094821038ULL);
const QImage c = kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 5; p.thickness = 0.4f; p.contrast = 0.6f; p.smoothness = 0.5f; rgb(p.dark, 10, 20, 200); rgb(p.light, 250, 200, 10); }));
QCOMPARE(fnv(c), 11894396318800434257ULL);
QCOMPARE(QByteArray(reinterpret_cast<const char *>(c.constScanLine(2)) + 4, 4), QByteArray("\x55\x4d\x8f\xbf", 4));
// Wide and tall: 32 bands of columns, lines and rows.
QCOMPARE(fnv(kernel(70, 300, made([](ScanlinesParams &p) { p.spacing = 3; p.thickness = 0.7f; p.dots = 0.3f; p.displace = 30; p.split = 2; p.blackLevel = 0.1f; p.smoothness = 1; p.originalColors = 1; }))),
5889846669877741430ULL);
QCOMPARE(fnv(kernel(70, 300, made([](ScanlinesParams &p) { p.spacing = 2; p.thickness = 1; p.wobble = 9; p.displace = -40; p.threshold = 0.5f; p.smoothness = 0.25f; }))),
1843572660750656508ULL);
// The glow eases near full brightness.
QImage lit = pattern(41, 29), glow = pattern(41, 29);
for (int y = 0; y < 29; ++y)
for (int x = 0; x < 41 * 4; ++x)
glow.scanLine(y)[x] = uchar(255 - glow.constScanLine(y)[x]);
dither_glow(lit.bits(), glow.constBits(), 41, 29, size_t(lit.bytesPerLine()), 1.3f);
QCOMPARE(fnv(lit), 14026896863046063157ULL);
}
// Each setting reaches its own field, scaled as Swift's.
void ScanlinesTests::theSettingsReachTheKernel()
{
ScanlinesSettings settings;
settings.lineSpacing = 6, settings.thickness = 90, settings.dots = 60, settings.wobble = 6, settings.displace = -15, settings.threshold = 30;
settings.split = 3, settings.density = 20, settings.contrast = -30, settings.blackLevel = 25, settings.smoothness = 80;
QCOMPARE(settings.apply(pattern(37, 53)), kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 6; p.thickness = 0.9f; p.dots = 0.6f; p.wobble = 6; p.displace = -15; p.threshold = 0.3f; p.split = 3; p.density = 0.2f; p.contrast = -0.3f; p.blackLevel = 0.25f; p.smoothness = 0.8f; })));
settings = ScanlinesSettings();
settings.lineSpacing = 5, settings.thickness = 40, settings.contrast = 60, settings.colors = DitherColors::twoColors;
settings.dark = AdjustmentColor(10 / 255.0, 20 / 255.0, 200 / 255.0), settings.light = AdjustmentColor(250 / 255.0, 200 / 255.0, 10 / 255.0);
QCOMPARE(settings.apply(pattern(37, 53)), kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 5; p.thickness = 0.4f; p.contrast = 0.6f; p.smoothness = 0.5f; rgb(p.dark, 10, 20, 200); rgb(p.light, 250, 200, 10); })));
// Picked colours count only for Two Colors.
settings.colors = DitherColors::blackWhite;
QCOMPARE(settings.apply(pattern(37, 53)), kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 5; p.thickness = 0.4f; p.contrast = 0.6f; p.smoothness = 0.5f; })));
settings.colors = DitherColors::original, settings.displace = 20, settings.lineSpacing = 4, settings.thickness = 70, settings.contrast = 0;
QCOMPARE(fnv(settings.apply(pattern(37, 53))), 14473728681183016108ULL);
}
void ScanlinesTests::normalizingClampsEverySetting()
{
const ScanlinesSettings defaults;
QVERIFY(defaults.lineSpacing == 4 && defaults.thickness == 70 && defaults.glow == 0 && defaults.dots == 0 && defaults.wobble == 0);
QVERIFY(defaults.displace == 0 && defaults.smoothness == 50 && defaults.threshold == 0 && defaults.split == 0 && defaults.blackLevel == 0);
QVERIFY(defaults.colors == DitherColors::blackWhite && defaults.dark == AdjustmentColor(0, 0, 0) && defaults.light == AdjustmentColor(1, 1, 1));
using Field = double ScanlinesSettings::*;
// Below, above, a fraction, not a number.
const std::tuple<Field, double, double, double, double, double> fields[] = {
{&ScanlinesSettings::lineSpacing, 2, 32, 5.5, 6, 4}, {&ScanlinesSettings::thickness, 5, 100, 33.3, 33.3, 70},
{&ScanlinesSettings::glow, 0, 100, 12.5, 12.5, 0}, {&ScanlinesSettings::dots, 0, 100, 33.3, 33.3, 0},
{&ScanlinesSettings::wobble, 0, 64, 3.5, 3.5, 0}, {&ScanlinesSettings::displace, -100, 100, -7.5, -7.5, 0},
{&ScanlinesSettings::smoothness, 0, 100, 12.5, 12.5, 50}, {&ScanlinesSettings::threshold, 0, 100, 12.5, 12.5, 0},
{&ScanlinesSettings::split, 0, 16, 2.5, 3, 0}, {&ScanlinesSettings::density, -100, 100, 12.5, 12.5, 0},
{&ScanlinesSettings::contrast, -100, 100, -12.5, -12.5, 0}, {&ScanlinesSettings::blackLevel, 0, 100, 12.5, 12.5, 0}};
for (const auto &[field, low, high, typed, kept, fallback] : fields) {
ScanlinesSettings settings;
settings.*field = low - 1000;
QCOMPARE(settings.normalized().*field, low);
settings.*field = high + 1000;
QCOMPARE(settings.normalized().*field, high);
settings.*field = typed;
QCOMPARE(settings.normalized().*field, kept);
settings.*field = std::nan("");
QCOMPARE(settings.normalized().*field, fallback);
}
// Applying normalizes first: past the ends draws as the ends.
ScanlinesSettings wild;
wild.lineSpacing = 5.6, wild.thickness = 500, wild.split = 2.4, wild.displace = 300;
QCOMPARE(wild.apply(pattern(37, 53)), wild.normalized().apply(pattern(37, 53)));
QCOMPARE(fnv(wild.apply(pattern(37, 53))), fnv(kernel(37, 53, made([](ScanlinesParams &p) { p.spacing = 6; p.thickness = 1; p.split = 2; p.displace = 100; p.smoothness = 0.5f; }))));
ScanlinesSettings colours;
colours.dark = AdjustmentColor(-1, 2, 0.5), colours.light = AdjustmentColor(3, -2, 0.25);
QVERIFY(colours.normalized().dark == AdjustmentColor(0, 1, 0.5) && colours.normalized().light == AdjustmentColor(1, 0, 0.25));
}
void ScanlinesTests::clearPixelsStayClear()
@@ -286,139 +329,159 @@ void ScanlinesTests::clearPixelsStayClear()
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.fillRect(QRect(0, 0, 8, 8), Qt::transparent);
painter.end();
const QImage result = scanlines(8, 100).apply(image);
const QImage result = render(image, [](ScanlinesSettings &s) { s.lineSpacing = 8, s.glow = 100; });
QCOMPARE(column(result, 2, 3), (std::vector<int>{0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}));
QCOMPARE(column(result, 2, 0)[4], 0);
}
void ScanlinesTests::everyBandOfATallImageIsDrawn()
{
// 150 two-row lines: 32 bands of 5, two empty.
const QImage image = filled(3, 300, Qt::white);
QCOMPARE(column(scanlines(2).apply(image), 1), repeated({241, 241}, 150));
// 70 lines of 8: bands of 3, the last one.
const std::vector<int> tall = column(scanlines(8).apply(filled(3, 560, Qt::white)), 1);
QCOMPARE(tall, repeated({0, 255, 255, 255, 255, 255, 255, 0}, 70));
}
void ScanlinesTests::bandsDrawAsOneBandDoes()
{
// One band of 62 rows, 32 of 300: tops agree.
QImage noise = BrushRaster::context(7, 300, false);
for (int y = 0; y < 300; ++y)
for (int x = 0; x < 7; ++x)
noise.setPixelColor(x, y, QColor((x * 37 + y * 11) % 256, (x * 91 + y * 53) % 256, (y * 29) % 256, 128 + (x + y) % 128));
DitherSettings settings = scanlines(2);
settings.colors = DitherColors::original;
settings.dots = 50;
settings.wobble = 5;
const QImage whole = settings.apply(noise), one = settings.apply(noise.copy(0, 0, 7, 62));
QCOMPARE(whole.copy(0, 0, 7, 62), one);
}
void ScanlinesTests::theGlowIsTheBlurredLightAtItsAmount()
// Swift's Core Image blur: the twin's within a few levels.
void ScanlinesTests::theGlowIsTheBlurredLightEasedNearWhite()
{
QImage image = filled(64, 160, Qt::black);
QPainter(&image).fillRect(QRect(0, 0, 32, 40), Qt::white);
for (const double glow : {20.0, 100.0}) {
// Three levels of the shrink's blur, times 2.5.
const int worst = glowError(image, glow);
const QImage lines = render(image, [](ScanlinesSettings &s) { s.lineSpacing = 8; });
const QImage result = render(image, [glow](ScanlinesSettings &s) { s.lineSpacing = 8, s.glow = glow; });
const QImage blurred = PixelAdjust::gaussianBlur(lines, 8 * 3 + 3, true);
const float amount = float(glow / 100 * 2.5);
int worst = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
float value = lines.constScanLine(y)[x * 4];
const float added = blurred.constScanLine(y)[x * 4] * amount, room = (255 - value) * 0.7f;
if (room > 0)
value += room * (1 - std::exp(-added / room));
worst = std::max(worst, std::abs(result.constScanLine(y)[x * 4] - int(std::lround(std::min(255.0f, value)))));
}
}
QVERIFY2(worst <= 8, qPrintable(QString::number(worst)));
}
// Below the light, at amount 0.5, it fades.
const std::vector<int> fading = column(scanlines(8, 20).apply(image), 16);
QVERIFY(fading[48] > 20 && fading[48] < fading[40] && fading[120] < 3);
}
void ScanlinesTests::theGlowScalesBackExactlyOffTheStep()
void ScanlinesTests::theFilterIsListedAndPreviewsWhole()
{
// 73 plus two 84-point pads is off the 6 step.
QImage image = filled(73, 73, Qt::black);
QPainter(&image).fillRect(QRect(0, 0, 40, 40), Qt::white);
const int worst = glowError(image, 100);
QVERIFY2(worst <= 8, qPrintable(QString::number(worst)));
QCOMPARE(rawValue(FilterKind::scanlines), QString("Scanlines"));
const auto at = std::find(allFilterKinds.begin(), allFilterKinds.end(), FilterKind::scanlines);
QVERIFY(at != allFilterKinds.end() && *(at - 1) == FilterKind::dither && *(at + 1) == FilterKind::tonalContrast);
QVERIFY(!isImageAdjustment(FilterKind::scanlines) && !isAutomatic(FilterKind::scanlines) && repeatsAsLastFilter(FilterKind::scanlines));
EditorSession session;
session.createDocument(3000, 20);
const QImage wide = filled(3000, 20, Qt::white);
session.insert(ImportedImage(wide, wide, QStringLiteral("Wide")));
session.beginFilter(FilterKind::scanlines);
QCOMPARE(session.filterEdit().value().previewScale, 1.0);
// Its commit is one step that draws the lines.
QVERIFY(session.filterEdit().value().settings.scanlines == ScanlinesSettings());
bool done = false;
session.commitFilter([&done] { done = true; });
QTRY_VERIFY(done);
QCOMPARE(session.history.undoName(), QString("Scanlines"));
const QImage drawn = session.document().value().layers.front().asset.value().image();
// Beam 1.4 round the middle 2: row 0 covers 0.4.
QCOMPARE(column(drawn, 5)[0], int(std::lround(255 * 1.35f * 0.4f)));
QCOMPARE(column(drawn, 5)[2], 255);
}
void ScanlinesTests::theGlowNeverPassesAlpha()
{
QImage image = BrushRaster::context(16, 32, false);
image.fill(QColor(255, 255, 255, 128));
const QImage result = scanlines(4, 100).apply(image);
for (int y = 0; y < 32; ++y) {
for (int x = 0; x < 16; ++x) {
const uchar *at = result.constScanLine(y) + x * 4;
QCOMPARE(int(at[3]), 128);
QVERIFY(at[0] <= 128 && at[1] <= 128 && at[2] <= 128);
}
}
QCOMPARE(int(result.constScanLine(0)[0]), 128);
}
void ScanlinesTests::theSheetShowsScanlinesRows()
void ScanlinesTests::theSheetShowsSwiftsRows()
{
Shown shown;
QCOMPARE(shown.rows(), (QStringList{"Style|", "Line Spacing|How far apart the screen's lines are",
"Glow|Light blooming around the lines, like a CRT's phosphors", "Dots|Break the lines into glowing beads",
"Wobble|Make the lines waver sideways down the screen, like a CRT losing sync",
"Density|More ink (darker) or less before dithering", "Contrast|", "Colors|"}));
// A value mid-range sits mid-travel; each unit is Swift's.
const std::tuple<const char *, double DitherSettings::*, double, QString> middles[] = {
{"lineSpacing", &DitherSettings::lineSpacing, 17, "px"}, {"glow", &DitherSettings::glow, 50, "%"},
{"dots", &DitherSettings::dots, 50, "%"}, {"wobble", &DitherSettings::wobble, 32, "px"}};
for (const auto &[name, setting, middle, unit] : middles) {
FilterSettings settings = shown.session.filterEdit().value().settings;
settings.dither.*setting = middle;
shown.session.updateFilter(settings, true);
QCOMPARE(shown.rows(),
(QStringList{"Line Spacing|How far apart the screen's lines are", "Thickness|How much of the gap a bright line fills; dimmer parts draw it thinner",
"Glow|Light blooming around the lines, like a CRT's phosphors",
"Dots|Break the lines into beads where the picture is darker than this, blending into solid lines where it's brighter",
"Displace|Lift the lines where the picture is bright, or lower them when negative, so they swell into its shapes",
"Smoothness|How far the brightness is smoothed before it displaces the lines, from sharp ridges to rounded hills",
"Threshold|Draw no line where the picture is darker than this, leaving the screen dark",
"Wobble|Make the lines waver sideways down the screen, like a CRT losing sync",
"Color Split|Move the red and blue apart, for colored fringes on the lines", "Density|Darker or lighter before the lines are drawn",
"Contrast|", "Black Level|How bright the lines are where the picture is black, so they show even over black", "Colors|"}));
using Field = double ScanlinesSettings::*;
const std::tuple<const char *, Field, double, double, QString> sliders[] = {
{"lineSpacing", &ScanlinesSettings::lineSpacing, 2, 32, "px"}, {"thickness", &ScanlinesSettings::thickness, 5, 100, "%"},
{"glow", &ScanlinesSettings::glow, 0, 100, "%"}, {"dots", &ScanlinesSettings::dots, 0, 100, "%"},
{"displace", &ScanlinesSettings::displace, -100, 100, "px"}, {"smoothness", &ScanlinesSettings::smoothness, 0, 100, "%"},
{"threshold", &ScanlinesSettings::threshold, 0, 100, "%"}, {"wobble", &ScanlinesSettings::wobble, 0, 64, "px"},
{"colorSplit", &ScanlinesSettings::split, 0, 16, "px"}, {"density", &ScanlinesSettings::density, -100, 100, ""},
{"contrast", &ScanlinesSettings::contrast, -100, 100, ""}, {"blackLevel", &ScanlinesSettings::blackLevel, 0, 100, "%"}};
for (const auto &[name, field, low, high, unit] : sliders) {
QSlider *slider = shown.sheet->findChild<QSlider *>(QString::fromLatin1(name) + "Slider");
QLineEdit *field = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name) + "Field");
QVERIFY2(slider && field, name);
QCOMPARE(slider->value(), slider->maximum() / 2);
QCOMPARE(slider->minimum(), 0);
const QList<QLabel *> labels = field->parentWidget()->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly);
QCOMPARE(labels.last()->text(), unit);
}
// Each slider's travel reaches its range's ends.
const std::tuple<const char *, double DitherSettings::*, double, double> sliders[] = {
{"lineSpacingSlider", &DitherSettings::lineSpacing, 2, 32}, {"glowSlider", &DitherSettings::glow, 0, 100},
{"dotsSlider", &DitherSettings::dots, 0, 100}, {"wobbleSlider", &DitherSettings::wobble, 0, 64}};
for (const auto &[name, setting, low, high] : sliders) {
QSlider *slider = shown.sheet->findChild<QSlider *>(QString::fromLatin1(name));
QVERIFY2(slider, name);
QLineEdit *edit = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name) + "Field");
QVERIFY2(slider && edit, name);
slider->setValue(slider->maximum());
QCOMPARE(shown.dither().*setting, high);
QCOMPARE(shown.lines().*field, high);
slider->setValue(slider->minimum());
QCOMPARE(shown.dither().*setting, low);
QCOMPARE(shown.lines().*field, low);
QCOMPARE(edit->parentWidget()->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly).last()->text(), unit);
}
// Pixel Size and Pixel Shape stay away under Scanlines.
FilterSettings settings = shown.session.filterEdit().value().settings;
settings.dither.pixelSize = 4;
shown.session.updateFilter(settings, true);
QVERIFY(!shown.rows().join(',').contains("Pixel"));
settings.dither.style = DitherStyle::bayer4;
shown.session.updateFilter(settings, true);
QVERIFY(shown.rows().contains("Pixel Shape|Draw each chunky pixel as a solid square, or as a round dot like a dot-matrix screen"));
QVERIFY(!shown.rows().join(',').contains("Glow"));
}
void ScanlinesTests::theRowsWriteTheirSettings()
{
Shown shown;
const std::tuple<const char *, double DitherSettings::*, const char *, double> rows[] = {
{"lineSpacingField", &DitherSettings::lineSpacing, "12", 12}, {"glowField", &DitherSettings::glow, "60", 60},
{"dotsField", &DitherSettings::dots, "25", 25}, {"wobbleField", &DitherSettings::wobble, "9", 9}};
for (const auto &[name, setting, typed, expected] : rows) {
const DitherSettings before = shown.dither();
QLineEdit *field = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name));
QVERIFY2(field, name);
field->setFocus();
field->selectAll();
QTest::keyClicks(field, QString::fromLatin1(typed));
QTest::keyClick(field, Qt::Key_Return);
DitherSettings wanted = before;
wanted.*setting = expected;
QVERIFY2(shown.dither() == wanted, name);
const std::tuple<const char *, double ScanlinesSettings::*, const char *, double> rows[] = {
{"lineSpacingField", &ScanlinesSettings::lineSpacing, "12", 12}, {"displaceField", &ScanlinesSettings::displace, "-30", -30},
{"colorSplitField", &ScanlinesSettings::split, "5", 5}, {"blackLevelField", &ScanlinesSettings::blackLevel, "40", 40},
{"smoothnessField", &ScanlinesSettings::smoothness, "10", 10}, {"thicknessField", &ScanlinesSettings::thickness, "55", 55}};
for (const auto &[name, field, typed, expected] : rows) {
ScanlinesSettings wanted = shown.lines();
QLineEdit *edit = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name));
QVERIFY2(edit, name);
edit->setFocus();
edit->selectAll();
QTest::keyClicks(edit, QString::fromLatin1(typed));
QTest::keyClick(edit, Qt::Key_Return);
wanted.*field = expected;
QVERIFY2(shown.lines() == wanted, name);
}
}
void ScanlinesTests::twoColorsPickTheScreenAndTheLines()
{
Shown shown;
auto *colors = shown.sheet->findChild<QComboBox *>(QStringLiteral("scanlinesColors"));
auto *dark = shown.sheet->findChild<SwatchButton *>(QStringLiteral("scanlinesDark"));
auto *light = shown.sheet->findChild<SwatchButton *>(QStringLiteral("scanlinesLight"));
QVERIFY(colors && dark && light && !dark->isVisible());
QVERIFY(dark->toolTip() == QString("Choose the screen's color") && light->toolTip() == QString("Choose the lines' color"));
colors->activated(int(DitherColors::twoColors));
QVERIFY(shown.lines().colors == DitherColors::twoColors && dark->isVisible() && light->isVisible());
// The menu follows the settings, however they change.
FilterSettings original = shown.session.filterEdit().value().settings;
original.scanlines.colors = DitherColors::original;
shown.session.updateFilter(original, true);
QVERIFY(colors->currentIndex() == int(DitherColors::original) && !dark->isVisible());
original.scanlines.colors = DitherColors::twoColors;
shown.session.updateFilter(original, true);
QCOMPARE(colors->currentIndex(), int(DitherColors::twoColors));
// The picker starts from the lines' own colour.
FilterSettings settings = shown.session.filterEdit().value().settings;
settings.scanlines.light = AdjustmentColor(0, 1, 0), settings.scanlines.dark = AdjustmentColor(0.2, 0, 0);
shown.session.updateFilter(settings, true);
light->click();
QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Light Color)"));
QVERIFY(shown.session.colorPicker().value().original == (PaletteColor{0, 1, 0}));
struct Paints : QObject {
int count = 0;
bool eventFilter(QObject *, QEvent *event) override { return count += event->type() == QEvent::Paint, false; }
} paints;
QCoreApplication::processEvents();
light->installEventFilter(&paints);
shown.session.setColorPickerHSB(PickerHSB(PaletteColor{1, 0, 0}));
shown.session.previewDitherColor();
// A new colour repaints the swatch.
QTRY_VERIFY(paints.count > 0);
QVERIFY(shown.lines().light == AdjustmentColor(1, 0, 0));
QVERIFY(shown.session.filterEdit().value().settings.dither.light == AdjustmentColor(1, 1, 1));
QCOMPARE(light->grab().toImage().pixelColor(12, 12), QColor(255, 0, 0));
shown.session.closeColorPicker(false);
QVERIFY(shown.lines().light == AdjustmentColor(0, 1, 0));
dark->click();
QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Dark Color)"));
QVERIFY(shown.session.colorPicker().value().original == (PaletteColor{0.2, 0, 0}));
shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0, 0, 1}));
shown.session.closeColorPicker(true);
QVERIFY(shown.lines().dark == AdjustmentColor(0, 0, 1));
}
QTEST_MAIN(ScanlinesTests)
#include "ScanlinesTests.moc"