Skip to content

Commit 17815ec

Browse files
Merge pull request #424 from SixLabors/js/colr-gradient-interpolation-3.1
Compose gradient transforms on the brush and consume Fonts 3.1.3
2 parents 22d08fc + 57892f9 commit 17815ec

19 files changed

Lines changed: 891 additions & 349 deletions

‎src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ internal static class GpuSceneDrawTag
1313
// These values are not a plain enum because each word also encodes the path-count, clip-count,
1414
// scene-word-count, and info-word-count increments consumed by the scan/reduction stages.
1515
// Visible-fill tags carry five extra info words: coverage data plus raster interest. Must match drawtag.wgsl.
16+
// Every gradient payload starts with the six words of the drawing-to-gradient transform.
1617
public const uint Nop = 0U;
1718
public const uint FillColor = 0x188U;
1819
public const uint FillRecolor = 0x184U;
19-
public const uint FillLinGradient = 0x254U;
20-
public const uint FillRadGradient = 0x3DCU;
21-
public const uint FillEllipticGradient = 0x35CU;
22-
public const uint FillSweepGradient = 0x394U;
20+
public const uint FillLinGradient = 0x26CU;
21+
public const uint FillRadGradient = 0x3F4U;
22+
public const uint FillEllipticGradient = 0x374U;
23+
public const uint FillSweepGradient = 0x3ACU;
2324
public const uint FillPathGradient = 0x190U;
2425
public const uint FillImage = 0x3D4U;
2526
public const uint BeginClip = 0x49U;
26-
public const uint EndClip = 0x21U;
27+
public const uint EndClip = 0x401U;
2728
public const uint FillInfoFlagsFillRuleBit = 1U;
2829
public const uint FillInfoFlagsAliasedBit = 0x40000000U;
2930

@@ -37,6 +38,6 @@ public static GpuSceneDrawMonoid Map(uint tagWord)
3738
=> new(
3839
tagWord != Nop ? 1U : 0U,
3940
tagWord & 1U,
40-
(tagWord >> 2) & 0x07U,
41+
(tagWord >> 2) & 0x0FU,
4142
(tagWord >> 6) & 0x0FU);
4243
}

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,21 @@ struct DrawMonoid {
2626
}
2727

2828
// Each draw object has a 32-bit draw tag, which is a bit-packed
29-
// version of the draw monoid: bit 0 = clip count, bits 2..4 = scene words,
29+
// version of the draw monoid: bit 0 = clip count, bits 2..5 = scene words,
3030
// bits 6..9 = info words (see map_draw_tag).
3131
// Visible-fill draw tags carry five extra info words: coverage data plus raster interest.
32+
// Every gradient payload starts with the six words of the drawing-to-gradient transform.
3233
const DRAWTAG_NOP = 0u;
3334
const DRAWTAG_FILL_COLOR = 0x188u;
3435
const DRAWTAG_FILL_RECOLOR = 0x184u;
35-
const DRAWTAG_FILL_LIN_GRADIENT = 0x254u;
36-
const DRAWTAG_FILL_RAD_GRADIENT = 0x3dcu;
37-
const DRAWTAG_FILL_ELLIPTIC_GRADIENT = 0x35cu;
38-
const DRAWTAG_FILL_SWEEP_GRADIENT = 0x394u;
36+
const DRAWTAG_FILL_LIN_GRADIENT = 0x26cu;
37+
const DRAWTAG_FILL_RAD_GRADIENT = 0x3f4u;
38+
const DRAWTAG_FILL_ELLIPTIC_GRADIENT = 0x374u;
39+
const DRAWTAG_FILL_SWEEP_GRADIENT = 0x3acu;
3940
const DRAWTAG_FILL_PATH_GRADIENT = 0x190u;
4041
const DRAWTAG_FILL_IMAGE = 0x3d4u;
4142
const DRAWTAG_BEGIN_CLIP = 0x49u;
42-
const DRAWTAG_END_CLIP = 0x21u;
43+
const DRAWTAG_END_CLIP = 0x401u;
4344

