@idle-screens/schema 3.12.1 → 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 +68 -3
- package/dist/index.d.ts +168 -8
- package/dist/index.js +1596 -362
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +105 -9
package/dist/index.js
CHANGED
|
@@ -290,6 +290,10 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
290
290
|
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
291
291
|
return lerpHex(a, b, kk);
|
|
292
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
|
+
}
|
|
293
297
|
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
294
298
|
return b.map((bv, i) => walk(a[i], bv, i));
|
|
295
299
|
}
|
|
@@ -306,6 +310,33 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
306
310
|
};
|
|
307
311
|
return walk(from, to, rootKey, specRoot);
|
|
308
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
|
+
}
|
|
309
340
|
function textStringsOf(sprite) {
|
|
310
341
|
if (sprite.kind === "text") return sprite.strings;
|
|
311
342
|
if (sprite.kind === "textBlock") return [sprite.text];
|
|
@@ -362,7 +393,7 @@ function steerablePaths(spec) {
|
|
|
362
393
|
if (!spec || typeof spec !== "object") return [];
|
|
363
394
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer", "group", "origin"]);
|
|
364
395
|
const SKIP_ROOT = /* @__PURE__ */ new Set(["timeline"]);
|
|
365
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
396
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands", "parts"]);
|
|
366
397
|
const layers = spec.layers;
|
|
367
398
|
const shadowedRootKeys = new Set(
|
|
368
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)) : []
|
|
@@ -674,6 +705,654 @@ function timelineSampleTimesAfter(spec, now, dense = false, only) {
|
|
|
674
705
|
return local.filter((s) => s > now);
|
|
675
706
|
}
|
|
676
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
|
+
|
|
677
1356
|
// src/validate.ts
|
|
678
1357
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
679
1358
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -719,7 +1398,8 @@ var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "c
|
|
|
719
1398
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
720
1399
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "feather", "colors", "colorWeights"]);
|
|
721
1400
|
var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
|
|
722
|
-
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"]);
|
|
723
1403
|
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
724
1404
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
725
1405
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
@@ -859,6 +1539,23 @@ function validateSpecCore(spec) {
|
|
|
859
1539
|
err("layers", "must be a non-empty array");
|
|
860
1540
|
} else {
|
|
861
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
|
+
}
|
|
862
1559
|
let total = 0;
|
|
863
1560
|
spec.layers.forEach((layer, i) => {
|
|
864
1561
|
total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
|
|
@@ -1232,10 +1929,20 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1232
1929
|
if (layer.links !== void 0) {
|
|
1233
1930
|
if (!isObj(layer.links)) err(`${path}.links`, "must be an object");
|
|
1234
1931
|
else {
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
+
);
|
|
1237
1945
|
}
|
|
1238
|
-
positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
|
|
1239
1946
|
if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
|
|
1240
1947
|
if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
|
|
1241
1948
|
err(`${path}.links.alpha`, "must be 0..1");
|
|
@@ -1476,6 +2183,34 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
1476
2183
|
err(`${path}.sides`, `must be an integer ${LIMITS.minPolygonSides}..${LIMITS.maxPolygonSides}`);
|
|
1477
2184
|
}
|
|
1478
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
|
+
}
|
|
1479
2214
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
1480
2215
|
color(sprite.color, `${path}.color`, err);
|
|
1481
2216
|
validatePalette(sprite, path, err);
|
|
@@ -1732,6 +2467,58 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
1732
2467
|
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
1733
2468
|
}
|
|
1734
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
|
+
}
|
|
1735
2522
|
function validateShapePoints(points, path, min, err) {
|
|
1736
2523
|
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
1737
2524
|
return err(path, `must be ${min}..${LIMITS.maxShapePoints} [x, y] pairs in unit coordinates (-1..1)`);
|
|
@@ -1893,206 +2680,39 @@ function validateSequence(seq) {
|
|
|
1893
2680
|
path: `segments[${i}].transition`,
|
|
1894
2681
|
code: "morph-structural-mismatch",
|
|
1895
2682
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1896
|
-
});
|
|
1897
|
-
} else if (s.scene.timeline === void 0 && next.scene.timeline === void 0 && morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" || s.transition.text === "dip" })) {
|
|
1898
|
-
warnings.push({
|
|
1899
|
-
path: `segments[${i}].transition`,
|
|
1900
|
-
code: "morph-nothing-morphable",
|
|
1901
|
-
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
1907
|
-
const sq = seq;
|
|
1908
|
-
const last = sq.segments.length - 1;
|
|
1909
|
-
const lastTr = sq.segments[last]?.transition;
|
|
1910
|
-
let why = null;
|
|
1911
|
-
if (sq.loop !== true) why = "the sequence does not loop";
|
|
1912
|
-
else if (last < 1) why = "there is only one segment";
|
|
1913
|
-
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
1914
|
-
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
1915
|
-
else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
|
|
1916
|
-
if (why || !canWrapMorph(sq)) {
|
|
1917
|
-
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
1921
|
-
}
|
|
1922
|
-
function assertValidSequence(seq) {
|
|
1923
|
-
const r = validateSequence(seq);
|
|
1924
|
-
if (!r.valid) {
|
|
1925
|
-
throw new Error(`invalid sequence:
|
|
1926
|
-
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
1927
|
-
}
|
|
1928
|
-
return seq;
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
// src/shapes.ts
|
|
1932
|
-
function isShapedSprite(s) {
|
|
1933
|
-
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
1934
|
-
}
|
|
1935
|
-
function barFraction(s, index) {
|
|
1936
|
-
const n = s.values.length;
|
|
1937
|
-
if (n === 0) return 0;
|
|
1938
|
-
const v = s.values[(index % n + n) % n] ?? 0;
|
|
1939
|
-
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
1940
|
-
if (!(max > 0)) return 0;
|
|
1941
|
-
return Math.max(0, Math.min(1, v / max));
|
|
1942
|
-
}
|
|
1943
|
-
function barBox(direction, len, thick) {
|
|
1944
|
-
switch (direction) {
|
|
1945
|
-
case "left":
|
|
1946
|
-
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1947
|
-
case "up":
|
|
1948
|
-
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1949
|
-
case "down":
|
|
1950
|
-
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1951
|
-
default:
|
|
1952
|
-
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
var DEFAULT_POLYGON_SIDES = 6;
|
|
1956
|
-
var STROKE_SAMPLES = 24;
|
|
1957
|
-
function polygonPoints(s, radius) {
|
|
1958
|
-
if (s.points && s.points.length >= 3) {
|
|
1959
|
-
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
1960
|
-
}
|
|
1961
|
-
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
1962
|
-
const out = [];
|
|
1963
|
-
for (let k = 0; k < n; k++) {
|
|
1964
|
-
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
1965
|
-
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
1966
|
-
}
|
|
1967
|
-
return out;
|
|
1968
|
-
}
|
|
1969
|
-
function polygonArea(pts) {
|
|
1970
|
-
let a = 0;
|
|
1971
|
-
for (let i = 0; i < pts.length; i++) {
|
|
1972
|
-
const p = pts[i];
|
|
1973
|
-
const q = pts[(i + 1) % pts.length];
|
|
1974
|
-
a += p.x * q.y - q.x * p.y;
|
|
1975
|
-
}
|
|
1976
|
-
return Math.abs(a) / 2;
|
|
1977
|
-
}
|
|
1978
|
-
function polygonFill(s, radius) {
|
|
1979
|
-
if (radius <= 0) return 0;
|
|
1980
|
-
const disc = Math.PI * radius * radius;
|
|
1981
|
-
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
1982
|
-
}
|
|
1983
|
-
function catmullRom(p0, p1, p2, p3, u) {
|
|
1984
|
-
const u2 = u * u;
|
|
1985
|
-
const u3 = u2 * u;
|
|
1986
|
-
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
1987
|
-
}
|
|
1988
|
-
function strokeSamples(s, halfSize, n) {
|
|
1989
|
-
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
1990
|
-
const m = pts.length;
|
|
1991
|
-
if (m < 2) return pts;
|
|
1992
|
-
const smooth = s.curve !== "linear" && m >= 3;
|
|
1993
|
-
const segs = m - 1;
|
|
1994
|
-
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
1995
|
-
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
1996
|
-
const out = [];
|
|
1997
|
-
for (let i = 0; i < n; i++) {
|
|
1998
|
-
const u = i / (n - 1) * segs;
|
|
1999
|
-
const seg = Math.min(segs - 1, Math.floor(u));
|
|
2000
|
-
const local = u - seg;
|
|
2001
|
-
const p1 = at(seg);
|
|
2002
|
-
const p2 = at(seg + 1);
|
|
2003
|
-
if (!smooth) {
|
|
2004
|
-
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
2005
|
-
} else {
|
|
2006
|
-
const p0 = at(seg - 1);
|
|
2007
|
-
const p3 = at(seg + 2);
|
|
2008
|
-
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
2683
|
+
});
|
|
2684
|
+
} else if (s.scene.timeline === void 0 && next.scene.timeline === void 0 && morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" || s.transition.text === "dip" })) {
|
|
2685
|
+
warnings.push({
|
|
2686
|
+
path: `segments[${i}].transition`,
|
|
2687
|
+
code: "morph-nothing-morphable",
|
|
2688
|
+
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2009
2691
|
}
|
|
2010
2692
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
2024
|
-
}
|
|
2025
|
-
function strokeWidthPx(s, scale) {
|
|
2026
|
-
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2027
|
-
}
|
|
2028
|
-
function featherRamp(u, feather) {
|
|
2029
|
-
const band = Math.max(0, Math.min(1, feather)) / 2;
|
|
2030
|
-
const d = Math.min(u, 1 - u);
|
|
2031
|
-
if (band <= 0) return d >= 0 ? 1 : 0;
|
|
2032
|
-
return Math.max(0, Math.min(1, d / band));
|
|
2033
|
-
}
|
|
2034
|
-
var FEATHER_MASK_SIZE = 128;
|
|
2035
|
-
function ribbonSides(pts, half) {
|
|
2036
|
-
const n = pts.length;
|
|
2037
|
-
const left = [];
|
|
2038
|
-
const right = [];
|
|
2039
|
-
if (n < 2) return { left, right };
|
|
2040
|
-
const dir = (a, b) => {
|
|
2041
|
-
const dx = b.x - a.x;
|
|
2042
|
-
const dy = b.y - a.y;
|
|
2043
|
-
const l = Math.hypot(dx, dy);
|
|
2044
|
-
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2045
|
-
};
|
|
2046
|
-
for (let i = 0; i < n; i++) {
|
|
2047
|
-
const tIn = i > 0 ? dir(pts[i - 1], pts[i]) : dir(pts[0], pts[1]);
|
|
2048
|
-
const tOut = i < n - 1 ? dir(pts[i], pts[i + 1]) : tIn;
|
|
2049
|
-
let tx = tIn.x + tOut.x;
|
|
2050
|
-
let ty = tIn.y + tOut.y;
|
|
2051
|
-
const tl = Math.hypot(tx, ty);
|
|
2052
|
-
if (tl < 1e-9) {
|
|
2053
|
-
tx = tOut.x;
|
|
2054
|
-
ty = tOut.y;
|
|
2055
|
-
} else {
|
|
2056
|
-
tx /= tl;
|
|
2057
|
-
ty /= tl;
|
|
2693
|
+
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
2694
|
+
const sq = seq;
|
|
2695
|
+
const last = sq.segments.length - 1;
|
|
2696
|
+
const lastTr = sq.segments[last]?.transition;
|
|
2697
|
+
let why = null;
|
|
2698
|
+
if (sq.loop !== true) why = "the sequence does not loop";
|
|
2699
|
+
else if (last < 1) why = "there is only one segment";
|
|
2700
|
+
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
2701
|
+
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
2702
|
+
else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
|
|
2703
|
+
if (why || !canWrapMorph(sq)) {
|
|
2704
|
+
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
2058
2705
|
}
|
|
2059
|
-
const hw = half[i] / Math.max(0.5, tx * tOut.x + ty * tOut.y);
|
|
2060
|
-
const p = pts[i];
|
|
2061
|
-
left.push({ x: p.x - ty * hw, y: p.y + tx * hw });
|
|
2062
|
-
right.push({ x: p.x + ty * hw, y: p.y - tx * hw });
|
|
2063
2706
|
}
|
|
2064
|
-
return {
|
|
2707
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
2065
2708
|
}
|
|
2066
|
-
function
|
|
2067
|
-
const
|
|
2068
|
-
if (
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const l = Math.hypot(dx, dy);
|
|
2074
|
-
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2075
|
-
};
|
|
2076
|
-
const { left, right } = ribbonSides(pts, Array.from({ length: n }, (_, i) => half(i)));
|
|
2077
|
-
const cap = (c, r, from) => {
|
|
2078
|
-
const out = [];
|
|
2079
|
-
const steps = 6;
|
|
2080
|
-
for (let k = 1; k < steps; k++) {
|
|
2081
|
-
const a = from - Math.PI * k / steps;
|
|
2082
|
-
out.push({ x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r });
|
|
2083
|
-
}
|
|
2084
|
-
return out;
|
|
2085
|
-
};
|
|
2086
|
-
const endT = dir(pts[n - 2], pts[n - 1]);
|
|
2087
|
-
const startT = dir(pts[0], pts[1]);
|
|
2088
|
-
const endA = Math.atan2(endT.y, endT.x) + Math.PI / 2;
|
|
2089
|
-
const startA = Math.atan2(startT.y, startT.x) - Math.PI / 2;
|
|
2090
|
-
return [
|
|
2091
|
-
...left,
|
|
2092
|
-
...cap(pts[n - 1], half(n - 1), endA),
|
|
2093
|
-
...right.reverse(),
|
|
2094
|
-
...cap(pts[0], half(0), startA)
|
|
2095
|
-
];
|
|
2709
|
+
function assertValidSequence(seq) {
|
|
2710
|
+
const r = validateSequence(seq);
|
|
2711
|
+
if (!r.valid) {
|
|
2712
|
+
throw new Error(`invalid sequence:
|
|
2713
|
+
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
2714
|
+
}
|
|
2715
|
+
return seq;
|
|
2096
2716
|
}
|
|
2097
2717
|
|
|
2098
2718
|
// src/simulate.ts
|
|
@@ -2616,9 +3236,13 @@ function wrapDist(pi, pj, doWrap, w, h) {
|
|
|
2616
3236
|
}
|
|
2617
3237
|
return Math.sqrt(dx * dx + dy * dy);
|
|
2618
3238
|
}
|
|
3239
|
+
function linkMaxDistPx(links, scale) {
|
|
3240
|
+
return links.maxDist === void 0 ? Infinity : links.maxDist * scale;
|
|
3241
|
+
}
|
|
2619
3242
|
function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
2620
3243
|
const n = positions.length;
|
|
2621
3244
|
const mode = links.mode ?? "nearest";
|
|
3245
|
+
const k = links.k ?? 1;
|
|
2622
3246
|
if (mode === "chain") {
|
|
2623
3247
|
const out = [];
|
|
2624
3248
|
for (let i = 0; i < n - 1; i++) {
|
|
@@ -2632,7 +3256,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2632
3256
|
const edges = /* @__PURE__ */ new Set();
|
|
2633
3257
|
const out = [];
|
|
2634
3258
|
for (let i = 0; i < n; i++) {
|
|
2635
|
-
for (let mIdx = 1; mIdx <=
|
|
3259
|
+
for (let mIdx = 1; mIdx <= k; mIdx++) {
|
|
2636
3260
|
const j = (i + mIdx * stride) % n;
|
|
2637
3261
|
if (j === i) continue;
|
|
2638
3262
|
const dist = wrapDist(positions[i], positions[j], doWrap, w, h);
|
|
@@ -2646,7 +3270,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2646
3270
|
}
|
|
2647
3271
|
return out;
|
|
2648
3272
|
}
|
|
2649
|
-
return linkPairs(positions,
|
|
3273
|
+
return linkPairs(positions, k, maxDistPx, doWrap, w, h).map(([i, j]) => ({
|
|
2650
3274
|
i,
|
|
2651
3275
|
j,
|
|
2652
3276
|
dist: wrapDist(positions[i], positions[j], doWrap, w, h)
|
|
@@ -3325,6 +3949,45 @@ function overridesStayValid(authored, overrides, now) {
|
|
|
3325
3949
|
}
|
|
3326
3950
|
return true;
|
|
3327
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
|
+
}
|
|
3328
3991
|
var SpecInstance = class {
|
|
3329
3992
|
constructor(spec, ctx, opts = {}) {
|
|
3330
3993
|
this.spec = spec;
|
|
@@ -3435,6 +4098,20 @@ var SpecInstance = class {
|
|
|
3435
4098
|
paintSeq = 0;
|
|
3436
4099
|
/** Per layer: ribbon area painted this frame (px²), and whether its trails ribbon (see ribbonAllowed). */
|
|
3437
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;
|
|
3438
4115
|
featherTints = /* @__PURE__ */ new Map();
|
|
3439
4116
|
/**
|
|
3440
4117
|
* The presentation pass when this instance applies a `finish`: the
|
|
@@ -3713,6 +4390,277 @@ var SpecInstance = class {
|
|
|
3713
4390
|
return rb.ribbon;
|
|
3714
4391
|
}
|
|
3715
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
|
+
}
|
|
3716
4664
|
makeCanvas(n) {
|
|
3717
4665
|
try {
|
|
3718
4666
|
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(n, n);
|
|
@@ -3944,24 +4892,26 @@ var SpecInstance = class {
|
|
|
3944
4892
|
if (sprite.kind === "polygon") {
|
|
3945
4893
|
const r = sz / 2;
|
|
3946
4894
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
3947
|
-
|
|
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
|
+
}
|
|
3948
4905
|
ctx.save();
|
|
3949
4906
|
ctx.translate(p.x, p.y);
|
|
3950
4907
|
if (rot) ctx.rotate(rot);
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
g.addColorStop(0, resolvedColor);
|
|
3954
|
-
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
3955
|
-
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
3956
|
-
ctx.fillStyle = g;
|
|
3957
|
-
} else {
|
|
3958
|
-
ctx.fillStyle = resolvedColor;
|
|
3959
|
-
}
|
|
4908
|
+
ctx.fillStyle = sprite.soft ? softFill(ctx, resolvedColor, r) : resolvedColor;
|
|
4909
|
+
const pts = polygonPoints(sprite, r);
|
|
3960
4910
|
ctx.beginPath();
|
|
3961
4911
|
ctx.moveTo(pts[0].x, pts[0].y);
|
|
3962
4912
|
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
|
3963
4913
|
ctx.closePath();
|
|
3964
|
-
ctx.fill();
|
|
4914
|
+
ctx.fill(polygonFillRule(sprite));
|
|
3965
4915
|
ctx.restore();
|
|
3966
4916
|
return;
|
|
3967
4917
|
}
|
|
@@ -4200,9 +5150,9 @@ var SpecInstance = class {
|
|
|
4200
5150
|
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
4201
5151
|
const wrap2 = built.layer.wrap !== false && motionWraps;
|
|
4202
5152
|
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
4203
|
-
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
4204
|
-
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
4205
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);
|
|
4206
5156
|
const defaultWidth = unitScale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
4207
5157
|
const lw = (links.width ?? defaultWidth) * unitScale;
|
|
4208
5158
|
ctx.lineWidth = lw;
|
|
@@ -4219,7 +5169,7 @@ var SpecInstance = class {
|
|
|
4219
5169
|
} else resolvedColor = "#e6e8ef";
|
|
4220
5170
|
}
|
|
4221
5171
|
let a = links.alpha ?? alphaAt(ei, t);
|
|
4222
|
-
if (links.falloff) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
5172
|
+
if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
4223
5173
|
ctx.globalAlpha = a * lifeA;
|
|
4224
5174
|
ctx.strokeStyle = resolvedColor;
|
|
4225
5175
|
let dx = pj.x - pi.x;
|
|
@@ -4246,38 +5196,212 @@ var SpecInstance = class {
|
|
|
4246
5196
|
this.drawBackground(t);
|
|
4247
5197
|
}
|
|
4248
5198
|
ctx.globalAlpha = 1;
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
const parentE = this.parentEntityFor(built);
|
|
4257
|
-
const tf = built.layer.transform;
|
|
4258
|
-
const gtf = group?.transform;
|
|
4259
|
-
if (tf || gtf) {
|
|
4260
|
-
ctx.save();
|
|
4261
|
-
if (gtf) this.applyLayerTransform(gtf, null);
|
|
4262
|
-
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
4263
|
-
}
|
|
4264
|
-
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
4265
|
-
this.drawLinks(built, t, lifeA, parentE);
|
|
4266
|
-
const override = this.paintOverrides?.get(li) ?? null;
|
|
4267
|
-
for (const e of built.entities) {
|
|
4268
|
-
this.drawTrail(built, e, t, lifeA, parentE);
|
|
4269
|
-
if (override === null) {
|
|
4270
|
-
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;
|
|
4271
5206
|
continue;
|
|
4272
5207
|
}
|
|
4273
|
-
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];
|
|
4274
|
-
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
4275
|
-
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
4276
5208
|
}
|
|
4277
|
-
|
|
5209
|
+
this.paintLayer(li, t);
|
|
5210
|
+
}
|
|
5211
|
+
ctx.globalAlpha = 1;
|
|
5212
|
+
ctx.globalCompositeOperation = "source-over";
|
|
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;
|
|
4278
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);
|
|
4279
5400
|
ctx.globalAlpha = 1;
|
|
4280
5401
|
ctx.globalCompositeOperation = "source-over";
|
|
5402
|
+
ctx.drawImage(plate.canvas, plate.box.x, plate.box.y);
|
|
5403
|
+
ctx.restore();
|
|
5404
|
+
return true;
|
|
4281
5405
|
}
|
|
4282
5406
|
/**
|
|
4283
5407
|
* A `transform`, in min(w,h) units (px under `units: 'px'`), about the
|
|
@@ -5150,6 +6274,27 @@ function hexLuma(hex) {
|
|
|
5150
6274
|
}
|
|
5151
6275
|
function spriteLuma(layer, e) {
|
|
5152
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
|
+
}
|
|
5153
6298
|
if (isShapedSprite(s)) {
|
|
5154
6299
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
5155
6300
|
}
|
|
@@ -5294,7 +6439,7 @@ function polyShape(cx, cy, raw) {
|
|
|
5294
6439
|
if (abs.length < 3) return null;
|
|
5295
6440
|
let br = 0;
|
|
5296
6441
|
for (const p of abs) br = Math.max(br, Math.hypot(p.x - cx, p.y - cy));
|
|
5297
|
-
const
|
|
6442
|
+
const inside2 = (x, y) => {
|
|
5298
6443
|
let hit = false;
|
|
5299
6444
|
for (let i = 0, j = abs.length - 1; i < abs.length; j = i++) {
|
|
5300
6445
|
const a = abs[i];
|
|
@@ -5317,7 +6462,7 @@ function polyShape(cx, cy, raw) {
|
|
|
5317
6462
|
cy,
|
|
5318
6463
|
br,
|
|
5319
6464
|
area,
|
|
5320
|
-
contains:
|
|
6465
|
+
contains: inside2,
|
|
5321
6466
|
/**
|
|
5322
6467
|
* `n` points spread along the perimeter by ARC LENGTH, not one share per
|
|
5323
6468
|
* edge — a long skinny rectangle has two edges carrying almost all of its
|
|
@@ -5346,13 +6491,13 @@ function polyShape(cx, cy, raw) {
|
|
|
5346
6491
|
for (let t = step; t > 0; t /= 8) {
|
|
5347
6492
|
const qx1 = px - uy * t;
|
|
5348
6493
|
const qy1 = py + ux * t;
|
|
5349
|
-
if (
|
|
6494
|
+
if (inside2(qx1, qy1)) {
|
|
5350
6495
|
out.push({ x: qx1, y: qy1 });
|
|
5351
6496
|
break;
|
|
5352
6497
|
}
|
|
5353
6498
|
const qx2 = px + uy * t;
|
|
5354
6499
|
const qy2 = py - ux * t;
|
|
5355
|
-
if (
|
|
6500
|
+
if (inside2(qx2, qy2)) {
|
|
5356
6501
|
out.push({ x: qx2, y: qy2 });
|
|
5357
6502
|
break;
|
|
5358
6503
|
}
|
|
@@ -5474,7 +6619,6 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
5474
6619
|
|
|
5475
6620
|
// src/perceive.ts
|
|
5476
6621
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
5477
|
-
var GLOW_SPREAD = 2.4;
|
|
5478
6622
|
var LINE_SALIENCE = 3.5;
|
|
5479
6623
|
var GHOST_TAPS = 12;
|
|
5480
6624
|
var POLYGON_OUTLINE_CELLS = 3;
|
|
@@ -5509,11 +6653,18 @@ function posOf(scene, e, t) {
|
|
|
5509
6653
|
const map = scene.transforms?.get(e);
|
|
5510
6654
|
return map ? map(p) : p;
|
|
5511
6655
|
}
|
|
5512
|
-
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) {
|
|
5513
6660
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
5514
|
-
|
|
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);
|
|
5515
6665
|
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
5516
|
-
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;
|
|
5517
6668
|
const align = s.align ?? "center";
|
|
5518
6669
|
const baseline = s.baseline ?? "middle";
|
|
5519
6670
|
return {
|
|
@@ -5559,7 +6710,24 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
5559
6710
|
}
|
|
5560
6711
|
return rs.progress;
|
|
5561
6712
|
}
|
|
5562
|
-
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) {
|
|
5563
6731
|
const nc = c1 - c0 + 1;
|
|
5564
6732
|
const cov = new Float64Array(nc * (r1 - r0 + 1));
|
|
5565
6733
|
const xMin = c0 * cellW;
|
|
@@ -5592,30 +6760,52 @@ function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
|
|
|
5592
6760
|
hits.sort((m, n) => m.x - n.x);
|
|
5593
6761
|
let wind = 0;
|
|
5594
6762
|
let start = 0;
|
|
6763
|
+
const filled = (w) => evenodd ? (w & 1) !== 0 : w !== 0;
|
|
5595
6764
|
for (const hit of hits) {
|
|
5596
|
-
const before = wind;
|
|
6765
|
+
const before = filled(wind);
|
|
5597
6766
|
wind += hit.dir;
|
|
5598
|
-
|
|
5599
|
-
|
|
6767
|
+
const after = filled(wind);
|
|
6768
|
+
if (!before && after) start = hit.x;
|
|
6769
|
+
else if (before && !after) addSpan(r - r0, start, hit.x);
|
|
5600
6770
|
}
|
|
5601
6771
|
}
|
|
5602
6772
|
}
|
|
5603
6773
|
return cov;
|
|
5604
6774
|
}
|
|
5605
|
-
function
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
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);
|
|
5619
6809
|
}
|
|
5620
6810
|
function luminanceGrid(spec, opts = {}) {
|
|
5621
6811
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
@@ -5712,11 +6902,14 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5712
6902
|
const r1 = Math.min(rows - 1, Math.floor((pos.y + rad) / cellH));
|
|
5713
6903
|
for (let r = r0; r <= r1; r++) {
|
|
5714
6904
|
for (let c = c0; c <= c1; c++) {
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
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
|
+
}
|
|
5720
6913
|
compose(r * cols + c, lum, Math.min(1, a * wgt), layer.blend);
|
|
5721
6914
|
}
|
|
5722
6915
|
}
|
|
@@ -5727,17 +6920,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5727
6920
|
const rot = rotationAt(e, tPass);
|
|
5728
6921
|
const cr = Math.cos(rot);
|
|
5729
6922
|
const sr = Math.sin(rot);
|
|
5730
|
-
const
|
|
5731
|
-
const
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
const n = pts[(i + 1) % pts.length];
|
|
5737
|
-
return acc + Math.hypot(n.x - q.x, n.y - q.y);
|
|
5738
|
-
}, 0);
|
|
5739
|
-
band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
|
|
5740
|
-
}
|
|
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 }));
|
|
5741
6929
|
let minX = Infinity;
|
|
5742
6930
|
let maxX = -Infinity;
|
|
5743
6931
|
let minY = Infinity;
|
|
@@ -5748,34 +6936,97 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5748
6936
|
minY = Math.min(minY, q.y);
|
|
5749
6937
|
maxY = Math.max(maxY, q.y);
|
|
5750
6938
|
}
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
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));
|
|
5755
6967
|
if (c0 > c1 || r0 > r1) return;
|
|
5756
|
-
const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
|
|
5757
6968
|
const nc = c1 - c0 + 1;
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
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;
|
|
6989
|
+
}
|
|
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)]);
|
|
5772
7024
|
}
|
|
5773
|
-
} else if (s.soft) {
|
|
5774
|
-
wgt = f * Math.max(0.1, 1 - Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6));
|
|
5775
|
-
} else {
|
|
5776
|
-
wgt = f;
|
|
5777
7025
|
}
|
|
5778
|
-
|
|
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);
|
|
7029
|
+
}
|
|
5779
7030
|
}
|
|
5780
7031
|
}
|
|
5781
7032
|
};
|
|
@@ -5791,7 +7042,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5791
7042
|
const p = posOf(scene, e, tPass);
|
|
5792
7043
|
const sz = layer.transform || layer.group !== void 0 ? sizeAt(e, tPass) * paintSize(spec, layer) : sizeAt(e, tPass);
|
|
5793
7044
|
const s = layer.sprite;
|
|
5794
|
-
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))) {
|
|
5795
7046
|
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
5796
7047
|
continue;
|
|
5797
7048
|
}
|
|
@@ -5845,7 +7096,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5845
7096
|
let halfX = sz / 2;
|
|
5846
7097
|
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
5847
7098
|
if (s.kind === "text") {
|
|
5848
|
-
const box = textBox(s, e, p);
|
|
7099
|
+
const box = textBox(s, e, p, fontScaleFor(spec, w, h), scale);
|
|
5849
7100
|
centerX = box.cx;
|
|
5850
7101
|
centerY = box.cy;
|
|
5851
7102
|
halfX = box.halfX;
|
|
@@ -5872,45 +7123,24 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5872
7123
|
const circular = s.kind === "circle" || s.kind === "ring" || s.kind === "polygon";
|
|
5873
7124
|
const soft = (s.kind === "circle" || s.kind === "polygon") && !!s.soft;
|
|
5874
7125
|
const shapeWeight = s.kind === "polygon" ? polygonFill(s, sz / 2) : s.kind === "rect" && s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
5875
|
-
const
|
|
5876
|
-
const
|
|
5877
|
-
const
|
|
5878
|
-
const
|
|
5879
|
-
const c0 = Math.max(0, Math.floor((centerX - reachX) / cellW));
|
|
5880
|
-
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
5881
|
-
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
5882
|
-
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));
|
|
5883
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);
|
|
5884
|
-
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";
|
|
5885
7132
|
for (let r = r0; r <= r1; r++) {
|
|
5886
7133
|
const fy = byArea ? Math.max(0, Math.min((r + 1) * cellH, centerY + halfY) - Math.max(r * cellH, centerY - halfY)) / cellH : 1;
|
|
5887
7134
|
if (fy <= 0) continue;
|
|
5888
7135
|
for (let c = c0; c <= c1; c++) {
|
|
5889
|
-
const dx = (c + 0.5) * cellW - centerX;
|
|
5890
|
-
const dy = (r + 0.5) * cellH - centerY;
|
|
5891
7136
|
if (circular) {
|
|
5892
|
-
const
|
|
5893
|
-
const
|
|
5894
|
-
if (
|
|
5895
|
-
if (s.kind === "ring") {
|
|
5896
|
-
const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
5897
|
-
if (d < halfX - strokeW) continue;
|
|
5898
|
-
}
|
|
5899
|
-
let wgt;
|
|
5900
|
-
if (glow) {
|
|
5901
|
-
const core = halfX;
|
|
5902
|
-
if (d <= core) wgt = 1;
|
|
5903
|
-
else {
|
|
5904
|
-
const f = Math.max(0, 1 - (d - core) / Math.max(reachX - core, 1e-6));
|
|
5905
|
-
wgt = f * f;
|
|
5906
|
-
}
|
|
5907
|
-
} else {
|
|
5908
|
-
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
5909
|
-
}
|
|
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;
|
|
5910
7140
|
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
5911
7141
|
} else if (byArea) {
|
|
5912
7142
|
const fx = Math.max(0, Math.min((c + 1) * cellW, centerX + halfX) - Math.max(c * cellW, centerX - halfX)) / cellW;
|
|
5913
|
-
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);
|
|
5914
7144
|
} else {
|
|
5915
7145
|
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
5916
7146
|
}
|
|
@@ -5919,9 +7149,8 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5919
7149
|
}
|
|
5920
7150
|
if (layer.links) {
|
|
5921
7151
|
const positions = entities.map((e) => posOf(scene, e, tPass));
|
|
5922
|
-
const maxDistPx = layer.links.maxDist * scale;
|
|
5923
7152
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
5924
|
-
const edges = linkEdges(layer.links, positions,
|
|
7153
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false && motionWraps, w, h);
|
|
5925
7154
|
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
5926
7155
|
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
5927
7156
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
@@ -6062,8 +7291,7 @@ function textSprites(spec, opts = {}) {
|
|
|
6062
7291
|
}
|
|
6063
7292
|
if (layer.sprite.kind !== "text") return;
|
|
6064
7293
|
const s = layer.sprite;
|
|
6065
|
-
const
|
|
6066
|
-
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));
|
|
6067
7295
|
out.push({ layerIndex, key: layer.key, strings: [...s.strings], count: entities.length, sizePx: Math.round(sizePx) });
|
|
6068
7296
|
});
|
|
6069
7297
|
return out;
|
|
@@ -6134,7 +7362,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
6134
7362
|
const meanTaper = s.taper ? 0.64 : 1;
|
|
6135
7363
|
entArea = pathLength(strokeSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
|
|
6136
7364
|
} else if (s.kind === "text") {
|
|
6137
|
-
const box = textBox(s, e, { x: 0, y: 0 });
|
|
7365
|
+
const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
|
|
6138
7366
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
6139
7367
|
} else if (s.kind === "textBlock") {
|
|
6140
7368
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
@@ -6154,7 +7382,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
6154
7382
|
if (layer.links && entities.length > 1) {
|
|
6155
7383
|
const positions = entities.map((e) => posOf(scene, e, t));
|
|
6156
7384
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
6157
|
-
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);
|
|
6158
7386
|
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
6159
7387
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
6160
7388
|
const lw = (layer.links.width ?? defaultWidth) * scale;
|
|
@@ -6608,8 +7836,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6608
7836
|
}
|
|
6609
7837
|
if (layer.links) {
|
|
6610
7838
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6611
|
-
const
|
|
6612
|
-
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);
|
|
6613
7840
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
6614
7841
|
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
6615
7842
|
const la = layer.links.alpha ?? 1;
|
|
@@ -6668,9 +7895,9 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6668
7895
|
if (!layer.links || layer.links.mode === "chain") continue;
|
|
6669
7896
|
const entities = allEntities[li];
|
|
6670
7897
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6671
|
-
const
|
|
6672
|
-
const pairs = linkPairs(positions, layer.links
|
|
6673
|
-
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);
|
|
6674
7901
|
if (maxPossible > 0 && pairs.length / maxPossible < 0.1) {
|
|
6675
7902
|
warnings.push({
|
|
6676
7903
|
path: `layers[${li}].links`,
|
|
@@ -6934,20 +8161,20 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
|
|
|
6934
8161
|
const c = positionAt(e, 0, w, h);
|
|
6935
8162
|
const dx = pt.x - c.x;
|
|
6936
8163
|
const dy = pt.y - c.y;
|
|
6937
|
-
let
|
|
6938
|
-
if (sp.kind === "circle")
|
|
8164
|
+
let inside2 = false;
|
|
8165
|
+
if (sp.kind === "circle") inside2 = Math.hypot(dx, dy) <= e.size / 2;
|
|
6939
8166
|
else if (sp.kind === "rect") {
|
|
6940
8167
|
const rot = ((e.rotate ?? 0) % Math.PI + Math.PI) % Math.PI;
|
|
6941
8168
|
const quarter = Math.abs(rot - Math.PI / 2) < 0.01;
|
|
6942
8169
|
if (rot > 0.01 && rot < Math.PI - 0.01 && !quarter) continue;
|
|
6943
8170
|
const hx = (quarter ? e.size2 ?? e.size : e.size) / 2;
|
|
6944
8171
|
const hy = (quarter ? e.size : e.size2 ?? e.size) / 2;
|
|
6945
|
-
|
|
8172
|
+
inside2 = Math.abs(dx) <= hx && Math.abs(dy) <= hy;
|
|
6946
8173
|
} else {
|
|
6947
8174
|
if (e.rotate) continue;
|
|
6948
|
-
|
|
8175
|
+
inside2 = pointInPolygon(dx, dy, polygonPoints(sp, e.size / 2));
|
|
6949
8176
|
}
|
|
6950
|
-
if (!
|
|
8177
|
+
if (!inside2) continue;
|
|
6951
8178
|
plate = sourceOverPlate(plate, hexRgb(hex), Math.min(1, e.alpha * a));
|
|
6952
8179
|
covered = true;
|
|
6953
8180
|
}
|
|
@@ -6955,13 +8182,13 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
|
|
|
6955
8182
|
return covered ? { rgb: plate, luma: 0.2126 * plate.r + 0.7152 * plate.g + 0.0722 * plate.b } : null;
|
|
6956
8183
|
}
|
|
6957
8184
|
function pointInPolygon(x, y, pts) {
|
|
6958
|
-
let
|
|
8185
|
+
let inside2 = false;
|
|
6959
8186
|
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
6960
8187
|
const a = pts[i];
|
|
6961
8188
|
const b = pts[j];
|
|
6962
|
-
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;
|
|
6963
8190
|
}
|
|
6964
|
-
return
|
|
8191
|
+
return inside2;
|
|
6965
8192
|
}
|
|
6966
8193
|
function entityReachesBox(e, r, box, w, h) {
|
|
6967
8194
|
if (e.motion === "static") {
|
|
@@ -7113,10 +8340,10 @@ function describeScene(spec, options = {}) {
|
|
|
7113
8340
|
let connectedComponents = null;
|
|
7114
8341
|
let isolatedNodes = null;
|
|
7115
8342
|
if (layer.links) {
|
|
7116
|
-
const
|
|
7117
|
-
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]);
|
|
7118
8345
|
linksDrawn = pairs.length;
|
|
7119
|
-
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);
|
|
7120
8347
|
const parent = Array.from({ length: entities.length }, (_, i) => i);
|
|
7121
8348
|
const find = (x) => {
|
|
7122
8349
|
while (parent[x] !== x) {
|
|
@@ -9617,6 +10844,7 @@ export {
|
|
|
9617
10844
|
LIMITS,
|
|
9618
10845
|
LOBBY_TALK_SPEC,
|
|
9619
10846
|
MATRIX_RAIN_SPEC,
|
|
10847
|
+
MAX_PATH_POINTS,
|
|
9620
10848
|
MURMUR_SPEC,
|
|
9621
10849
|
NOSTALGHIA_CANDLE_SPEC,
|
|
9622
10850
|
ORRERY_SPEC,
|
|
@@ -9672,6 +10900,7 @@ export {
|
|
|
9672
10900
|
fieldRgbAt,
|
|
9673
10901
|
fieldSampleTime,
|
|
9674
10902
|
finishStrength,
|
|
10903
|
+
flattenPath,
|
|
9675
10904
|
grainOffset,
|
|
9676
10905
|
grainTilePixels,
|
|
9677
10906
|
graphemeClusters,
|
|
@@ -9679,12 +10908,15 @@ export {
|
|
|
9679
10908
|
headingAt,
|
|
9680
10909
|
hexToRgb255,
|
|
9681
10910
|
ignoredPropertyPaths,
|
|
10911
|
+
isPathPolygon,
|
|
9682
10912
|
latticeHash,
|
|
9683
10913
|
layerCohesion,
|
|
10914
|
+
lerpPathD,
|
|
9684
10915
|
lerpSpec,
|
|
9685
10916
|
lerpValue,
|
|
9686
10917
|
lifeAlphaAt,
|
|
9687
10918
|
linkEdges,
|
|
10919
|
+
linkMaxDistPx,
|
|
9688
10920
|
linkPairs,
|
|
9689
10921
|
luminanceGrid,
|
|
9690
10922
|
manifestFor,
|
|
@@ -9693,6 +10925,8 @@ export {
|
|
|
9693
10925
|
motionStats,
|
|
9694
10926
|
nextKeyAfter,
|
|
9695
10927
|
normalizeSeed,
|
|
10928
|
+
pathFrame,
|
|
10929
|
+
pathParts,
|
|
9696
10930
|
perceiveScene,
|
|
9697
10931
|
perceiveSequenceFrame,
|
|
9698
10932
|
pickScreenOp,
|