From 641497918c36e765310512f2b16fb5506e68429f Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 8 Sep 2025 14:08:51 +0100 Subject: [PATCH] Direct2D: Always call SetTransform for gradient/image brushes This partly reverts commit ad28684b10c4568c85f8186fb9d409c65ca8de75. Prior to that change, getBrush() would always end up calling SetTransform on gradient/image brushes. This is important because, when drawing text, we combine the current brush transform with the text transform. If we don't reset the brush transform each time, these transforms end up accumulating across frames. --- ...ce_Direct2DGraphicsContextImpl_windows.cpp | 20 +---- .../juce_Direct2DGraphicsContext_windows.cpp | 77 +++++++++++++++++-- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp index 6df36af92b..1a6cba8a01 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContextImpl_windows.cpp @@ -471,19 +471,8 @@ public: if (fillType.isGradient()) { - auto p1 = fillType.gradient->point1; - auto p2 = fillType.gradient->point2; - - if (transform.isOnlyTranslation()) - { - Point translation { transform.getTranslationX(), transform.getTranslationY() }; - p1 += translation; - p2 += translation; - } - else - { - currentBrush->SetTransform (D2DUtilities::transformToMatrix (transform)); - } + const auto p1 = fillType.gradient->point1; + const auto p2 = fillType.gradient->point2; if (fillType.gradient->isRadial) { @@ -498,11 +487,8 @@ public: linearGradient->SetEndPoint ({ p2.x, p2.y }); } } - else - { - currentBrush->SetTransform (D2DUtilities::transformToMatrix (transform)); - } + currentBrush->SetTransform (D2DUtilities::transformToMatrix (transform)); currentBrush->SetOpacity (fillType.getOpacity()); return currentBrush; diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp index efd547698b..628ee1bc0c 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp @@ -1386,6 +1386,13 @@ public: testGradientFillTransform (1.0f); testGradientFillTransform (1.5f); } + + beginTest ("Text gradient fill transform should compose with world transform correctly"); + { + testTextGradientFillTransform (2.0f); + testTextGradientFillTransform (1.5f); + testTextGradientFillTransform (1.0f); + } } static Image createEdgeMask (int sourceWidth, @@ -1487,14 +1494,6 @@ public: 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 }; @@ -1512,6 +1511,68 @@ public: expect (image.getPixelAt (blackPosition.getX(), blackPosition.getY()) == Colours::black); } } + + void testTextGradientFillTransform (float scale) + { + const auto typeface = loadTypeface (FontBinaryData::Karla_Regular_Typo_Off_Offsets_Off); + + constexpr int size = 500; + + Image image { Image::RGB, + roundToInt (size * scale), + roundToInt (size * scale), + true }; + + const auto fillCol1 = Colours::cyan; + const auto fillCol2 = Colours::magenta; + const auto fillColMiddle = fillCol1.interpolatedWith (fillCol2, 0.5f); + + { + Graphics g { image }; + g.addTransform (AffineTransform::scale (scale)); + + g.setFont (FontOptions { typeface }.withPointHeight (50)); + g.setGradientFill ({ fillCol1, { size * 0.5f - 80, 0 }, fillCol2, { size * 0.5f + 80, 0.0f }, false }); + + for (auto i = 0; i != 10; ++i) + { + g.drawText (String::repeatedString ("-", 100), + Rectangle { size * 2, size }.translated (i * 50 - 500, i * 50), + Justification::topLeft, + false); + } + } + + const auto getPixelAtScaled = [&image, scale] (Point p) + { + const auto scaled = p.toFloat().transformedBy (AffineTransform::scale (scale)).roundToInt(); + return image.getPixelAt (scaled.x, scaled.y); + }; + + expect (approximatelyEqual (getPixelAtScaled ({ 15, 27 }), fillCol1)); + expect (approximatelyEqual (getPixelAtScaled ({ 485, 27 }), fillCol2)); + + expect (approximatelyEqual (getPixelAtScaled ({ 15, 77 }), fillCol1)); + expect (approximatelyEqual (getPixelAtScaled ({ 485, 77 }), fillCol2)); + expect (approximatelyEqual (getPixelAtScaled ({ 250, 77 }), fillColMiddle)); + + expect (approximatelyEqual (getPixelAtScaled ({ 15, 477 }), fillCol1)); + expect (approximatelyEqual (getPixelAtScaled ({ 485, 477 }), fillCol2)); + expect (approximatelyEqual (getPixelAtScaled ({ 250, 477 }), fillColMiddle)); + } + + static bool 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; + } + + static Typeface::Ptr loadTypeface (Span data) + { + return Typeface::createSystemTypefaceFor (data.data(), data.size()); + } }; static Direct2DGraphicsContextTests direct2DGraphicsContextTests;