From 35fe3ac714ce50926174b6640ab04bd20678d784 Mon Sep 17 00:00:00 2001 From: attila Date: Mon, 18 Aug 2025 13:59:57 +0200 Subject: [PATCH] Direct2D: Fix gradient fill when the brush is transformed with not just translation The code contains a performance optimisation for cases where the world transform is translation only. In this case instead of applying the brush transformation first and then the world translation, the order is reversed. The translation is applied first and then the brush transformation. Flipping the transformations however is only correct in the special case when both transformations are translation only. --- ...ce_Direct2DGraphicsContextImpl_windows.cpp | 2 +- .../juce_Direct2DGraphicsContext_windows.cpp | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp index d1a6e20651..f209171fca 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp @@ -469,7 +469,7 @@ public: { if ((flags & BrushTransformFlags::applyWorldTransform) != 0) { - if (currentTransform.isOnlyTranslated) + if (currentTransform.isOnlyTranslated && fillType.transform.isOnlyTranslation()) translation = currentTransform.offset.toFloat(); else transform = currentTransform.getTransform(); diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp index 9cd61bee03..efd547698b 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp @@ -1380,6 +1380,12 @@ public: compareImages (targetNative, targetSoftware, 1, pixelsToIgnore); } + + beginTest ("Gradient fill transform should compose with world transform correctly"); + { + testGradientFillTransform (1.0f); + testGradientFillTransform (1.5f); + } } static Image createEdgeMask (int sourceWidth, @@ -1433,6 +1439,79 @@ public: const auto averageError = (double) accumulatedError / (double) numSamples; expect (std::abs (averageError) < 1.0 && maxAbsError < 10); } + + void testGradientFillTransform (float scale) + { + constexpr int size = 500; + constexpr int circleSize = 100; + constexpr int brushTranslation = 20; + + Image image { Image::RGB, + roundToInt (size * scale), + roundToInt (size * scale), + true }; + + { + for (int i = 0; i < size / circleSize; ++i) + { + Graphics g { image }; + + g.addTransform (AffineTransform::scale (scale)); + g.addTransform (AffineTransform::translation ((float) i * circleSize, (float) i * circleSize)); + + const auto fillCol1 { Colours::red }; + const auto fillCol2 { Colours::green }; + const auto centreLoc = circleSize / 2.0f; + + FillType innerGlowGrad = ColourGradient { fillCol1, + { centreLoc, centreLoc }, + fillCol2, + { centreLoc, 0.0f }, + true }; + + innerGlowGrad.gradient->addColour (0.19, fillCol1); + + innerGlowGrad.transform = AffineTransform::scale (1.1f, 0.9f, centreLoc, centreLoc) + .followedBy (AffineTransform::translation (brushTranslation, + brushTranslation)); + + g.setFillType (innerGlowGrad); + g.fillEllipse (0, 0, (float) circleSize, (float) circleSize); + } + } + + for (int i = 0; i < size / circleSize; ++i) + { + const auto getScaled = [scale] (Point p) + { + return p.toFloat().transformedBy (AffineTransform::scale (scale)).roundToInt(); + }; + + const auto approximatelyEqual = [] (const Colour& a, const Colour& b) + { + return std::abs (a.getRed() - b.getRed()) < 2 + && std::abs (a.getGreen() - b.getGreen()) < 2 + && std::abs (a.getBlue() - b.getBlue()) < 2 + && std::abs (a.getAlpha() - b.getAlpha()) < 2; + }; + + const Point centre { circleSize / 2, circleSize / 2 }; + const Point brushOffset { brushTranslation, brushTranslation }; + + const auto redPosition = getScaled (centre + brushOffset); + expect (image.getPixelAt (redPosition.getX(), redPosition.getY()) == Colours::red); + + const auto mostlyRedPosition = getScaled (centre); + expect (approximatelyEqual (image.getPixelAt (mostlyRedPosition.getX(), mostlyRedPosition.getY()), + Colour { 138, 59, 0 })); + + const auto greenPosition = getScaled (centre.withY (2)); + expect (image.getPixelAt (greenPosition.getX(), greenPosition.getY()) == Colours::green); + + const auto blackPosition = getScaled ({ circleSize - 2, 2 }); + expect (image.getPixelAt (blackPosition.getX(), blackPosition.getY()) == Colours::black); + } + } }; static Direct2DGraphicsContextTests direct2DGraphicsContextTests;