14.19 round 1: Scanlines normalized with the filter, Two Colors rounded, sheet tests apart

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 10:53:13 -04:00
1 parent 1a129b9a23
commit 65bb367e34
4 files changed
+214 -166

No files matched your search

+16 -165
View File
@@ -2,14 +2,7 @@
#include "Document/EditorSession.h"
#include "Document/PixelAdjust.h"
#include "Document/Scanlines.h"
#include "UI/ColorPaletteControls.h"
#include "UI/FilterSheet.h"
#include <QComboBox>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QPainter>
#include <QSlider>
#include <QtTest>
#include <numeric>
extern "C" {
@@ -102,35 +95,6 @@ QImage kernel(int width, int height, const ScanlinesParams &params)
return image;
}
struct Shown {
EditorSession session;
std::unique_ptr<FilterSheet> sheet;
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::scanlines);
sheet = std::make_unique<FilterSheet>(session);
sheet->show();
}
const ScanlinesSettings &lines() const { return session.filterEdit().value().settings.scanlines; }
QStringList rows() const
{
QStringList titles;
QLayout *column = sheet->layout();
for (int index = 0; index < column->count(); ++index) {
QWidget *each = column->itemAt(index)->widget();
if (!each || each->isHidden())
continue;
for (QLabel *label : each->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly)) {
if (label->buddy())
titles << label->text() + u'|' + each->toolTip();
}
}
return titles;
}
};
}
class ScanlinesTests : public QObject {
@@ -149,10 +113,6 @@ private slots:
void normalizingClampsEverySetting();
void clearPixelsStayClear();
void theGlowIsTheBlurredLightEasedNearWhite();
void theFilterIsListedAndPreviewsWhole();
void theSheetShowsSwiftsRows();
void theRowsWriteTheirSettings();
void twoColorsPickTheScreenAndTheLines();
};
void ScanlinesTests::scanlinesAreLinesOfLightThatBloomWithBrightness()
@@ -279,6 +239,17 @@ void ScanlinesTests::theSettingsReachTheKernel()
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); })));
// Half a channel rounds to 128, lit at 1.35 times.
ScanlinesSettings halves;
halves.colors = DitherColors::twoColors, halves.dark = AdjustmentColor(0.5, 0.5, 0.5), halves.light = AdjustmentColor(0.5, 0.5, 0.5);
const QImage greyLines = halves.apply(filled(8, 8, Qt::white));
const int lit = int(std::lround(128 / 255.0f * 1.35f * 255));
const auto pixel = [&greyLines](int y) {
const uchar *at = greyLines.constScanLine(y);
return std::array<int, 4>{at[0], at[1], at[2], at[3]};
};
QCOMPARE(pixel(1), (std::array<int, 4>{lit, lit, lit, 255}));
QCOMPARE(pixel(0)[0], int(std::lround(128 + (lit - 128) * 0.4f)));
// 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; })));
@@ -317,6 +288,11 @@ void ScanlinesTests::normalizingClampsEverySetting()
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; }))));
// The filter's settings normalize Scanlines with the rest.
FilterSettings filter;
filter.scanlines = wild;
QVERIFY(filter.normalized().scanlines == wild.normalized());
QVERIFY(filter.normalized().scanlines.lineSpacing == 6 && filter.normalized().scanlines.thickness == 100 && filter.normalized().scanlines.split == 2);
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));
@@ -358,130 +334,5 @@ void ScanlinesTests::theGlowIsTheBlurredLightEasedNearWhite()
}
}
void ScanlinesTests::theFilterIsListedAndPreviewsWhole()
{
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::theSheetShowsSwiftsRows()
{
Shown shown;
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 *edit = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name) + "Field");
QVERIFY2(slider && edit, name);
slider->setValue(slider->maximum());
QCOMPARE(shown.lines().*field, high);
slider->setValue(slider->minimum());
QCOMPARE(shown.lines().*field, low);
QCOMPARE(edit->parentWidget()->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly).last()->text(), unit);
}
}
void ScanlinesTests::theRowsWriteTheirSettings()
{
Shown shown;
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"