Skip to content

Compose gradient transforms on the brush and consume Fonts 3.1.3 - #424

Merged
JimBobSquarePants merged 3 commits into
release/3.1.xfrom
js/colr-gradient-interpolation-3.1
Sep 17, 2026
Merged

JimBobSquarePants merged 3 commits into
release/3.1.xfrom
js/colr-gradient-interpolation-3.1

Conversation

@JimBobSquarePants

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

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 PaintTransform into 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. GradientBrush carries a GradientTransform from the gradient's space to the drawing, the SVG gradientTransform idea. Every gradient brush has a constructor overload that takes it, and Transform() 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. RichTextGlyphRenderer builds 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_leaf composes 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.Transform against 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.

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.
@JimBobSquarePants JimBobSquarePants added the bug Something isn't working label Sep 17, 2026
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 80%. Comparing base (22d08fc) to head (57892f9).

Files with missing lines Patch % Lines
...mageSharp.Drawing/Processing/SweepGradientBrush.cs 40% 20 Missing and 1 partial ⚠️
...ageSharp.Drawing/Processing/RadialGradientBrush.cs 77% 6 Missing and 1 partial ⚠️
...eSharp.Drawing/Processing/EllipticGradientBrush.cs 81% 3 Missing and 2 partials ⚠️
src/ImageSharp.Drawing/Processing/GradientBrush.cs 78% 4 Missing ⚠️
...rc/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs 88% 1 Missing and 1 partial ⚠️
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           
Flag Coverage Δ
unittests 80% <76%> (-1%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JimBobSquarePants
JimBobSquarePants merged commit 17815ec into release/3.1.x Sep 17, 2026
11 of 12 checks passed
@JimBobSquarePants
JimBobSquarePants deleted the js/colr-gradient-interpolation-3.1 branch September 17, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant