Skip to content

Commit 0d80bcf

Browse files
Compute stroke arc sweeps from relative angles
WGSL leaves atan2 implementation-defined when x is zero, and axis-aligned caps and joins produce exactly those inputs; an adapter returning an indeterminate value there collapsed the sweep and dropped entire round-cap arcs. Cap and join arcs now derive their sweep from atan2(cross, dot), which stays away from the poles for every non-degenerate offset pair, and generate interior points by rotating the start offset instead of evaluating absolute angles.
1 parent c17df3d commit 0d80bcf

1 file changed

Lines changed: 22 additions & 15 deletions

File tree

  • src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource

src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -557,14 +557,19 @@ fn stroke_chain_arc(
557557
stroke_chain_point(path_ix, last, center + to_offset, transform);
558558
return;
559559
}
560-
let start_angle = atan2(from_offset.y, from_offset.x);
561-
let end_angle = atan2(to_offset.y, to_offset.x);
562-
let sweep = stroke_normalize_positive_angle(end_angle - start_angle);
560+
// WGSL atan2 is implementation-defined when x is zero, and axis-aligned caps produce
561+
// exactly those inputs. The sweep must come from atan2(cross, dot) and interior points
562+
// must rotate the start offset; absolute angles of the offsets are not computable here.
563+
let cross_ft = (from_offset.x * to_offset.y) - (from_offset.y * to_offset.x);
564+
let sweep = stroke_normalize_positive_angle(atan2(cross_ft, dot(from_offset, to_offset)));
563565
let n = stroke_arc_subdivision_count(radius, sweep, arc_detail_scale);
564566
let step = sweep / f32(n + 1u);
567+
let rot_c = cos(step);
568+
let rot_s = sin(step);
569+
var offset = from_offset;
565570
for (var i = 1u; i <= n; i += 1u) {
566-
let a = start_angle + (step * f32(i));
567-
stroke_chain_point(path_ix, last, center + (vec2(cos(a), sin(a)) * radius), transform);
571+
offset = vec2((offset.x * rot_c) - (offset.y * rot_s), (offset.x * rot_s) + (offset.y * rot_c));
572+
stroke_chain_point(path_ix, last, center + offset, transform);
568573
}
569574
stroke_chain_point(path_ix, last, center + to_offset, transform);
570575
}
@@ -577,20 +582,22 @@ fn stroke_calc_arc(
577582
v1: vec2f, o1: vec2f, o2: vec2f,
578583
half_width: f32, arc_detail_scale: f32, transform: Transform,
579584
) {
580-
var a1 = atan2(o1.y, o1.x);
581-
var a2 = atan2(o2.y, o2.x);
585+
// WGSL atan2 is implementation-defined when x is zero, and axis-aligned joins produce
586+
// exactly those inputs. The sweep must come from atan2(cross, dot) and interior points
587+
// must rotate o1; absolute angles of the offsets are not computable here.
588+
let cross_oo = (o1.x * o2.y) - (o1.y * o2.x);
589+
let sweep = stroke_normalize_positive_angle(atan2(cross_oo, dot(o1, o2)));
582590
let da = acos(half_width / (half_width + (0.125 / arc_detail_scale))) * 2.0;
583591
stroke_chain_point(path_ix, last, v1 + o1, transform);
584-
if a1 > a2 {
585-
a2 += 6.283185307179586;
586-
}
587592
// Bounded for GPU safety; matches the CPU count for all real detail scales.
588-
let n = clamp(i32((a2 - a1) / da), 0, 1024);
589-
let step = (a2 - a1) / f32(n + 1);
590-
a1 += step;
593+
let n = clamp(i32(sweep / da), 0, 1024);
594+
let step = sweep / f32(n + 1);
595+
let rot_c = cos(step);
596+
let rot_s = sin(step);
597+
var offset = o1;
591598
for (var i = 0; i < n; i += 1) {
592-
stroke_chain_point(path_ix, last, v1 + (vec2(cos(a1), sin(a1)) * half_width), transform);
593-
a1 += step;
599+
offset = vec2((offset.x * rot_c) - (offset.y * rot_s), (offset.x * rot_s) + (offset.y * rot_c));
600+
stroke_chain_point(path_ix, last, v1 + offset, transform);
594601
}
595602
stroke_chain_point(path_ix, last, v1 + o2, transform);
596603
}

0 commit comments

Comments
 (0)