4445
// The first word of each draw info stream entry contains the flags. This is not part of the
4546
// draw object stream but is used after the draw objects have been reduced on the GPU.
@@ -88,7 +89,7 @@ fn map_draw_tag(tag_word: u32) -> DrawMonoid {
8889
var c: DrawMonoid;
8990
c.path_ix = u32(tag_word != DRAWTAG_NOP);
9091
c.clip_ix = tag_word & 1u;
91-
c.scene_offset = (tag_word >> 2u) & 0x07u;
92+
c.scene_offset = (tag_word >> 2u) & 0x0fu;
9293
c.info_offset = (tag_word >> 6u) & 0x0fu;
9394
return c;
9495
}

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl‎

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ fn main(
147147
{
148148
let bbox = path_bbox[m.path_ix];
149149
let draw_flags = bbox.draw_flags;
150+
// Every gradient payload starts with the drawing-to-gradient transform, the inverse
151+
// of the brush's gradient transform, so the geometry that follows is in the
152+
// gradient's own space.
153+
var user_to_gradient = transform_identity();
154+
if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT ||
155+
tag_word == DRAWTAG_FILL_ELLIPTIC_GRADIENT || tag_word == DRAWTAG_FILL_SWEEP_GRADIENT
156+
{
157+
user_to_gradient = Transform(
158+
bitcast<vec4<f32>>(vec4(scene[dd + 1u], scene[dd + 2u], scene[dd + 3u], scene[dd + 4u])),
159+
bitcast<vec2<f32>>(vec2(scene[dd + 5u], scene[dd + 6u])),
160+
vec3(0.0, 0.0, 1.0));
161+
}
150162
switch tag_word {
151163
case DRAWTAG_FILL_COLOR: {
152164
info[di] = draw_flags;
@@ -159,8 +171,8 @@ fn main(
159171
}
160172
case DRAWTAG_FILL_LIN_GRADIENT: {
161173
info[di] = draw_flags;
162-
let p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
163-
let p1 = bitcast<vec2<f32>>(vec2(scene[dd + 3u], scene[dd + 4u]));
174+
let p0 = bitcast<vec2<f32>>(vec2(scene[dd + 7u], scene[dd + 8u]));
175+
let p1 = bitcast<vec2<f32>>(vec2(scene[dd + 9u], scene[dd + 10u]));
164176
// Encode the gradient as a line equation so fine can
165177
// evaluate the parameter as t = dot(p, line_xy) + line_c.
166178
let dxy = p1 - p0;
@@ -173,6 +185,15 @@ fn main(
173185
line_c = -dot(p0, line_xy);
174186
}
175187

188+
// The projection is linear, so the drawing-to-gradient transform folds into
189+
// the line equation and fine evaluates it directly in drawing space.
190+
let u = user_to_gradient;
191+
let folded_xy = vec2(
192+
u.matrx.x * line_xy.x + u.matrx.y * line_xy.y,
193+
u.matrx.z * line_xy.x + u.matrx.w * line_xy.y);
194+
line_c = line_c + dot(u.translate, line_xy);
195+
line_xy = folded_xy;
196+
176197
// The CPU brush defines a zero-length axis as the gradient end. The
177198
// initialized equation therefore evaluates t=1 everywhere without NaN.
178199
info[di + 1u] = bitcast<u32>(line_xy.x);
@@ -185,11 +206,10 @@ fn main(
185206
// This epsilon matches what Skia uses
186207
let GRADIENT_EPSILON = 1.0 / f32(1u << 12u);
187208
info[di] = draw_flags;
188-
var p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
189-
var p1 = bitcast<vec2<f32>>(vec2(scene[dd + 3u], scene[dd + 4u]));
190-
var r0 = bitcast<f32>(scene[dd + 5u]);
191-
var r1 = bitcast<f32>(scene[dd + 6u]);
192-
let user_to_gradient = transform_identity();
209+
var p0 = bitcast<vec2<f32>>(vec2(scene[dd + 7u], scene[dd + 8u]));
210+
var p1 = bitcast<vec2<f32>>(vec2(scene[dd + 9u], scene[dd + 10u]));
211+
var r0 = bitcast<f32>(scene[dd + 11u]);
212+
var r1 = bitcast<f32>(scene[dd + 12u]);
193213
var xform = transform_identity();
194214
var focal_x = 0.0;
195215
var radius = 0.0;
@@ -257,9 +277,9 @@ fn main(
257277
}
258278
case DRAWTAG_FILL_ELLIPTIC_GRADIENT: {
259279
info[di] = draw_flags;
260-
let center = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
261-
let axis_end = bitcast<vec2<f32>>(vec2(scene[dd + 3u], scene[dd + 4u]));
262-
let second_end = bitcast<vec2<f32>>(vec2(scene[dd + 5u], scene[dd + 6u]));
280+
let center = bitcast<vec2<f32>>(vec2(scene[dd + 7u], scene[dd + 8u]));
281+
let axis_end = bitcast<vec2<f32>>(vec2(scene[dd + 9u], scene[dd + 10u]));
282+
let second_end = bitcast<vec2<f32>>(vec2(scene[dd + 11u], scene[dd + 12u]));
263283
let dxy = axis_end - center;
264284
let axis = length(dxy);
265285
let second_axis_len = length(second_end - center);
@@ -305,25 +325,36 @@ fn main(
305325
xlat_y = -(m1 * center.x + m3 * center.y);
306326
}
307327

308-
info[di + 1u] = bitcast<u32>(m0);
309-
info[di + 2u] = bitcast<u32>(m1);
310-
info[di + 3u] = bitcast<u32>(m2);
311-
info[di + 4u] = bitcast<u32>(m3);
312-
info[di + 5u] = bitcast<u32>(xlat_x);
313-
info[di + 6u] = bitcast<u32>(xlat_y);
328+
// Samples pass through the drawing-to-gradient transform before the ellipse
329+
// mapping. Degenerate kinds keep the center as their translation, so it moves
330+
// to its drawing position and the transform folds into the matrix only.
331+
let ellipse = Transform(vec4(m0, m1, m2, m3), vec2(xlat_x, xlat_y), vec3(0.0, 0.0, 1.0));
332+
var composed = transform_mul(ellipse, user_to_gradient);
333+
if kind != ELLIPTIC_GRAD_KIND_NORMAL {
334+
composed.translate = transform_apply(transform_inverse(user_to_gradient), center);
335+
}
336+
337+
info[di + 1u] = bitcast<u32>(composed.matrx.x);
338+
info[di + 2u] = bitcast<u32>(composed.matrx.y);
339+
info[di + 3u] = bitcast<u32>(composed.matrx.z);
340+
info[di + 4u] = bitcast<u32>(composed.matrx.w);
341+
info[di + 5u] = bitcast<u32>(composed.translate.x);
342+
info[di + 6u] = bitcast<u32>(composed.translate.y);
314343
info[di + 7u] = kind;
315344
}
316345
case DRAWTAG_FILL_SWEEP_GRADIENT: {
317346
info[di] = draw_flags;
318-
let p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
319-
info[di + 1u] = bitcast<u32>(1.0);
320-
info[di + 2u] = bitcast<u32>(0.0);
321-
info[di + 3u] = bitcast<u32>(0.0);
322-
info[di + 4u] = bitcast<u32>(1.0);
323-
info[di + 5u] = bitcast<u32>(-p0.x);
324-
info[di + 6u] = bitcast<u32>(-p0.y);
325-
info[di + 7u] = scene[dd + 3u];
326-
info[di + 8u] = scene[dd + 4u];
347+
let p0 = bitcast<vec2<f32>>(vec2(scene[dd + 7u], scene[dd + 8u]));
348+
// The sample maps into the gradient's space and then becomes center-relative.
349+
let u = user_to_gradient;
350+
info[di + 1u] = bitcast<u32>(u.matrx.x);
351+
info[di + 2u] = bitcast<u32>(u.matrx.y);
352+
info[di + 3u] = bitcast<u32>(u.matrx.z);
353+
info[di + 4u] = bitcast<u32>(u.matrx.w);
354+
info[di + 5u] = bitcast<u32>(u.translate.x - p0.x);
355+
info[di + 6u] = bitcast<u32>(u.translate.y - p0.y);
356+
info[di + 7u] = scene[dd + 9u];
357+
info[di + 8u] = scene[dd + 10u];
327358
}
328359
case DRAWTAG_FILL_PATH_GRADIENT: {
329360
info[di] = draw_flags;

‎src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6358,10 +6358,10 @@ private static int GetDrawDataWordCount(uint drawTag)
63586358
GpuSceneDrawTag.BeginClip => 2,
63596359
GpuSceneDrawTag.FillColor => 2,
63606360
GpuSceneDrawTag.FillRecolor => 1,
6361-
GpuSceneDrawTag.FillLinGradient => 5,
6362-
GpuSceneDrawTag.FillRadGradient => 7,
6363-
GpuSceneDrawTag.FillEllipticGradient => 7,
6364-
GpuSceneDrawTag.FillSweepGradient => 5,
6361+
GpuSceneDrawTag.FillLinGradient => 11,
6362+
GpuSceneDrawTag.FillRadGradient => 13,
6363+
GpuSceneDrawTag.FillEllipticGradient => 13,
6364+
GpuSceneDrawTag.FillSweepGradient => 11,
63656365
GpuSceneDrawTag.FillPathGradient => 4,
63666366
GpuSceneDrawTag.FillImage => 5,
63676367
GpuSceneDrawTag.EndClip => 0,
@@ -6948,12 +6948,36 @@ private static void AppendLinearGradientData(
69486948
gradientRowCount++;
69496949

69506950
drawData.Add(indexMode);
6951+
AppendGradientTransform(brush, ref drawData);
69516952
drawData.Add(BitcastSingle(brush.StartPoint.X));
69526953
drawData.Add(BitcastSingle(brush.StartPoint.Y));
69536954
drawData.Add(BitcastSingle(brush.EndPoint.X));
69546955
drawData.Add(BitcastSingle(brush.EndPoint.Y));
69556956
}
69566957

6958+
/// <summary>
6959+
/// Appends the transform from drawing coordinates to the gradient's coordinate space, which
6960+
/// is the affine part of the brush's gradient transform inverted.
6961+
/// </summary>
6962+
/// <param name="brush">The gradient brush.</param>
6963+
/// <param name="drawData">The draw-data stream.</param>
6964+
private static void AppendGradientTransform(GradientBrush brush, ref OwnedStream<uint> drawData)
6965+
{
6966+
// A transform that cannot be inverted has no gradient space. The zero matrix maps every
6967+
// sample to the origin, so the brush paints one color where the CPU renderer paints nothing.
6968+
if (!brush.TryGetInverseTransform(out Matrix3x2 m))
6969+
{
6970+
m = default;
6971+
}
6972+
6973+
drawData.Add(BitcastSingle(m.M11));
6974+
drawData.Add(BitcastSingle(m.M12));
6975+
drawData.Add(BitcastSingle(m.M21));
6976+
drawData.Add(BitcastSingle(m.M22));
6977+
drawData.Add(BitcastSingle(m.M31));
6978+
drawData.Add(BitcastSingle(m.M32));
6979+
}
6980+
69576981
/// <summary>
69586982
/// Appends the radial gradient payload and its packed ramp row.
69596983
/// </summary>
@@ -6995,6 +7019,7 @@ private static void AppendRadialGradientData(
69957019
}
69967020

69977021
drawData.Add(indexMode);
7022+
AppendGradientTransform(brush, ref drawData);
69987023
drawData.Add(BitcastSingle(center0.X));
69997024
drawData.Add(BitcastSingle(center0.Y));
70007025
drawData.Add(BitcastSingle(center1.X));
@@ -7033,6 +7058,7 @@ private static void AppendEllipticGradientData(
70337058
PointF localSecondEnd = new(localCenter.X + localPerpendicular.X, localCenter.Y + localPerpendicular.Y);
70347059

70357060
drawData.Add(indexMode);
7061+
AppendGradientTransform(brush, ref drawData);
70367062
drawData.Add(BitcastSingle(localCenter.X));
70377063
drawData.Add(BitcastSingle(localCenter.Y));
70387064
drawData.Add(BitcastSingle(localAxisEnd.X));
@@ -7074,6 +7100,7 @@ private static void AppendSweepGradientData(
70747100
float t1 = t0 + (sweepDegrees / 360F);
70757101

70767102
drawData.Add(indexMode);
7103+
AppendGradientTransform(brush, ref drawData);
70777104
drawData.Add(BitcastSingle(brush.Center.X));
70787105
drawData.Add(BitcastSingle(brush.Center.Y));
70797106
drawData.Add(BitcastSingle(t0));

‎src/ImageSharp.Drawing/ImageSharp.Drawing.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<InternalsVisibleTo Include="SixLabors.ImageSharp.Drawing.WebGPU" Key="$(SixLaborsPublicKey)" />
4545
</ItemGroup>
4646
<ItemGroup>
47-
<PackageReference Include="SixLabors.Fonts" Version="3.1.2" />
47+
<PackageReference Include="SixLabors.Fonts" Version="3.1.3" />
4848
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.2" />
4949
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.1" />
5050
</ItemGroup>

0 commit comments

Comments
 (0)