14.1: Color Overlay, Inner Glow and Inner Shadow recolour translucent pixels fully; Add layer effect
This commit is contained in:
7 files changed
+142
-19
No files matched your search
@@ -0,0 +1,100 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/LayerEffects+Renderer.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's ColorOverlayTests, and the inner effects alike.
|
||||
namespace {
|
||||
QImage square(const QColor &colour, int size = 20)
|
||||
{
|
||||
QImage image = BrushRaster::context(size, size, false);
|
||||
image.fill(colour);
|
||||
return image;
|
||||
}
|
||||
|
||||
// The premultiplied RGBA bytes at the image's middle.
|
||||
std::array<int, 4> middle(const QImage &image)
|
||||
{
|
||||
const uchar *pixel = image.constScanLine(image.height() / 2) + (image.width() / 2) * 4;
|
||||
return {pixel[0], pixel[1], pixel[2], pixel[3]};
|
||||
}
|
||||
|
||||
std::array<int, 4> rendered(const QImage &source, const LayerEffects &effects)
|
||||
{
|
||||
const QImage image = LayerEffectsRenderer::render(source, std::nullopt, effects).image;
|
||||
if (image.format() != QImage::Format_RGBA8888_Premultiplied)
|
||||
qFatal("the renderer's format changed");
|
||||
return middle(image);
|
||||
}
|
||||
|
||||
bool near(const std::array<int, 4> &pixel, const std::array<int, 4> &expected, int tolerance)
|
||||
{
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
if (std::abs(pixel[channel] - expected[channel]) > tolerance)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString text(const std::array<int, 4> &pixel)
|
||||
{
|
||||
return QStringLiteral("%1 %2 %3 %4").arg(pixel[0]).arg(pixel[1]).arg(pixel[2]).arg(pixel[3]);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorOverlayTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void overlayRecolorsTranslucentPixelsAndKeepsTheirAlpha();
|
||||
void overlayOpacityMixesWithTheLayersOwnColor();
|
||||
void overlayLeavesTheShadowBeneathTranslucentPixels();
|
||||
void innerEffectsRecolourTranslucentPixelsAndKeepTheirAlpha();
|
||||
};
|
||||
|
||||
void ColorOverlayTests::overlayRecolorsTranslucentPixelsAndKeepsTheirAlpha()
|
||||
{
|
||||
// Half-clear black under white: half-clear white, no grey band.
|
||||
LayerEffects effects;
|
||||
effects.colorOverlay = ColorOverlayEffect{.red = 1, .green = 1, .blue = 1, .opacity = 1};
|
||||
const std::array<int, 4> pixel = rendered(square(QColor(0, 0, 0, 128)), effects);
|
||||
QVERIFY2(near(pixel, {128, 128, 128, 128}, 1), qPrintable(text(pixel)));
|
||||
}
|
||||
|
||||
void ColorOverlayTests::overlayOpacityMixesWithTheLayersOwnColor()
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 0, .blue = 1, .opacity = 0.5};
|
||||
const std::array<int, 4> pixel = rendered(square(QColor(255, 0, 0)), effects);
|
||||
QVERIFY2(near(pixel, {128, 0, 128, 255}, 1), qPrintable(text(pixel)));
|
||||
}
|
||||
|
||||
void ColorOverlayTests::overlayLeavesTheShadowBeneathTranslucentPixels()
|
||||
{
|
||||
// A red shadow under half-clear black stays red beneath.
|
||||
LayerEffects effects;
|
||||
effects.colorOverlay = ColorOverlayEffect{.red = 1, .green = 1, .blue = 1, .opacity = 1};
|
||||
effects.shadow = ShadowEffect{.angle = 90, .distance = 0, .blur = 0, .red = 1, .green = 0, .blue = 0, .opacity = 1};
|
||||
const std::array<int, 4> pixel = rendered(square(QColor(0, 0, 0, 128)), effects);
|
||||
QVERIFY2(near(pixel, {192, 128, 128, 192}, 1), qPrintable(text(pixel)));
|
||||
}
|
||||
|
||||
void ColorOverlayTests::innerEffectsRecolourTranslucentPixelsAndKeepTheirAlpha()
|
||||
{
|
||||
const QImage half = square(QColor(0, 0, 0, 128));
|
||||
// A glow a hair wide falls by half.
|
||||
LayerEffects shadowed;
|
||||
shadowed.innerShadow = InnerShadowEffect{.angle = 0, .distance = 60, .blur = 0.01, .red = 1, .green = 0, .blue = 0, .opacity = 1};
|
||||
std::array<int, 4> pixel = rendered(half, shadowed);
|
||||
QVERIFY2(near(pixel, {128, 0, 0, 128}, 1), qPrintable(text(pixel)));
|
||||
// A hair-wide glow falls by what the shape lacks: half.
|
||||
LayerEffects glowing;
|
||||
glowing.innerGlow = InnerGlowEffect{.size = 0.01, .red = 1, .green = 1, .blue = 1, .opacity = 1};
|
||||
pixel = rendered(half, glowing);
|
||||
QVERIFY2(near(pixel, {64, 64, 64, 128}, 1), qPrintable(text(pixel)));
|
||||
// A shadow beneath stays untinted.
|
||||
shadowed.shadow = ShadowEffect{.angle = 90, .distance = 0, .blur = 0, .red = 0, .green = 0, .blue = 1, .opacity = 1};
|
||||
pixel = rendered(half, shadowed);
|
||||
QVERIFY2(near(pixel, {128, 0, 64, 192}, 1), qPrintable(text(pixel)));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ColorOverlayTests)
|
||||
#include "ColorOverlayTests.moc"
|
||||
@@ -107,7 +107,7 @@ void EffectsPanelTests::theFooterAddsEachEffectAndOpensItsPanel()
|
||||
{
|
||||
Editor editor;
|
||||
auto &button = find<QToolButton>(editor.view, "layerEffects");
|
||||
QCOMPARE(button.toolTip(), QString("Layer effects: stroke and drop shadow"));
|
||||
QCOMPARE(button.toolTip(), QString("Add layer effect"));
|
||||
QCOMPARE(button.accessibleName(), QString("Layer effects"));
|
||||
QCOMPARE(button.popupMode(), QToolButton::InstantPopup);
|
||||
// Swift's sparkles, in the footer's ink: three four-pointed stars.
|
||||
|
||||
@@ -106,14 +106,14 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
if (glowingInside) {
|
||||
const std::vector<float> softened = shader.softened(shader.shape, float(effects.innerGlow.value().size / 2));
|
||||
for (size_t index = 0; index < innerGlow.size(); ++index)
|
||||
innerGlow[index] = std::clamp(shader.shape[index] * (1.f - softened[index]), 0.f, 1.f);
|
||||
innerGlow[index] = std::clamp(1.f - softened[index], 0.f, 1.f);
|
||||
}
|
||||
if (on(effects.innerShadow)) {
|
||||
const std::vector<float> moved = shader.softened(shader.shift(shader.shape, float(effects.innerShadow.value().offset().width()),
|
||||
float(effects.innerShadow.value().offset().height())),
|
||||
float(effects.innerShadow.value().blur / 2));
|
||||
for (size_t index = 0; index < inner.size(); ++index)
|
||||
inner[index] = std::clamp(shader.shape[index] * (1.f - moved[index]), 0.f, 1.f);
|
||||
inner[index] = std::clamp(1.f - moved[index], 0.f, 1.f);
|
||||
}
|
||||
QImage result(pixels.size(), QImage::Format_RGBA8888_Premultiplied);
|
||||
for (int y = 0; y < pixels.height(); ++y) {
|
||||
@@ -135,15 +135,24 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
blend(effects.stroke.value().color(), strokeCoverage);
|
||||
const uchar *source = pixels.constScanLine(y) + x * 4;
|
||||
const float sourceAlpha = float(source[3]) / 255.f;
|
||||
float pixel[3];
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
colour[channel] = float(source[channel]) / 255.f + colour[channel] * (1.f - sourceAlpha);
|
||||
alpha = sourceAlpha + alpha * (1.f - sourceAlpha);
|
||||
pixel[channel] = float(source[channel]) / 255.f;
|
||||
// MSL's mix: x + (y - x) * a.
|
||||
const auto recolour = [&](const PaletteColor &paint, float amount) {
|
||||
const float to[3] = {float(paint.red), float(paint.green), float(paint.blue)};
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
pixel[channel] = pixel[channel] + (to[channel] * sourceAlpha - pixel[channel]) * amount;
|
||||
};
|
||||
if (on(effects.colorOverlay))
|
||||
blend(effects.colorOverlay.value().color(), std::clamp(shader.shape[index] * float(effects.colorOverlay.value().opacity), 0.f, 1.f));
|
||||
recolour(effects.colorOverlay.value().color(), float(effects.colorOverlay.value().opacity));
|
||||
if (glowingInside)
|
||||
blend(effects.innerGlow.value().color(), std::clamp(innerGlow[index] * float(effects.innerGlow.value().opacity), 0.f, 1.f));
|
||||
recolour(effects.innerGlow.value().color(), std::clamp(innerGlow[index] * float(effects.innerGlow.value().opacity), 0.f, 1.f));
|
||||
if (on(effects.innerShadow))
|
||||
blend(effects.innerShadow.value().color(), std::clamp(inner[index] * float(effects.innerShadow.value().opacity), 0.f, 1.f));
|
||||
recolour(effects.innerShadow.value().color(), std::clamp(inner[index] * float(effects.innerShadow.value().opacity), 0.f, 1.f));
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
colour[channel] = pixel[channel] + colour[channel] * (1.f - sourceAlpha);
|
||||
alpha = sourceAlpha + alpha * (1.f - sourceAlpha);
|
||||
if (stroke && effects.stroke.value().inside)
|
||||
blend(effects.stroke.value().color(), strokeCoverage);
|
||||
uchar *out = result.scanLine(y) + x * 4;
|
||||
|
||||
@@ -256,6 +256,7 @@ compositor_test(AdjustmentPanelTests)
|
||||
compositor_test(LayerEffectsTests)
|
||||
compositor_test(OuterGlowTests)
|
||||
compositor_test(InnerGlowTests)
|
||||
compositor_test(ColorOverlayTests)
|
||||
compositor_test(LayerEffectsSessionTests)
|
||||
compositor_test(LayerEffectsKernelTests)
|
||||
compositor_test(EffectsPreviewCacheTests)
|
||||
|
||||
Reference in new issue
Block a user