@idle-screens/schema 3.12.0 → 3.13.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/FORMAT.md +111 -19
- package/dist/index.d.ts +170 -9
- package/dist/index.js +1998 -344
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +105 -9
package/dist/index.js
CHANGED
|
@@ -279,11 +279,21 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
279
279
|
const walk = (a, b, key, root = false) => {
|
|
280
280
|
if (typeof a === "number" && typeof b === "number") {
|
|
281
281
|
const v = a + (b - a) * kk;
|
|
282
|
-
|
|
282
|
+
if (key === "count") return Math.max(1, Math.round(v));
|
|
283
|
+
if (key === "octaves") return Math.round(v);
|
|
284
|
+
if (key === "quantize") {
|
|
285
|
+
const q = Math.round(v);
|
|
286
|
+
return q < 2 ? 0 : q;
|
|
287
|
+
}
|
|
288
|
+
return v;
|
|
283
289
|
}
|
|
284
290
|
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
285
291
|
return lerpHex(a, b, kk);
|
|
286
292
|
}
|
|
293
|
+
if (key === "path" && typeof a === "string" && typeof b === "string" && a !== b) {
|
|
294
|
+
const m = lerpPathD(a, b, kk);
|
|
295
|
+
if (m !== null) return m;
|
|
296
|
+
}
|
|
287
297
|
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
288
298
|
return b.map((bv, i) => walk(a[i], bv, i));
|
|
289
299
|
}
|
|
@@ -300,6 +310,33 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
300
310
|
};
|
|
301
311
|
return walk(from, to, rootKey, specRoot);
|
|
302
312
|
}
|
|
313
|
+
var PATH_TOKEN = /[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
|
|
314
|
+
function lerpPathD(a, b, k) {
|
|
315
|
+
const ta = a.match(PATH_TOKEN) ?? [];
|
|
316
|
+
const tb = b.match(PATH_TOKEN) ?? [];
|
|
317
|
+
if (ta.length !== tb.length) return null;
|
|
318
|
+
const out = [];
|
|
319
|
+
let cmd = "";
|
|
320
|
+
let argIndex = 0;
|
|
321
|
+
for (let i = 0; i < ta.length; i++) {
|
|
322
|
+
const x = ta[i], y = tb[i];
|
|
323
|
+
const isCmdA = /^[a-zA-Z]$/.test(x), isCmdB = /^[a-zA-Z]$/.test(y);
|
|
324
|
+
if (isCmdA !== isCmdB) return null;
|
|
325
|
+
if (isCmdA) {
|
|
326
|
+
if (x !== y) return null;
|
|
327
|
+
cmd = x.toUpperCase();
|
|
328
|
+
argIndex = 0;
|
|
329
|
+
out.push(x);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const va = Number(x), vb = Number(y);
|
|
333
|
+
const flag = cmd === "A" && (argIndex % 7 === 3 || argIndex % 7 === 4);
|
|
334
|
+
argIndex++;
|
|
335
|
+
const v = va + (vb - va) * k;
|
|
336
|
+
out.push(flag ? String(Math.round(v)) : String(+v.toFixed(5)));
|
|
337
|
+
}
|
|
338
|
+
return out.join(" ");
|
|
339
|
+
}
|
|
303
340
|
function textStringsOf(sprite) {
|
|
304
341
|
if (sprite.kind === "text") return sprite.strings;
|
|
305
342
|
if (sprite.kind === "textBlock") return [sprite.text];
|
|
@@ -356,7 +393,7 @@ function steerablePaths(spec) {
|
|
|
356
393
|
if (!spec || typeof spec !== "object") return [];
|
|
357
394
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer", "group", "origin"]);
|
|
358
395
|
const SKIP_ROOT = /* @__PURE__ */ new Set(["timeline"]);
|
|
359
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
396
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands", "parts"]);
|
|
360
397
|
const layers = spec.layers;
|
|
361
398
|
const shadowedRootKeys = new Set(
|
|
362
399
|
Array.isArray(layers) ? layers.map((l) => l && typeof l === "object" ? l.key : void 0).filter((k) => typeof k === "string" && STEERABLE_ROOT_KEYS.has(k)) : []
|
|
@@ -668,6 +705,654 @@ function timelineSampleTimesAfter(spec, now, dense = false, only) {
|
|
|
668
705
|
return local.filter((s) => s > now);
|
|
669
706
|
}
|
|
670
707
|
|
|
708
|
+
// src/shapes.ts
|
|
709
|
+
function isShapedSprite(s) {
|
|
710
|
+
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
711
|
+
}
|
|
712
|
+
function barFraction(s, index) {
|
|
713
|
+
const n = s.values.length;
|
|
714
|
+
if (n === 0) return 0;
|
|
715
|
+
const v = s.values[(index % n + n) % n] ?? 0;
|
|
716
|
+
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
717
|
+
if (!(max > 0)) return 0;
|
|
718
|
+
return Math.max(0, Math.min(1, v / max));
|
|
719
|
+
}
|
|
720
|
+
function barBox(direction, len, thick) {
|
|
721
|
+
switch (direction) {
|
|
722
|
+
case "left":
|
|
723
|
+
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
724
|
+
case "up":
|
|
725
|
+
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
726
|
+
case "down":
|
|
727
|
+
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
728
|
+
default:
|
|
729
|
+
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
var DEFAULT_POLYGON_SIDES = 6;
|
|
733
|
+
var STROKE_SAMPLES = 24;
|
|
734
|
+
function polygonPoints(s, radius) {
|
|
735
|
+
if (isPathPolygon(s)) {
|
|
736
|
+
const unit = flattenSprite(s);
|
|
737
|
+
if (unit.points.length >= 3) return unit.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
738
|
+
}
|
|
739
|
+
if (s.points && s.points.length >= 3) {
|
|
740
|
+
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
741
|
+
}
|
|
742
|
+
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
743
|
+
const out = [];
|
|
744
|
+
for (let k = 0; k < n; k++) {
|
|
745
|
+
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
746
|
+
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
function polygonArea(pts) {
|
|
751
|
+
let a = 0;
|
|
752
|
+
for (let i = 0; i < pts.length; i++) {
|
|
753
|
+
const p = pts[i];
|
|
754
|
+
const q = pts[(i + 1) % pts.length];
|
|
755
|
+
a += p.x * q.y - q.x * p.y;
|
|
756
|
+
}
|
|
757
|
+
return Math.abs(a) / 2;
|
|
758
|
+
}
|
|
759
|
+
function polygonFill(s, radius) {
|
|
760
|
+
if (radius <= 0) return 0;
|
|
761
|
+
const disc = Math.PI * radius * radius;
|
|
762
|
+
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
763
|
+
}
|
|
764
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
765
|
+
const u2 = u * u;
|
|
766
|
+
const u3 = u2 * u;
|
|
767
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
768
|
+
}
|
|
769
|
+
function strokeSamples(s, halfSize, n) {
|
|
770
|
+
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
771
|
+
const m = pts.length;
|
|
772
|
+
if (m < 2) return pts;
|
|
773
|
+
const smooth = s.curve !== "linear" && m >= 3;
|
|
774
|
+
const segs = m - 1;
|
|
775
|
+
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
776
|
+
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
777
|
+
const out = [];
|
|
778
|
+
for (let i = 0; i < n; i++) {
|
|
779
|
+
const u = i / (n - 1) * segs;
|
|
780
|
+
const seg = Math.min(segs - 1, Math.floor(u));
|
|
781
|
+
const local = u - seg;
|
|
782
|
+
const p1 = at(seg);
|
|
783
|
+
const p2 = at(seg + 1);
|
|
784
|
+
if (!smooth) {
|
|
785
|
+
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
786
|
+
} else {
|
|
787
|
+
const p0 = at(seg - 1);
|
|
788
|
+
const p3 = at(seg + 2);
|
|
789
|
+
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
function pathLength(pts) {
|
|
795
|
+
let l = 0;
|
|
796
|
+
for (let i = 1; i < pts.length; i++) {
|
|
797
|
+
const dx = pts[i].x - pts[i - 1].x;
|
|
798
|
+
const dy = pts[i].y - pts[i - 1].y;
|
|
799
|
+
l += Math.sqrt(dx * dx + dy * dy);
|
|
800
|
+
}
|
|
801
|
+
return l;
|
|
802
|
+
}
|
|
803
|
+
function strokeTaper(u) {
|
|
804
|
+
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
805
|
+
}
|
|
806
|
+
function strokeWidthPx(s, scale) {
|
|
807
|
+
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
808
|
+
}
|
|
809
|
+
function featherRamp(u, feather) {
|
|
810
|
+
const band = Math.max(0, Math.min(1, feather)) / 2;
|
|
811
|
+
const d = Math.min(u, 1 - u);
|
|
812
|
+
if (band <= 0) return d >= 0 ? 1 : 0;
|
|
813
|
+
return Math.max(0, Math.min(1, d / band));
|
|
814
|
+
}
|
|
815
|
+
var FEATHER_MASK_SIZE = 128;
|
|
816
|
+
function ribbonSides(pts, half) {
|
|
817
|
+
const n = pts.length;
|
|
818
|
+
const left = [];
|
|
819
|
+
const right = [];
|
|
820
|
+
if (n < 2) return { left, right };
|
|
821
|
+
const dir = (a, b) => {
|
|
822
|
+
const dx = b.x - a.x;
|
|
823
|
+
const dy = b.y - a.y;
|
|
824
|
+
const l = Math.hypot(dx, dy);
|
|
825
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
826
|
+
};
|
|
827
|
+
for (let i = 0; i < n; i++) {
|
|
828
|
+
const tIn = i > 0 ? dir(pts[i - 1], pts[i]) : dir(pts[0], pts[1]);
|
|
829
|
+
const tOut = i < n - 1 ? dir(pts[i], pts[i + 1]) : tIn;
|
|
830
|
+
let tx = tIn.x + tOut.x;
|
|
831
|
+
let ty = tIn.y + tOut.y;
|
|
832
|
+
const tl = Math.hypot(tx, ty);
|
|
833
|
+
if (tl < 1e-9) {
|
|
834
|
+
tx = tOut.x;
|
|
835
|
+
ty = tOut.y;
|
|
836
|
+
} else {
|
|
837
|
+
tx /= tl;
|
|
838
|
+
ty /= tl;
|
|
839
|
+
}
|
|
840
|
+
const hw = half[i] / Math.max(0.5, tx * tOut.x + ty * tOut.y);
|
|
841
|
+
const p = pts[i];
|
|
842
|
+
left.push({ x: p.x - ty * hw, y: p.y + tx * hw });
|
|
843
|
+
right.push({ x: p.x + ty * hw, y: p.y - tx * hw });
|
|
844
|
+
}
|
|
845
|
+
return { left, right };
|
|
846
|
+
}
|
|
847
|
+
function strokeOutline(pts, lw, taper = true) {
|
|
848
|
+
const n = pts.length;
|
|
849
|
+
if (n < 2) return [];
|
|
850
|
+
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
|
|
851
|
+
const dir = (a, b) => {
|
|
852
|
+
const dx = b.x - a.x;
|
|
853
|
+
const dy = b.y - a.y;
|
|
854
|
+
const l = Math.hypot(dx, dy);
|
|
855
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
856
|
+
};
|
|
857
|
+
const { left, right } = ribbonSides(pts, Array.from({ length: n }, (_, i) => half(i)));
|
|
858
|
+
const cap = (c, r, from) => {
|
|
859
|
+
const out = [];
|
|
860
|
+
const steps = 6;
|
|
861
|
+
for (let k = 1; k < steps; k++) {
|
|
862
|
+
const a = from - Math.PI * k / steps;
|
|
863
|
+
out.push({ x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r });
|
|
864
|
+
}
|
|
865
|
+
return out;
|
|
866
|
+
};
|
|
867
|
+
const endT = dir(pts[n - 2], pts[n - 1]);
|
|
868
|
+
const startT = dir(pts[0], pts[1]);
|
|
869
|
+
const endA = Math.atan2(endT.y, endT.x) + Math.PI / 2;
|
|
870
|
+
const startA = Math.atan2(startT.y, startT.x) - Math.PI / 2;
|
|
871
|
+
return [
|
|
872
|
+
...left,
|
|
873
|
+
...cap(pts[n - 1], half(n - 1), endA),
|
|
874
|
+
...right.reverse(),
|
|
875
|
+
...cap(pts[0], half(0), startA)
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
var MAX_PATH_POINTS = 4096;
|
|
879
|
+
var MAX_SCENE_PATH_POINTS = 16384;
|
|
880
|
+
var PATH_TOLERANCE = 5e-4;
|
|
881
|
+
var pathCache = { auto: /* @__PURE__ */ new Map(), nonzero: /* @__PURE__ */ new Map(), evenodd: /* @__PURE__ */ new Map() };
|
|
882
|
+
function flattenPath(d, rule = "auto") {
|
|
883
|
+
const cache = pathCache[rule] ?? pathCache.auto;
|
|
884
|
+
const hit = cache.get(d);
|
|
885
|
+
if (hit) return hit;
|
|
886
|
+
let out;
|
|
887
|
+
try {
|
|
888
|
+
const parsed = parseAndFlatten(d);
|
|
889
|
+
const rings = parsed.rings.map(simplifyRing).filter((r) => r.length >= 3);
|
|
890
|
+
const lines = parsed.lines.map((l) => ({ points: l.closed ? simplifyRing(l.points) : simplifyLine(l.points), closed: l.closed }));
|
|
891
|
+
out = { points: rule === "auto" ? joinRings(rings) : bridge(rings), subpaths: rings.length, lines };
|
|
892
|
+
} catch (e) {
|
|
893
|
+
out = { points: [], subpaths: 0, lines: [], error: e instanceof Error ? e.message : String(e) };
|
|
894
|
+
}
|
|
895
|
+
if (cache.size > 256) cache.clear();
|
|
896
|
+
cache.set(d, out);
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
899
|
+
var spriteFlat = /* @__PURE__ */ new WeakMap();
|
|
900
|
+
function flattenSprite(s) {
|
|
901
|
+
const d = s.parts ? s.parts.filter((p) => p.fill !== false).map((p) => p.path).join(" ") : s.path ?? "";
|
|
902
|
+
const rule = s.parts ? "auto" : s.fillRule ?? "auto";
|
|
903
|
+
const hit = spriteFlat.get(s);
|
|
904
|
+
if (hit && hit.d === d && hit.rule === rule) return hit.flat;
|
|
905
|
+
const flat = flattenPath(d, rule);
|
|
906
|
+
spriteFlat.set(s, { d, rule, flat });
|
|
907
|
+
return flat;
|
|
908
|
+
}
|
|
909
|
+
function polygonFillRule(s) {
|
|
910
|
+
return typeof s.path === "string" && s.fillRule === "evenodd" ? "evenodd" : "nonzero";
|
|
911
|
+
}
|
|
912
|
+
function isPathPolygon(s) {
|
|
913
|
+
return s.kind === "polygon" && (typeof s.path === "string" || Array.isArray(s.parts));
|
|
914
|
+
}
|
|
915
|
+
var spriteParts = /* @__PURE__ */ new WeakMap();
|
|
916
|
+
function pathParts(s) {
|
|
917
|
+
const key = s.parts ?? s.path;
|
|
918
|
+
const hit = spriteParts.get(s);
|
|
919
|
+
if (hit && hit.key === key) return hit.parts;
|
|
920
|
+
const src = s.parts ?? (typeof s.path === "string" ? [{ path: s.path, fillRule: s.fillRule, fill: s.fill, gradient: s.gradient, stroke: s.stroke }] : []);
|
|
921
|
+
const parts = src.map((p) => {
|
|
922
|
+
const flat = flattenPath(p.path, p.fillRule ?? "auto");
|
|
923
|
+
return {
|
|
924
|
+
flat,
|
|
925
|
+
color: s.parts ? p.color ?? null : null,
|
|
926
|
+
fill: p.fill !== false,
|
|
927
|
+
rule: p.fillRule === "evenodd" ? "evenodd" : "nonzero",
|
|
928
|
+
gradient: p.gradient,
|
|
929
|
+
stroke: p.stroke,
|
|
930
|
+
opacity: p.opacity ?? 1,
|
|
931
|
+
box: boxOfFlat(flat)
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
spriteParts.set(s, { key, parts });
|
|
935
|
+
return parts;
|
|
936
|
+
}
|
|
937
|
+
function pathFrame(s, r, w, h) {
|
|
938
|
+
const span = s.span ?? "box";
|
|
939
|
+
const sx = span === "width" || span === "screen" ? w / 2 : r;
|
|
940
|
+
const sy = span === "height" || span === "screen" ? h / 2 : r;
|
|
941
|
+
const [ox, oy] = s.origin ?? [0, 0];
|
|
942
|
+
return { sx, sy, ox, oy, uniform: sx === sy };
|
|
943
|
+
}
|
|
944
|
+
var flatBoxes = /* @__PURE__ */ new WeakMap();
|
|
945
|
+
function boxOfFlat(flat) {
|
|
946
|
+
const hit = flatBoxes.get(flat);
|
|
947
|
+
if (hit) return hit;
|
|
948
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
949
|
+
for (const [x, y] of flat.points.length ? flat.points : flat.lines.flatMap((l) => l.points)) {
|
|
950
|
+
x0 = Math.min(x0, x);
|
|
951
|
+
x1 = Math.max(x1, x);
|
|
952
|
+
y0 = Math.min(y0, y);
|
|
953
|
+
y1 = Math.max(y1, y);
|
|
954
|
+
}
|
|
955
|
+
if (!Number.isFinite(x0)) {
|
|
956
|
+
x0 = -1;
|
|
957
|
+
x1 = 1;
|
|
958
|
+
y0 = -1;
|
|
959
|
+
y1 = 1;
|
|
960
|
+
}
|
|
961
|
+
const box = { cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, hw: Math.max((x1 - x0) / 2, 1e-6), hh: Math.max((y1 - y0) / 2, 1e-6) };
|
|
962
|
+
flatBoxes.set(flat, box);
|
|
963
|
+
return box;
|
|
964
|
+
}
|
|
965
|
+
var pathIds = /* @__PURE__ */ new Map();
|
|
966
|
+
var pathSeq = 0;
|
|
967
|
+
var paintSigs = /* @__PURE__ */ new WeakMap();
|
|
968
|
+
function pathPaintSig(s) {
|
|
969
|
+
const hit = paintSigs.get(s);
|
|
970
|
+
if (hit !== void 0) return hit;
|
|
971
|
+
const sig = JSON.stringify(s, (k, v) => {
|
|
972
|
+
if (k !== "path" || typeof v !== "string") return v;
|
|
973
|
+
let id = pathIds.get(v);
|
|
974
|
+
if (id === void 0) {
|
|
975
|
+
if (pathIds.size > 4096) pathIds.clear();
|
|
976
|
+
id = ++pathSeq;
|
|
977
|
+
pathIds.set(v, id);
|
|
978
|
+
}
|
|
979
|
+
return `#${id}`;
|
|
980
|
+
});
|
|
981
|
+
paintSigs.set(s, sig);
|
|
982
|
+
return sig;
|
|
983
|
+
}
|
|
984
|
+
function gradientSpace(part, u, v) {
|
|
985
|
+
if (part.gradient?.units !== "part") return [u, v];
|
|
986
|
+
return [(u - part.box.cx) / part.box.hw, (v - part.box.cy) / part.box.hh];
|
|
987
|
+
}
|
|
988
|
+
function segmentsFor(chord, curvature) {
|
|
989
|
+
const n = Math.ceil(Math.sqrt(Math.max(curvature, chord * 0.05) / (4 * PATH_TOLERANCE)));
|
|
990
|
+
return Math.max(2, Math.min(64, n));
|
|
991
|
+
}
|
|
992
|
+
function parseAndFlatten(d) {
|
|
993
|
+
const tokens = d.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
|
|
994
|
+
let i = 0;
|
|
995
|
+
const num = () => {
|
|
996
|
+
const t = tokens[i++];
|
|
997
|
+
if (t === void 0 || /^[a-zA-Z]$/.test(t)) throw new Error("expected a number");
|
|
998
|
+
return Number(t);
|
|
999
|
+
};
|
|
1000
|
+
const flag = () => {
|
|
1001
|
+
const t = tokens[i];
|
|
1002
|
+
if (t !== void 0 && /^[01]{2,}/.test(t)) {
|
|
1003
|
+
tokens[i] = t.slice(1);
|
|
1004
|
+
return Number(t[0]);
|
|
1005
|
+
}
|
|
1006
|
+
return num();
|
|
1007
|
+
};
|
|
1008
|
+
const rings = [];
|
|
1009
|
+
let ring = [];
|
|
1010
|
+
let cx = 0, cy = 0, sx = 0, sy = 0;
|
|
1011
|
+
let lastCtrl = null;
|
|
1012
|
+
let lastCmd = "";
|
|
1013
|
+
const push = (x, y) => {
|
|
1014
|
+
ring.push([x, y]);
|
|
1015
|
+
cx = x;
|
|
1016
|
+
cy = y;
|
|
1017
|
+
};
|
|
1018
|
+
const lines = [];
|
|
1019
|
+
const close = (closed = false) => {
|
|
1020
|
+
if (ring.length >= 2) lines.push({ points: ring.slice(), closed });
|
|
1021
|
+
if (ring.length >= 3) rings.push(ring);
|
|
1022
|
+
ring = [];
|
|
1023
|
+
};
|
|
1024
|
+
const cubic = (x1, y1, x2, y2, x, y) => {
|
|
1025
|
+
const dev = Math.hypot(x1 - cx, y1 - cy) + Math.hypot(x2 - x1, y2 - y1) + Math.hypot(x - x2, y - y2) - Math.hypot(x - cx, y - cy);
|
|
1026
|
+
const n = segmentsFor(Math.hypot(x - cx, y - cy), dev * 4);
|
|
1027
|
+
const x0 = cx, y0 = cy;
|
|
1028
|
+
for (let k = 1; k <= n; k++) {
|
|
1029
|
+
const t = k / n, u = 1 - t;
|
|
1030
|
+
push(
|
|
1031
|
+
u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x,
|
|
1032
|
+
u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
lastCtrl = [x2, y2];
|
|
1036
|
+
};
|
|
1037
|
+
const quad = (x1, y1, x, y) => {
|
|
1038
|
+
const dev = Math.hypot(x1 - cx, y1 - cy) + Math.hypot(x - x1, y - y1) - Math.hypot(x - cx, y - cy);
|
|
1039
|
+
const n = segmentsFor(Math.hypot(x - cx, y - cy), dev * 4);
|
|
1040
|
+
const x0 = cx, y0 = cy;
|
|
1041
|
+
for (let k = 1; k <= n; k++) {
|
|
1042
|
+
const t = k / n, u = 1 - t;
|
|
1043
|
+
push(u * u * x0 + 2 * u * t * x1 + t * t * x, u * u * y0 + 2 * u * t * y1 + t * t * y);
|
|
1044
|
+
}
|
|
1045
|
+
lastCtrl = [x1, y1];
|
|
1046
|
+
};
|
|
1047
|
+
const arc = (rx, ry, phiDeg, large, sweep, x, y) => {
|
|
1048
|
+
const x0 = cx, y0 = cy;
|
|
1049
|
+
if (rx === 0 || ry === 0 || x0 === x && y0 === y) {
|
|
1050
|
+
push(x, y);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
rx = Math.abs(rx);
|
|
1054
|
+
ry = Math.abs(ry);
|
|
1055
|
+
const phi = phiDeg * Math.PI / 180, cp = Math.cos(phi), sp = Math.sin(phi);
|
|
1056
|
+
const dx = (x0 - x) / 2, dy = (y0 - y) / 2;
|
|
1057
|
+
const x1p = cp * dx + sp * dy, y1p = -sp * dx + cp * dy;
|
|
1058
|
+
const lam = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
1059
|
+
if (lam > 1) {
|
|
1060
|
+
rx *= Math.sqrt(lam);
|
|
1061
|
+
ry *= Math.sqrt(lam);
|
|
1062
|
+
}
|
|
1063
|
+
const num2 = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
1064
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
1065
|
+
let co = Math.sqrt(Math.max(0, num2 / den));
|
|
1066
|
+
if (large === sweep) co = -co;
|
|
1067
|
+
const cxp = co * rx * y1p / ry, cyp = -co * ry * x1p / rx;
|
|
1068
|
+
const ccx = cp * cxp - sp * cyp + (x0 + x) / 2, ccy = sp * cxp + cp * cyp + (y0 + y) / 2;
|
|
1069
|
+
const ang = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
1070
|
+
const t1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
1071
|
+
let dt = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
1072
|
+
if (!sweep && dt > 0) dt -= 2 * Math.PI;
|
|
1073
|
+
if (sweep && dt < 0) dt += 2 * Math.PI;
|
|
1074
|
+
const r = Math.max(rx, ry);
|
|
1075
|
+
const n = Math.max(2, Math.min(96, Math.ceil(Math.abs(dt) / (2 * Math.acos(Math.max(0, 1 - PATH_TOLERANCE / r))))));
|
|
1076
|
+
for (let k = 1; k <= n; k++) {
|
|
1077
|
+
const t = t1 + dt * k / n;
|
|
1078
|
+
const ex = rx * Math.cos(t), ey = ry * Math.sin(t);
|
|
1079
|
+
push(cp * ex - sp * ey + ccx, sp * ex + cp * ey + ccy);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
while (i < tokens.length) {
|
|
1083
|
+
let cmd = tokens[i];
|
|
1084
|
+
if (/^[a-zA-Z]$/.test(cmd)) i++;
|
|
1085
|
+
else if (lastCmd) cmd = lastCmd === "M" ? "L" : lastCmd === "m" ? "l" : lastCmd;
|
|
1086
|
+
else throw new Error("path must start with M");
|
|
1087
|
+
const rel = cmd === cmd.toLowerCase() && cmd !== "z" ? true : false;
|
|
1088
|
+
const ox = rel ? cx : 0, oy = rel ? cy : 0;
|
|
1089
|
+
const C = cmd.toUpperCase();
|
|
1090
|
+
const prevCmd = lastCmd.toUpperCase();
|
|
1091
|
+
if (C !== "S" && C !== "T") lastCtrl = null;
|
|
1092
|
+
switch (C) {
|
|
1093
|
+
case "M": {
|
|
1094
|
+
close();
|
|
1095
|
+
const x = num() + ox, y = num() + oy;
|
|
1096
|
+
push(x, y);
|
|
1097
|
+
sx = x;
|
|
1098
|
+
sy = y;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
case "L":
|
|
1102
|
+
push(num() + ox, num() + oy);
|
|
1103
|
+
break;
|
|
1104
|
+
case "H":
|
|
1105
|
+
push(num() + ox, cy);
|
|
1106
|
+
break;
|
|
1107
|
+
case "V":
|
|
1108
|
+
push(cx, num() + oy);
|
|
1109
|
+
break;
|
|
1110
|
+
case "C": {
|
|
1111
|
+
const a = num() + ox, b = num() + oy, c = num() + ox, e = num() + oy;
|
|
1112
|
+
cubic(a, b, c, e, num() + ox, num() + oy);
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case "S": {
|
|
1116
|
+
const r = lastCtrl && (prevCmd === "C" || prevCmd === "S") ? [2 * cx - lastCtrl[0], 2 * cy - lastCtrl[1]] : [cx, cy];
|
|
1117
|
+
const c = num() + ox, e = num() + oy;
|
|
1118
|
+
cubic(r[0], r[1], c, e, num() + ox, num() + oy);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "Q": {
|
|
1122
|
+
const a = num() + ox, b = num() + oy;
|
|
1123
|
+
quad(a, b, num() + ox, num() + oy);
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case "T": {
|
|
1127
|
+
const r = lastCtrl && (prevCmd === "Q" || prevCmd === "T") ? [2 * cx - lastCtrl[0], 2 * cy - lastCtrl[1]] : [cx, cy];
|
|
1128
|
+
quad(r[0], r[1], num() + ox, num() + oy);
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case "A": {
|
|
1132
|
+
const rx = num(), ry = num(), ph = num(), la = flag(), sw = flag();
|
|
1133
|
+
arc(rx, ry, ph, la, sw, num() + ox, num() + oy);
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
case "Z":
|
|
1137
|
+
cx = sx;
|
|
1138
|
+
cy = sy;
|
|
1139
|
+
close(true);
|
|
1140
|
+
break;
|
|
1141
|
+
default:
|
|
1142
|
+
throw new Error(`unsupported path command '${cmd}'`);
|
|
1143
|
+
}
|
|
1144
|
+
lastCmd = cmd;
|
|
1145
|
+
}
|
|
1146
|
+
close();
|
|
1147
|
+
for (const r of rings) {
|
|
1148
|
+
const a = r[0], b = r[r.length - 1];
|
|
1149
|
+
if (Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-9) r.pop();
|
|
1150
|
+
}
|
|
1151
|
+
for (const l of lines) {
|
|
1152
|
+
const a = l.points[0], b = l.points[l.points.length - 1];
|
|
1153
|
+
if (l.closed && l.points.length > 2 && Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-9) l.points.pop();
|
|
1154
|
+
}
|
|
1155
|
+
return { rings, lines };
|
|
1156
|
+
}
|
|
1157
|
+
function simplifyLine(r) {
|
|
1158
|
+
if (r.length <= 2) return r;
|
|
1159
|
+
const keep = new Uint8Array(r.length);
|
|
1160
|
+
keep[0] = 1;
|
|
1161
|
+
keep[r.length - 1] = 1;
|
|
1162
|
+
const stack = [[0, r.length - 1]];
|
|
1163
|
+
while (stack.length) {
|
|
1164
|
+
const [a, b] = stack.pop();
|
|
1165
|
+
const A = r[a], B = r[b];
|
|
1166
|
+
const dx = B[0] - A[0], dy = B[1] - A[1], len = Math.hypot(dx, dy);
|
|
1167
|
+
let best = -1, bd = 0;
|
|
1168
|
+
for (let k = a + 1; k < b; k++) {
|
|
1169
|
+
const P = r[k];
|
|
1170
|
+
const d = len > 1e-12 ? Math.abs(dx * (A[1] - P[1]) - dy * (A[0] - P[0])) / len : Math.hypot(P[0] - A[0], P[1] - A[1]);
|
|
1171
|
+
if (d > bd) {
|
|
1172
|
+
bd = d;
|
|
1173
|
+
best = k;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (best >= 0 && bd > PATH_TOLERANCE) {
|
|
1177
|
+
keep[best] = 1;
|
|
1178
|
+
stack.push([a, best], [best, b]);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return r.filter((_, k) => keep[k] === 1);
|
|
1182
|
+
}
|
|
1183
|
+
function simplifyRing(r) {
|
|
1184
|
+
if (r.length <= 4) return r;
|
|
1185
|
+
let far = 0, fd = -1;
|
|
1186
|
+
for (let k = 1; k < r.length; k++) {
|
|
1187
|
+
const d = (r[k][0] - r[0][0]) ** 2 + (r[k][1] - r[0][1]) ** 2;
|
|
1188
|
+
if (d > fd) {
|
|
1189
|
+
fd = d;
|
|
1190
|
+
far = k;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const keep = new Uint8Array(r.length);
|
|
1194
|
+
keep[0] = 1;
|
|
1195
|
+
keep[far] = 1;
|
|
1196
|
+
const at = (k) => r[k % r.length];
|
|
1197
|
+
const stack = [[0, far], [far, r.length]];
|
|
1198
|
+
while (stack.length) {
|
|
1199
|
+
const [a, b] = stack.pop();
|
|
1200
|
+
const A = at(a), B = at(b);
|
|
1201
|
+
const dx = B[0] - A[0], dy = B[1] - A[1], len = Math.hypot(dx, dy);
|
|
1202
|
+
let best = -1, bd = 0;
|
|
1203
|
+
for (let k = a + 1; k < b; k++) {
|
|
1204
|
+
const P = at(k);
|
|
1205
|
+
const d = len > 1e-12 ? Math.abs(dx * (A[1] - P[1]) - dy * (A[0] - P[0])) / len : Math.hypot(P[0] - A[0], P[1] - A[1]);
|
|
1206
|
+
if (d > bd) {
|
|
1207
|
+
bd = d;
|
|
1208
|
+
best = k;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (best >= 0 && bd > PATH_TOLERANCE) {
|
|
1212
|
+
keep[best % r.length] = 1;
|
|
1213
|
+
stack.push([a, best], [best, b]);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return r.filter((_, k) => keep[k] === 1);
|
|
1217
|
+
}
|
|
1218
|
+
function signedArea(r) {
|
|
1219
|
+
let a = 0;
|
|
1220
|
+
for (let k = 0; k < r.length; k++) {
|
|
1221
|
+
const p = r[k], q = r[(k + 1) % r.length];
|
|
1222
|
+
a += p[0] * q[1] - q[0] * p[1];
|
|
1223
|
+
}
|
|
1224
|
+
return a / 2;
|
|
1225
|
+
}
|
|
1226
|
+
function inside(pt, r) {
|
|
1227
|
+
let c = false;
|
|
1228
|
+
for (let k = 0, j = r.length - 1; k < r.length; j = k++) {
|
|
1229
|
+
const a = r[k], b = r[j];
|
|
1230
|
+
if (a[1] > pt[1] !== b[1] > pt[1] && pt[0] < (b[0] - a[0]) * (pt[1] - a[1]) / (b[1] - a[1]) + a[0]) c = !c;
|
|
1231
|
+
}
|
|
1232
|
+
return c;
|
|
1233
|
+
}
|
|
1234
|
+
function containsRing(r, rb, sub, sb) {
|
|
1235
|
+
if (sb.x0 < rb.x0 || sb.x1 > rb.x1 || sb.y0 < rb.y0 || sb.y1 > rb.y1) return false;
|
|
1236
|
+
for (const p of sub) if (!inside(p, r)) return false;
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
function boxOf(r) {
|
|
1240
|
+
const b = { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity };
|
|
1241
|
+
for (const [x, y] of r) {
|
|
1242
|
+
b.x0 = Math.min(b.x0, x);
|
|
1243
|
+
b.x1 = Math.max(b.x1, x);
|
|
1244
|
+
b.y0 = Math.min(b.y0, y);
|
|
1245
|
+
b.y1 = Math.max(b.y1, y);
|
|
1246
|
+
}
|
|
1247
|
+
return b;
|
|
1248
|
+
}
|
|
1249
|
+
function joinRings(rings) {
|
|
1250
|
+
if (rings.length === 0) return [];
|
|
1251
|
+
const boxes = rings.map(boxOf);
|
|
1252
|
+
const areas = rings.map((r) => Math.abs(signedArea(r)));
|
|
1253
|
+
const order = rings.map((_, i) => i).sort((p, q) => areas[q] - areas[p]);
|
|
1254
|
+
const mult = new Array(rings.length).fill(0);
|
|
1255
|
+
const net = new Array(rings.length).fill(0);
|
|
1256
|
+
const done = [];
|
|
1257
|
+
for (const n of order) {
|
|
1258
|
+
const containers = done.filter((m) => areas[m] > areas[n] && containsRing(rings[m], boxes[m], rings[n], boxes[n]));
|
|
1259
|
+
const around = containers.reduce((sum, m) => sum + mult[m], 0);
|
|
1260
|
+
let want = 1;
|
|
1261
|
+
if (containers.length) {
|
|
1262
|
+
const parent = containers.reduce((p, m) => areas[m] < areas[p] ? m : p);
|
|
1263
|
+
want = net[parent] !== 0 ? 0 : 1;
|
|
1264
|
+
}
|
|
1265
|
+
net[n] = want;
|
|
1266
|
+
mult[n] = want - around;
|
|
1267
|
+
done.push(n);
|
|
1268
|
+
}
|
|
1269
|
+
const traced = [];
|
|
1270
|
+
const trace = (r, m) => {
|
|
1271
|
+
const o = Math.sign(signedArea(r)) === Math.sign(m) ? r : r.slice().reverse();
|
|
1272
|
+
for (let k = 0; k < Math.abs(m); k++) traced.push(o);
|
|
1273
|
+
};
|
|
1274
|
+
for (let n = 0; n < rings.length; n++) {
|
|
1275
|
+
if (mult[n] !== 0) trace(rings[n], mult[n]);
|
|
1276
|
+
}
|
|
1277
|
+
const holes = rings.map((_, n) => n).filter((n) => net[n] === 0 && mult[n] !== 0 && isConvex(rings[n]));
|
|
1278
|
+
for (let m = 0; m < rings.length; m++) {
|
|
1279
|
+
if (mult[m] <= 0) continue;
|
|
1280
|
+
const bm = boxes[m];
|
|
1281
|
+
const crossing = holes.filter((n) => {
|
|
1282
|
+
const bn = boxes[n];
|
|
1283
|
+
if (n === m || bm.x1 <= bn.x0 || bm.x0 >= bn.x1 || bm.y1 <= bn.y0 || bm.y0 >= bn.y1) return false;
|
|
1284
|
+
return !containsRing(rings[m], bm, rings[n], bn) && !containsRing(rings[n], bn, rings[m], bm);
|
|
1285
|
+
});
|
|
1286
|
+
for (const n of crossing) {
|
|
1287
|
+
if (crossing.some((o) => o !== n && areas[o] > areas[n] && containsRing(rings[o], boxes[o], rings[n], boxes[n]))) continue;
|
|
1288
|
+
const piece = clipToConvex(rings[m], rings[n]);
|
|
1289
|
+
if (piece.length >= 3 && Math.abs(signedArea(piece)) > 1e-9) trace(piece, -mult[m]);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return bridge(traced);
|
|
1293
|
+
}
|
|
1294
|
+
function isConvex(r) {
|
|
1295
|
+
let sign = 0;
|
|
1296
|
+
for (let k = 0; k < r.length; k++) {
|
|
1297
|
+
const a = r[k], b = r[(k + 1) % r.length], c = r[(k + 2) % r.length];
|
|
1298
|
+
const z = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
|
|
1299
|
+
if (Math.abs(z) < 1e-12) continue;
|
|
1300
|
+
if (sign === 0) sign = Math.sign(z);
|
|
1301
|
+
else if (Math.sign(z) !== sign) return false;
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
function clipToConvex(subject, clip) {
|
|
1306
|
+
const o = Math.sign(signedArea(clip)) || 1;
|
|
1307
|
+
let out = subject;
|
|
1308
|
+
for (let k = 0; k < clip.length && out.length; k++) {
|
|
1309
|
+
const a = clip[k], b = clip[(k + 1) % clip.length];
|
|
1310
|
+
const side = (p) => o * ((b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]));
|
|
1311
|
+
const cut = (p, q) => {
|
|
1312
|
+
const sp = side(p), sq = side(q), t = sp / (sp - sq);
|
|
1313
|
+
return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
|
|
1314
|
+
};
|
|
1315
|
+
const next = [];
|
|
1316
|
+
for (let i = 0; i < out.length; i++) {
|
|
1317
|
+
const p = out[i], q = out[(i + 1) % out.length];
|
|
1318
|
+
const pin = side(p) >= 0, qin = side(q) >= 0;
|
|
1319
|
+
if (pin) next.push(p);
|
|
1320
|
+
if (pin !== qin) next.push(cut(p, q));
|
|
1321
|
+
}
|
|
1322
|
+
out = next;
|
|
1323
|
+
}
|
|
1324
|
+
return out;
|
|
1325
|
+
}
|
|
1326
|
+
function bridge(rings) {
|
|
1327
|
+
if (rings.length === 0) return [];
|
|
1328
|
+
let ring = rings[0].slice();
|
|
1329
|
+
for (let n = 1; n < rings.length; n++) {
|
|
1330
|
+
const sub = rings[n];
|
|
1331
|
+
let bi = 0, bj = 0, best = Infinity;
|
|
1332
|
+
for (let a = 0; a < ring.length; a++) {
|
|
1333
|
+
for (let b = 0; b < sub.length; b++) {
|
|
1334
|
+
const dd = (ring[a][0] - sub[b][0]) ** 2 + (ring[a][1] - sub[b][1]) ** 2;
|
|
1335
|
+
if (dd < best) {
|
|
1336
|
+
best = dd;
|
|
1337
|
+
bi = a;
|
|
1338
|
+
bj = b;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const rot = sub.slice(bj).concat(sub.slice(0, bj));
|
|
1343
|
+
ring = ring.slice(0, bi + 1).concat(rot, [rot[0], ring[bi]], ring.slice(bi + 1));
|
|
1344
|
+
}
|
|
1345
|
+
return ring;
|
|
1346
|
+
}
|
|
1347
|
+
function pathDrawCost(s) {
|
|
1348
|
+
let c = 0;
|
|
1349
|
+
for (const p of pathParts(s)) {
|
|
1350
|
+
if (p.fill) c += p.flat.points.length * (p.gradient ? 2 : 1);
|
|
1351
|
+
if (p.stroke) for (const l of p.flat.lines) c += l.points.length * 3;
|
|
1352
|
+
}
|
|
1353
|
+
return c;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
671
1356
|
// src/validate.ts
|
|
672
1357
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
673
1358
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -713,7 +1398,8 @@ var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "c
|
|
|
713
1398
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
714
1399
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "feather", "colors", "colorWeights"]);
|
|
715
1400
|
var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
|
|
716
|
-
var
|
|
1401
|
+
var KNOWN_PATH_PART = /* @__PURE__ */ new Set(["path", "color", "fillRule", "fill", "gradient", "stroke", "opacity"]);
|
|
1402
|
+
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "path", "fillRule", "fill", "gradient", "stroke", "parts", "span", "origin", "soft", "colors", "colorWeights"]);
|
|
717
1403
|
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
718
1404
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
719
1405
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
@@ -853,6 +1539,23 @@ function validateSpecCore(spec) {
|
|
|
853
1539
|
err("layers", "must be a non-empty array");
|
|
854
1540
|
} else {
|
|
855
1541
|
if (spec.layers.length > LIMITS.maxLayers) err("layers", `at most ${LIMITS.maxLayers} layers`);
|
|
1542
|
+
{
|
|
1543
|
+
let pathPoints = 0;
|
|
1544
|
+
for (const l of spec.layers) {
|
|
1545
|
+
const sp = isObj(l) ? l.sprite : void 0;
|
|
1546
|
+
if (!sp || sp.kind !== "polygon") continue;
|
|
1547
|
+
const pieces = Array.isArray(sp.parts) ? sp.parts : typeof sp.path === "string" ? [sp] : [];
|
|
1548
|
+
const n = typeof l.count === "number" && l.count > 0 ? l.count : 1;
|
|
1549
|
+
for (const pc of pieces) {
|
|
1550
|
+
if (!isObj(pc) || typeof pc.path !== "string") continue;
|
|
1551
|
+
const fl = flattenPath(pc.path, pc.fillRule ?? "auto");
|
|
1552
|
+
pathPoints += ((pc.fill === false ? 0 : fl.points.length) + (isObj(pc.stroke) ? fl.lines.reduce((m, x) => m + x.points.length, 0) : 0)) * n;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (pathPoints > MAX_SCENE_PATH_POINTS) {
|
|
1556
|
+
err("layers", `path outlines total ${pathPoints} points (flattened points \xD7 count); the scene budget is ${MAX_SCENE_PATH_POINTS}`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
856
1559
|
let total = 0;
|
|
857
1560
|
spec.layers.forEach((layer, i) => {
|
|
858
1561
|
total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
|
|
@@ -1226,10 +1929,20 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1226
1929
|
if (layer.links !== void 0) {
|
|
1227
1930
|
if (!isObj(layer.links)) err(`${path}.links`, "must be an object");
|
|
1228
1931
|
else {
|
|
1229
|
-
|
|
1230
|
-
|
|
1932
|
+
const chain = layer.links.mode === "chain";
|
|
1933
|
+
if (!chain || layer.links.k !== void 0) {
|
|
1934
|
+
if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
|
|
1935
|
+
err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (!chain || layer.links.maxDist !== void 0 || layer.links.falloff === true) {
|
|
1939
|
+
positiveScalar(
|
|
1940
|
+
layer.links.maxDist,
|
|
1941
|
+
`${path}.links.maxDist`,
|
|
1942
|
+
err,
|
|
1943
|
+
chain && layer.links.maxDist === void 0 ? "is required (> 0) when a chain sets falloff: true \u2014 it is the fade distance" : void 0
|
|
1944
|
+
);
|
|
1231
1945
|
}
|
|
1232
|
-
positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
|
|
1233
1946
|
if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
|
|
1234
1947
|
if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
|
|
1235
1948
|
err(`${path}.links.alpha`, "must be 0..1");
|
|
@@ -1470,6 +2183,34 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
1470
2183
|
err(`${path}.sides`, `must be an integer ${LIMITS.minPolygonSides}..${LIMITS.maxPolygonSides}`);
|
|
1471
2184
|
}
|
|
1472
2185
|
if (sprite.points !== void 0) validateShapePoints(sprite.points, `${path}.points`, 3, err);
|
|
2186
|
+
if (sprite.parts !== void 0) {
|
|
2187
|
+
for (const k of ["path", "points", "sides", "gradient", "stroke", "fill", "fillRule"]) {
|
|
2188
|
+
if (sprite[k] !== void 0) err(`${path}.${k}`, `with parts, put ${k} on each part (or drop parts)`);
|
|
2189
|
+
}
|
|
2190
|
+
if (!Array.isArray(sprite.parts) || sprite.parts.length === 0 || sprite.parts.length > MAX_PATH_PARTS) {
|
|
2191
|
+
err(`${path}.parts`, `must be 1..${MAX_PATH_PARTS} parts, each { path, color?, gradient?, stroke?, fill?, fillRule?, opacity? }`);
|
|
2192
|
+
} else {
|
|
2193
|
+
sprite.parts.forEach((part, i) => {
|
|
2194
|
+
const pp = `${path}.parts[${i}]`;
|
|
2195
|
+
if (!isObj(part)) return err(pp, "must be { path, color?, gradient?, stroke?, fill?, fillRule?, opacity? }");
|
|
2196
|
+
validatePathPiece(part, pp, err, warn);
|
|
2197
|
+
if (part.color !== void 0) color(part.color, `${pp}.color`, err);
|
|
2198
|
+
if (part.opacity !== void 0 && (!isNum(part.opacity) || part.opacity < 0 || part.opacity > 1)) err(`${pp}.opacity`, "must be 0..1");
|
|
2199
|
+
for (const k of unknownKeys(part, KNOWN_PATH_PART)) warn(`${pp}.${k}`, "unknown-property", `unknown part property '${k}' \u2014 will be ignored`);
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
} else if (sprite.path !== void 0) {
|
|
2203
|
+
if (sprite.points !== void 0 || sprite.sides !== void 0) err(`${path}.path`, "use path, points or sides, not more than one");
|
|
2204
|
+
validatePathPiece(sprite, path, err, warn);
|
|
2205
|
+
} else {
|
|
2206
|
+
for (const k of ["gradient", "stroke", "fill"]) if (sprite[k] !== void 0) err(`${path}.${k}`, `${k} applies only to a polygon with a path (or parts)`);
|
|
2207
|
+
if (sprite.fillRule !== void 0) warn(`${path}.fillRule`, "unknown-property", "fillRule applies only to a path \u2014 ignored");
|
|
2208
|
+
}
|
|
2209
|
+
if (sprite.span !== void 0 || sprite.origin !== void 0) {
|
|
2210
|
+
if (sprite.path === void 0 && sprite.parts === void 0) err(`${path}.span`, "span and origin apply only to a polygon with a path (or parts)");
|
|
2211
|
+
if (sprite.span !== void 0 && !["box", "width", "height", "screen"].includes(sprite.span)) err(`${path}.span`, "must be 'box' | 'width' | 'height' | 'screen'");
|
|
2212
|
+
if (sprite.origin !== void 0 && !(Array.isArray(sprite.origin) && sprite.origin.length === 2 && sprite.origin.every((v) => isNum(v) && Math.abs(v) <= 1))) err(`${path}.origin`, "must be [x, y] in the unit box (-1..1): the point placed at the position");
|
|
2213
|
+
}
|
|
1473
2214
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
1474
2215
|
color(sprite.color, `${path}.color`, err);
|
|
1475
2216
|
validatePalette(sprite, path, err);
|
|
@@ -1726,6 +2467,58 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
1726
2467
|
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
1727
2468
|
}
|
|
1728
2469
|
}
|
|
2470
|
+
var isPair = (v, lim = 2) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && Math.abs(v[0]) <= lim && Math.abs(v[1]) <= lim;
|
|
2471
|
+
var MAX_PATH_PARTS = 256;
|
|
2472
|
+
function validatePathPiece(piece, path, err, warn) {
|
|
2473
|
+
if (typeof piece.path !== "string" || piece.path.length === 0 || piece.path.length > 65536) {
|
|
2474
|
+
err(`${path}.path`, "must be an SVG path d string (1..65536 chars) in the -1..1 unit box");
|
|
2475
|
+
} else {
|
|
2476
|
+
const flat = flattenPath(piece.path, piece.fillRule ?? "auto");
|
|
2477
|
+
const filled = piece.fill !== false;
|
|
2478
|
+
if (flat.error) err(`${path}.path`, `not a usable path: ${flat.error}`);
|
|
2479
|
+
else if (filled && flat.points.length < 3) err(`${path}.path`, "must enclose an area (at least 3 points), or set fill: false and a stroke for line art");
|
|
2480
|
+
else if (!filled && flat.lines.length === 0) err(`${path}.path`, "must draw at least one line (2+ points)");
|
|
2481
|
+
else if (flat.points.length > MAX_PATH_POINTS) err(`${path}.path`, `flattens to ${flat.points.length} points; at most ${MAX_PATH_POINTS}`);
|
|
2482
|
+
else if ([...flat.points, ...flat.lines.flatMap((l) => l.points)].some(([x, y]) => Math.abs(x) > 1.0001 || Math.abs(y) > 1.0001)) err(`${path}.path`, "must stay inside the -1..1 unit box (curves included)");
|
|
2483
|
+
}
|
|
2484
|
+
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2485
|
+
if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
|
|
2486
|
+
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
|
|
2487
|
+
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err);
|
|
2488
|
+
if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
|
|
2489
|
+
void warn;
|
|
2490
|
+
}
|
|
2491
|
+
function validatePathGradient(g, path, err) {
|
|
2492
|
+
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
2493
|
+
if (g.type === "linear") {
|
|
2494
|
+
if (!isPair(g.from)) err(`${path}.from`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2495
|
+
if (!isPair(g.to)) err(`${path}.to`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2496
|
+
} else if (g.type === "radial") {
|
|
2497
|
+
if (!isPair(g.center)) err(`${path}.center`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2498
|
+
if (!isNum(g.radius) || g.radius <= 0 || g.radius > 4) err(`${path}.radius`, "must be > 0 and <= 4 (unit-box units)");
|
|
2499
|
+
if (g.focus !== void 0 && !isPair(g.focus)) err(`${path}.focus`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2500
|
+
} else {
|
|
2501
|
+
return err(`${path}.type`, "must be 'linear' | 'radial'");
|
|
2502
|
+
}
|
|
2503
|
+
if (g.units !== void 0 && g.units !== "box" && g.units !== "part") err(`${path}.units`, "must be 'box' (the drawing's unit box, default) | 'part' (the part's own box, -1..1)");
|
|
2504
|
+
if (!Array.isArray(g.stops) || g.stops.length < 2 || g.stops.length > 8) return err(`${path}.stops`, "must be 2..8 stops");
|
|
2505
|
+
let last = -Infinity;
|
|
2506
|
+
g.stops.forEach((st, i) => {
|
|
2507
|
+
if (!isObj(st)) return err(`${path}.stops[${i}]`, "must be { at, color, opacity? }");
|
|
2508
|
+
if (!isNum(st.at) || st.at < 0 || st.at > 1 || st.at < last) err(`${path}.stops[${i}].at`, "must be 0..1, not decreasing");
|
|
2509
|
+
else last = st.at;
|
|
2510
|
+
color(st.color, `${path}.stops[${i}].color`, err);
|
|
2511
|
+
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
function validatePathStroke(st, path, err) {
|
|
2515
|
+
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap? }");
|
|
2516
|
+
color(st.color, `${path}.color`, err);
|
|
2517
|
+
if (!isNum(st.width) || st.width <= 0 || st.width > 0.5) err(`${path}.width`, "must be > 0 and <= 0.5 (unit-box units: 0.01 is 1% of the drawing's half-size)");
|
|
2518
|
+
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2519
|
+
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2520
|
+
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2521
|
+
}
|
|
1729
2522
|
function validateShapePoints(points, path, min, err) {
|
|
1730
2523
|
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
1731
2524
|
return err(path, `must be ${min}..${LIMITS.maxShapePoints} [x, y] pairs in unit coordinates (-1..1)`);
|
|
@@ -1922,110 +2715,6 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
1922
2715
|
return seq;
|
|
1923
2716
|
}
|
|
1924
2717
|
|
|
1925
|
-
// src/shapes.ts
|
|
1926
|
-
function isShapedSprite(s) {
|
|
1927
|
-
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
1928
|
-
}
|
|
1929
|
-
function barFraction(s, index) {
|
|
1930
|
-
const n = s.values.length;
|
|
1931
|
-
if (n === 0) return 0;
|
|
1932
|
-
const v = s.values[(index % n + n) % n] ?? 0;
|
|
1933
|
-
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
1934
|
-
if (!(max > 0)) return 0;
|
|
1935
|
-
return Math.max(0, Math.min(1, v / max));
|
|
1936
|
-
}
|
|
1937
|
-
function barBox(direction, len, thick) {
|
|
1938
|
-
switch (direction) {
|
|
1939
|
-
case "left":
|
|
1940
|
-
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1941
|
-
case "up":
|
|
1942
|
-
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1943
|
-
case "down":
|
|
1944
|
-
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1945
|
-
default:
|
|
1946
|
-
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
var DEFAULT_POLYGON_SIDES = 6;
|
|
1950
|
-
var STROKE_SAMPLES = 24;
|
|
1951
|
-
function polygonPoints(s, radius) {
|
|
1952
|
-
if (s.points && s.points.length >= 3) {
|
|
1953
|
-
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
1954
|
-
}
|
|
1955
|
-
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
1956
|
-
const out = [];
|
|
1957
|
-
for (let k = 0; k < n; k++) {
|
|
1958
|
-
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
1959
|
-
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
1960
|
-
}
|
|
1961
|
-
return out;
|
|
1962
|
-
}
|
|
1963
|
-
function polygonArea(pts) {
|
|
1964
|
-
let a = 0;
|
|
1965
|
-
for (let i = 0; i < pts.length; i++) {
|
|
1966
|
-
const p = pts[i];
|
|
1967
|
-
const q = pts[(i + 1) % pts.length];
|
|
1968
|
-
a += p.x * q.y - q.x * p.y;
|
|
1969
|
-
}
|
|
1970
|
-
return Math.abs(a) / 2;
|
|
1971
|
-
}
|
|
1972
|
-
function polygonFill(s, radius) {
|
|
1973
|
-
if (radius <= 0) return 0;
|
|
1974
|
-
const disc = Math.PI * radius * radius;
|
|
1975
|
-
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
1976
|
-
}
|
|
1977
|
-
function catmullRom(p0, p1, p2, p3, u) {
|
|
1978
|
-
const u2 = u * u;
|
|
1979
|
-
const u3 = u2 * u;
|
|
1980
|
-
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
1981
|
-
}
|
|
1982
|
-
function strokeSamples(s, halfSize, n) {
|
|
1983
|
-
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
1984
|
-
const m = pts.length;
|
|
1985
|
-
if (m < 2) return pts;
|
|
1986
|
-
const smooth = s.curve !== "linear" && m >= 3;
|
|
1987
|
-
const segs = m - 1;
|
|
1988
|
-
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
1989
|
-
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
1990
|
-
const out = [];
|
|
1991
|
-
for (let i = 0; i < n; i++) {
|
|
1992
|
-
const u = i / (n - 1) * segs;
|
|
1993
|
-
const seg = Math.min(segs - 1, Math.floor(u));
|
|
1994
|
-
const local = u - seg;
|
|
1995
|
-
const p1 = at(seg);
|
|
1996
|
-
const p2 = at(seg + 1);
|
|
1997
|
-
if (!smooth) {
|
|
1998
|
-
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
1999
|
-
} else {
|
|
2000
|
-
const p0 = at(seg - 1);
|
|
2001
|
-
const p3 = at(seg + 2);
|
|
2002
|
-
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
return out;
|
|
2006
|
-
}
|
|
2007
|
-
function pathLength(pts) {
|
|
2008
|
-
let l = 0;
|
|
2009
|
-
for (let i = 1; i < pts.length; i++) {
|
|
2010
|
-
const dx = pts[i].x - pts[i - 1].x;
|
|
2011
|
-
const dy = pts[i].y - pts[i - 1].y;
|
|
2012
|
-
l += Math.sqrt(dx * dx + dy * dy);
|
|
2013
|
-
}
|
|
2014
|
-
return l;
|
|
2015
|
-
}
|
|
2016
|
-
function strokeTaper(u) {
|
|
2017
|
-
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
2018
|
-
}
|
|
2019
|
-
function strokeWidthPx(s, scale) {
|
|
2020
|
-
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2021
|
-
}
|
|
2022
|
-
function featherAlphas(steps, additive) {
|
|
2023
|
-
const out = [];
|
|
2024
|
-
for (let k = 1; k <= steps; k++) out.push(additive ? 1 / steps : 1 / (steps - k + 1));
|
|
2025
|
-
return out;
|
|
2026
|
-
}
|
|
2027
|
-
var FEATHER_STEPS = 6;
|
|
2028
|
-
|
|
2029
2718
|
// src/simulate.ts
|
|
2030
2719
|
function entityScales(spec, w, h) {
|
|
2031
2720
|
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
@@ -2547,9 +3236,13 @@ function wrapDist(pi, pj, doWrap, w, h) {
|
|
|
2547
3236
|
}
|
|
2548
3237
|
return Math.sqrt(dx * dx + dy * dy);
|
|
2549
3238
|
}
|
|
3239
|
+
function linkMaxDistPx(links, scale) {
|
|
3240
|
+
return links.maxDist === void 0 ? Infinity : links.maxDist * scale;
|
|
3241
|
+
}
|
|
2550
3242
|
function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
2551
3243
|
const n = positions.length;
|
|
2552
3244
|
const mode = links.mode ?? "nearest";
|
|
3245
|
+
const k = links.k ?? 1;
|
|
2553
3246
|
if (mode === "chain") {
|
|
2554
3247
|
const out = [];
|
|
2555
3248
|
for (let i = 0; i < n - 1; i++) {
|
|
@@ -2563,7 +3256,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2563
3256
|
const edges = /* @__PURE__ */ new Set();
|
|
2564
3257
|
const out = [];
|
|
2565
3258
|
for (let i = 0; i < n; i++) {
|
|
2566
|
-
for (let mIdx = 1; mIdx <=
|
|
3259
|
+
for (let mIdx = 1; mIdx <= k; mIdx++) {
|
|
2567
3260
|
const j = (i + mIdx * stride) % n;
|
|
2568
3261
|
if (j === i) continue;
|
|
2569
3262
|
const dist = wrapDist(positions[i], positions[j], doWrap, w, h);
|
|
@@ -2577,7 +3270,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2577
3270
|
}
|
|
2578
3271
|
return out;
|
|
2579
3272
|
}
|
|
2580
|
-
return linkPairs(positions,
|
|
3273
|
+
return linkPairs(positions, k, maxDistPx, doWrap, w, h).map(([i, j]) => ({
|
|
2581
3274
|
i,
|
|
2582
3275
|
j,
|
|
2583
3276
|
dist: wrapDist(positions[i], positions[j], doWrap, w, h)
|
|
@@ -3088,6 +3781,79 @@ function screen(fp, pat, alpha, off, w, h) {
|
|
|
3088
3781
|
p.restore();
|
|
3089
3782
|
}
|
|
3090
3783
|
|
|
3784
|
+
// src/trail.ts
|
|
3785
|
+
function trailFillBudget(layerCount) {
|
|
3786
|
+
return Math.max(4, Math.min(48, Math.floor(240 / Math.max(1, layerCount))));
|
|
3787
|
+
}
|
|
3788
|
+
function trailEchoes(trail, t, headAlpha, headSize, posAt, w, h, wrap2, fillBudget, minAlpha = 0, additive = false) {
|
|
3789
|
+
const fade = trail.fade ?? 1;
|
|
3790
|
+
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
3791
|
+
const alphaAtK = (k) => headAlpha * (1 - k * fade);
|
|
3792
|
+
const radiusAtK = (k) => headSize / 2 * (1 - k * 0.7);
|
|
3793
|
+
const jumps = (a, b) => wrap2 && (Math.abs(b.x - a.x) > w / 2 || Math.abs(b.y - a.y) > h / 2);
|
|
3794
|
+
const head = posAt(t);
|
|
3795
|
+
const base = [];
|
|
3796
|
+
let prev = head;
|
|
3797
|
+
for (let s = 1; s <= n; s++) {
|
|
3798
|
+
const k = s / n;
|
|
3799
|
+
const pastT = t - k * trail.length;
|
|
3800
|
+
if (pastT < 0) break;
|
|
3801
|
+
const pos = posAt(pastT);
|
|
3802
|
+
if (jumps(prev, pos)) break;
|
|
3803
|
+
prev = pos;
|
|
3804
|
+
const a = alphaAtK(k);
|
|
3805
|
+
if (a <= minAlpha) break;
|
|
3806
|
+
const r = radiusAtK(k);
|
|
3807
|
+
if (r < 0.2) break;
|
|
3808
|
+
base.push({ t: pastT, x: pos.x, y: pos.y, k, alpha: a, r });
|
|
3809
|
+
}
|
|
3810
|
+
if (base.length === 0) return base;
|
|
3811
|
+
const need = [];
|
|
3812
|
+
let total = 0;
|
|
3813
|
+
for (let i = 0; i < base.length; i++) {
|
|
3814
|
+
const a = i === 0 ? head : base[i - 1];
|
|
3815
|
+
const b = base[i];
|
|
3816
|
+
const d = Math.hypot(b.x - a.x, b.y - a.y);
|
|
3817
|
+
const m = d > b.r ? Math.ceil(d / b.r) - 1 : 0;
|
|
3818
|
+
need.push(m);
|
|
3819
|
+
total += m;
|
|
3820
|
+
}
|
|
3821
|
+
if (total === 0) return base;
|
|
3822
|
+
const scale = total > fillBudget ? fillBudget / total : 1;
|
|
3823
|
+
const out = [];
|
|
3824
|
+
let last = head;
|
|
3825
|
+
for (let i = 0; i < base.length; i++) {
|
|
3826
|
+
const b = base[i];
|
|
3827
|
+
const m = Math.floor(need[i] * scale);
|
|
3828
|
+
if (m === 0) {
|
|
3829
|
+
out.push(b);
|
|
3830
|
+
last = b;
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
const a = i === 0 ? head : base[i - 1];
|
|
3834
|
+
const spacing = Math.hypot(b.x - a.x, b.y - a.y) / (m + 1);
|
|
3835
|
+
const share = (alpha, r) => {
|
|
3836
|
+
const k = Math.min(1, spacing / (2 * r));
|
|
3837
|
+
return additive ? alpha * k : 1 - Math.pow(1 - Math.min(1, alpha), k);
|
|
3838
|
+
};
|
|
3839
|
+
const t0 = i === 0 ? t : base[i - 1].t;
|
|
3840
|
+
const k0 = i === 0 ? 0 : base[i - 1].k;
|
|
3841
|
+
for (let j = 1; j <= m; j++) {
|
|
3842
|
+
const f = j / (m + 1);
|
|
3843
|
+
const tt = t0 + (b.t - t0) * f;
|
|
3844
|
+
const k = k0 + (b.k - k0) * f;
|
|
3845
|
+
const pos = posAt(tt);
|
|
3846
|
+
if (jumps(last, pos)) continue;
|
|
3847
|
+
last = pos;
|
|
3848
|
+
const r = radiusAtK(k);
|
|
3849
|
+
out.push({ t: tt, x: pos.x, y: pos.y, k, alpha: share(alphaAtK(k), r), r });
|
|
3850
|
+
}
|
|
3851
|
+
out.push({ ...b, alpha: share(b.alpha, b.r) });
|
|
3852
|
+
last = b;
|
|
3853
|
+
}
|
|
3854
|
+
return out;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3091
3857
|
// src/compile.ts
|
|
3092
3858
|
var DEFAULT_STEER_DUR = 1e3;
|
|
3093
3859
|
function applyRetainedDeltas(spec, deltas) {
|
|
@@ -3183,6 +3949,45 @@ function overridesStayValid(authored, overrides, now) {
|
|
|
3183
3949
|
}
|
|
3184
3950
|
return true;
|
|
3185
3951
|
}
|
|
3952
|
+
var PATH_BLIT_MIN_POINTS = 128;
|
|
3953
|
+
var PLATE_MIN_POINTS = 2500;
|
|
3954
|
+
var PATH_BLIT_PER_SPRITE = 8;
|
|
3955
|
+
var PATH_SLOT_MAX = 96;
|
|
3956
|
+
var PATH_SPRITE_PER_SPRITE = 96;
|
|
3957
|
+
var PATH_SPRITE_MAX_PIXELS = 512 * 512;
|
|
3958
|
+
var PATH_BLIT_MIN_DENSITY = 1500;
|
|
3959
|
+
var PATH_BLIT_MAX_PIXELS = 16e6;
|
|
3960
|
+
function ringPath2D(lines) {
|
|
3961
|
+
const p2 = new Path2D();
|
|
3962
|
+
for (const l of lines) {
|
|
3963
|
+
if (l.points.length < 2) continue;
|
|
3964
|
+
p2.moveTo(l.points[0][0], l.points[0][1]);
|
|
3965
|
+
for (let i = 1; i < l.points.length; i++) p2.lineTo(l.points[i][0], l.points[i][1]);
|
|
3966
|
+
if (l.closed) p2.closePath();
|
|
3967
|
+
}
|
|
3968
|
+
return p2;
|
|
3969
|
+
}
|
|
3970
|
+
function traceLines(g, lines) {
|
|
3971
|
+
g.beginPath();
|
|
3972
|
+
for (const l of lines) {
|
|
3973
|
+
if (l.points.length < 2) continue;
|
|
3974
|
+
g.moveTo(l.points[0][0], l.points[0][1]);
|
|
3975
|
+
for (let i = 1; i < l.points.length; i++) g.lineTo(l.points[i][0], l.points[i][1]);
|
|
3976
|
+
if (l.closed) g.closePath();
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
function pathGradientFill(g, gr) {
|
|
3980
|
+
const grad = gr.type === "linear" ? g.createLinearGradient(gr.from[0], gr.from[1], gr.to[0], gr.to[1]) : g.createRadialGradient((gr.focus ?? gr.center)[0], (gr.focus ?? gr.center)[1], 0, gr.center[0], gr.center[1], gr.radius);
|
|
3981
|
+
for (const st of gr.stops) grad.addColorStop(st.at, st.opacity === void 0 ? st.color : hexToRgba(st.color, st.opacity));
|
|
3982
|
+
return grad;
|
|
3983
|
+
}
|
|
3984
|
+
function softFill(g, color2, r) {
|
|
3985
|
+
const grad = g.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
3986
|
+
grad.addColorStop(0, color2);
|
|
3987
|
+
grad.addColorStop(0.35, hexToRgba(color2, 0.75));
|
|
3988
|
+
grad.addColorStop(1, hexToRgba(color2, 0));
|
|
3989
|
+
return grad;
|
|
3990
|
+
}
|
|
3186
3991
|
var SpecInstance = class {
|
|
3187
3992
|
constructor(spec, ctx, opts = {}) {
|
|
3188
3993
|
this.spec = spec;
|
|
@@ -3285,6 +4090,29 @@ var SpecInstance = class {
|
|
|
3285
4090
|
fieldShortSide;
|
|
3286
4091
|
/** The `field` background's cached raster; null until a field is first painted. */
|
|
3287
4092
|
field = null;
|
|
4093
|
+
featherWhite = /* @__PURE__ */ new Map();
|
|
4094
|
+
/** Per-entity pre-rendered feathered rects (see `blitFeather`), and the device pixels they hold. */
|
|
4095
|
+
featherBlits = /* @__PURE__ */ new WeakMap();
|
|
4096
|
+
featherBlitPx = 0;
|
|
4097
|
+
featherBlitN = 0;
|
|
4098
|
+
paintSeq = 0;
|
|
4099
|
+
/** Per layer: ribbon area painted this frame (px²), and whether its trails ribbon (see ribbonAllowed). */
|
|
4100
|
+
ribbonBudget = /* @__PURE__ */ new WeakMap();
|
|
4101
|
+
/**
|
|
4102
|
+
* Render-once cache for path polygons (polygon.path). A detailed drawing is
|
|
4103
|
+
* rasterised once per (colour, device size, rotation, sub-pixel offset)
|
|
4104
|
+
* into its own bitmap and copied 1:1 at a whole-pixel offset every frame
|
|
4105
|
+
* after — so a static 7,000-point drawing costs a blit, not a re-fill.
|
|
4106
|
+
* Keyed by the sprite object, so a steer or republish (a new spec object)
|
|
4107
|
+
* starts clean. A drawing whose key keeps changing (spinning, trailing,
|
|
4108
|
+
* growing) stops being cached and draws from a reused Path2D instead.
|
|
4109
|
+
*/
|
|
4110
|
+
pathBlits = /* @__PURE__ */ new Map();
|
|
4111
|
+
/** Test/bench switch: `globalThis.__idleNoPathCache = true` before compiling draws every path directly. */
|
|
4112
|
+
noPathCache = globalThis.__idleNoPathCache === true;
|
|
4113
|
+
/** Device pixels held by every pathBlits bitmap — capped so a TV never pays more than PATH_BLIT_MAX_PIXELS of memory. */
|
|
4114
|
+
pathBlitPixels = 0;
|
|
4115
|
+
featherTints = /* @__PURE__ */ new Map();
|
|
3288
4116
|
/**
|
|
3289
4117
|
* The presentation pass when this instance applies a `finish`: the
|
|
3290
4118
|
* visible canvas plus the cached screen tiles. When set, `this.canvas` is
|
|
@@ -3449,36 +4277,81 @@ var SpecInstance = class {
|
|
|
3449
4277
|
const trail = built.layer.trail;
|
|
3450
4278
|
if (!trail) return;
|
|
3451
4279
|
const { ctx, w, h } = this;
|
|
3452
|
-
const fade = trail.fade ?? 1;
|
|
3453
|
-
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
3454
4280
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
3455
4281
|
const headSize = sizeAt(e, t);
|
|
3456
4282
|
const sprite = built.layer.sprite;
|
|
4283
|
+
const shaped = isShapedSprite(sprite) && sprite.kind !== "circle";
|
|
4284
|
+
const isSoft = sprite.kind === "circle" && !!sprite.soft;
|
|
4285
|
+
const hardCircle = sprite.kind === "circle" && !sprite.soft;
|
|
4286
|
+
const ribbon = hardCircle && this.ribbonAllowed(built);
|
|
4287
|
+
const posAt = (pt) => this.entityPos(e, pt, parentE);
|
|
4288
|
+
const fill = shaped || hardCircle ? 0 : trailFillBudget(built.entities.length);
|
|
4289
|
+
const echoes = trailEchoes(trail, t, headAlpha, headSize, posAt, w, h, built.layer.wrap !== false, fill, 0, built.layer.blend === "lighter");
|
|
4290
|
+
if (echoes.length === 0) return;
|
|
4291
|
+
const head = posAt(t);
|
|
4292
|
+
if (hardCircle) {
|
|
4293
|
+
const rb = this.ribbonBudget.get(built);
|
|
4294
|
+
let prev = head;
|
|
4295
|
+
let r0 = headSize / 2;
|
|
4296
|
+
for (const ec of echoes) {
|
|
4297
|
+
rb.area += Math.hypot(ec.x - prev.x, ec.y - prev.y) * (r0 + ec.r);
|
|
4298
|
+
prev = ec;
|
|
4299
|
+
r0 = ec.r;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
if (shaped) {
|
|
4303
|
+
for (const ec of echoes) this.drawEntity(built, e, ec.t, 1, parentE, void 0, { x: ec.x, y: ec.y, size: ec.r * 2, alpha: ec.alpha });
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
3457
4306
|
const resolvedColor = isShapedSprite(sprite) ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
4307
|
+
if (ribbon) {
|
|
4308
|
+
const pts = [head];
|
|
4309
|
+
const radii = [headSize / 2];
|
|
4310
|
+
const alphas = [headAlpha];
|
|
4311
|
+
let prevT = t;
|
|
4312
|
+
for (const ec of echoes) {
|
|
4313
|
+
const p0 = pts[pts.length - 1];
|
|
4314
|
+
const r0 = radii[radii.length - 1];
|
|
4315
|
+
const a0 = alphas[alphas.length - 1];
|
|
4316
|
+
const sub = Math.max(1, Math.min(8, Math.ceil(Math.hypot(ec.x - p0.x, ec.y - p0.y) / 12)));
|
|
4317
|
+
for (let j = 1; j < sub; j++) {
|
|
4318
|
+
const f = j / sub;
|
|
4319
|
+
pts.push(posAt(prevT + (ec.t - prevT) * f));
|
|
4320
|
+
radii.push(r0 + (ec.r - r0) * f);
|
|
4321
|
+
alphas.push(a0 + (ec.alpha - a0) * f);
|
|
4322
|
+
}
|
|
4323
|
+
pts.push(ec);
|
|
4324
|
+
radii.push(ec.r);
|
|
4325
|
+
alphas.push(ec.alpha);
|
|
4326
|
+
prevT = ec.t;
|
|
4327
|
+
}
|
|
4328
|
+
const { left, right } = ribbonSides(pts, radii);
|
|
4329
|
+
ctx.fillStyle = resolvedColor;
|
|
4330
|
+
const tol = Math.max(4e-3, headAlpha * 0.04);
|
|
4331
|
+
let from = 0;
|
|
4332
|
+
while (from < pts.length - 1) {
|
|
4333
|
+
const a0 = alphas[from + 1];
|
|
4334
|
+
let to = from + 1;
|
|
4335
|
+
let sum = a0;
|
|
4336
|
+
while (to < pts.length - 1 && Math.abs(alphas[to + 1] - a0) <= tol) {
|
|
4337
|
+
sum += alphas[to + 1];
|
|
4338
|
+
to++;
|
|
4339
|
+
}
|
|
4340
|
+
ctx.globalAlpha = sum / (to - from);
|
|
4341
|
+
ctx.beginPath();
|
|
4342
|
+
ctx.moveTo(left[from].x, left[from].y);
|
|
4343
|
+
for (let i = from + 1; i <= to; i++) ctx.lineTo(left[i].x, left[i].y);
|
|
4344
|
+
for (let i = to; i >= from; i--) ctx.lineTo(right[i].x, right[i].y);
|
|
4345
|
+
ctx.closePath();
|
|
4346
|
+
ctx.fill();
|
|
4347
|
+
from = to;
|
|
4348
|
+
}
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
for (const ec of echoes) {
|
|
4352
|
+
ctx.globalAlpha = ec.alpha;
|
|
3480
4353
|
if (isSoft) {
|
|
3481
|
-
const g = ctx.createRadialGradient(
|
|
4354
|
+
const g = ctx.createRadialGradient(ec.x, ec.y, 0, ec.x, ec.y, ec.r);
|
|
3482
4355
|
g.addColorStop(0, resolvedColor);
|
|
3483
4356
|
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
3484
4357
|
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
@@ -3487,18 +4360,473 @@ var SpecInstance = class {
|
|
|
3487
4360
|
ctx.fillStyle = resolvedColor;
|
|
3488
4361
|
}
|
|
3489
4362
|
ctx.beginPath();
|
|
3490
|
-
ctx.arc(
|
|
4363
|
+
ctx.arc(ec.x, ec.y, ec.r, 0, Math.PI * 2);
|
|
3491
4364
|
ctx.fill();
|
|
3492
4365
|
}
|
|
3493
4366
|
}
|
|
3494
|
-
|
|
4367
|
+
/**
|
|
4368
|
+
* Whether this layer's hard-circle trails draw as ribbons this frame. A
|
|
4369
|
+
* ribbon paints its trail's whole length, so a layer of many fast, long
|
|
4370
|
+
* trails can paint far more pixels than its dots did — under `lighter` on
|
|
4371
|
+
* a CPU rasteriser that was 2.4× the frame time (60 trails, 3 s, 0.4/s).
|
|
4372
|
+
* Past a tenth of the viewport's area in ribbon (measured on the
|
|
4373
|
+
* previous frame) the layer falls back to the time-sample discs it always
|
|
4374
|
+
* drew; it returns to ribbons below 70 % of that, so a layer near the line
|
|
4375
|
+
* does not flicker between the two.
|
|
4376
|
+
*/
|
|
4377
|
+
ribbonAllowed(built) {
|
|
4378
|
+
let rb = this.ribbonBudget.get(built);
|
|
4379
|
+
if (!rb) {
|
|
4380
|
+
rb = { seq: this.paintSeq, area: 0, ribbon: true };
|
|
4381
|
+
this.ribbonBudget.set(built, rb);
|
|
4382
|
+
}
|
|
4383
|
+
if (rb.seq !== this.paintSeq) {
|
|
4384
|
+
const budget = 0.1 * this.w * this.h;
|
|
4385
|
+
if (rb.ribbon && rb.area > budget) rb.ribbon = false;
|
|
4386
|
+
else if (!rb.ribbon && rb.area < 0.7 * budget) rb.ribbon = true;
|
|
4387
|
+
rb.seq = this.paintSeq;
|
|
4388
|
+
rb.area = 0;
|
|
4389
|
+
}
|
|
4390
|
+
return rb.ribbon;
|
|
4391
|
+
}
|
|
4392
|
+
/** A fresh 2D canvas (DOM or Offscreen), or null where none can be made. */
|
|
4393
|
+
/**
|
|
4394
|
+
* A drawing's cache slot, keyed by what it draws (pathPaintSig), not by the
|
|
4395
|
+
* sprite object — glides and timelines replace that object every frame.
|
|
4396
|
+
* Least-recently-used slots are evicted past PATH_SLOT_MAX, and their
|
|
4397
|
+
* bitmaps' pixels returned to the budget.
|
|
4398
|
+
*/
|
|
4399
|
+
pathSlot(sprite) {
|
|
4400
|
+
const sig = pathPaintSig(sprite);
|
|
4401
|
+
let slot = this.pathBlits.get(sig);
|
|
4402
|
+
if (slot) {
|
|
4403
|
+
this.pathBlits.delete(sig);
|
|
4404
|
+
this.pathBlits.set(sig, slot);
|
|
4405
|
+
return slot;
|
|
4406
|
+
}
|
|
4407
|
+
if (this.pathBlits.size >= PATH_SLOT_MAX) {
|
|
4408
|
+
const [oldSig, old] = this.pathBlits.entries().next().value;
|
|
4409
|
+
for (const ent of old.entries.values()) this.pathBlitPixels -= ent.px;
|
|
4410
|
+
this.pathBlits.delete(oldSig);
|
|
4411
|
+
}
|
|
4412
|
+
slot = { entries: /* @__PURE__ */ new Map(), flushes: 0, off: false };
|
|
4413
|
+
this.pathBlits.set(sig, slot);
|
|
4414
|
+
return slot;
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Whether a layer's path drawing looks the same as last frame — the caches
|
|
4418
|
+
* serve only steady drawings; one that is gliding (colour, gradient, shape)
|
|
4419
|
+
* draws directly until it settles, instead of rasterising a bitmap a frame.
|
|
4420
|
+
*/
|
|
4421
|
+
// Keyed by the layer's place in the scene, never the per-layer record: a
|
|
4422
|
+
// scene that animates a baked field (alpha, size) rebuilds every record
|
|
4423
|
+
// every frame, and a drawing beside it is still steady.
|
|
4424
|
+
pathSteady = /* @__PURE__ */ new Map();
|
|
4425
|
+
drawingSteady(built, sprite) {
|
|
4426
|
+
const sig = pathPaintSig(sprite);
|
|
4427
|
+
const li = this.layers.indexOf(built);
|
|
4428
|
+
const prev = this.pathSteady.get(li);
|
|
4429
|
+
if (prev && prev.seq === this.paintSeq && prev.sig === sig) return prev.steady;
|
|
4430
|
+
const steady = prev !== void 0 && prev.sig === sig;
|
|
4431
|
+
this.pathSteady.set(li, { sig, seq: this.paintSeq, steady });
|
|
4432
|
+
return steady;
|
|
4433
|
+
}
|
|
4434
|
+
/**
|
|
4435
|
+
* Draw a path polygon in its own unit box: the caller has moved to the
|
|
4436
|
+
* entity and rotated; this scales by r. Parts paint back to front (a single
|
|
4437
|
+
* `path` is one part). Each fill is its gradient (drawing-box coordinates,
|
|
4438
|
+
* or with units 'part' the part's own box, filled under that box's
|
|
4439
|
+
* transform so a radial reads as the part's ellipse), the soft radial
|
|
4440
|
+
* falloff, or flat colour; each stroke traces the part's subpaths as drawn,
|
|
4441
|
+
* width in unit-box units. Outlines are Path2Ds built once per sprite.
|
|
4442
|
+
*/
|
|
4443
|
+
drawPathUnit(g, sprite, color2, fr, r) {
|
|
4444
|
+
const slot = this.pathSlot(sprite);
|
|
4445
|
+
const parts = pathParts(sprite);
|
|
4446
|
+
if (!slot.parts2d) slot.parts2d = parts.map(() => ({}));
|
|
4447
|
+
if (fr.ox || fr.oy) g.translate(-fr.ox * fr.sx, -fr.oy * fr.sy);
|
|
4448
|
+
g.scale(fr.sx, fr.sy);
|
|
4449
|
+
const a0 = g.globalAlpha;
|
|
4450
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4451
|
+
const part = parts[i];
|
|
4452
|
+
const cache = slot.parts2d[i];
|
|
4453
|
+
const pc = part.color ?? color2;
|
|
4454
|
+
if (part.fill && part.flat.points.length >= 3) {
|
|
4455
|
+
g.globalAlpha = a0 * part.opacity;
|
|
4456
|
+
if (part.gradient?.units === "part") {
|
|
4457
|
+
const { cx, cy, hw, hh } = part.box;
|
|
4458
|
+
if (cache.fillPart === void 0) {
|
|
4459
|
+
cache.fillPart = typeof Path2D === "undefined" ? null : ringPath2D([{ points: part.flat.points.map(([x, y]) => [(x - cx) / hw, (y - cy) / hh]), closed: true }]);
|
|
4460
|
+
}
|
|
4461
|
+
g.save();
|
|
4462
|
+
g.transform(hw, 0, 0, hh, cx, cy);
|
|
4463
|
+
g.fillStyle = pathGradientFill(g, part.gradient);
|
|
4464
|
+
if (cache.fillPart) g.fill(cache.fillPart, part.rule);
|
|
4465
|
+
else {
|
|
4466
|
+
traceLines(g, [{ points: part.flat.points.map(([x, y]) => [(x - cx) / hw, (y - cy) / hh]), closed: true }]);
|
|
4467
|
+
g.fill(part.rule);
|
|
4468
|
+
}
|
|
4469
|
+
g.restore();
|
|
4470
|
+
} else {
|
|
4471
|
+
g.fillStyle = part.gradient ? pathGradientFill(g, part.gradient) : sprite.soft && !sprite.parts ? softFill(g, pc, 1) : pc;
|
|
4472
|
+
if (cache.fill === void 0) cache.fill = typeof Path2D === "undefined" ? null : ringPath2D([{ points: part.flat.points, closed: true }]);
|
|
4473
|
+
if (cache.fill) g.fill(cache.fill, part.rule);
|
|
4474
|
+
else {
|
|
4475
|
+
traceLines(g, [{ points: part.flat.points, closed: true }]);
|
|
4476
|
+
g.fill(part.rule);
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
const st = part.stroke;
|
|
4481
|
+
if (st && part.flat.lines.length) {
|
|
4482
|
+
g.globalAlpha = a0 * part.opacity * (st.opacity ?? 1);
|
|
4483
|
+
g.strokeStyle = st.color;
|
|
4484
|
+
g.lineJoin = st.join ?? "round";
|
|
4485
|
+
g.lineCap = st.cap ?? "round";
|
|
4486
|
+
if (fr.uniform) {
|
|
4487
|
+
g.lineWidth = st.width * r / fr.sx;
|
|
4488
|
+
if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
|
|
4489
|
+
if (cache.stroke) g.stroke(cache.stroke);
|
|
4490
|
+
else {
|
|
4491
|
+
traceLines(g, part.flat.lines);
|
|
4492
|
+
g.stroke();
|
|
4493
|
+
}
|
|
4494
|
+
} else {
|
|
4495
|
+
const scaled = part.flat.lines.map((l) => ({ closed: l.closed, points: l.points.map(([x, y]) => [x * fr.sx, y * fr.sy]) }));
|
|
4496
|
+
g.save();
|
|
4497
|
+
g.scale(1 / fr.sx, 1 / fr.sy);
|
|
4498
|
+
g.lineWidth = st.width * r;
|
|
4499
|
+
traceLines(g, scaled);
|
|
4500
|
+
g.stroke();
|
|
4501
|
+
g.restore();
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
g.globalAlpha = a0;
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Draw a path polygon from its render-once bitmap. False when it cannot be
|
|
4509
|
+
* blitted exactly — a rotated or skewed layer transform, a bitmap past the
|
|
4510
|
+
* memory cap, a drawing too sparse for its area, or one that keeps
|
|
4511
|
+
* changing — and the caller draws it directly.
|
|
4512
|
+
*/
|
|
4513
|
+
blitPath(sprite, p, r, rot, color2, fr) {
|
|
4514
|
+
const ctx = this.ctx;
|
|
4515
|
+
const slot = this.pathSlot(sprite);
|
|
4516
|
+
if (slot.off) return false;
|
|
4517
|
+
if (slot.sprite) return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4518
|
+
const m = ctx.getTransform();
|
|
4519
|
+
if (Math.abs(m.b) > 1e-9 || Math.abs(m.c) > 1e-9 || Math.abs(m.a - m.d) > 1e-9 || !(m.a > 0)) {
|
|
4520
|
+
slot.sprite = true;
|
|
4521
|
+
return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4522
|
+
}
|
|
4523
|
+
const k = m.a;
|
|
4524
|
+
const dx = k * p.x + m.e;
|
|
4525
|
+
const dy = k * p.y + m.f;
|
|
4526
|
+
const ix = Math.floor(dx);
|
|
4527
|
+
const iy = Math.floor(dy);
|
|
4528
|
+
const fx = Math.round((dx - ix) * 8) / 8;
|
|
4529
|
+
const fy = Math.round((dy - iy) * 8) / 8;
|
|
4530
|
+
const key = `${color2}|${(r * k).toFixed(3)}|${(fr.sx * k).toFixed(3)}|${(fr.sy * k).toFixed(3)}|${fr.ox},${fr.oy}|${rot.toFixed(5)}|${fx}|${fy}`;
|
|
4531
|
+
let hit = slot.entries.get(key);
|
|
4532
|
+
if (!hit) {
|
|
4533
|
+
if (slot.entries.size >= PATH_BLIT_PER_SPRITE) {
|
|
4534
|
+
for (const old of slot.entries.values()) this.pathBlitPixels -= old.px;
|
|
4535
|
+
slot.entries.clear();
|
|
4536
|
+
if (++slot.flushes > 1) {
|
|
4537
|
+
slot.sprite = true;
|
|
4538
|
+
slot.flushes = 0;
|
|
4539
|
+
return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
const c = Math.cos(rot);
|
|
4543
|
+
const sn = Math.sin(rot);
|
|
4544
|
+
const s = r * k;
|
|
4545
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
4546
|
+
const grow = (u) => {
|
|
4547
|
+
const ux = (u[0] - fr.ox) * fr.sx * k, uy = (u[1] - fr.oy) * fr.sy * k;
|
|
4548
|
+
const x = ux * c - uy * sn;
|
|
4549
|
+
const y = ux * sn + uy * c;
|
|
4550
|
+
if (x < x0) x0 = x;
|
|
4551
|
+
if (x > x1) x1 = x;
|
|
4552
|
+
if (y < y0) y0 = y;
|
|
4553
|
+
if (y > y1) y1 = y;
|
|
4554
|
+
};
|
|
4555
|
+
let sw = 0;
|
|
4556
|
+
for (const part of pathParts(sprite)) {
|
|
4557
|
+
if (part.fill) for (const u of part.flat.points) grow(u);
|
|
4558
|
+
if (part.stroke) {
|
|
4559
|
+
sw = Math.max(sw, part.stroke.width);
|
|
4560
|
+
for (const l of part.flat.lines) for (const u of l.points) grow(u);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
const pad = 2 + Math.ceil(sw * s);
|
|
4564
|
+
const bx = Math.floor(fx + x0) - pad;
|
|
4565
|
+
const by = Math.floor(fy + y0) - pad;
|
|
4566
|
+
const w = Math.ceil(fx + x1) - bx + pad;
|
|
4567
|
+
const h = Math.ceil(fy + y1) - by + pad;
|
|
4568
|
+
if (!(w > 0 && h > 0) || !Number.isFinite(w + h)) return false;
|
|
4569
|
+
if (pathDrawCost(sprite) / (w * h / 1e6) < PATH_BLIT_MIN_DENSITY) {
|
|
4570
|
+
slot.off = true;
|
|
4571
|
+
return false;
|
|
4572
|
+
}
|
|
4573
|
+
if (this.pathBlitPixels + w * h > PATH_BLIT_MAX_PIXELS) return false;
|
|
4574
|
+
const made = this.makeCanvas(1);
|
|
4575
|
+
if (!made) return false;
|
|
4576
|
+
made.canvas.width = w;
|
|
4577
|
+
made.canvas.height = h;
|
|
4578
|
+
const g = made.ctx;
|
|
4579
|
+
g.setTransform(k, 0, 0, k, fx - bx, fy - by);
|
|
4580
|
+
if (rot) g.rotate(rot);
|
|
4581
|
+
this.drawPathUnit(g, sprite, color2, fr, r);
|
|
4582
|
+
hit = { canvas: made.canvas, x0: bx, y0: by, px: w * h };
|
|
4583
|
+
this.pathBlitPixels += hit.px;
|
|
4584
|
+
slot.entries.set(key, hit);
|
|
4585
|
+
}
|
|
4586
|
+
ctx.save();
|
|
4587
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4588
|
+
ctx.drawImage(hit.canvas, ix + hit.x0, iy + hit.y0);
|
|
4589
|
+
ctx.restore();
|
|
4590
|
+
return true;
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* The moving-sprite cache: for a drawing whose exact key keeps changing. It
|
|
4594
|
+
* is rasterised once per colour, UNROTATED, at a device radius rounded UP to
|
|
4595
|
+
* the next 1/8-octave step (so it is only ever shrunk, never enlarged), then
|
|
4596
|
+
* drawn every frame at its true sub-pixel position, rotation and size under
|
|
4597
|
+
* the context's own transform, with smoothing. Not pixel-exact like the
|
|
4598
|
+
* still cache (edges resample by well under a pixel), but a 7-part drifting
|
|
4599
|
+
* balloon costs one image draw instead of seven fills and strokes.
|
|
4600
|
+
*/
|
|
4601
|
+
spriteBlit(sprite, slot, p, r, rot, color2, fr) {
|
|
4602
|
+
const ctx = this.ctx;
|
|
4603
|
+
if (!fr.uniform || fr.sx !== r) return false;
|
|
4604
|
+
const m = ctx.getTransform();
|
|
4605
|
+
const k = Math.sqrt(Math.abs(m.a * m.d - m.b * m.c));
|
|
4606
|
+
const s = r * k;
|
|
4607
|
+
if (!(s >= 1)) return false;
|
|
4608
|
+
const bucket = 2 ** (Math.ceil(Math.log2(s) * 8) / 8);
|
|
4609
|
+
const key = `S|${color2}|${bucket.toFixed(3)}`;
|
|
4610
|
+
let hit = slot.entries.get(key);
|
|
4611
|
+
if (!hit) {
|
|
4612
|
+
if (slot.entries.size >= PATH_SPRITE_PER_SPRITE) {
|
|
4613
|
+
for (const old of slot.entries.values()) this.pathBlitPixels -= old.px;
|
|
4614
|
+
slot.entries.clear();
|
|
4615
|
+
if (++slot.flushes > 2) {
|
|
4616
|
+
slot.off = true;
|
|
4617
|
+
return false;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity, sw = 0;
|
|
4621
|
+
const grow = (u2) => {
|
|
4622
|
+
if (u2[0] < x0) x0 = u2[0];
|
|
4623
|
+
if (u2[0] > x1) x1 = u2[0];
|
|
4624
|
+
if (u2[1] < y0) y0 = u2[1];
|
|
4625
|
+
if (u2[1] > y1) y1 = u2[1];
|
|
4626
|
+
};
|
|
4627
|
+
for (const part of pathParts(sprite)) {
|
|
4628
|
+
if (part.fill) for (const u2 of part.flat.points) grow([u2[0] - fr.ox, u2[1] - fr.oy]);
|
|
4629
|
+
if (part.stroke) {
|
|
4630
|
+
sw = Math.max(sw, part.stroke.width);
|
|
4631
|
+
for (const l of part.flat.lines) for (const u2 of l.points) grow([u2[0] - fr.ox, u2[1] - fr.oy]);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
const pad = 2 + Math.ceil(sw * bucket);
|
|
4635
|
+
const ox = Math.ceil(-x0 * bucket) + pad;
|
|
4636
|
+
const oy = Math.ceil(-y0 * bucket) + pad;
|
|
4637
|
+
const w = ox + Math.ceil(x1 * bucket) + pad;
|
|
4638
|
+
const h = oy + Math.ceil(y1 * bucket) + pad;
|
|
4639
|
+
if (!(w > 0 && h > 0) || !Number.isFinite(w + h)) return false;
|
|
4640
|
+
if (w * h > PATH_SPRITE_MAX_PIXELS) {
|
|
4641
|
+
slot.off = true;
|
|
4642
|
+
return false;
|
|
4643
|
+
}
|
|
4644
|
+
if (this.pathBlitPixels + w * h > PATH_BLIT_MAX_PIXELS) return false;
|
|
4645
|
+
const made = this.makeCanvas(1);
|
|
4646
|
+
if (!made) return false;
|
|
4647
|
+
made.canvas.width = w;
|
|
4648
|
+
made.canvas.height = h;
|
|
4649
|
+
made.ctx.setTransform(1, 0, 0, 1, ox, oy);
|
|
4650
|
+
this.drawPathUnit(made.ctx, sprite, color2, { sx: bucket, sy: bucket, ox: fr.ox, oy: fr.oy, uniform: true }, bucket);
|
|
4651
|
+
hit = { canvas: made.canvas, x0: ox, y0: oy, px: w * h };
|
|
4652
|
+
this.pathBlitPixels += hit.px;
|
|
4653
|
+
slot.entries.set(key, hit);
|
|
4654
|
+
}
|
|
4655
|
+
const u = r / bucket;
|
|
4656
|
+
ctx.save();
|
|
4657
|
+
ctx.translate(p.x, p.y);
|
|
4658
|
+
if (rot) ctx.rotate(rot);
|
|
4659
|
+
ctx.imageSmoothingEnabled = true;
|
|
4660
|
+
ctx.drawImage(hit.canvas, -hit.x0 * u, -hit.y0 * u, hit.canvas.width * u, hit.canvas.height * u);
|
|
4661
|
+
ctx.restore();
|
|
4662
|
+
return true;
|
|
4663
|
+
}
|
|
4664
|
+
makeCanvas(n) {
|
|
4665
|
+
try {
|
|
4666
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(n, n);
|
|
4667
|
+
canvas.width = n;
|
|
4668
|
+
canvas.height = n;
|
|
4669
|
+
const c = canvas.getContext("2d");
|
|
4670
|
+
return c ? { canvas, ctx: c } : null;
|
|
4671
|
+
} catch {
|
|
4672
|
+
return null;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
/**
|
|
4676
|
+
* A feathered rect from a per-entity bitmap of it, pre-rendered at device
|
|
4677
|
+
* resolution with its rotation baked in, copied 1:1 at a whole-pixel
|
|
4678
|
+
* offset. A smooth falloff is a sampled image draw — on a CPU rasteriser
|
|
4679
|
+
* (a Raspberry Pi wall) ~3× the six flat fills it replaced, ~10× when
|
|
4680
|
+
* rotated and stretched — while a 1:1 integer copy costs about one flat
|
|
4681
|
+
* fill. The bitmap is rebuilt only when the rect's size, rotation or
|
|
4682
|
+
* feather changes (in device pixels, to a quarter pixel / 0.06°), and
|
|
4683
|
+
* re-tinted (one copy + one flat fill) only when its colour changes, so a
|
|
4684
|
+
* drifting block or a colour glide stays cheap. A rect whose shape
|
|
4685
|
+
* changes every frame (`grow`, `spin`), a canvas under a rotating or
|
|
4686
|
+
* mirroring transform, or a scene past the 8 Mpx cache budget draws the
|
|
4687
|
+
* stretched mask directly instead. Returns false when it did not draw.
|
|
4688
|
+
*/
|
|
4689
|
+
blitFeather(e, p, sz, rh, rot, color2, feather) {
|
|
4690
|
+
const ctx = this.ctx;
|
|
4691
|
+
const m = typeof ctx.getTransform === "function" ? ctx.getTransform() : null;
|
|
4692
|
+
if (!m || m.b !== 0 || m.c !== 0 || m.a <= 0 || m.d <= 0) return false;
|
|
4693
|
+
const cos = Math.abs(Math.cos(rot));
|
|
4694
|
+
const sin = Math.abs(Math.sin(rot));
|
|
4695
|
+
const bw = Math.ceil((cos * sz + sin * rh) * m.a) + 2;
|
|
4696
|
+
const bh = Math.ceil((sin * sz + cos * rh) * m.d) + 2;
|
|
4697
|
+
const shapeKey = `${feather}|${Math.round(sz * m.a * 4)}|${Math.round(rh * m.d * 4)}|${Math.round(rot * 1e3)}|${bw}x${bh}`;
|
|
4698
|
+
let b = this.featherBlits.get(e);
|
|
4699
|
+
if (!b || b.shapeKey !== shapeKey) {
|
|
4700
|
+
if (b) {
|
|
4701
|
+
if (b.seenKey !== shapeKey) {
|
|
4702
|
+
b.seenKey = shapeKey;
|
|
4703
|
+
b.seenAt = this.paintSeq;
|
|
4704
|
+
}
|
|
4705
|
+
if (b.seenAt === this.paintSeq) return false;
|
|
4706
|
+
}
|
|
4707
|
+
const px = bw * bh;
|
|
4708
|
+
const reuse = b && b.w === bw && b.h === bh;
|
|
4709
|
+
if (!reuse && this.featherBlitPx + px - (b ? b.w * b.h : 0) > 8e6) return false;
|
|
4710
|
+
const white = this.featherWhite.get(feather);
|
|
4711
|
+
if (!white) return false;
|
|
4712
|
+
const wc = reuse ? b.white : this.makeCanvas(1);
|
|
4713
|
+
const tc = reuse ? b.tint : this.makeCanvas(1);
|
|
4714
|
+
if (!wc || !tc) return false;
|
|
4715
|
+
if (!reuse) {
|
|
4716
|
+
wc.canvas.width = bw;
|
|
4717
|
+
wc.canvas.height = bh;
|
|
4718
|
+
tc.canvas.width = bw;
|
|
4719
|
+
tc.canvas.height = bh;
|
|
4720
|
+
this.featherBlitPx += px - (b ? b.w * b.h : 0);
|
|
4721
|
+
}
|
|
4722
|
+
const w2 = wc.ctx;
|
|
4723
|
+
w2.setTransform(1, 0, 0, 1, 0, 0);
|
|
4724
|
+
w2.clearRect(0, 0, bw, bh);
|
|
4725
|
+
w2.translate(bw / 2, bh / 2);
|
|
4726
|
+
w2.scale(m.a, m.d);
|
|
4727
|
+
if (rot) w2.rotate(rot);
|
|
4728
|
+
w2.imageSmoothingEnabled = true;
|
|
4729
|
+
w2.drawImage(white, -sz / 2, -rh / 2, sz, rh);
|
|
4730
|
+
w2.setTransform(1, 0, 0, 1, 0, 0);
|
|
4731
|
+
b = { shapeKey, w: bw, h: bh, white: wc, tint: tc, tintKey: "", tintAt: 0, stagger: this.featherBlitN++ % 8, seenKey: shapeKey, seenAt: this.paintSeq };
|
|
4732
|
+
this.featherBlits.set(e, b);
|
|
4733
|
+
}
|
|
4734
|
+
if (b.tintKey !== color2 && (b.tintKey === "" || this.paintSeq - b.tintAt >= 8)) {
|
|
4735
|
+
const t2 = b.tint.ctx;
|
|
4736
|
+
t2.setTransform(1, 0, 0, 1, 0, 0);
|
|
4737
|
+
t2.globalCompositeOperation = "copy";
|
|
4738
|
+
t2.drawImage(b.white.canvas, 0, 0);
|
|
4739
|
+
t2.globalCompositeOperation = "source-in";
|
|
4740
|
+
t2.fillStyle = color2;
|
|
4741
|
+
t2.fillRect(0, 0, b.w, b.h);
|
|
4742
|
+
t2.globalCompositeOperation = "source-over";
|
|
4743
|
+
b.tintKey = color2;
|
|
4744
|
+
b.tintAt = this.paintSeq - (b.stagger ?? 0);
|
|
4745
|
+
b.stagger = 0;
|
|
4746
|
+
}
|
|
4747
|
+
const cx = m.a * p.x + m.e;
|
|
4748
|
+
const cy = m.d * p.y + m.f;
|
|
4749
|
+
ctx.save();
|
|
4750
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4751
|
+
ctx.drawImage(b.tint.canvas, Math.round(cx - b.w / 2), Math.round(cy - b.h / 2));
|
|
4752
|
+
ctx.restore();
|
|
4753
|
+
return true;
|
|
4754
|
+
}
|
|
4755
|
+
/**
|
|
4756
|
+
* A feathered rect's mask in `color`: FEATHER_MASK_SIZE² texels of
|
|
4757
|
+
* `featherRamp(u) × featherRamp(v)`, drawn stretched to the rect with
|
|
4758
|
+
* smoothing on, so the falloff is continuous at any size (X383).
|
|
4759
|
+
*
|
|
4760
|
+
* The ramp itself is built ONCE per feather (a white alpha mask, the only
|
|
4761
|
+
* per-pixel work). A colour is a cheap tint of it — clear, draw the mask,
|
|
4762
|
+
* `source-in` fill — into a pooled canvas, cached per colour × feather.
|
|
4763
|
+
* A colour glide or a palette larger than the pool re-tints a pooled
|
|
4764
|
+
* canvas (three GPU draws) rather than rebuilding pixels in JS.
|
|
4765
|
+
*/
|
|
4766
|
+
featherMask(color2, feather) {
|
|
4767
|
+
const key = `${color2}|${feather}`;
|
|
4768
|
+
const hit = this.featherTints.get(key);
|
|
4769
|
+
if (hit) {
|
|
4770
|
+
this.featherTints.delete(key);
|
|
4771
|
+
this.featherTints.set(key, hit);
|
|
4772
|
+
return hit.canvas;
|
|
4773
|
+
}
|
|
4774
|
+
const N = FEATHER_MASK_SIZE;
|
|
4775
|
+
let white = this.featherWhite.get(feather);
|
|
4776
|
+
if (!white) {
|
|
4777
|
+
const made = this.makeCanvas(N);
|
|
4778
|
+
if (!made) return null;
|
|
4779
|
+
const img = made.ctx.createImageData(N, N);
|
|
4780
|
+
const data = img.data;
|
|
4781
|
+
const ramp = [];
|
|
4782
|
+
for (let i = 0; i < N; i++) ramp.push(featherRamp((i + 0.5) / N, feather));
|
|
4783
|
+
let o = 0;
|
|
4784
|
+
for (let y = 0; y < N; y++) {
|
|
4785
|
+
const ry = ramp[y];
|
|
4786
|
+
for (let x = 0; x < N; x++) {
|
|
4787
|
+
data[o++] = 255;
|
|
4788
|
+
data[o++] = 255;
|
|
4789
|
+
data[o++] = 255;
|
|
4790
|
+
data[o++] = Math.round(255 * ry * ramp[x]);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
made.ctx.putImageData(img, 0, 0);
|
|
4794
|
+
if (this.featherWhite.size >= 8) this.featherWhite.delete(this.featherWhite.keys().next().value);
|
|
4795
|
+
white = made.canvas;
|
|
4796
|
+
this.featherWhite.set(feather, white);
|
|
4797
|
+
}
|
|
4798
|
+
let slot = null;
|
|
4799
|
+
if (this.featherTints.size >= 48) {
|
|
4800
|
+
const oldest = this.featherTints.keys().next().value;
|
|
4801
|
+
slot = this.featherTints.get(oldest);
|
|
4802
|
+
this.featherTints.delete(oldest);
|
|
4803
|
+
} else {
|
|
4804
|
+
slot = this.makeCanvas(N);
|
|
4805
|
+
if (!slot) return null;
|
|
4806
|
+
}
|
|
4807
|
+
const tctx = slot.ctx;
|
|
4808
|
+
tctx.globalCompositeOperation = "copy";
|
|
4809
|
+
tctx.drawImage(white, 0, 0);
|
|
4810
|
+
tctx.globalCompositeOperation = "source-in";
|
|
4811
|
+
tctx.fillStyle = color2;
|
|
4812
|
+
tctx.fillRect(0, 0, N, N);
|
|
4813
|
+
tctx.globalCompositeOperation = "source-over";
|
|
4814
|
+
this.featherTints.set(key, slot);
|
|
4815
|
+
return slot.canvas;
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Paint one entity at time `t`. `echo` paints a trail echo instead: the
|
|
4819
|
+
* entity as it was at `t` (rotation, heading, values), but at the echo's
|
|
4820
|
+
* position, size and alpha.
|
|
4821
|
+
*/
|
|
4822
|
+
drawEntity(built, e, t, lifeA, parentE, paint, echo) {
|
|
3495
4823
|
const { ctx } = this;
|
|
3496
|
-
const p = this.entityPos(e, t, parentE);
|
|
4824
|
+
const p = echo ? { x: echo.x, y: echo.y, flip: false } : this.entityPos(e, t, parentE);
|
|
3497
4825
|
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
3498
|
-
const sz = sizeAt(e, t);
|
|
4826
|
+
const sz = echo ? echo.size : sizeAt(e, t);
|
|
3499
4827
|
const rot = rotationAt(e, t);
|
|
3500
4828
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
3501
|
-
ctx.globalAlpha = alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
4829
|
+
ctx.globalAlpha = echo ? echo.alpha : alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
3502
4830
|
if (sprite.kind === "ring") {
|
|
3503
4831
|
const r = sz / 2;
|
|
3504
4832
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -3529,23 +4857,18 @@ var SpecInstance = class {
|
|
|
3529
4857
|
if (sprite.kind === "rect") {
|
|
3530
4858
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
3531
4859
|
const rh = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
4860
|
+
const feather = sprite.feather ?? 0;
|
|
4861
|
+
const mask = feather > 0 ? this.featherMask(resolvedColor, feather) : null;
|
|
4862
|
+
if (mask && this.blitFeather(e, p, sz, rh, rot, resolvedColor, feather)) return;
|
|
3532
4863
|
ctx.save();
|
|
3533
4864
|
ctx.translate(p.x, p.y);
|
|
3534
4865
|
if (rot) ctx.rotate(rot);
|
|
3535
4866
|
ctx.fillStyle = resolvedColor;
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
for (let j = 1; j <= FEATHER_STEPS; j++) {
|
|
3542
|
-
const f = 1 - feather * (j - 1) / FEATHER_STEPS;
|
|
3543
|
-
const fw = sz * f;
|
|
3544
|
-
const fh = rh * f;
|
|
3545
|
-
ctx.globalAlpha = base * alphas[j - 1];
|
|
3546
|
-
ctx.fillRect(-fw / 2, -fh / 2, fw, fh);
|
|
3547
|
-
}
|
|
3548
|
-
ctx.globalAlpha = base;
|
|
4867
|
+
if (mask) {
|
|
4868
|
+
const prevSmooth = ctx.imageSmoothingEnabled;
|
|
4869
|
+
ctx.imageSmoothingEnabled = true;
|
|
4870
|
+
ctx.drawImage(mask, -sz / 2, -rh / 2, sz, rh);
|
|
4871
|
+
ctx.imageSmoothingEnabled = prevSmooth;
|
|
3549
4872
|
} else {
|
|
3550
4873
|
ctx.fillRect(-sz / 2, -rh / 2, sz, rh);
|
|
3551
4874
|
}
|
|
@@ -3569,24 +4892,26 @@ var SpecInstance = class {
|
|
|
3569
4892
|
if (sprite.kind === "polygon") {
|
|
3570
4893
|
const r = sz / 2;
|
|
3571
4894
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
3572
|
-
|
|
4895
|
+
if (isPathPolygon(sprite)) {
|
|
4896
|
+
const fr = pathFrame(sprite, r, this.w, this.h);
|
|
4897
|
+
if (pathDrawCost(sprite) >= PATH_BLIT_MIN_POINTS && !this.noPathCache && this.drawingSteady(built, sprite) && this.blitPath(sprite, p, r, rot, resolvedColor, fr)) return;
|
|
4898
|
+
ctx.save();
|
|
4899
|
+
ctx.translate(p.x, p.y);
|
|
4900
|
+
if (rot) ctx.rotate(rot);
|
|
4901
|
+
if (r > 0 && fr.sx > 0 && fr.sy > 0) this.drawPathUnit(ctx, sprite, resolvedColor, fr, r);
|
|
4902
|
+
ctx.restore();
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
3573
4905
|
ctx.save();
|
|
3574
4906
|
ctx.translate(p.x, p.y);
|
|
3575
4907
|
if (rot) ctx.rotate(rot);
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
g.addColorStop(0, resolvedColor);
|
|
3579
|
-
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
3580
|
-
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
3581
|
-
ctx.fillStyle = g;
|
|
3582
|
-
} else {
|
|
3583
|
-
ctx.fillStyle = resolvedColor;
|
|
3584
|
-
}
|
|
4908
|
+
ctx.fillStyle = sprite.soft ? softFill(ctx, resolvedColor, r) : resolvedColor;
|
|
4909
|
+
const pts = polygonPoints(sprite, r);
|
|
3585
4910
|
ctx.beginPath();
|
|
3586
4911
|
ctx.moveTo(pts[0].x, pts[0].y);
|
|
3587
4912
|
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
|
3588
4913
|
ctx.closePath();
|
|
3589
|
-
ctx.fill();
|
|
4914
|
+
ctx.fill(polygonFillRule(sprite));
|
|
3590
4915
|
ctx.restore();
|
|
3591
4916
|
return;
|
|
3592
4917
|
}
|
|
@@ -3603,13 +4928,13 @@ var SpecInstance = class {
|
|
|
3603
4928
|
ctx.lineCap = "round";
|
|
3604
4929
|
ctx.lineJoin = "round";
|
|
3605
4930
|
if (sprite.taper) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4931
|
+
const outline = strokeOutline(pts, lw);
|
|
4932
|
+
ctx.fillStyle = resolvedColor;
|
|
4933
|
+
ctx.beginPath();
|
|
4934
|
+
ctx.moveTo(outline[0].x, outline[0].y);
|
|
4935
|
+
for (let i = 1; i < outline.length; i++) ctx.lineTo(outline[i].x, outline[i].y);
|
|
4936
|
+
ctx.closePath();
|
|
4937
|
+
ctx.fill();
|
|
3613
4938
|
} else {
|
|
3614
4939
|
ctx.lineWidth = lw;
|
|
3615
4940
|
ctx.beginPath();
|
|
@@ -3825,9 +5150,9 @@ var SpecInstance = class {
|
|
|
3825
5150
|
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
3826
5151
|
const wrap2 = built.layer.wrap !== false && motionWraps;
|
|
3827
5152
|
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
3828
|
-
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
3829
|
-
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
3830
5153
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
5154
|
+
const maxDistPx = linkMaxDistPx(links, unitScale);
|
|
5155
|
+
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
3831
5156
|
const defaultWidth = unitScale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
3832
5157
|
const lw = (links.width ?? defaultWidth) * unitScale;
|
|
3833
5158
|
ctx.lineWidth = lw;
|
|
@@ -3844,7 +5169,7 @@ var SpecInstance = class {
|
|
|
3844
5169
|
} else resolvedColor = "#e6e8ef";
|
|
3845
5170
|
}
|
|
3846
5171
|
let a = links.alpha ?? alphaAt(ei, t);
|
|
3847
|
-
if (links.falloff) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
5172
|
+
if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
3848
5173
|
ctx.globalAlpha = a * lifeA;
|
|
3849
5174
|
ctx.strokeStyle = resolvedColor;
|
|
3850
5175
|
let dx = pj.x - pi.x;
|
|
@@ -3863,6 +5188,7 @@ var SpecInstance = class {
|
|
|
3863
5188
|
* showing through — the ghosting smear. */
|
|
3864
5189
|
paintFrame(t, bgAlpha) {
|
|
3865
5190
|
const { ctx } = this;
|
|
5191
|
+
this.paintSeq++;
|
|
3866
5192
|
if (this.authored) this.resolveAuthored(t);
|
|
3867
5193
|
if (!this.transparent) {
|
|
3868
5194
|
ctx.globalAlpha = bgAlpha;
|
|
@@ -3870,39 +5196,213 @@ var SpecInstance = class {
|
|
|
3870
5196
|
this.drawBackground(t);
|
|
3871
5197
|
}
|
|
3872
5198
|
ctx.globalAlpha = 1;
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
const parentE = this.parentEntityFor(built);
|
|
3881
|
-
const tf = built.layer.transform;
|
|
3882
|
-
const gtf = group?.transform;
|
|
3883
|
-
if (tf || gtf) {
|
|
3884
|
-
ctx.save();
|
|
3885
|
-
if (gtf) this.applyLayerTransform(gtf, null);
|
|
3886
|
-
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
3887
|
-
}
|
|
3888
|
-
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
3889
|
-
this.drawLinks(built, t, lifeA, parentE);
|
|
3890
|
-
const override = this.paintOverrides?.get(li) ?? null;
|
|
3891
|
-
for (const e of built.entities) {
|
|
3892
|
-
this.drawTrail(built, e, t, lifeA, parentE);
|
|
3893
|
-
if (override === null) {
|
|
3894
|
-
this.drawEntity(built, e, t, lifeA, parentE);
|
|
5199
|
+
const n = this.layers.length;
|
|
5200
|
+
for (let li = 0; li < n; li++) {
|
|
5201
|
+
if (!this.noPathCache && this.plateable(li, t)) {
|
|
5202
|
+
let j = li;
|
|
5203
|
+
while (j + 1 < n && this.plateable(j + 1, t)) j++;
|
|
5204
|
+
if (j > li && this.runPoints(li, j) >= PLATE_MIN_POINTS && this.paintPlate(li, j, t)) {
|
|
5205
|
+
li = j;
|
|
3895
5206
|
continue;
|
|
3896
5207
|
}
|
|
3897
|
-
const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
|
|
3898
|
-
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
3899
|
-
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
3900
5208
|
}
|
|
3901
|
-
|
|
5209
|
+
this.paintLayer(li, t);
|
|
3902
5210
|
}
|
|
3903
5211
|
ctx.globalAlpha = 1;
|
|
3904
5212
|
ctx.globalCompositeOperation = "source-over";
|
|
3905
5213
|
}
|
|
5214
|
+
/** Paint layer `li` at time t onto this.ctx (the scene canvas, or a plate being rebuilt). */
|
|
5215
|
+
paintLayer(li, t) {
|
|
5216
|
+
const { ctx } = this;
|
|
5217
|
+
const built = this.layers[li];
|
|
5218
|
+
const opacity = built.layer.opacity;
|
|
5219
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5220
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
5221
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
5222
|
+
if (lifeA <= 0) return;
|
|
5223
|
+
const parentE = this.parentEntityFor(built);
|
|
5224
|
+
const tf = built.layer.transform;
|
|
5225
|
+
const gtf = group?.transform;
|
|
5226
|
+
if (tf || gtf) {
|
|
5227
|
+
ctx.save();
|
|
5228
|
+
if (gtf) this.applyLayerTransform(gtf, null);
|
|
5229
|
+
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
5230
|
+
}
|
|
5231
|
+
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
5232
|
+
this.drawLinks(built, t, lifeA, parentE);
|
|
5233
|
+
const override = this.paintOverrides?.get(li) ?? null;
|
|
5234
|
+
for (const e of built.entities) {
|
|
5235
|
+
this.drawTrail(built, e, t, lifeA, parentE);
|
|
5236
|
+
if (override === null) {
|
|
5237
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
|
|
5241
|
+
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
5242
|
+
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
5243
|
+
}
|
|
5244
|
+
if (tf || gtf) ctx.restore();
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5247
|
+
* Static plates. Consecutive path-polygon layers painted source-over, with
|
|
5248
|
+
* no trails, links or text overrides, are composited into ONE full-frame
|
|
5249
|
+
* bitmap and copied once per frame while nothing they draw has changed —
|
|
5250
|
+
* the change test is the drawn values themselves (each entity's position,
|
|
5251
|
+
* size, rotation, alpha and colour, the layer's life alpha and transforms),
|
|
5252
|
+
* so anything animated simply repaints its plate that frame. Source-over is
|
|
5253
|
+
* associative, so the plate over the frame equals the layers over it.
|
|
5254
|
+
*/
|
|
5255
|
+
plates = /* @__PURE__ */ new Map();
|
|
5256
|
+
/** Outline points a run of path layers fills per frame (points × entities). */
|
|
5257
|
+
runPoints(a, b) {
|
|
5258
|
+
let n = 0;
|
|
5259
|
+
for (let li = a; li <= b; li++) {
|
|
5260
|
+
const built = this.layers[li];
|
|
5261
|
+
const sp = built.layer.sprite;
|
|
5262
|
+
if (sp.kind === "polygon" && isPathPolygon(sp)) n += pathDrawCost(sp) * built.entities.length;
|
|
5263
|
+
}
|
|
5264
|
+
return n;
|
|
5265
|
+
}
|
|
5266
|
+
/** Last drawn-value signature per layer, for plate stability. */
|
|
5267
|
+
layerSigs = /* @__PURE__ */ new Map();
|
|
5268
|
+
/**
|
|
5269
|
+
* A layer joins a plate only if it can (path polygon, source-over, no
|
|
5270
|
+
* trails/links/text override) AND nothing it draws changed since the last
|
|
5271
|
+
* frame — so a pulsing or moving layer splits the run and paints live, while
|
|
5272
|
+
* the static layers around it keep their plates.
|
|
5273
|
+
*/
|
|
5274
|
+
plateable(li, t) {
|
|
5275
|
+
const built = this.layers[li];
|
|
5276
|
+
const l = built.layer;
|
|
5277
|
+
const sp = l.sprite;
|
|
5278
|
+
if (!isPathPolygon(sp)) return false;
|
|
5279
|
+
if ((l.blend ?? "source-over") !== "source-over") return false;
|
|
5280
|
+
if (l.trail || l.links || this.paintOverrides?.has(li)) return false;
|
|
5281
|
+
if (built.entities.length > 64) return false;
|
|
5282
|
+
const sig = this.layerSig(li, t);
|
|
5283
|
+
const prev = this.layerSigs.get(li);
|
|
5284
|
+
this.layerSigs.set(li, sig);
|
|
5285
|
+
return prev === sig;
|
|
5286
|
+
}
|
|
5287
|
+
layerSig(li, t) {
|
|
5288
|
+
const built = this.layers[li];
|
|
5289
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5290
|
+
const opacity = built.layer.opacity;
|
|
5291
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
5292
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
5293
|
+
let sig = `${pathPaintSig(built.layer.sprite)}:${lifeA.toFixed(4)}:${JSON.stringify(built.layer.transform ?? null)}:${JSON.stringify(group?.transform ?? null)}`;
|
|
5294
|
+
const parentE = this.parentEntityFor(built);
|
|
5295
|
+
for (const e of built.entities) {
|
|
5296
|
+
const p = this.entityPos(e, t, parentE);
|
|
5297
|
+
sig += `|${p.x.toFixed(2)},${p.y.toFixed(2)},${sizeAt(e, t).toFixed(3)},${rotationAt(e, t).toFixed(5)},${alphaAt(e, t).toFixed(4)},${e.colorIndex}`;
|
|
5298
|
+
}
|
|
5299
|
+
return sig;
|
|
5300
|
+
}
|
|
5301
|
+
plateSig(a, b) {
|
|
5302
|
+
let sig = `${a}-${b}`;
|
|
5303
|
+
for (let li = a; li <= b; li++) sig += `#${this.layerSigs.get(li)}`;
|
|
5304
|
+
return sig;
|
|
5305
|
+
}
|
|
5306
|
+
/** Max |point| of a path polygon's unit outline (its reach as a fraction of the entity radius). */
|
|
5307
|
+
pathReach = /* @__PURE__ */ new WeakMap();
|
|
5308
|
+
reachOf(sp) {
|
|
5309
|
+
let r = this.pathReach.get(sp);
|
|
5310
|
+
if (r === void 0) {
|
|
5311
|
+
r = 0;
|
|
5312
|
+
let sw = 0;
|
|
5313
|
+
for (const part of pathParts(sp)) {
|
|
5314
|
+
for (const [x, y] of part.flat.points) r = Math.max(r, Math.hypot(x, y));
|
|
5315
|
+
for (const l of part.flat.lines) for (const [x, y] of l.points) r = Math.max(r, Math.hypot(x, y));
|
|
5316
|
+
if (part.stroke) sw = Math.max(sw, part.stroke.width);
|
|
5317
|
+
}
|
|
5318
|
+
r += sw;
|
|
5319
|
+
this.pathReach.set(sp, r || 1);
|
|
5320
|
+
}
|
|
5321
|
+
return r || 1;
|
|
5322
|
+
}
|
|
5323
|
+
/**
|
|
5324
|
+
* Device-pixel box a run of layers can paint into, or null for "the whole
|
|
5325
|
+
* frame" (a layer/group transform moves things in ways this does not model).
|
|
5326
|
+
*/
|
|
5327
|
+
runBox(a, b, t) {
|
|
5328
|
+
const m = this.ctx.getTransform();
|
|
5329
|
+
if (Math.abs(m.b) > 1e-9 || Math.abs(m.c) > 1e-9) return null;
|
|
5330
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
5331
|
+
for (let li = a; li <= b; li++) {
|
|
5332
|
+
const built = this.layers[li];
|
|
5333
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5334
|
+
if (built.layer.transform || group?.transform) return null;
|
|
5335
|
+
const reach = this.reachOf(built.layer.sprite);
|
|
5336
|
+
const parentE = this.parentEntityFor(built);
|
|
5337
|
+
for (const e of built.entities) {
|
|
5338
|
+
const p = this.entityPos(e, t, parentE);
|
|
5339
|
+
const sp = built.layer.sprite;
|
|
5340
|
+
const fr = pathFrame(sp, sizeAt(e, t) / 2, this.w, this.h);
|
|
5341
|
+
const ext = Math.max(fr.sx, fr.sy) * (reach + Math.hypot(fr.ox, fr.oy));
|
|
5342
|
+
x0 = Math.min(x0, p.x - ext);
|
|
5343
|
+
x1 = Math.max(x1, p.x + ext);
|
|
5344
|
+
y0 = Math.min(y0, p.y - ext);
|
|
5345
|
+
y1 = Math.max(y1, p.y + ext);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
const W = this.canvas.width, H = this.canvas.height;
|
|
5349
|
+
const bx = Math.max(0, Math.floor(m.a * x0 + m.e) - 2);
|
|
5350
|
+
const by = Math.max(0, Math.floor(m.d * y0 + m.f) - 2);
|
|
5351
|
+
const bw = Math.min(W, Math.ceil(m.a * x1 + m.e) + 2) - bx;
|
|
5352
|
+
const bh = Math.min(H, Math.ceil(m.d * y1 + m.f) + 2) - by;
|
|
5353
|
+
return bw > 0 && bh > 0 ? { x: bx, y: by, w: bw, h: bh } : { x: 0, y: 0, w: 0, h: 0 };
|
|
5354
|
+
}
|
|
5355
|
+
/** Paint layers a..b from their plate, rebuilding it if anything drawn changed. False = paint them normally. */
|
|
5356
|
+
paintPlate(a, b, t) {
|
|
5357
|
+
const sig = this.plateSig(a, b);
|
|
5358
|
+
let plate = this.plates.get(a);
|
|
5359
|
+
if (!plate || plate.sig !== sig || plate.end !== b || plate.frameW !== this.canvas.width || plate.frameH !== this.canvas.height) {
|
|
5360
|
+
const box = this.runBox(a, b, t) ?? { x: 0, y: 0, w: this.canvas.width, h: this.canvas.height };
|
|
5361
|
+
if (!plate) {
|
|
5362
|
+
if (this.plates.size >= 4) this.plates.delete(this.plates.keys().next().value);
|
|
5363
|
+
const made = this.makeCanvas(1);
|
|
5364
|
+
if (!made) return false;
|
|
5365
|
+
plate = { ...made, end: b, sig: "", box, frameW: 0, frameH: 0 };
|
|
5366
|
+
this.plates.set(a, plate);
|
|
5367
|
+
}
|
|
5368
|
+
plate.box = box;
|
|
5369
|
+
plate.frameW = this.canvas.width;
|
|
5370
|
+
plate.frameH = this.canvas.height;
|
|
5371
|
+
if (box.w === 0 || box.h === 0) {
|
|
5372
|
+
plate.sig = sig;
|
|
5373
|
+
plate.end = b;
|
|
5374
|
+
return true;
|
|
5375
|
+
}
|
|
5376
|
+
if (plate.canvas.width !== box.w) plate.canvas.width = box.w;
|
|
5377
|
+
if (plate.canvas.height !== box.h) plate.canvas.height = box.h;
|
|
5378
|
+
const pc = plate.ctx;
|
|
5379
|
+
pc.setTransform(1, 0, 0, 1, 0, 0);
|
|
5380
|
+
pc.globalAlpha = 1;
|
|
5381
|
+
pc.globalCompositeOperation = "source-over";
|
|
5382
|
+
pc.clearRect(0, 0, box.w, box.h);
|
|
5383
|
+
const m = this.ctx.getTransform();
|
|
5384
|
+
pc.setTransform(m.a, m.b, m.c, m.d, m.e - box.x, m.f - box.y);
|
|
5385
|
+
const self = this;
|
|
5386
|
+
const main = self.ctx;
|
|
5387
|
+
self.ctx = pc;
|
|
5388
|
+
try {
|
|
5389
|
+
for (let li = a; li <= b; li++) this.paintLayer(li, t);
|
|
5390
|
+
} finally {
|
|
5391
|
+
self.ctx = main;
|
|
5392
|
+
}
|
|
5393
|
+
plate.sig = sig;
|
|
5394
|
+
plate.end = b;
|
|
5395
|
+
}
|
|
5396
|
+
if (plate.box.w === 0 || plate.box.h === 0) return true;
|
|
5397
|
+
const ctx = this.ctx;
|
|
5398
|
+
ctx.save();
|
|
5399
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
5400
|
+
ctx.globalAlpha = 1;
|
|
5401
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5402
|
+
ctx.drawImage(plate.canvas, plate.box.x, plate.box.y);
|
|
5403
|
+
ctx.restore();
|
|
5404
|
+
return true;
|
|
5405
|
+
}
|
|
3906
5406
|
/**
|
|
3907
5407
|
* A `transform`, in min(w,h) units (px under `units: 'px'`), about the
|
|
3908
5408
|
* viewport centre — or, with `origin: 'anchor'`, about `layer`'s own anchor
|
|
@@ -4774,6 +6274,27 @@ function hexLuma(hex) {
|
|
|
4774
6274
|
}
|
|
4775
6275
|
function spriteLuma(layer, e) {
|
|
4776
6276
|
const s = layer.sprite;
|
|
6277
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
6278
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
6279
|
+
let sum = 0, wsum = 0;
|
|
6280
|
+
for (const part of pathParts(s)) {
|
|
6281
|
+
const pc = hexLuma(part.color ?? base);
|
|
6282
|
+
let l;
|
|
6283
|
+
if (!part.fill) l = part.stroke ? hexLuma(part.stroke.color) : pc;
|
|
6284
|
+
else if (part.gradient) {
|
|
6285
|
+
let g = 0;
|
|
6286
|
+
for (const st of part.gradient.stops) {
|
|
6287
|
+
const o = st.opacity ?? 1;
|
|
6288
|
+
g += hexLuma(st.color) * o + pc * (1 - o);
|
|
6289
|
+
}
|
|
6290
|
+
l = g / part.gradient.stops.length;
|
|
6291
|
+
} else l = pc;
|
|
6292
|
+
const w = Math.max(1e-4, part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x, y }))) : part.box.hw * part.box.hh * 0.1) * part.opacity;
|
|
6293
|
+
sum += l * w;
|
|
6294
|
+
wsum += w;
|
|
6295
|
+
}
|
|
6296
|
+
if (wsum > 0) return sum / wsum;
|
|
6297
|
+
}
|
|
4777
6298
|
if (isShapedSprite(s)) {
|
|
4778
6299
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
4779
6300
|
}
|
|
@@ -4918,7 +6439,7 @@ function polyShape(cx, cy, raw) {
|
|
|
4918
6439
|
if (abs.length < 3) return null;
|
|
4919
6440
|
let br = 0;
|
|
4920
6441
|
for (const p of abs) br = Math.max(br, Math.hypot(p.x - cx, p.y - cy));
|
|
4921
|
-
const
|
|
6442
|
+
const inside2 = (x, y) => {
|
|
4922
6443
|
let hit = false;
|
|
4923
6444
|
for (let i = 0, j = abs.length - 1; i < abs.length; j = i++) {
|
|
4924
6445
|
const a = abs[i];
|
|
@@ -4941,7 +6462,7 @@ function polyShape(cx, cy, raw) {
|
|
|
4941
6462
|
cy,
|
|
4942
6463
|
br,
|
|
4943
6464
|
area,
|
|
4944
|
-
contains:
|
|
6465
|
+
contains: inside2,
|
|
4945
6466
|
/**
|
|
4946
6467
|
* `n` points spread along the perimeter by ARC LENGTH, not one share per
|
|
4947
6468
|
* edge — a long skinny rectangle has two edges carrying almost all of its
|
|
@@ -4970,13 +6491,13 @@ function polyShape(cx, cy, raw) {
|
|
|
4970
6491
|
for (let t = step; t > 0; t /= 8) {
|
|
4971
6492
|
const qx1 = px - uy * t;
|
|
4972
6493
|
const qy1 = py + ux * t;
|
|
4973
|
-
if (
|
|
6494
|
+
if (inside2(qx1, qy1)) {
|
|
4974
6495
|
out.push({ x: qx1, y: qy1 });
|
|
4975
6496
|
break;
|
|
4976
6497
|
}
|
|
4977
6498
|
const qx2 = px + uy * t;
|
|
4978
6499
|
const qy2 = py - ux * t;
|
|
4979
|
-
if (
|
|
6500
|
+
if (inside2(qx2, qy2)) {
|
|
4980
6501
|
out.push({ x: qx2, y: qy2 });
|
|
4981
6502
|
break;
|
|
4982
6503
|
}
|
|
@@ -5098,7 +6619,6 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
5098
6619
|
|
|
5099
6620
|
// src/perceive.ts
|
|
5100
6621
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
5101
|
-
var GLOW_SPREAD = 2.4;
|
|
5102
6622
|
var LINE_SALIENCE = 3.5;
|
|
5103
6623
|
var GHOST_TAPS = 12;
|
|
5104
6624
|
var POLYGON_OUTLINE_CELLS = 3;
|
|
@@ -5133,11 +6653,18 @@ function posOf(scene, e, t) {
|
|
|
5133
6653
|
const map = scene.transforms?.get(e);
|
|
5134
6654
|
return map ? map(p) : p;
|
|
5135
6655
|
}
|
|
5136
|
-
function
|
|
6656
|
+
function fontScaleFor(spec, w, h) {
|
|
6657
|
+
return spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
6658
|
+
}
|
|
6659
|
+
function textFontPx(s, size, fontScale) {
|
|
5137
6660
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
5138
|
-
|
|
6661
|
+
return m ? Number(m[1]) * fontScale : size;
|
|
6662
|
+
}
|
|
6663
|
+
function textBox(s, e, p, fontScale, unitScale) {
|
|
6664
|
+
const fh = textFontPx(s, e.size, fontScale);
|
|
5139
6665
|
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
5140
|
-
const
|
|
6666
|
+
const est = Math.max(fh, 0.62 * fh * Math.min(str.length, 48));
|
|
6667
|
+
const fw = s.maxWidth ? Math.min(est, s.maxWidth * unitScale) : est;
|
|
5141
6668
|
const align = s.align ?? "center";
|
|
5142
6669
|
const baseline = s.baseline ?? "middle";
|
|
5143
6670
|
return {
|
|
@@ -5183,7 +6710,24 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
5183
6710
|
}
|
|
5184
6711
|
return rs.progress;
|
|
5185
6712
|
}
|
|
5186
|
-
function
|
|
6713
|
+
function gradientAt(gr, u, v) {
|
|
6714
|
+
let t;
|
|
6715
|
+
if (gr.type === "linear") {
|
|
6716
|
+
const dx = gr.to[0] - gr.from[0], dy = gr.to[1] - gr.from[1];
|
|
6717
|
+
const len2 = dx * dx + dy * dy || 1;
|
|
6718
|
+
t = ((u - gr.from[0]) * dx + (v - gr.from[1]) * dy) / len2;
|
|
6719
|
+
} else {
|
|
6720
|
+
t = Math.hypot(u - gr.center[0], v - gr.center[1]) / gr.radius;
|
|
6721
|
+
}
|
|
6722
|
+
const st = gr.stops;
|
|
6723
|
+
t = Math.max(st[0].at, Math.min(st[st.length - 1].at, t));
|
|
6724
|
+
let k = 0;
|
|
6725
|
+
while (k < st.length - 2 && t > st[k + 1].at) k++;
|
|
6726
|
+
const A = st[k], B = st[Math.min(k + 1, st.length - 1)];
|
|
6727
|
+
const w = B.at > A.at ? (t - A.at) / (B.at - A.at) : 0;
|
|
6728
|
+
return { luma: hexLuma(A.color) * (1 - w) + hexLuma(B.color) * w, opacity: (A.opacity ?? 1) * (1 - w) + (B.opacity ?? 1) * w };
|
|
6729
|
+
}
|
|
6730
|
+
function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1, evenodd = false) {
|
|
5187
6731
|
const nc = c1 - c0 + 1;
|
|
5188
6732
|
const cov = new Float64Array(nc * (r1 - r0 + 1));
|
|
5189
6733
|
const xMin = c0 * cellW;
|
|
@@ -5216,30 +6760,52 @@ function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
|
|
|
5216
6760
|
hits.sort((m, n) => m.x - n.x);
|
|
5217
6761
|
let wind = 0;
|
|
5218
6762
|
let start = 0;
|
|
6763
|
+
const filled = (w) => evenodd ? (w & 1) !== 0 : w !== 0;
|
|
5219
6764
|
for (const hit of hits) {
|
|
5220
|
-
const before = wind;
|
|
6765
|
+
const before = filled(wind);
|
|
5221
6766
|
wind += hit.dir;
|
|
5222
|
-
|
|
5223
|
-
|
|
6767
|
+
const after = filled(wind);
|
|
6768
|
+
if (!before && after) start = hit.x;
|
|
6769
|
+
else if (before && !after) addSpan(r - r0, start, hit.x);
|
|
5224
6770
|
}
|
|
5225
6771
|
}
|
|
5226
6772
|
}
|
|
5227
6773
|
return cov;
|
|
5228
6774
|
}
|
|
5229
|
-
function
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
6775
|
+
function softProfile(u) {
|
|
6776
|
+
if (u <= 0) return 1;
|
|
6777
|
+
if (u >= 1) return 0;
|
|
6778
|
+
return u <= 0.35 ? 1 - 0.25 * u / 0.35 : 0.75 * (1 - (u - 0.35) / 0.65);
|
|
6779
|
+
}
|
|
6780
|
+
var SOFT_DISC_MEAN = 0.3783;
|
|
6781
|
+
function discCellInk(cx, cy, rOuter, rInner, soft, x0, y0, cw, ch) {
|
|
6782
|
+
const nx = Math.max(x0, Math.min(cx, x0 + cw)) - cx;
|
|
6783
|
+
const ny = Math.max(y0, Math.min(cy, y0 + ch)) - cy;
|
|
6784
|
+
const near = Math.hypot(nx, ny);
|
|
6785
|
+
const fx = Math.max(Math.abs(x0 - cx), Math.abs(x0 + cw - cx));
|
|
6786
|
+
const fy = Math.max(Math.abs(y0 - cy), Math.abs(y0 + ch - cy));
|
|
6787
|
+
const far = Math.hypot(fx, fy);
|
|
6788
|
+
if (near >= rOuter || far <= rInner) return 0;
|
|
6789
|
+
if (far <= rOuter && near >= rInner) return soft ? softProfile(Math.hypot(x0 + cw / 2 - cx, y0 + ch / 2 - cy) / rOuter) : 1;
|
|
6790
|
+
if (cx - rOuter >= x0 && cx + rOuter <= x0 + cw && cy - rOuter >= y0 && cy + rOuter <= y0 + ch) {
|
|
6791
|
+
return Math.PI * (soft ? rOuter * rOuter * SOFT_DISC_MEAN : rOuter * rOuter - rInner * rInner) / (cw * ch);
|
|
6792
|
+
}
|
|
6793
|
+
const bx0 = Math.max(x0, cx - rOuter);
|
|
6794
|
+
const by0 = Math.max(y0, cy - rOuter);
|
|
6795
|
+
const sx = Math.min(x0 + cw, cx + rOuter) - bx0;
|
|
6796
|
+
const sy = Math.min(y0 + ch, cy + rOuter) - by0;
|
|
6797
|
+
if (sx <= 0 || sy <= 0) return 0;
|
|
6798
|
+
const step = Math.max(rOuter / 4, 1e-6);
|
|
6799
|
+
const ni = Math.max(4, Math.min(16, Math.ceil(sx / step)));
|
|
6800
|
+
const nj = Math.max(4, Math.min(16, Math.ceil(sy / step)));
|
|
6801
|
+
let ink = 0;
|
|
6802
|
+
for (let j = 0; j < nj; j++) {
|
|
6803
|
+
for (let i = 0; i < ni; i++) {
|
|
6804
|
+
const d = Math.hypot(bx0 + (i + 0.5) * sx / ni - cx, by0 + (j + 0.5) * sy / nj - cy);
|
|
6805
|
+
if (d <= rOuter && d >= rInner) ink += soft ? softProfile(d / rOuter) : 1;
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
return ink / (ni * nj) * sx * sy / (cw * ch);
|
|
5243
6809
|
}
|
|
5244
6810
|
function luminanceGrid(spec, opts = {}) {
|
|
5245
6811
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
@@ -5300,42 +6866,50 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5300
6866
|
else if (blend === "multiply") cells[idx] = cur * (1 - a * (1 - lum));
|
|
5301
6867
|
else cells[idx] = cur * (1 - a) + lum * a;
|
|
5302
6868
|
};
|
|
5303
|
-
const splatTrail = (layer, e, tPass, lifeA, alphaScale) => {
|
|
6869
|
+
const splatTrail = (layer, e, tPass, lifeA, alphaScale, layerCount) => {
|
|
5304
6870
|
const trail = layer.trail;
|
|
5305
6871
|
if (!trail) return;
|
|
5306
|
-
const
|
|
5307
|
-
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
5308
|
-
const headAlpha = alphaAt(e, tPass) * lifeA;
|
|
6872
|
+
const headAlpha = alphaAt(e, tPass) * lifeA * alphaScale;
|
|
5309
6873
|
const headSize = sizeAt(e, tPass);
|
|
5310
6874
|
const lum = spriteLuma(layer, e);
|
|
5311
6875
|
const soft = layer.sprite.kind === "circle" && !!layer.sprite.soft;
|
|
5312
|
-
const
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
6876
|
+
const echoes = trailEchoes(
|
|
6877
|
+
trail,
|
|
6878
|
+
tPass,
|
|
6879
|
+
headAlpha,
|
|
6880
|
+
headSize,
|
|
6881
|
+
(pt) => posOf(scene, e, pt),
|
|
6882
|
+
w,
|
|
6883
|
+
h,
|
|
6884
|
+
layer.wrap !== false,
|
|
6885
|
+
// As the renderer: a shape echoes at its time samples only; circles fill
|
|
6886
|
+
// (a hard circle's ribbon reads, on the coarse grid, as filled discs).
|
|
6887
|
+
isShapedSprite(layer.sprite) && layer.sprite.kind !== "circle" ? 0 : trailFillBudget(layerCount),
|
|
6888
|
+
4e-3,
|
|
6889
|
+
layer.blend === "lighter"
|
|
6890
|
+
);
|
|
6891
|
+
for (const ec of echoes) {
|
|
6892
|
+
const a = ec.alpha;
|
|
6893
|
+
const rad = ec.r;
|
|
6894
|
+
if (layer.sprite.kind === "polygon" && rad * 2 >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
6895
|
+
splatPolygonOutline(layer, layer.sprite, e, ec, rad * 2, Math.min(1, a), lum, ec.t);
|
|
6896
|
+
continue;
|
|
6897
|
+
}
|
|
6898
|
+
const pos = ec;
|
|
5328
6899
|
const c0 = Math.max(0, Math.floor((pos.x - rad) / cellW));
|
|
5329
6900
|
const c1 = Math.min(cols - 1, Math.floor((pos.x + rad) / cellW));
|
|
5330
6901
|
const r0 = Math.max(0, Math.floor((pos.y - rad) / cellH));
|
|
5331
6902
|
const r1 = Math.min(rows - 1, Math.floor((pos.y + rad) / cellH));
|
|
5332
6903
|
for (let r = r0; r <= r1; r++) {
|
|
5333
6904
|
for (let c = c0; c <= c1; c++) {
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
6905
|
+
let wgt = 1;
|
|
6906
|
+
if (soft) {
|
|
6907
|
+
wgt = discCellInk(pos.x, pos.y, rad, 0, true, c * cellW, r * cellH, cellW, cellH);
|
|
6908
|
+
if (wgt <= 0) continue;
|
|
6909
|
+
} else {
|
|
6910
|
+
const d = Math.hypot((c + 0.5) * cellW - pos.x, (r + 0.5) * cellH - pos.y);
|
|
6911
|
+
if (d > rad + Math.min(cellW, cellH) / 2) continue;
|
|
6912
|
+
}
|
|
5339
6913
|
compose(r * cols + c, lum, Math.min(1, a * wgt), layer.blend);
|
|
5340
6914
|
}
|
|
5341
6915
|
}
|
|
@@ -5346,17 +6920,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5346
6920
|
const rot = rotationAt(e, tPass);
|
|
5347
6921
|
const cr = Math.cos(rot);
|
|
5348
6922
|
const sr = Math.sin(rot);
|
|
5349
|
-
const
|
|
5350
|
-
const
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
const n = pts[(i + 1) % pts.length];
|
|
5356
|
-
return acc + Math.hypot(n.x - q.x, n.y - q.y);
|
|
5357
|
-
}, 0);
|
|
5358
|
-
band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
|
|
5359
|
-
}
|
|
6923
|
+
const fr = isPathPolygon(s) ? pathFrame(s, radius, w, h) : { sx: radius, sy: radius, ox: 0, oy: 0, uniform: true };
|
|
6924
|
+
const mapUnit = (u, v) => {
|
|
6925
|
+
const qx = (u - fr.ox) * fr.sx, qy = (v - fr.oy) * fr.sy;
|
|
6926
|
+
return { x: p.x + qx * cr - qy * sr, y: p.y + qx * sr + qy * cr };
|
|
6927
|
+
};
|
|
6928
|
+
const pts = isPathPolygon(s) ? flattenSprite(s).points.map(([u, v]) => mapUnit(u, v)) : polygonPoints(s, radius).map((q) => ({ x: p.x + q.x * cr - q.y * sr, y: p.y + q.x * sr + q.y * cr }));
|
|
5360
6929
|
let minX = Infinity;
|
|
5361
6930
|
let maxX = -Infinity;
|
|
5362
6931
|
let minY = Infinity;
|
|
@@ -5367,34 +6936,97 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5367
6936
|
minY = Math.min(minY, q.y);
|
|
5368
6937
|
maxY = Math.max(maxY, q.y);
|
|
5369
6938
|
}
|
|
5370
|
-
const
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
6939
|
+
const strokeQuadsPx = (part) => {
|
|
6940
|
+
if (!part.stroke) return [];
|
|
6941
|
+
const hw = part.stroke.width * radius / 2;
|
|
6942
|
+
const out = [];
|
|
6943
|
+
for (const l of part.flat.lines) {
|
|
6944
|
+
const m = l.points.map(([u, v]) => mapUnit(u, v));
|
|
6945
|
+
const n = l.closed ? m.length : m.length - 1;
|
|
6946
|
+
for (let i = 0; i < n; i++) {
|
|
6947
|
+
const A = m[i], B = m[(i + 1) % m.length];
|
|
6948
|
+
const dx = B.x - A.x, dy = B.y - A.y, len = Math.hypot(dx, dy) || 1;
|
|
6949
|
+
const nx = -dy / len * hw, ny = dx / len * hw;
|
|
6950
|
+
out.push([{ x: A.x + nx, y: A.y + ny }, { x: B.x + nx, y: B.y + ny }, { x: B.x - nx, y: B.y - ny }, { x: A.x - nx, y: A.y - ny }]);
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
return out;
|
|
6954
|
+
};
|
|
6955
|
+
if (isPathPolygon(s)) {
|
|
6956
|
+
for (const part of pathParts(s)) for (const quad of strokeQuadsPx(part)) for (const q of quad) {
|
|
6957
|
+
minX = Math.min(minX, q.x);
|
|
6958
|
+
maxX = Math.max(maxX, q.x);
|
|
6959
|
+
minY = Math.min(minY, q.y);
|
|
6960
|
+
maxY = Math.max(maxY, q.y);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
const c0 = Math.max(0, Math.floor(minX / cellW));
|
|
6964
|
+
const c1 = Math.min(cols - 1, Math.floor(maxX / cellW));
|
|
6965
|
+
const r0 = Math.max(0, Math.floor(minY / cellH));
|
|
6966
|
+
const r1 = Math.min(rows - 1, Math.floor(maxY / cellH));
|
|
5374
6967
|
if (c0 > c1 || r0 > r1) return;
|
|
5375
|
-
const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
|
|
5376
6968
|
const nc = c1 - c0 + 1;
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
6969
|
+
const unitAt = (x, y) => {
|
|
6970
|
+
const dx = x - p.x, dy = y - p.y;
|
|
6971
|
+
return [(dx * cr + dy * sr) / Math.max(fr.sx, 1e-6) + fr.ox, (-dx * sr + dy * cr) / Math.max(fr.sy, 1e-6) + fr.oy];
|
|
6972
|
+
};
|
|
6973
|
+
const parts = isPathPolygon(s) ? pathParts(s) : null;
|
|
6974
|
+
const fillPass = (ring, rule, partLum, partA, part) => {
|
|
6975
|
+
const cov = polygonCellCoverage(ring, cellW, cellH, c0, c1, r0, r1, rule);
|
|
6976
|
+
for (let r = r0; r <= r1; r++) {
|
|
6977
|
+
for (let c = c0; c <= c1; c++) {
|
|
6978
|
+
const f = cov[(r - r0) * nc + (c - c0)];
|
|
6979
|
+
if (!(f > 0)) continue;
|
|
6980
|
+
const x = (c + 0.5) * cellW;
|
|
6981
|
+
const y = (r + 0.5) * cellH;
|
|
6982
|
+
let cl = partLum, ca = 1;
|
|
6983
|
+
if (part?.gradient) {
|
|
6984
|
+
const [u, v] = unitAt(x, y);
|
|
6985
|
+
const [gu, gv] = gradientSpace(part, u, v);
|
|
6986
|
+
const g = gradientAt(part.gradient, gu, gv);
|
|
6987
|
+
cl = g.luma;
|
|
6988
|
+
ca = g.opacity;
|
|
5391
6989
|
}
|
|
5392
|
-
|
|
5393
|
-
wgt
|
|
5394
|
-
}
|
|
5395
|
-
|
|
6990
|
+
const wgt = s.soft && !parts ? f * softProfile(Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6)) : f;
|
|
6991
|
+
if (wgt > 0) compose(r * cols + c, cl, a * partA * ca * Math.min(1, wgt), layer.blend);
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
};
|
|
6995
|
+
if (!parts) {
|
|
6996
|
+
fillPass(pts, polygonFillRule(s) === "evenodd", lum, 1, null);
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
for (const part of parts) {
|
|
7000
|
+
const partLum = part.color ? hexLuma(part.color) : lum;
|
|
7001
|
+
if (part.fill && part.flat.points.length >= 3) {
|
|
7002
|
+
fillPass(part.flat.points.map(([u, v]) => mapUnit(u, v)), part.rule === "evenodd", partLum, part.opacity, part);
|
|
7003
|
+
}
|
|
7004
|
+
if (part.stroke) {
|
|
7005
|
+
const sa = (part.stroke.opacity ?? 1) * part.opacity;
|
|
7006
|
+
const sl = hexLuma(part.stroke.color);
|
|
7007
|
+
const scov = new Float64Array(nc * (r1 - r0 + 1));
|
|
7008
|
+
for (const qp of strokeQuadsPx(part)) {
|
|
7009
|
+
let qx0 = Infinity, qx1 = -Infinity, qy0 = Infinity, qy1 = -Infinity;
|
|
7010
|
+
for (const q of qp) {
|
|
7011
|
+
qx0 = Math.min(qx0, q.x);
|
|
7012
|
+
qx1 = Math.max(qx1, q.x);
|
|
7013
|
+
qy0 = Math.min(qy0, q.y);
|
|
7014
|
+
qy1 = Math.max(qy1, q.y);
|
|
7015
|
+
}
|
|
7016
|
+
const a0 = Math.max(c0, Math.floor(qx0 / cellW)), a1 = Math.min(c1, Math.floor(qx1 / cellW));
|
|
7017
|
+
const b0 = Math.max(r0, Math.floor(qy0 / cellH)), b1 = Math.min(r1, Math.floor(qy1 / cellH));
|
|
7018
|
+
if (a0 > a1 || b0 > b1) continue;
|
|
7019
|
+
const qc = polygonCellCoverage(qp, cellW, cellH, a0, a1, b0, b1);
|
|
7020
|
+
const qn = a1 - a0 + 1;
|
|
7021
|
+
for (let r = b0; r <= b1; r++) for (let c = a0; c <= a1; c++) {
|
|
7022
|
+
const i = (r - r0) * nc + (c - c0);
|
|
7023
|
+
scov[i] = Math.min(1, scov[i] + qc[(r - b0) * qn + (c - a0)]);
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) {
|
|
7027
|
+
const f = scov[(r - r0) * nc + (c - c0)];
|
|
7028
|
+
if (f > 0) compose(r * cols + c, sl, a * sa * f, layer.blend);
|
|
5396
7029
|
}
|
|
5397
|
-
if (wgt > 0) compose(r * cols + c, lum, a * Math.min(1, wgt), layer.blend);
|
|
5398
7030
|
}
|
|
5399
7031
|
}
|
|
5400
7032
|
};
|
|
@@ -5403,14 +7035,14 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5403
7035
|
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
|
|
5404
7036
|
if (lifeA <= 0) continue;
|
|
5405
7037
|
for (const e of entities) {
|
|
5406
|
-
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
7038
|
+
splatTrail(layer, e, tPass, lifeA, alphaScale, entities.length);
|
|
5407
7039
|
const a = Math.min(1, alphaAt(e, tPass) * lifeA) * alphaScale;
|
|
5408
7040
|
if (a <= 4e-3) continue;
|
|
5409
7041
|
const lum = spriteLuma(layer, e);
|
|
5410
7042
|
const p = posOf(scene, e, tPass);
|
|
5411
7043
|
const sz = layer.transform || layer.group !== void 0 ? sizeAt(e, tPass) * paintSize(spec, layer) : sizeAt(e, tPass);
|
|
5412
7044
|
const s = layer.sprite;
|
|
5413
|
-
if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
7045
|
+
if (s.kind === "polygon" && (sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH) || isPathPolygon(s) && ((s.span ?? "box") !== "box" || s.origin !== void 0))) {
|
|
5414
7046
|
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
5415
7047
|
continue;
|
|
5416
7048
|
}
|
|
@@ -5464,7 +7096,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5464
7096
|
let halfX = sz / 2;
|
|
5465
7097
|
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
5466
7098
|
if (s.kind === "text") {
|
|
5467
|
-
const box = textBox(s, e, p);
|
|
7099
|
+
const box = textBox(s, e, p, fontScaleFor(spec, w, h), scale);
|
|
5468
7100
|
centerX = box.cx;
|
|
5469
7101
|
centerY = box.cy;
|
|
5470
7102
|
halfX = box.halfX;
|
|
@@ -5491,45 +7123,24 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5491
7123
|
const circular = s.kind === "circle" || s.kind === "ring" || s.kind === "polygon";
|
|
5492
7124
|
const soft = (s.kind === "circle" || s.kind === "polygon") && !!s.soft;
|
|
5493
7125
|
const shapeWeight = s.kind === "polygon" ? polygonFill(s, sz / 2) : s.kind === "rect" && s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
5494
|
-
const
|
|
5495
|
-
const
|
|
5496
|
-
const
|
|
5497
|
-
const
|
|
5498
|
-
const c0 = Math.max(0, Math.floor((centerX - reachX) / cellW));
|
|
5499
|
-
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
5500
|
-
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
5501
|
-
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
7126
|
+
const c0 = Math.max(0, Math.floor((centerX - halfX) / cellW));
|
|
7127
|
+
const c1 = Math.min(cols - 1, Math.floor((centerX + halfX) / cellW));
|
|
7128
|
+
const r0 = Math.max(0, Math.floor((centerY - halfY) / cellH));
|
|
7129
|
+
const r1 = Math.min(rows - 1, Math.floor((centerY + halfY) / cellH));
|
|
5502
7130
|
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1) : 1);
|
|
5503
|
-
const byArea = s.kind === "rect" || s.kind === "bar";
|
|
7131
|
+
const byArea = s.kind === "rect" || s.kind === "bar" || s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock";
|
|
5504
7132
|
for (let r = r0; r <= r1; r++) {
|
|
5505
7133
|
const fy = byArea ? Math.max(0, Math.min((r + 1) * cellH, centerY + halfY) - Math.max(r * cellH, centerY - halfY)) / cellH : 1;
|
|
5506
7134
|
if (fy <= 0) continue;
|
|
5507
7135
|
for (let c = c0; c <= c1; c++) {
|
|
5508
|
-
const dx = (c + 0.5) * cellW - centerX;
|
|
5509
|
-
const dy = (r + 0.5) * cellH - centerY;
|
|
5510
7136
|
if (circular) {
|
|
5511
|
-
const
|
|
5512
|
-
const
|
|
5513
|
-
if (
|
|
5514
|
-
if (s.kind === "ring") {
|
|
5515
|
-
const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
5516
|
-
if (d < halfX - strokeW) continue;
|
|
5517
|
-
}
|
|
5518
|
-
let wgt;
|
|
5519
|
-
if (glow) {
|
|
5520
|
-
const core = halfX;
|
|
5521
|
-
if (d <= core) wgt = 1;
|
|
5522
|
-
else {
|
|
5523
|
-
const f = Math.max(0, 1 - (d - core) / Math.max(reachX - core, 1e-6));
|
|
5524
|
-
wgt = f * f;
|
|
5525
|
-
}
|
|
5526
|
-
} else {
|
|
5527
|
-
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
5528
|
-
}
|
|
7137
|
+
const inner = s.kind === "ring" ? Math.max(0, halfX - (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) : 0;
|
|
7138
|
+
const wgt = discCellInk(centerX, centerY, halfX, inner, soft, c * cellW, r * cellH, cellW, cellH);
|
|
7139
|
+
if (wgt <= 0) continue;
|
|
5529
7140
|
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
5530
7141
|
} else if (byArea) {
|
|
5531
7142
|
const fx = Math.max(0, Math.min((c + 1) * cellW, centerX + halfX) - Math.max(c * cellW, centerX - halfX)) / cellW;
|
|
5532
|
-
if (fx > 0) compose(r * cols + c, lum, a * shapeWeight * fx * fy, layer.blend);
|
|
7143
|
+
if (fx > 0) compose(r * cols + c, lum, a * inkWeight * shapeWeight * fx * fy, layer.blend);
|
|
5533
7144
|
} else {
|
|
5534
7145
|
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
5535
7146
|
}
|
|
@@ -5538,9 +7149,8 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5538
7149
|
}
|
|
5539
7150
|
if (layer.links) {
|
|
5540
7151
|
const positions = entities.map((e) => posOf(scene, e, tPass));
|
|
5541
|
-
const maxDistPx = layer.links.maxDist * scale;
|
|
5542
7152
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
5543
|
-
const edges = linkEdges(layer.links, positions,
|
|
7153
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false && motionWraps, w, h);
|
|
5544
7154
|
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
5545
7155
|
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
5546
7156
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
@@ -5681,8 +7291,7 @@ function textSprites(spec, opts = {}) {
|
|
|
5681
7291
|
}
|
|
5682
7292
|
if (layer.sprite.kind !== "text") return;
|
|
5683
7293
|
const s = layer.sprite;
|
|
5684
|
-
const
|
|
5685
|
-
const sizePx = m ? Number(m[1]) : entities[0] ? sizeAt(entities[0], t) : 0;
|
|
7294
|
+
const sizePx = textFontPx(s, entities[0] ? sizeAt(entities[0], t) : 0, fontScaleFor(spec, scene.w, scene.h));
|
|
5686
7295
|
out.push({ layerIndex, key: layer.key, strings: [...s.strings], count: entities.length, sizePx: Math.round(sizePx) });
|
|
5687
7296
|
});
|
|
5688
7297
|
return out;
|
|
@@ -5753,7 +7362,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
5753
7362
|
const meanTaper = s.taper ? 0.64 : 1;
|
|
5754
7363
|
entArea = pathLength(strokeSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
|
|
5755
7364
|
} else if (s.kind === "text") {
|
|
5756
|
-
const box = textBox(s, e, { x: 0, y: 0 });
|
|
7365
|
+
const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
|
|
5757
7366
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
5758
7367
|
} else if (s.kind === "textBlock") {
|
|
5759
7368
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
@@ -5773,7 +7382,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
5773
7382
|
if (layer.links && entities.length > 1) {
|
|
5774
7383
|
const positions = entities.map((e) => posOf(scene, e, t));
|
|
5775
7384
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
5776
|
-
const edges = linkEdges(layer.links, positions, layer.links
|
|
7385
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false && motionWraps, w, h);
|
|
5777
7386
|
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
5778
7387
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
5779
7388
|
const lw = (layer.links.width ?? defaultWidth) * scale;
|
|
@@ -6227,8 +7836,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6227
7836
|
}
|
|
6228
7837
|
if (layer.links) {
|
|
6229
7838
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6230
|
-
const
|
|
6231
|
-
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false, w, h);
|
|
7839
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h);
|
|
6232
7840
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
6233
7841
|
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
6234
7842
|
const la = layer.links.alpha ?? 1;
|
|
@@ -6287,9 +7895,9 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6287
7895
|
if (!layer.links || layer.links.mode === "chain") continue;
|
|
6288
7896
|
const entities = allEntities[li];
|
|
6289
7897
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6290
|
-
const
|
|
6291
|
-
const pairs = linkPairs(positions, layer.links
|
|
6292
|
-
const maxPossible = Math.min(entities.length *
|
|
7898
|
+
const k = layer.links.k ?? 1;
|
|
7899
|
+
const pairs = linkPairs(positions, k, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h);
|
|
7900
|
+
const maxPossible = Math.min(entities.length * k, entities.length * (entities.length - 1) / 2);
|
|
6293
7901
|
if (maxPossible > 0 && pairs.length / maxPossible < 0.1) {
|
|
6294
7902
|
warnings.push({
|
|
6295
7903
|
path: `layers[${li}].links`,
|
|
@@ -6354,7 +7962,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6354
7962
|
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
6355
7963
|
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
6356
7964
|
const str = s.kind === "textBlock" ? s.text : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
6357
|
-
textBoxes.push({ li, label, alpha, str, ...box });
|
|
7965
|
+
textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
|
|
6358
7966
|
}
|
|
6359
7967
|
}
|
|
6360
7968
|
const asFraction = (b) => ({
|
|
@@ -6409,6 +8017,45 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6409
8017
|
});
|
|
6410
8018
|
}
|
|
6411
8019
|
}
|
|
8020
|
+
const cellFlagged = /* @__PURE__ */ new Set();
|
|
8021
|
+
for (let i = 0; i < textBoxes.length; i++) {
|
|
8022
|
+
const a = textBoxes[i];
|
|
8023
|
+
if (cellFlagged.has(a.li)) continue;
|
|
8024
|
+
const layer = spec.layers[a.li];
|
|
8025
|
+
const layout = layer.layout;
|
|
8026
|
+
if (layout?.type !== "list" && layout?.type !== "table") continue;
|
|
8027
|
+
for (let j = i + 1; j < textBoxes.length; j++) {
|
|
8028
|
+
const b = textBoxes[j];
|
|
8029
|
+
if (b.li !== a.li) continue;
|
|
8030
|
+
const ix = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
8031
|
+
const iy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
8032
|
+
if (ix <= 0 || iy <= 0) continue;
|
|
8033
|
+
const smaller = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
|
|
8034
|
+
if (smaller <= 0 || ix * iy / smaller <= 0.1) continue;
|
|
8035
|
+
cellFlagged.add(a.li);
|
|
8036
|
+
const cells = textBoxes.filter((x) => x.li === a.li);
|
|
8037
|
+
const dflt = scale === 1 ? LIMITS.defaultListGapPx : LIMITS.defaultListGap;
|
|
8038
|
+
const g = layout.gap ?? dflt;
|
|
8039
|
+
const gx = typeof g === "number" ? g : g.x ?? dflt;
|
|
8040
|
+
const gy = typeof g === "number" ? g : g.y ?? dflt;
|
|
8041
|
+
const cols = layout.type === "list" ? 1 : Math.max(1, Math.round(layout.columns));
|
|
8042
|
+
const unitPx = (v) => scale === 1 ? v : v * scale;
|
|
8043
|
+
const fromPx = (px) => scale === 1 ? Math.ceil(px) : Math.ceil(px / scale * 100) / 100;
|
|
8044
|
+
const maxW = cells.reduce((m, c) => Math.max(m, c.rawW), 0);
|
|
8045
|
+
const maxH = cells.reduce((m, c) => Math.max(m, c.rawH), 0);
|
|
8046
|
+
const fixes = [];
|
|
8047
|
+
const across = cols > 1 && maxW > unitPx(gx);
|
|
8048
|
+
if (across) fixes.push(`its labels are up to ~${Math.round(maxW)}px wide against a ${Math.round(unitPx(gx))}px column pitch (gap${typeof g === "number" ? "" : ".x"} ${gx}) \u2014 raise gap${typeof g === "number" ? "" : ".x"} to \u2265 ${fromPx(maxW * 1.1)}`);
|
|
8049
|
+
if (!across || maxH > unitPx(gy)) fixes.push(`rows are ~${Math.round(maxH)}px tall against a ${Math.round(unitPx(gy))}px row pitch (gap${typeof g === "number" ? "" : ".y"} ${gy}) \u2014 raise gap${typeof g === "number" ? "" : ".y"} to \u2265 ${fromPx(maxH * 1.1)}`);
|
|
8050
|
+
warnings.push({
|
|
8051
|
+
path: `layers[${a.li}].layout.gap`,
|
|
8052
|
+
code: "layout-text-overlap",
|
|
8053
|
+
message: `${a.label} cells overlap at ${w}\xD7${h}: a ${layout.type}'s \`gap\` is the pitch between cell anchors, not the space between cells; ${fixes.join("; and ")}, or shrink the text`,
|
|
8054
|
+
boxes: [asFraction(a), asFraction(b)]
|
|
8055
|
+
});
|
|
8056
|
+
break;
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
6412
8059
|
const readFlagged = /* @__PURE__ */ new Set();
|
|
6413
8060
|
for (const b of textBoxes) {
|
|
6414
8061
|
if (readFlagged.has(b.li)) continue;
|
|
@@ -6514,20 +8161,20 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
|
|
|
6514
8161
|
const c = positionAt(e, 0, w, h);
|
|
6515
8162
|
const dx = pt.x - c.x;
|
|
6516
8163
|
const dy = pt.y - c.y;
|
|
6517
|
-
let
|
|
6518
|
-
if (sp.kind === "circle")
|
|
8164
|
+
let inside2 = false;
|
|
8165
|
+
if (sp.kind === "circle") inside2 = Math.hypot(dx, dy) <= e.size / 2;
|
|
6519
8166
|
else if (sp.kind === "rect") {
|
|
6520
8167
|
const rot = ((e.rotate ?? 0) % Math.PI + Math.PI) % Math.PI;
|
|
6521
8168
|
const quarter = Math.abs(rot - Math.PI / 2) < 0.01;
|
|
6522
8169
|
if (rot > 0.01 && rot < Math.PI - 0.01 && !quarter) continue;
|
|
6523
8170
|
const hx = (quarter ? e.size2 ?? e.size : e.size) / 2;
|
|
6524
8171
|
const hy = (quarter ? e.size : e.size2 ?? e.size) / 2;
|
|
6525
|
-
|
|
8172
|
+
inside2 = Math.abs(dx) <= hx && Math.abs(dy) <= hy;
|
|
6526
8173
|
} else {
|
|
6527
8174
|
if (e.rotate) continue;
|
|
6528
|
-
|
|
8175
|
+
inside2 = pointInPolygon(dx, dy, polygonPoints(sp, e.size / 2));
|
|
6529
8176
|
}
|
|
6530
|
-
if (!
|
|
8177
|
+
if (!inside2) continue;
|
|
6531
8178
|
plate = sourceOverPlate(plate, hexRgb(hex), Math.min(1, e.alpha * a));
|
|
6532
8179
|
covered = true;
|
|
6533
8180
|
}
|
|
@@ -6535,13 +8182,13 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
|
|
|
6535
8182
|
return covered ? { rgb: plate, luma: 0.2126 * plate.r + 0.7152 * plate.g + 0.0722 * plate.b } : null;
|
|
6536
8183
|
}
|
|
6537
8184
|
function pointInPolygon(x, y, pts) {
|
|
6538
|
-
let
|
|
8185
|
+
let inside2 = false;
|
|
6539
8186
|
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
6540
8187
|
const a = pts[i];
|
|
6541
8188
|
const b = pts[j];
|
|
6542
|
-
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x)
|
|
8189
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside2 = !inside2;
|
|
6543
8190
|
}
|
|
6544
|
-
return
|
|
8191
|
+
return inside2;
|
|
6545
8192
|
}
|
|
6546
8193
|
function entityReachesBox(e, r, box, w, h) {
|
|
6547
8194
|
if (e.motion === "static") {
|
|
@@ -6693,10 +8340,10 @@ function describeScene(spec, options = {}) {
|
|
|
6693
8340
|
let connectedComponents = null;
|
|
6694
8341
|
let isolatedNodes = null;
|
|
6695
8342
|
if (layer.links) {
|
|
6696
|
-
const
|
|
6697
|
-
const pairs =
|
|
8343
|
+
const n = entities.length;
|
|
8344
|
+
const pairs = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h).map(({ i, j }) => [i, j]);
|
|
6698
8345
|
linksDrawn = pairs.length;
|
|
6699
|
-
linksExpected = Math.
|
|
8346
|
+
linksExpected = layer.links.mode === "chain" ? Math.max(0, n - 1) + (layer.links.closed && n > 2 ? 1 : 0) : Math.min(n * (layer.links.k ?? 1), n * (n - 1) / 2);
|
|
6700
8347
|
const parent = Array.from({ length: entities.length }, (_, i) => i);
|
|
6701
8348
|
const find = (x) => {
|
|
6702
8349
|
while (parent[x] !== x) {
|
|
@@ -9197,6 +10844,7 @@ export {
|
|
|
9197
10844
|
LIMITS,
|
|
9198
10845
|
LOBBY_TALK_SPEC,
|
|
9199
10846
|
MATRIX_RAIN_SPEC,
|
|
10847
|
+
MAX_PATH_POINTS,
|
|
9200
10848
|
MURMUR_SPEC,
|
|
9201
10849
|
NOSTALGHIA_CANDLE_SPEC,
|
|
9202
10850
|
ORRERY_SPEC,
|
|
@@ -9252,6 +10900,7 @@ export {
|
|
|
9252
10900
|
fieldRgbAt,
|
|
9253
10901
|
fieldSampleTime,
|
|
9254
10902
|
finishStrength,
|
|
10903
|
+
flattenPath,
|
|
9255
10904
|
grainOffset,
|
|
9256
10905
|
grainTilePixels,
|
|
9257
10906
|
graphemeClusters,
|
|
@@ -9259,12 +10908,15 @@ export {
|
|
|
9259
10908
|
headingAt,
|
|
9260
10909
|
hexToRgb255,
|
|
9261
10910
|
ignoredPropertyPaths,
|
|
10911
|
+
isPathPolygon,
|
|
9262
10912
|
latticeHash,
|
|
9263
10913
|
layerCohesion,
|
|
10914
|
+
lerpPathD,
|
|
9264
10915
|
lerpSpec,
|
|
9265
10916
|
lerpValue,
|
|
9266
10917
|
lifeAlphaAt,
|
|
9267
10918
|
linkEdges,
|
|
10919
|
+
linkMaxDistPx,
|
|
9268
10920
|
linkPairs,
|
|
9269
10921
|
luminanceGrid,
|
|
9270
10922
|
manifestFor,
|
|
@@ -9273,6 +10925,8 @@ export {
|
|
|
9273
10925
|
motionStats,
|
|
9274
10926
|
nextKeyAfter,
|
|
9275
10927
|
normalizeSeed,
|
|
10928
|
+
pathFrame,
|
|
10929
|
+
pathParts,
|
|
9276
10930
|
perceiveScene,
|
|
9277
10931
|
perceiveSequenceFrame,
|
|
9278
10932
|
pickScreenOp,
|