@@ -50,14 +50,18 @@ struct Params {
5050 color_blend_mode: u32,
5151 alpha_composition_mode: u32,
5252 blend_percentage: u32,
53- solid_r : u32,
54- solid_g : u32,
55- solid_b : u32,
56- solid_a : u32,
53+ gp0 : u32,
54+ gp1 : u32,
55+ gp2 : u32,
56+ gp3 : u32,
5757 rasterization_mode: u32,
5858 antialias_threshold: u32,
59- pad0: u32,
60- pad1: u32,
59+ gp4: u32,
60+ gp5: u32,
61+ gp6: u32,
62+ gp7: u32,
63+ stops_offset: u32,
64+ stop_count: u32,
6165 };
6266
6367 struct DispatchConfig {
@@ -87,6 +91,16 @@ struct DispatchConfig {
8791 @group(0) @binding(5) var<uniform> dispatch_config: DispatchConfig;
8892 @group(0) @binding(6) var<storage, read> band_offsets: array<u32>;
8993
94+ struct ColorStop {
95+ ratio: f32,
96+ r: f32,
97+ g: f32,
98+ b: f32,
99+ a: f32,
100+ };
101+
102+ @group(0) @binding(7) var<storage, read> color_stops: array<ColorStop>;
103+
90104 // Workgroup shared memory for per-tile coverage accumulation.
91105 // Layout: 16 rows x 16 columns. Index = row * 16 + col.
92106 var<workgroup> tile_cover: array<atomic<i32>, 256>;
@@ -101,10 +115,208 @@ struct DispatchConfig {
101115 const EO_MASK: i32 = 511;
102116 const EO_PERIOD: i32 = 512;
103117
118+ // Brush type constants. Must match PreparedBrushType in WebGPUDrawingBackend.cs.
119+ const BRUSH_SOLID: u32 = 0u;
120+ const BRUSH_IMAGE: u32 = 1u;
121+ const BRUSH_LINEAR_GRADIENT: u32 = 2u;
122+ const BRUSH_RADIAL_GRADIENT: u32 = 3u;
123+ const BRUSH_RADIAL_GRADIENT_TWO_CIRCLE: u32 = 4u;
124+ const BRUSH_ELLIPTIC_GRADIENT: u32 = 5u;
125+ const BRUSH_SWEEP_GRADIENT: u32 = 6u;
126+ const BRUSH_PATTERN: u32 = 7u;
127+ const BRUSH_RECOLOR: u32 = 8u;
128+
104129 fn u32_to_f32(bits: u32) -> f32 {
105130 return bitcast<f32>(bits);
106131 }
107132
133+ // Exact copy of C# GradientBrushApplicator.this[x, y] color sampling.
134+ // Combines repetition mode + GetGradientSegment + lerp into one function.
135+ // Returns vec4(0) with alpha=0 for DontFill outside [0,1].
136+ fn sample_brush_gradient(raw_t: f32, mode: u32, offset: u32, count: u32) -> vec4<f32> {
137+ if count == 0u { return vec4<f32>(0.0); }
138+
139+ var t = raw_t;
140+
141+ // C# switch (this.repetitionMode)
142+ if mode == 1u {
143+ // Repeat: positionOnCompleteGradient %= 1;
144+ t = t % 1.0;
145+ } else if mode == 2u {
146+ // Reflect: positionOnCompleteGradient %= 2;
147+ // if (positionOnCompleteGradient > 1) { positionOnCompleteGradient = 2 - positionOnCompleteGradient; }
148+ t = t % 2.0;
149+ if t > 1.0 { t = 2.0 - t; }
150+ } else if mode == 3u {
151+ // DontFill: if (positionOnCompleteGradient is > 1 or < 0) { return Transparent; }
152+ if t < 0.0 || t > 1.0 { return vec4<f32>(0.0); }
153+ }
154+ // mode 0 (None): do nothing
155+
156+ if count == 1u {
157+ let s = color_stops[offset];
158+ return vec4<f32>(s.r, s.g, s.b, s.a);
159+ }
160+
161+ // C# GetGradientSegment
162+ // ColorStop localGradientFrom = this.colorStops[0];
163+ // ColorStop localGradientTo = default;
164+ // foreach (ColorStop colorStop in this.colorStops)
165+ // {
166+ // localGradientTo = colorStop;
167+ // if (colorStop.Ratio > positionOnCompleteGradient) { break; }
168+ // localGradientFrom = localGradientTo;
169+ // }
170+ var from_idx = 0u;
171+ var to_idx = 0u;
172+ for (var i = 0u; i < count; i++) {
173+ to_idx = i;
174+ if color_stops[offset + i].ratio > t {
175+ break;
176+ }
177+ from_idx = i;
178+ }
179+
180+ let from_stop = color_stops[offset + from_idx];
181+ let to_stop = color_stops[offset + to_idx];
182+
183+ // C#: if (from.Color.Equals(to.Color)) { return from.Color.ToPixel<TPixel>(); }
184+ let from_color = vec4<f32>(from_stop.r, from_stop.g, from_stop.b, from_stop.a);
185+ let to_color = vec4<f32>(to_stop.r, to_stop.g, to_stop.b, to_stop.a);
186+ if all(from_color == to_color) {
187+ return from_color;
188+ }
189+
190+ // C#: float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio);
191+ let range = to_stop.ratio - from_stop.ratio;
192+ let local_t = (t - from_stop.ratio) / range;
193+
194+ // C#: Vector4.Lerp(from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), onLocalGradient)
195+ return mix(from_color, to_color, local_t);
196+ }
197+
198+ // Linear gradient: project pixel onto gradient axis.
199+ fn linear_gradient_t(x: f32, y: f32, cmd: Params) -> f32 {
200+ let start_x = u32_to_f32(cmd.gp0);
201+ let start_y = u32_to_f32(cmd.gp1);
202+ let end_x = u32_to_f32(cmd.gp2);
203+ let end_y = u32_to_f32(cmd.gp3);
204+ let along_x = end_x - start_x;
205+ let along_y = end_y - start_y;
206+ let along_sq = along_x * along_x + along_y * along_y;
207+ if along_sq < 1e-12 { return 0.0; }
208+ let dx = x - start_x;
209+ let dy = y - start_y;
210+ return (dx * along_x + dy * along_y) / along_sq;
211+ }
212+
213+ // Single-circle radial gradient.
214+ // gp0=cx, gp1=cy, gp2=radius, gp3=repetition_mode
215+ fn radial_gradient_t(x: f32, y: f32, cmd: Params) -> f32 {
216+ let cx = u32_to_f32(cmd.gp0);
217+ let cy = u32_to_f32(cmd.gp1);
218+ let radius = u32_to_f32(cmd.gp2);
219+ if radius < 1e-20 { return 0.0; }
220+ return length(vec2<f32>(x - cx, y - cy)) / radius;
221+ }
222+
223+ // Two-circle radial gradient.
224+ // gp0=c0.x, gp1=c0.y, gp2=c1.x, gp3=c1.y, gp4=r0, gp5=r1
225+ fn radial_gradient_two_t(x: f32, y: f32, cmd: Params) -> f32 {
226+ let c0x = u32_to_f32(cmd.gp0);
227+ let c0y = u32_to_f32(cmd.gp1);
228+ let c1x = u32_to_f32(cmd.gp2);
229+ let c1y = u32_to_f32(cmd.gp3);
230+ let r0 = u32_to_f32(cmd.gp4);
231+ let r1 = u32_to_f32(cmd.gp5);
232+
233+ let dx_c = c1x - c0x;
234+ let dy_c = c1y - c0y;
235+ let dr = r1 - r0;
236+ let dd = dx_c * dx_c + dy_c * dy_c;
237+ let denom = dd - dr * dr;
238+
239+ let qx = x - c0x;
240+ let qy = y - c0y;
241+
242+ // Concentric case (centers equal) or degenerate (denom == 0).
243+ if dd < 1e-10 || abs(denom) < 1e-10 {
244+ let dist = length(vec2<f32>(qx, qy));
245+ let abs_dr = max(abs(dr), 1e-20);
246+ return (dist - r0) / abs_dr;
247+ }
248+
249+ // General case: t = (q·d - r0*dr) / denom.
250+ let num = qx * dx_c + qy * dy_c - r0 * dr;
251+ return num / denom;
252+ }
253+
254+ // Elliptic gradient. Computes rotation and radii from raw brush properties.
255+ // gp0=center.x, gp1=center.y, gp2=refEnd.x, gp3=refEnd.y, gp4=axisRatio
256+ fn elliptic_gradient_t(x: f32, y: f32, cmd: Params) -> f32 {
257+ let cx = u32_to_f32(cmd.gp0);
258+ let cy = u32_to_f32(cmd.gp1);
259+ let ref_x = u32_to_f32(cmd.gp2);
260+ let ref_y = u32_to_f32(cmd.gp3);
261+ let axis_ratio = u32_to_f32(cmd.gp4);
262+
263+ let ref_dx = ref_x - cx;
264+ let ref_dy = ref_y - cy;
265+ let rotation = atan2(ref_dy, ref_dx);
266+ let cos_r = cos(rotation);
267+ let sin_r = sin(rotation);
268+ let rx_sq = ref_dx * ref_dx + ref_dy * ref_dy;
269+ let ry_sq = rx_sq * axis_ratio * axis_ratio;
270+
271+ let px = x - cx;
272+ let py = y - cy;
273+ let rotated_x = px * cos_r - py * sin_r;
274+ let rotated_y = px * sin_r + py * cos_r;
275+
276+ if rx_sq < 1e-20 { return 0.0; }
277+ if ry_sq < 1e-20 { return 0.0; }
278+ return rotated_x * rotated_x / rx_sq + rotated_y * rotated_y / ry_sq;
279+ }
280+
281+ // Sweep (angular) gradient. Computes radians and sweep from raw degrees.
282+ // gp0=center.x, gp1=center.y, gp2=startAngleDegrees, gp3=endAngleDegrees
283+ fn sweep_gradient_t(x: f32, y: f32, cmd: Params) -> f32 {
284+ let cx = u32_to_f32(cmd.gp0);
285+ let cy = u32_to_f32(cmd.gp1);
286+ let start_deg = u32_to_f32(cmd.gp2);
287+ let end_deg = u32_to_f32(cmd.gp3);
288+
289+ let start_rad = start_deg * 0.017453292; // PI / 180
290+ let end_rad = end_deg * 0.017453292;
291+
292+ // Compute sweep, normalizing to (0, 2PI].
293+ var sweep = (end_rad - start_rad) % 6.283185307;
294+ if sweep <= 0.0 { sweep += 6.283185307; }
295+ if abs(sweep) < 1e-6 { sweep = 6.283185307; }
296+ let is_full = abs(sweep - 6.283185307) < 1e-6;
297+ let inv_sweep = 1.0 / sweep;
298+
299+ let dx = x - cx;
300+ let dy = y - cy;
301+
302+ // atan2(-dy, dx) gives clockwise angles in y-down space.
303+ var angle = atan2(-dy, dx);
304+ if angle < 0.0 { angle += 6.283185307; }
305+
306+ // Rotate basis by 180 degrees.
307+ angle += 3.141592653;
308+ if angle >= 6.283185307 { angle -= 6.283185307; }
309+
310+ // Phase measured clockwise from start.
311+ var phase = angle - start_rad;
312+ if phase < 0.0 { phase += 6.283185307; }
313+
314+ if is_full {
315+ return phase / 6.283185307;
316+ }
317+ return phase * inv_sweep;
318+ }
319+
108320 __DECODE_TEXEL_FUNCTION__
109321
110322 __ENCODE_OUTPUT_FUNCTION__
@@ -952,12 +1164,12 @@ fn cs_main(
9521164 let effective_coverage = coverage_value * blend_percentage;
9531165
9541166 var brush = vec4<f32>(
955- u32_to_f32(command.solid_r ),
956- u32_to_f32(command.solid_g ),
957- u32_to_f32(command.solid_b ),
958- u32_to_f32(command.solid_a ));
1167+ u32_to_f32(command.gp0 ),
1168+ u32_to_f32(command.gp1 ),
1169+ u32_to_f32(command.gp2 ),
1170+ u32_to_f32(command.gp3 ));
9591171
960- if command.brush_type == 1u {
1172+ if command.brush_type == BRUSH_IMAGE {
9611173 let origin_x = bitcast<i32>(command.brush_origin_x);
9621174 let origin_y = bitcast<i32>(command.brush_origin_y);
9631175 let region_x = i32(command.brush_region_x);
@@ -967,6 +1179,65 @@ fn cs_main(
9671179 let sample_x = positive_mod(dest_x_i32 - origin_x, region_w) + region_x;
9681180 let sample_y = positive_mod(dest_y_i32 - origin_y, region_h) + region_y;
9691181 brush = __LOAD_BRUSH__;
1182+ } else if command.brush_type == BRUSH_LINEAR_GRADIENT {
1183+ let px = f32(source_x) + 0.5;
1184+ let py = f32(source_y) + 0.5;
1185+ let raw_t = linear_gradient_t(px, py, command);
1186+ brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count);
1187+ } else if command.brush_type == BRUSH_RADIAL_GRADIENT {
1188+ let px = f32(source_x) + 0.5;
1189+ let py = f32(source_y) + 0.5;
1190+ let raw_t = radial_gradient_t(px, py, command);
1191+ brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count);
1192+ } else if command.brush_type == BRUSH_RADIAL_GRADIENT_TWO_CIRCLE {
1193+ let px = f32(source_x) + 0.5;
1194+ let py = f32(source_y) + 0.5;
1195+ let raw_t = radial_gradient_two_t(px, py, command);
1196+ brush = sample_brush_gradient(raw_t, command.gp6, command.stops_offset, command.stop_count);
1197+ } else if command.brush_type == BRUSH_ELLIPTIC_GRADIENT {
1198+ let px = f32(source_x) + 0.5;
1199+ let py = f32(source_y) + 0.5;
1200+ let raw_t = elliptic_gradient_t(px, py, command);
1201+ brush = sample_brush_gradient(raw_t, command.gp5, command.stops_offset, command.stop_count);
1202+ } else if command.brush_type == BRUSH_SWEEP_GRADIENT {
1203+ let px = f32(source_x) + 0.5;
1204+ let py = f32(source_y) + 0.5;
1205+ let raw_t = sweep_gradient_t(px, py, command);
1206+ brush = sample_brush_gradient(raw_t, command.gp4, command.stops_offset, command.stop_count);
1207+ } else if command.brush_type == BRUSH_PATTERN {
1208+ let pw = u32_to_f32(command.gp0);
1209+ let ph = u32_to_f32(command.gp1);
1210+ let ox = u32_to_f32(command.gp2);
1211+ let oy = u32_to_f32(command.gp3);
1212+ let fx = f32(source_x) - ox;
1213+ let fy = f32(source_y) - oy;
1214+ let pw_i = i32(pw);
1215+ let ph_i = i32(ph);
1216+ let pxi = ((i32(fx) % pw_i) + pw_i) % pw_i;
1217+ let pyi = ((i32(fy) % ph_i) + ph_i) % ph_i;
1218+ let idx = command.stops_offset + u32(pyi) * u32(pw_i) + u32(pxi);
1219+ let c = color_stops[idx];
1220+ brush = vec4<f32>(c.r, c.g, c.b, c.a);
1221+ } else if command.brush_type == BRUSH_RECOLOR {
1222+ let src_r = u32_to_f32(command.gp0);
1223+ let src_g = u32_to_f32(command.gp1);
1224+ let src_b = u32_to_f32(command.gp2);
1225+ let src_a = u32_to_f32(command.gp3);
1226+ let tgt_r = u32_to_f32(command.gp4);
1227+ let tgt_g = u32_to_f32(command.gp5);
1228+ let tgt_b = u32_to_f32(command.gp6);
1229+ let tgt_a = u32_to_f32(command.gp7);
1230+ let threshold = bitcast<f32>(command.stops_offset);
1231+ let dr = destination.r - src_r;
1232+ let dg = destination.g - src_g;
1233+ let db = destination.b - src_b;
1234+ let da = destination.a - src_a;
1235+ let dist_sq = dr * dr + dg * dg + db * db + da * da;
1236+ if dist_sq <= threshold * threshold {
1237+ brush = vec4<f32>(tgt_r, tgt_g, tgt_b, tgt_a);
1238+ } else {
1239+ brush = destination;
1240+ }
9701241 }
9711242
9721243 let src = vec4<f32>(brush.rgb, brush.a * effective_coverage);
0 commit comments