From e11b8f5ccb07450f8a61c2e3de15896e80cf8f8c Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 8 May 2024 19:50:50 +0100 Subject: [PATCH] CoreGraphics: Slightly shrink excluded clip regions in order to draw transformed opaque components without border artefacts Previously, drawing an opaque, scaled component with CoreGraphics could lead to visible artefacts around the edge of the component. When drawing the parent of an opaque component, the area covered by the opaque component is excluded from the clip region. If the clip region is non-integral when transformed into device space, anti-aliasing will be applied on the edges of the clip region. Similarly, when drawing the opaque component itself, anti-aliasing will be applied at the edges of the component. When the two drawings are superimposed, the foreground anti-aliased pixels will be blended with the background anti-aliased pixels, leading to a noticeable border around the component. Ideally, only the foreground anti-aliasing should be applied, and the background should not be anti-aliased around its edges. --- .../juce_graphics/geometry/juce_Rectangle.h | 18 +++++++- .../native/juce_CoreGraphicsContext_mac.h | 2 +- .../native/juce_CoreGraphicsContext_mac.mm | 43 ++++++++++++++----- .../native/juce_RenderingHelpers.h | 14 +----- 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/modules/juce_graphics/geometry/juce_Rectangle.h b/modules/juce_graphics/geometry/juce_Rectangle.h index dcd382570c..898359a282 100644 --- a/modules/juce_graphics/geometry/juce_Rectangle.h +++ b/modules/juce_graphics/geometry/juce_Rectangle.h @@ -848,7 +848,7 @@ public: /** Returns the smallest integer-aligned rectangle that completely contains this one. This is only relevant for floating-point rectangles, of course. - @see toFloat(), toNearestInt(), toNearestIntEdges() + @see toFloat(), toNearestInt(), toNearestIntEdges(), getLargestIntegerWithin() */ Rectangle getSmallestIntegerContainer() const noexcept { @@ -858,6 +858,22 @@ public: detail::ceilAsInt (pos.y + h)); } + /** Returns the largest integer-aligned rectangle that is completely contained by this one. + Returns an empty rectangle, outside the original rectangle, if no integer-aligned rectangle + is contained by this one. + This is only relevant for floating-point rectangles, of course. + @see toFloat(), toNearestInt(), toNearestIntEdges(), getSmallestIntegerContainer() + */ + Rectangle getLargestIntegerWithin() const noexcept + { + const auto l = detail::ceilAsInt (pos.x); + const auto t = detail::ceilAsInt (pos.y); + const auto r = detail::floorAsInt (pos.x + w); + const auto b = detail::floorAsInt (pos.y + h); + + return { l, t, jmax (0, r - l), jmax (0, b - t) }; + } + /** Casts this rectangle to a Rectangle. This uses roundToInt to snap x, y, width and height to the nearest integer (losing precision). If the rectangle already uses integers, this will simply return a copy. diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.h b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.h index 31007602af..d613d85bcd 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.h +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.h @@ -144,7 +144,7 @@ private: void flip() const; void applyTransform (const AffineTransform&) const; void drawImage (const Image&, const AffineTransform&, bool fillEntireClipAsTiles); - bool clipToRectangleListWithoutTest (const RectangleList&); + bool clipToRectangleListWithoutTest (const RectangleList&); void fillCGRect (const CGRect&, bool replaceExistingContents); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreGraphicsContext) diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm index 6a6ea8d0a8..d9fb5e90ec 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm @@ -298,7 +298,7 @@ bool CoreGraphicsContext::clipToRectangle (const Rectangle& r) return ! isClipEmpty(); } -bool CoreGraphicsContext::clipToRectangleListWithoutTest (const RectangleList& clipRegion) +bool CoreGraphicsContext::clipToRectangleListWithoutTest (const RectangleList& clipRegion) { if (clipRegion.isEmpty()) { @@ -307,27 +307,48 @@ bool CoreGraphicsContext::clipToRectangleListWithoutTest (const RectangleList rects (numRects); + std::vector rects ((size_t) clipRegion.getNumRectangles()); + std::transform (clipRegion.begin(), clipRegion.end(), rects.begin(), [this] (const auto& r) + { + return CGRectMake (r.getX(), flipHeight - r.getBottom(), r.getWidth(), r.getHeight()); + }); - int i = 0; - for (auto& r : clipRegion) - rects[i++] = CGRectMake (r.getX(), flipHeight - r.getBottom(), r.getWidth(), r.getHeight()); - - CGContextClipToRects (context.get(), rects, numRects); + CGContextClipToRects (context.get(), rects.data(), rects.size()); lastClipRect.reset(); return true; } bool CoreGraphicsContext::clipToRectangleList (const RectangleList& clipRegion) { - return clipToRectangleListWithoutTest (clipRegion) && ! isClipEmpty(); + RectangleList converted; + + for (auto& rect : clipRegion) + converted.add (rect.toFloat()); + + return clipToRectangleListWithoutTest (converted) && ! isClipEmpty(); } void CoreGraphicsContext::excludeClipRectangle (const Rectangle& r) { - RectangleList remaining (getClipBounds()); - remaining.subtract (r); + const auto cgTransform = CGContextGetUserSpaceToDeviceSpaceTransform (context.get()); + const auto transform = AffineTransform { (float) cgTransform.a, + (float) cgTransform.c, + (float) cgTransform.tx, + (float) cgTransform.b, + (float) cgTransform.d, + (float) cgTransform.ty }; + + const auto flip = [this] (auto rect) { return rect.withY ((float) (flipHeight - rect.getBottom())); }; + const auto flipped = flip (r.toFloat()); + + const auto snapped = flipped.toFloat().transformedBy (transform).getLargestIntegerWithin().toFloat(); + + const auto correctedRect = transform.isOnlyTranslationOrScale() + ? snapped.transformedBy (transform.inverted()) + : flipped.toFloat(); + + RectangleList remaining (getClipBounds().toFloat()); + remaining.subtract (flip (correctedRect)); clipToRectangleListWithoutTest (remaining); } diff --git a/modules/juce_graphics/native/juce_RenderingHelpers.h b/modules/juce_graphics/native/juce_RenderingHelpers.h index 3190c5fd66..5fdf236803 100644 --- a/modules/juce_graphics/native/juce_RenderingHelpers.h +++ b/modules/juce_graphics/native/juce_RenderingHelpers.h @@ -2061,16 +2061,6 @@ public: return clip != nullptr; } - static Rectangle getLargestIntegerWithin (Rectangle r) - { - auto x1 = (int) std::ceil (r.getX()); - auto y1 = (int) std::ceil (r.getY()); - auto x2 = (int) std::floor (r.getRight()); - auto y2 = (int) std::floor (r.getBottom()); - - return { x1, y1, x2 - x1, y2 - y1 }; - } - bool excludeClipRectangle (Rectangle r) { if (clip != nullptr) @@ -2079,11 +2069,11 @@ public: if (transform.isOnlyTranslated) { - clip = clip->excludeClipRectangle (getLargestIntegerWithin (transform.translated (r.toFloat()))); + clip = clip->excludeClipRectangle (transform.translated (r.toFloat()).getLargestIntegerWithin()); } else if (! transform.isRotated) { - clip = clip->excludeClipRectangle (getLargestIntegerWithin (transform.boundsAfterTransform (r.toFloat()))); + clip = clip->excludeClipRectangle (transform.boundsAfterTransform (r.toFloat()).getLargestIntegerWithin()); } else {