14.19: Filter › Scanlines, its own filter, on Swift's cdde327 kernel
This commit is contained in:
26 files changed
+905
-479
No files matched your search
@@ -102,7 +102,7 @@ QString ColorPickerTarget::title() const
|
||||
if (kind == Kind::vignette)
|
||||
return QStringLiteral("Color Picker (Vignette Color)");
|
||||
if (kind == Kind::dither)
|
||||
return light ? QStringLiteral("Color Picker (Dither Light Color)") : QStringLiteral("Color Picker (Dither Dark Color)");
|
||||
return light ? QStringLiteral("Color Picker (Light Color)") : QStringLiteral("Color Picker (Dark Color)");
|
||||
if (kind == Kind::dialog)
|
||||
return QStringLiteral("Color Picker (%1)").arg(dialogTitle);
|
||||
return background ? QStringLiteral("Color Picker (Background Color)") : QStringLiteral("Color Picker (Foreground Color)");
|
||||
@@ -288,9 +288,11 @@ void EditorSession::openVignetteColorPicker()
|
||||
void EditorSession::openDitherColorPicker(bool light)
|
||||
{
|
||||
// A commit keeps the project busy: no `committing` term.
|
||||
if (!canEditPalette() || m_colorPicker || !m_filterEdit || m_filterEdit->kind != FilterKind::dither)
|
||||
if (!canEditPalette() || m_colorPicker || !m_filterEdit || (m_filterEdit->kind != FilterKind::dither && m_filterEdit->kind != FilterKind::scanlines))
|
||||
return;
|
||||
const AdjustmentColor value = light ? m_filterEdit->settings.dither.light : m_filterEdit->settings.dither.dark;
|
||||
const FilterSettings &settings = m_filterEdit->settings;
|
||||
const bool scanlines = m_filterEdit->kind == FilterKind::scanlines;
|
||||
const AdjustmentColor value = light ? (scanlines ? settings.scanlines.light : settings.dither.light) : (scanlines ? settings.scanlines.dark : settings.dither.dark);
|
||||
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::dither, false, std::nullopt, false, LayerEffectKind{}, light},
|
||||
PaletteColor{value.red, value.green, value.blue});
|
||||
notify();
|
||||
@@ -302,11 +304,14 @@ void EditorSession::previewDitherColor()
|
||||
setDitherColor(m_colorPicker->color(), m_colorPicker->target.light);
|
||||
}
|
||||
|
||||
// Its picker lives only while its Dither edit is open.
|
||||
// Its picker lives only while Dither or Scanlines is open.
|
||||
void EditorSession::setDitherColor(const PaletteColor &color, bool light)
|
||||
{
|
||||
FilterSettings settings = m_filterEdit.value().settings;
|
||||
(light ? settings.dither.light : settings.dither.dark) = AdjustmentColor(color);
|
||||
if (m_filterEdit->kind == FilterKind::scanlines)
|
||||
(light ? settings.scanlines.light : settings.scanlines.dark) = AdjustmentColor(color);
|
||||
else
|
||||
(light ? settings.dither.light : settings.dither.dark) = AdjustmentColor(color);
|
||||
if (settings == m_filterEdit->settings)
|
||||
return;
|
||||
updateFilter(settings, m_filterEdit->preview);
|
||||
|
||||
@@ -47,7 +47,7 @@ struct ColorPickerTarget {
|
||||
bool highlights = false;
|
||||
// An effect's colour: which effect.
|
||||
LayerEffectKind effect{};
|
||||
// Dither's Two Colors: the light one, else the dark.
|
||||
// Dither's or Scanlines' Two Colors: light, else dark.
|
||||
bool light = false;
|
||||
// A dialog's own colour, such as Export JPEG's background.
|
||||
QString dialogTitle{};
|
||||
|
||||
+5
-48
@@ -64,7 +64,6 @@ 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");
|
||||
}
|
||||
@@ -92,12 +91,12 @@ bool isHalftone(DitherStyle style)
|
||||
|
||||
bool drawsMarks(DitherStyle style)
|
||||
{
|
||||
return !hasTones(style) && style != DitherStyle::scanlines;
|
||||
return !hasTones(style);
|
||||
}
|
||||
|
||||
bool usesPixelSize(DitherStyle style)
|
||||
{
|
||||
return style != DitherStyle::ascii && style != DitherStyle::scanlines;
|
||||
return style != DitherStyle::ascii;
|
||||
}
|
||||
|
||||
QString rawValue(DitherPixelShape shape)
|
||||
@@ -122,10 +121,6 @@ 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);
|
||||
@@ -146,7 +141,7 @@ DitherSettings DitherSettings::normalized() const
|
||||
QImage DitherSettings::apply(const QImage &image) const
|
||||
{
|
||||
const DitherSettings settings = normalized();
|
||||
// ASCII and Scanlines draw full size: chunks would blur them.
|
||||
// ASCII draws full size: chunks would break its characters.
|
||||
const int block = usesPixelSize(settings.style) ? int(settings.pixelSize) : 1;
|
||||
const QImage source = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
if (source.isNull())
|
||||
@@ -174,8 +169,6 @@ 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.
|
||||
@@ -194,44 +187,9 @@ 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;
|
||||
// Far sides fill whole steps: both scales are exact.
|
||||
const auto whole = [step](int side) { return (side + step - 1) / step * step; };
|
||||
QImage padded = BrushRaster::context(whole(width + 2 * pad), whole(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, smallHeight = padded.height() / 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(style == DitherStyle::scanlines ? lineSpacing : cellSize);
|
||||
const int cell = int(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};
|
||||
@@ -242,8 +200,7 @@ 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()), .dots = float(dots / 100),
|
||||
.wobble = float(wobble)};
|
||||
.glyphCoverage = glyphs.coverage.data(), .glyphCount = int(glyphs.coverage.size())};
|
||||
failed = dither_apply(pixels, size_t(width), size_t(height), size_t(stride), ¶ms) == 0;
|
||||
});
|
||||
if (failed)
|
||||
|
||||
+3
-12
@@ -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, scanlines };
|
||||
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, DitherStyle::scanlines};
|
||||
DitherStyle::patterns, DitherStyle::ascii};
|
||||
QString rawValue(DitherStyle style);
|
||||
// Swift's groups: diffusion, ordered, halftone, marks.
|
||||
int ditherGroup(DitherStyle style);
|
||||
@@ -19,7 +19,7 @@ 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.
|
||||
// ASCII draws at full resolution, not chunky.
|
||||
bool usesPixelSize(DitherStyle style);
|
||||
|
||||
// A chunky pixel: a square, or a dot on dark.
|
||||
@@ -37,8 +37,6 @@ 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.
|
||||
@@ -48,12 +46,6 @@ 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.
|
||||
@@ -86,5 +78,4 @@ struct DitherSettings {
|
||||
|
||||
private:
|
||||
QImage dither(const QImage &image) const;
|
||||
QImage glowing(const QImage &image) const;
|
||||
};
|
||||
@@ -28,6 +28,7 @@ QString rawValue(FilterKind kind)
|
||||
case FilterKind::vignette: return QStringLiteral("Vignette");
|
||||
case FilterKind::bloomGlow: return QStringLiteral("Bloom / Glow");
|
||||
case FilterKind::dither: return QStringLiteral("Dither");
|
||||
case FilterKind::scanlines: return QStringLiteral("Scanlines");
|
||||
case FilterKind::tonalContrast: return QStringLiteral("Tonal Contrast");
|
||||
case FilterKind::lensCorrection: return QStringLiteral("Lens Correction");
|
||||
case FilterKind::cameraRaw: return QStringLiteral("Camera Raw Filter");
|
||||
@@ -93,6 +94,7 @@ FilterSettings FilterSettings::normalized() const
|
||||
result.gradientMap = gradientMap.normalized();
|
||||
result.grain = grain.normalized();
|
||||
result.dither = dither.normalized();
|
||||
result.scanlines = scanlines.normalized();
|
||||
result.cameraRaw = cameraRaw.normalized();
|
||||
return result;
|
||||
}
|
||||
@@ -164,6 +166,7 @@ QImage PixelFilter::run(const FilterJob &job)
|
||||
// Grain sits in layer pixels; the job's seed patterns it.
|
||||
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
|
||||
case FilterKind::dither: image = settings.dither.apply(job.image); break;
|
||||
case FilterKind::scanlines: image = settings.scanlines.apply(job.image); break;
|
||||
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
|
||||
case FilterKind::contentAwareFill: image = ContentFill::run(job); break;
|
||||
// Unclamped: a blur spreads into the room made for it.
|
||||
@@ -278,8 +281,9 @@ void FilterEdit::prepare(const ImportedImage &source, const LayerTransform &plac
|
||||
{
|
||||
const QSize size = source.size();
|
||||
mapping = BrushRaster::pixelToDocument(placed, size.width(), size.height());
|
||||
// Noise, grain and dither preview whole: enlarged, they look coarse.
|
||||
const bool whole = kind == FilterKind::addNoise || kind == FilterKind::grain || kind == FilterKind::dither || kind == FilterKind::contentAwareFill
|
||||
// Noise, grain, dither, scanlines preview whole: enlarged, they coarsen.
|
||||
const bool whole = kind == FilterKind::addNoise || kind == FilterKind::grain || kind == FilterKind::dither || kind == FilterKind::scanlines
|
||||
|| kind == FilterKind::contentAwareFill
|
||||
|| kind == FilterKind::removeBackground;
|
||||
const double factor = whole ? 1 : std::min(1.0, previewLimit / std::max(size.width(), size.height()));
|
||||
if (!(factor < 1)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include "Document/CameraRaw.h"
|
||||
#include "Document/Curves.h"
|
||||
#include "Document/Dither.h"
|
||||
#include "Document/Scanlines.h"
|
||||
#include "Document/ImageAdjustments.h"
|
||||
#include "Document/LayerTransform.h"
|
||||
#include "Document/Selection.h"
|
||||
@@ -17,7 +17,7 @@ struct ImageLayer;
|
||||
|
||||
// The Filter and Image menus' filters.
|
||||
enum class FilterKind {
|
||||
gaussianBlur, motionBlur, addNoise, vignette, bloomGlow, dither, tonalContrast, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves,
|
||||
gaussianBlur, motionBlur, addNoise, vignette, bloomGlow, dither, scanlines, tonalContrast, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves,
|
||||
exposure, gradientMap, grain, blackWhite, colorBalance
|
||||
};
|
||||
// The menu's title, Swift's rawValue.
|
||||
@@ -31,7 +31,7 @@ bool repeatsAsLastFilter(FilterKind kind);
|
||||
// Swift's allCases, the menus' order.
|
||||
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
|
||||
FilterKind::vignette, FilterKind::bloomGlow, FilterKind::dither,
|
||||
FilterKind::tonalContrast,
|
||||
FilterKind::scanlines, FilterKind::tonalContrast,
|
||||
FilterKind::lensCorrection, FilterKind::cameraRaw, FilterKind::removeBackground,
|
||||
FilterKind::contentAwareFill,
|
||||
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
|
||||
@@ -79,6 +79,7 @@ struct FilterSettings {
|
||||
BlackWhiteSettings blackWhite{};
|
||||
ColorBalanceSettings colorBalance{};
|
||||
DitherSettings dither{};
|
||||
ScanlinesSettings scanlines{};
|
||||
CameraRawSettings cameraRaw{};
|
||||
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
|
||||
// How far, in pixels, the mask is pulled onto edges.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "Document/Scanlines.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
extern "C" {
|
||||
#include "DitherPixels.h"
|
||||
}
|
||||
|
||||
// Swift's Scanlines.swift: its settings, the kernel, the glow.
|
||||
namespace {
|
||||
std::array<uint8_t, 3> bytes(const AdjustmentColor &color)
|
||||
{
|
||||
return {uint8_t(std::lround(color.red * 255)), uint8_t(std::lround(color.green * 255)), uint8_t(std::lround(color.blue * 255))};
|
||||
}
|
||||
}
|
||||
|
||||
ScanlinesSettings ScanlinesSettings::normalized() const
|
||||
{
|
||||
using ImageAdjustmentPixels::clamp;
|
||||
ScanlinesSettings result = *this;
|
||||
result.lineSpacing = std::round(clamp(lineSpacing, lineSpacingLow, lineSpacingHigh, 4));
|
||||
result.thickness = clamp(thickness, 5, 100, 70);
|
||||
result.glow = clamp(glow, 0, 100, 0);
|
||||
result.dots = clamp(dots, 0, 100, 0);
|
||||
result.wobble = clamp(wobble, wobbleLow, wobbleHigh, 0);
|
||||
result.displace = clamp(displace, displaceLow, displaceHigh, 0);
|
||||
result.smoothness = clamp(smoothness, 0, 100, 50);
|
||||
result.threshold = clamp(threshold, 0, 100, 0);
|
||||
result.split = std::round(clamp(split, splitLow, splitHigh, 0));
|
||||
result.density = clamp(density, -100, 100, 0);
|
||||
result.contrast = clamp(contrast, -100, 100, 0);
|
||||
result.blackLevel = clamp(blackLevel, 0, 100, 0);
|
||||
result.dark = dark.clamped();
|
||||
result.light = light.clamped();
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage ScanlinesSettings::apply(const QImage &image) const
|
||||
{
|
||||
const ScanlinesSettings settings = normalized();
|
||||
const bool two = settings.colors == DitherColors::twoColors;
|
||||
const std::array<uint8_t, 3> darkColor = two ? bytes(settings.dark) : std::array<uint8_t, 3>{0, 0, 0};
|
||||
const std::array<uint8_t, 3> lightColor = two ? bytes(settings.light) : std::array<uint8_t, 3>{255, 255, 255};
|
||||
bool failed = false;
|
||||
const QImage drawn = ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
|
||||
ScanlinesParams params{.spacing = int(settings.lineSpacing), .thickness = float(settings.thickness / 100), .dots = float(settings.dots / 100),
|
||||
.wobble = float(settings.wobble), .displace = float(settings.displace), .threshold = float(settings.threshold / 100),
|
||||
.split = float(settings.split), .density = float(settings.density / 100), .contrast = float(settings.contrast / 100),
|
||||
.blackLevel = float(settings.blackLevel / 100), .smoothness = float(settings.smoothness / 100),
|
||||
.originalColors = settings.colors == DitherColors::original ? 1 : 0, .dark = {darkColor[0], darkColor[1], darkColor[2]},
|
||||
.light = {lightColor[0], lightColor[1], lightColor[2]}};
|
||||
failed = scanlines_apply(pixels, size_t(width), size_t(height), size_t(stride), ¶ms) == 0;
|
||||
});
|
||||
if (failed)
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
return settings.glow > 0 ? settings.glowing(drawn) : drawn;
|
||||
}
|
||||
|
||||
// The lines' light blurred wide and added back: phosphor bloom.
|
||||
QImage ScanlinesSettings::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;
|
||||
// Far sides fill whole steps: both scales are exact.
|
||||
const auto whole = [step](int side) { return (side + step - 1) / step * step; };
|
||||
QImage padded = BrushRaster::context(whole(width + 2 * pad), whole(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, smallHeight = padded.height() / 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;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include "Document/Dither.h"
|
||||
|
||||
// Filter › Scanlines: the picture as a CRT draws it.
|
||||
struct ScanlinesSettings {
|
||||
static constexpr double lineSpacingLow = 2, lineSpacingHigh = 32;
|
||||
static constexpr double wobbleLow = 0, wobbleHigh = 64;
|
||||
static constexpr double displaceLow = -100, displaceHigh = 100;
|
||||
static constexpr double splitLow = 0, splitHigh = 16;
|
||||
// How far apart the lines are, in layer pixels.
|
||||
double lineSpacing = 4;
|
||||
// How much of the gap a bright line fills, 5–100%.
|
||||
double thickness = 70;
|
||||
// 0–100%: light blooming round the lines.
|
||||
double glow = 0;
|
||||
// 0–100%: darker tones bead the lines into dots.
|
||||
double dots = 0;
|
||||
// Pixels the lines waver sideways, down the screen.
|
||||
double wobble = 0;
|
||||
// Pixels a line rises over brightness; negative, falls.
|
||||
double displace = 0;
|
||||
// 0–100%: how far brightness is smoothed before displacing.
|
||||
double smoothness = 50;
|
||||
// 0–100%: darker tones draw no line.
|
||||
double threshold = 0;
|
||||
// Pixels the red and blue move apart.
|
||||
double split = 0;
|
||||
// −100…100: darker or lighter, flatter or punchier.
|
||||
double density = 0;
|
||||
double contrast = 0;
|
||||
// 0–100%: the lines' brightness over black.
|
||||
double blackLevel = 0;
|
||||
DitherColors colors = DitherColors::blackWhite;
|
||||
AdjustmentColor dark = AdjustmentColor(0, 0, 0);
|
||||
AdjustmentColor light = AdjustmentColor(1, 1, 1);
|
||||
ScanlinesSettings normalized() const;
|
||||
QImage apply(const QImage &image) const;
|
||||
friend bool operator==(const ScanlinesSettings &, const ScanlinesSettings &) = default;
|
||||
|
||||
private:
|
||||
QImage glowing(const QImage &image) const;
|
||||
};
|
||||
Reference in new issue
Block a user