vgpu-render 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -13
- package/dist/curve.d.ts +25 -0
- package/dist/curve.js +53 -0
- package/dist/frame-draw-list-2d.d.ts +128 -19
- package/dist/frame-draw-list-2d.js +347 -57
- package/dist/geometry-generators.d.ts +9 -0
- package/dist/geometry-generators.js +36 -0
- package/dist/gpu-debug-draw-2d.js +10 -2
- package/dist/shape2d-shader.js +292 -5
- package/package.json +1 -1
package/dist/shape2d-shader.js
CHANGED
|
@@ -59,6 +59,8 @@ struct In {
|
|
|
59
59
|
@location(12) color: vec4f,
|
|
60
60
|
/** bit0: filled; bits 8..15: polygon vertex count. */
|
|
61
61
|
@location(13) flags: u32,
|
|
62
|
+
/** (visible, gap, phase) of a dashed outline in shape-space units; gap 0 disables it. */
|
|
63
|
+
@location(14) dash: vec3f,
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
struct Out {
|
|
@@ -75,6 +77,7 @@ struct Out {
|
|
|
75
77
|
@location(7) rotation: f32,
|
|
76
78
|
@location(8) @interpolate(flat, either) sides: u32,
|
|
77
79
|
@location(9) @interpolate(flat, either) flags: u32,
|
|
80
|
+
@location(10) dash: vec3f,
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
fn rotate2(p: vec2f, angle: f32) -> vec2f {
|
|
@@ -171,12 +174,17 @@ fn sd_rhombus(p: vec2f, a: f32, b: f32) -> f32 {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/** Sector of at most half a turn: the intersection of the disc and the two edge half-planes. */
|
|
174
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Angular wedge of sweep radians from start: only the two bounding half-planes, no radius.
|
|
179
|
+
* Keeping the disc out of it matters for the complementary wedge below, where a radius term would
|
|
180
|
+
* clamp the opposite side to zero and leave the uncovered wedge of an arc or sector painted.
|
|
181
|
+
*/
|
|
182
|
+
fn sd_wedge(p: vec2f, start: f32, sweep: f32) -> f32 {
|
|
175
183
|
let end = start + sweep;
|
|
176
184
|
// Inward normals of the two straight edges, in the order the sweep turns.
|
|
177
185
|
let startEdge = vec2f(sin(start), -cos(start));
|
|
178
186
|
let endEdge = vec2f(-sin(end), cos(end));
|
|
179
|
-
return max(
|
|
187
|
+
return max(dot(p, startEdge), dot(p, endEdge));
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
/**
|
|
@@ -186,9 +194,9 @@ fn sd_convex_sector(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
|
186
194
|
fn sd_sector(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
187
195
|
let disc = length(p) - radius;
|
|
188
196
|
if (abs(sweep) >= TAU - EPSILON) { return disc; }
|
|
189
|
-
if (abs(sweep) <= PI) { return max(disc,
|
|
197
|
+
if (abs(sweep) <= PI) { return max(disc, sd_wedge(p, start, sweep)); }
|
|
190
198
|
let complementSweep = select(TAU + sweep, TAU - sweep, sweep > 0.0);
|
|
191
|
-
let complement =
|
|
199
|
+
let complement = sd_wedge(p, start + sweep, complementSweep);
|
|
192
200
|
return max(disc, -complement);
|
|
193
201
|
}
|
|
194
202
|
|
|
@@ -227,6 +235,256 @@ fn shape_distance(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: ve
|
|
|
227
235
|
}
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Perimeter parameter of the point on a rectangle's outline nearest to the sample, measured
|
|
240
|
+
* anticlockwise from the (sx, sy) corner. The stroke only touches pixels within a pixel or two
|
|
241
|
+
* of the outline, so picking the closer of the two candidate edges there is exact.
|
|
242
|
+
*/
|
|
243
|
+
fn rect_perimeter(p: vec2f, halfSize: vec2f) -> f32 {
|
|
244
|
+
let sx = halfSize.x;
|
|
245
|
+
let sy = halfSize.y;
|
|
246
|
+
let q = clamp(p, -halfSize, halfSize);
|
|
247
|
+
let overX = abs(p.x) - sx;
|
|
248
|
+
let overY = abs(p.y) - sy;
|
|
249
|
+
if (overY >= overX) {
|
|
250
|
+
if (p.y >= 0.0) { return sx - q.x; }
|
|
251
|
+
return 2.0 * sx + 2.0 * sy + (sx + q.x);
|
|
252
|
+
}
|
|
253
|
+
if (p.x < 0.0) { return 2.0 * sx + (sy - q.y); }
|
|
254
|
+
return 4.0 * sx + 2.0 * sy + (q.y + sy);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Closed-outline length in shape-space units, or -1 for an open outline (an arc). A closed
|
|
259
|
+
* outline snaps its dash period to a whole number of periods, so the pattern meets itself at the
|
|
260
|
+
* perimeter start instead of jumping by P mod period.
|
|
261
|
+
*/
|
|
262
|
+
fn perimeter_total(shape: u32, size: vec2f, vertexB: vec2f, vertexC: vec2f, params: vec4f, sides: u32) -> f32 {
|
|
263
|
+
switch shape {
|
|
264
|
+
case SHAPE_RECT: { return 4.0 * (size.x + size.y); }
|
|
265
|
+
case SHAPE_ROUNDED_RECT: {
|
|
266
|
+
let corner = clamp(params.x, 0.0, min(size.x, size.y));
|
|
267
|
+
let inner = max(size - vec2f(corner), vec2f(0.0));
|
|
268
|
+
return 4.0 * (inner.x + inner.y) + 2.0 * PI * corner;
|
|
269
|
+
}
|
|
270
|
+
case SHAPE_CIRCLE: { return TAU * size.x; }
|
|
271
|
+
case SHAPE_ELLIPSE: { return ellipse_arc(TAU, size); }
|
|
272
|
+
case SHAPE_ARC: { return -1.0; }
|
|
273
|
+
case SHAPE_ANNULUS: { return TAU * (size.x + size.y); }
|
|
274
|
+
case SHAPE_CAPSULE: { return 2.0 * PI * size.x + 4.0 * size.y; }
|
|
275
|
+
case SHAPE_POLYGON: {
|
|
276
|
+
let count = max(sides, 3u);
|
|
277
|
+
return 2.0 * f32(count) * size.x * sin(PI / f32(count));
|
|
278
|
+
}
|
|
279
|
+
case SHAPE_TRIANGLE: {
|
|
280
|
+
return length(vertexB - size) + length(vertexC - vertexB) + length(size - vertexC);
|
|
281
|
+
}
|
|
282
|
+
case SHAPE_RHOMBUS: { return 4.0 * length(vec2f(0.0, size.y) - vec2f(size.x, 0.0)); }
|
|
283
|
+
case SHAPE_SECTOR: { return abs(params.w) * size.x + 2.0 * size.x; }
|
|
284
|
+
case SHAPE_SEGMENT: {
|
|
285
|
+
let sweepAbs = abs(params.w);
|
|
286
|
+
return sweepAbs * size.x + 2.0 * size.x * sin(sweepAbs * 0.5);
|
|
287
|
+
}
|
|
288
|
+
default: { return -1.0; }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Wraps an angle into [0, TAU). */
|
|
293
|
+
fn wrap_tau(angle: f32) -> f32 {
|
|
294
|
+
let wrapped = angle % TAU;
|
|
295
|
+
return select(wrapped + TAU, wrapped, wrapped >= 0.0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Distance from the sample to the segment a-b, and how far along that segment it sits. */
|
|
299
|
+
fn nearest_edge(p: vec2f, a: vec2f, b: vec2f) -> vec2f {
|
|
300
|
+
let edge = b - a;
|
|
301
|
+
let along = clamp(dot(p - a, edge) / max(dot(edge, edge), EPSILON), 0.0, 1.0);
|
|
302
|
+
return vec2f(length(p - (a + edge * along)), along * length(edge));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Arc length of an ellipse from angle 0 to theta, Simpson-integrated in 12 steps. The ellipse
|
|
307
|
+
* has no elementary arc-length formula, so this is the one shape whose dash spacing is an
|
|
308
|
+
* approximation rather than a closed form.
|
|
309
|
+
*/
|
|
310
|
+
fn ellipse_arc(theta: f32, radii: vec2f) -> f32 {
|
|
311
|
+
let steps = 12u;
|
|
312
|
+
let h = theta / f32(steps);
|
|
313
|
+
var sum = 0.0;
|
|
314
|
+
for (var i = 0u; i <= steps; i += 1u) {
|
|
315
|
+
let a = h * f32(i);
|
|
316
|
+
let speed = sqrt(radii.x * radii.x * sin(a) * sin(a) + radii.y * radii.y * cos(a) * cos(a));
|
|
317
|
+
var weight = 2.0;
|
|
318
|
+
if (i == 0u || i == steps) { weight = 1.0; }
|
|
319
|
+
else if (i % 2u == 1u) { weight = 4.0; }
|
|
320
|
+
sum = sum + weight * speed;
|
|
321
|
+
}
|
|
322
|
+
return sum * h / 3.0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Rounded rectangle: four straight edges and four quarter arcs, from the top-right corner. */
|
|
326
|
+
fn rounded_rect_perimeter(p: vec2f, halfSize: vec2f, cornerRadius: f32) -> f32 {
|
|
327
|
+
let corner = clamp(cornerRadius, 0.0, min(halfSize.x, halfSize.y));
|
|
328
|
+
let inner = max(halfSize - vec2f(corner), vec2f(0.0));
|
|
329
|
+
let center = clamp(p, -inner, inner);
|
|
330
|
+
let delta = p - center;
|
|
331
|
+
let straightX = 2.0 * inner.x;
|
|
332
|
+
let straightY = 2.0 * inner.y;
|
|
333
|
+
let quarter = corner * PI * 0.5;
|
|
334
|
+
let leftBase = straightX + quarter;
|
|
335
|
+
let bottomBase = straightX + 2.0 * quarter + straightY;
|
|
336
|
+
// Right edge starts after the bottom-right quarter arc: two straight runs, three quarter
|
|
337
|
+
// arcs and one straight height have already been walked.
|
|
338
|
+
let rightBase = 2.0 * straightX + 3.0 * quarter + straightY;
|
|
339
|
+
let topRightBase = 2.0 * straightX + 2.0 * straightY + 3.0 * quarter;
|
|
340
|
+
if (abs(delta.x) > EPSILON && abs(delta.y) > EPSILON) {
|
|
341
|
+
let phi = wrap_tau(atan2(delta.y, delta.x));
|
|
342
|
+
if (phi < PI * 0.5) { return topRightBase + phi * corner; }
|
|
343
|
+
if (phi < PI) { return straightX + (phi - PI * 0.5) * corner; }
|
|
344
|
+
if (phi < PI * 1.5) { return leftBase + straightY + (phi - PI) * corner; }
|
|
345
|
+
return bottomBase + straightX + (phi - PI * 1.5) * corner;
|
|
346
|
+
}
|
|
347
|
+
if (abs(delta.y) > EPSILON) {
|
|
348
|
+
if (delta.y > 0.0) { return inner.x - p.x; }
|
|
349
|
+
return bottomBase + (p.x + inner.x);
|
|
350
|
+
}
|
|
351
|
+
if (delta.x > 0.0) { return rightBase + (p.y + inner.y); }
|
|
352
|
+
return leftBase + (inner.y - p.y);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Capsule: upper cap, left edge, lower cap, right edge, from the upper-right cap start. */
|
|
356
|
+
fn capsule_perimeter(p: vec2f, radius: f32, halfLength: f32) -> f32 {
|
|
357
|
+
let arc = PI * radius;
|
|
358
|
+
if (p.y >= halfLength) {
|
|
359
|
+
return clamp(atan2(p.y - halfLength, p.x), 0.0, PI) * radius;
|
|
360
|
+
}
|
|
361
|
+
if (p.y <= -halfLength) {
|
|
362
|
+
let angle = clamp(wrap_tau(atan2(p.y + halfLength, p.x)), PI, TAU);
|
|
363
|
+
return arc + 2.0 * halfLength + (angle - PI) * radius;
|
|
364
|
+
}
|
|
365
|
+
if (p.x >= 0.0) { return 2.0 * arc + 2.0 * halfLength + (p.y + halfLength); }
|
|
366
|
+
return arc + (halfLength - p.y);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Annulus: outer ring forwards, then inner ring backwards, so the band stays continuous. */
|
|
370
|
+
fn annulus_perimeter(p: vec2f, outer: f32, inner: f32) -> f32 {
|
|
371
|
+
let radius = length(p);
|
|
372
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
373
|
+
if (abs(radius - outer) <= abs(radius - inner)) { return angle * outer; }
|
|
374
|
+
return TAU * outer + (TAU - angle) * inner;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Arc: only the swept wedge has a perimeter, so samples past an end clamp to that end. */
|
|
378
|
+
fn arc_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
379
|
+
let sweepAbs = abs(sweep);
|
|
380
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
381
|
+
var offset = wrap_tau(angle - start);
|
|
382
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
383
|
+
return min(offset, sweepAbs) * radius;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Sector: arc, then the radius at the arc end, then the radius back to the arc start. */
|
|
387
|
+
fn sector_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
388
|
+
let sweepAbs = abs(sweep);
|
|
389
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
390
|
+
var offset = wrap_tau(angle - start);
|
|
391
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
392
|
+
let startDir = vec2f(cos(start), sin(start)) * radius;
|
|
393
|
+
let endDir = vec2f(cos(start + sweep), sin(start + sweep)) * radius;
|
|
394
|
+
let startEdge = nearest_edge(p, vec2f(0.0), startDir);
|
|
395
|
+
let endEdge = nearest_edge(p, vec2f(0.0), endDir);
|
|
396
|
+
let arcLength = sweepAbs * radius;
|
|
397
|
+
let arcDistance = select(abs(length(p) - radius), min(length(p - startDir), length(p - endDir)), offset > sweepAbs);
|
|
398
|
+
if (arcDistance <= startEdge.x && arcDistance <= endEdge.x) {
|
|
399
|
+
return min(offset, sweepAbs) * radius;
|
|
400
|
+
}
|
|
401
|
+
if (endEdge.x <= startEdge.x) { return arcLength + (radius - endEdge.y); }
|
|
402
|
+
return arcLength + radius + startEdge.y;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Segment: the arc, then the chord from the arc end back to its start. */
|
|
406
|
+
fn segment_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
407
|
+
let sweepAbs = abs(sweep);
|
|
408
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
409
|
+
var offset = wrap_tau(angle - start);
|
|
410
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
411
|
+
let arcStart = vec2f(cos(start), sin(start)) * radius;
|
|
412
|
+
let arcEnd = vec2f(cos(start + sweep), sin(start + sweep)) * radius;
|
|
413
|
+
let chord = nearest_edge(p, arcEnd, arcStart);
|
|
414
|
+
let arcLength = sweepAbs * radius;
|
|
415
|
+
let arcDistance = select(abs(length(p) - radius), min(length(p - arcStart), length(p - arcEnd)), offset > sweepAbs);
|
|
416
|
+
if (arcDistance <= chord.x) { return min(offset, sweepAbs) * radius; }
|
|
417
|
+
return arcLength + chord.y;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Regular polygon: pick the angular sector, then the projection onto that edge. */
|
|
421
|
+
fn polygon_perimeter(p: vec2f, radius: f32, sides: u32) -> f32 {
|
|
422
|
+
let count = max(sides, 3u);
|
|
423
|
+
let step = TAU / f32(count);
|
|
424
|
+
let edgeLength = 2.0 * radius * sin(PI / f32(count));
|
|
425
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
426
|
+
let index = min(u32(floor(angle / step)), count - 1u);
|
|
427
|
+
let start = vec2f(cos(step * f32(index)), sin(step * f32(index))) * radius;
|
|
428
|
+
let end = vec2f(cos(step * f32(index + 1u)), sin(step * f32(index + 1u))) * radius;
|
|
429
|
+
return f32(index) * edgeLength + nearest_edge(p, start, end).y;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Triangle: three edges, from the first vertex. */
|
|
433
|
+
fn triangle_perimeter(p: vec2f, a: vec2f, b: vec2f, c: vec2f) -> f32 {
|
|
434
|
+
let ab = nearest_edge(p, a, b);
|
|
435
|
+
let bc = nearest_edge(p, b, c);
|
|
436
|
+
let ca = nearest_edge(p, c, a);
|
|
437
|
+
let lab = length(b - a);
|
|
438
|
+
let lbc = length(c - b);
|
|
439
|
+
if (ab.x <= bc.x && ab.x <= ca.x) { return ab.y; }
|
|
440
|
+
if (bc.x <= ca.x) { return lab + bc.y; }
|
|
441
|
+
return lab + lbc + ca.y;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Rhombus: four edges through the four diagonal tips, from the +x tip. */
|
|
445
|
+
fn rhombus_perimeter(p: vec2f, halfWidth: f32, halfHeight: f32) -> f32 {
|
|
446
|
+
let v0 = vec2f(halfWidth, 0.0);
|
|
447
|
+
let v1 = vec2f(0.0, halfHeight);
|
|
448
|
+
let v2 = vec2f(-halfWidth, 0.0);
|
|
449
|
+
let v3 = vec2f(0.0, -halfHeight);
|
|
450
|
+
let e0 = nearest_edge(p, v0, v1);
|
|
451
|
+
let e1 = nearest_edge(p, v1, v2);
|
|
452
|
+
let e2 = nearest_edge(p, v2, v3);
|
|
453
|
+
let e3 = nearest_edge(p, v3, v0);
|
|
454
|
+
let l0 = length(v1 - v0);
|
|
455
|
+
let l1 = length(v2 - v1);
|
|
456
|
+
let l2 = length(v3 - v2);
|
|
457
|
+
if (e0.x <= e1.x && e0.x <= e2.x && e0.x <= e3.x) { return e0.y; }
|
|
458
|
+
if (e1.x <= e2.x && e1.x <= e3.x) { return l0 + e1.y; }
|
|
459
|
+
if (e2.x <= e3.x) { return l0 + l1 + e2.y; }
|
|
460
|
+
return l0 + l1 + l2 + e3.y;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Arc length along the outline at the nearest outline point, in shape-space units. Every
|
|
465
|
+
* primitive now has a closed form except the ellipse, which is integrated numerically.
|
|
466
|
+
*/
|
|
467
|
+
fn perimeter_length(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: vec2f, params: vec4f, sides: u32) -> f32 {
|
|
468
|
+
switch shape {
|
|
469
|
+
case SHAPE_RECT: { return rect_perimeter(p, size); }
|
|
470
|
+
case SHAPE_ROUNDED_RECT: { return rounded_rect_perimeter(p, size, params.x); }
|
|
471
|
+
case SHAPE_CIRCLE: { return wrap_tau(atan2(p.y, p.x)) * size.x; }
|
|
472
|
+
case SHAPE_ELLIPSE: {
|
|
473
|
+
let theta = wrap_tau(atan2(p.y / max(size.y, EPSILON), p.x / max(size.x, EPSILON)));
|
|
474
|
+
return ellipse_arc(theta, size);
|
|
475
|
+
}
|
|
476
|
+
case SHAPE_ARC: { return arc_perimeter(p, size.x, params.z, params.w); }
|
|
477
|
+
case SHAPE_ANNULUS: { return annulus_perimeter(p, size.x, size.y); }
|
|
478
|
+
case SHAPE_CAPSULE: { return capsule_perimeter(p, size.x, size.y); }
|
|
479
|
+
case SHAPE_POLYGON: { return polygon_perimeter(p, size.x, sides); }
|
|
480
|
+
case SHAPE_TRIANGLE: { return triangle_perimeter(p, size, vertexB, vertexC); }
|
|
481
|
+
case SHAPE_RHOMBUS: { return rhombus_perimeter(p, size.x, size.y); }
|
|
482
|
+
case SHAPE_SECTOR: { return sector_perimeter(p, size.x, params.z, params.w); }
|
|
483
|
+
case SHAPE_SEGMENT: { return segment_perimeter(p, size.x, params.z, params.w); }
|
|
484
|
+
default: { return -1.0; }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
230
488
|
fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
231
489
|
let ndc = clip.xy / max(clip.w, EPSILON);
|
|
232
490
|
return vec2f((ndc.x * 0.5 + 0.5) * camera.viewportSize.x, (0.5 - ndc.y * 0.5) * camera.viewportSize.y);
|
|
@@ -264,6 +522,7 @@ fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
|
264
522
|
out.rotation = rotation;
|
|
265
523
|
out.sides = (input.flags >> 8u) & 0xffu;
|
|
266
524
|
out.flags = input.flags;
|
|
525
|
+
out.dash = input.dash;
|
|
267
526
|
return out;
|
|
268
527
|
}
|
|
269
528
|
|
|
@@ -280,7 +539,35 @@ fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
|
280
539
|
// Relative to the outer edge of the stroke, so one coverage ramp serves fill and stroke alike.
|
|
281
540
|
let signedPixel = select(abs(distancePixel) - max(input.params.y, 0.0) * 0.5, distancePixel, filled);
|
|
282
541
|
let width = max(fwidth(signedPixel), EPSILON);
|
|
283
|
-
|
|
542
|
+
var coverage = clamp(0.5 - signedPixel / width, 0.0, 1.0);
|
|
543
|
+
if (coverage <= 0.0) { discard; }
|
|
544
|
+
|
|
545
|
+
// Dashed outlines: walk the perimeter parameter and cut the stroke in the gaps. The transition
|
|
546
|
+
// is spread over one canvas pixel so a dash edge is as soft as the shape's own edge.
|
|
547
|
+
if ((input.flags & 1u) == 0u && input.dash.y > 0.0) {
|
|
548
|
+
let path = perimeter_length(input.shape, local, input.size, input.vertexB, input.vertexC, input.params, input.sides);
|
|
549
|
+
if (path >= 0.0) {
|
|
550
|
+
let requested = input.dash.x + input.dash.y;
|
|
551
|
+
let total = perimeter_total(input.shape, input.size, input.vertexB, input.vertexC, input.params, input.sides);
|
|
552
|
+
var period = requested;
|
|
553
|
+
var visible = input.dash.x;
|
|
554
|
+
if (total > 0.0) {
|
|
555
|
+
// Snap the period to a whole number of turns, keeping the requested duty cycle: the
|
|
556
|
+
// closed outline then ends exactly where its dash pattern started.
|
|
557
|
+
let count = max(round(total / requested), 1.0);
|
|
558
|
+
period = total / count;
|
|
559
|
+
visible = period * (input.dash.x / requested);
|
|
560
|
+
}
|
|
561
|
+
let along = fract((path + input.dash.z) / period) * period;
|
|
562
|
+
let pixel = 1.0 / scale;
|
|
563
|
+
// Distance to the nearest pattern boundary: the dash start, the dash end, and the next
|
|
564
|
+
// dash start that fract() wraps around to.
|
|
565
|
+
let edge = min(min(along, abs(along - visible)), period - along);
|
|
566
|
+
var dashCoverage = smoothstep(0.0, pixel, edge);
|
|
567
|
+
if (along > visible) { dashCoverage = 1.0 - dashCoverage; }
|
|
568
|
+
coverage = coverage * dashCoverage;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
284
571
|
if (coverage <= 0.0) { discard; }
|
|
285
572
|
return vec4f(input.color.rgb, input.color.a * coverage);
|
|
286
573
|
}
|