Skip to content

Commit 6f7e0c6

Browse files
GPU: add gradient, pattern & recolor brushes
1 parent fc1c65c commit 6f7e0c6

48 files changed

Lines changed: 1474 additions & 332 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,10 @@ SixLabors.ImageSharp.Drawing
1111
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp.Drawing/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp.Drawing/actions)
1212
[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp.Drawing/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp.Drawing)
1313
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE)
14-
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
1514

1615
</div>
1716

18-
### **ImageSharp.Drawing** provides extensions to ImageSharp containing powerful, Cross-Platform 2D polygon manipulation and drawing APIs.
19-
20-
Designed to democratize image processing, ImageSharp.Drawing brings you an incredibly powerful yet beautifully simple API.
21-
22-
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios.
17+
**ImageSharp.Drawing** is a cross-platform 2D drawing library built on top of [ImageSharp](https://github.com/SixLabors/ImageSharp). It provides path construction, polygon manipulation, fills, strokes, gradient brushes, pattern brushes, and text rendering. Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard).
2318

2419
## License
2520

src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs

Lines changed: 282 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)