Compose gradient transforms on the brush and consume Fonts 3.1.3 - #424
Merged
JimBobSquarePants merged 3 commits intoSep 17, 2026
Merged
Conversation
Transforming a gradient brush projected its geometry point by point, which reduces any affine transform to a similarity. A COLRv1 radial paint squashed by its PaintTransform into a flat ellipse became a circle, and the pad region of that circle painted the toned thumbs-up opaque dark. GradientBrush carries a GradientTransform from the gradient's space to the drawing, every brush has a constructor overload that takes it, and Transform() composes it exactly for affine matrices. The old point projection remains for projective matrices, which have no affine inverse. Renderers fold the inverse: the linear brush into its axis, the two-circle radial brush into its canonical transform, and the others map each sample once when a transform is set. The glyph renderer builds paint-space brushes with the paint's transform composed after the glyph's. Interpolation is unchanged: premultiplied, as the CSS Color 4 gradient rules require.
Every gradient payload starts with the six words of the inverse affine transform. The draw tag's scene-size field widens to four bits to hold the larger payloads. draw_leaf reads the transform once and composes it per gradient kind: the linear brush folds it into its line equation, the radial and elliptic brushes multiply it into their canonical transforms, and the sweep brush carries the matrix and translation. Adds a CPU and GPU parity test over the four transformed brushes.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## release/3.1.x #424 +/- ##
=============================================
- Coverage 80% 80% -1%
=============================================
Files 232 232
Lines 27407 27413 +6
Branches 3121 3126 +5
=============================================
- Hits 21981 21944 -37
- Misses 4535 4578 +43
Partials 891 891
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerequisites
Description
Fixes the dark toned thumbs-up from #418 with the installed Segoe UI Emoji 1.70, and takes the Fonts 3.1.3 release that carries the matching shaping and cmap fixes (SixLabors/Fonts#565).
Cause. Transforming a gradient brush projected its geometry point by point, which reduces any affine transform to a similarity. A COLRv1 radial paint squashed by its
PaintTransforminto a flat shadow ellipse became a large circle, and the pad region of that circle painted the finger opaque dark. Interpolation was not involved and stays premultiplied per CSS Color 4.Brushes.
GradientBrushcarries aGradientTransformfrom the gradient's space to the drawing, the SVGgradientTransformidea. Every gradient brush has a constructor overload that takes it, andTransform()composes it exactly for affine matrices. The existing point projection remains for projective matrices, which have no affine inverse. Renderers fold the inverse so a transformed brush costs the same per sample as a plain one where the shape allows: the linear brush folds it into its axis, the two-circle radial brush into its canonical transform, and the elliptic, sweep and single-circle brushes map each sample once only when a transform is set.RichTextGlyphRendererbuilds paint-space brushes with the paint's transform composed after the glyph's.WebGPU. Every gradient payload starts with the six words of the inverse affine transform, the draw tag's scene-size field widens to four bits, and
draw_leafcomposes the transform per gradient kind: folded into the linear line equation, multiplied into the radial and elliptic canonical transforms, carried as matrix and translation for the sweep.Verification. The 192pt toned thumbs-up matches Chromium's rendering at the same size: identical ink bounds (200x252), mean max-channel difference of 3.08 levels, the remainder being edge anti-aliasing and the premultiplied ramp fade.
Tests. Gradient transform tests for composition, independent axis scaling, skew and reflection across the four brushes, an exact CPU comparison of
Brush.Transformagainst the constructor transform, glyph renderer tests for paint-space brushes, a CPU and GPU parity test over the four transformed brushes, and regenerated references for the COLRv1 and SVG emoji tests and the Noto emoji grid. Full suite: 2763 passed on net8.0 and net10.0.