@idle-screens/schema 3.15.0 → 3.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +111 -59
- package/dist/index.d.ts +152 -19
- package/dist/index.js +513 -134
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +11 -7
package/dist/index.js
CHANGED
|
@@ -106,6 +106,8 @@ var LIMITS = {
|
|
|
106
106
|
maxShapePoints: 24,
|
|
107
107
|
// polygon.points / stroke.points
|
|
108
108
|
maxTextNumberDecimals: 6,
|
|
109
|
+
maxTextNumberMinDigits: 12,
|
|
110
|
+
// a counter's zero-padded integer digits
|
|
109
111
|
maxTextAffix: 64,
|
|
110
112
|
// characters of a counter's prefix / suffix
|
|
111
113
|
maxRevealRise: 1,
|
|
@@ -165,7 +167,7 @@ function readSpecPath(spec, path) {
|
|
|
165
167
|
const loc = resolveSpecPath(spec, path);
|
|
166
168
|
return loc ? loc.parent[loc.key] : void 0;
|
|
167
169
|
}
|
|
168
|
-
var TRANSFORM_IDENTITY = { x: 0, y: 0, scale: 1, scaleX: 1, rotate: 0 };
|
|
170
|
+
var TRANSFORM_IDENTITY = { x: 0, y: 0, scale: 1, scaleX: 1, scaleY: 1, rotate: 0 };
|
|
169
171
|
function alignTransform(a, b) {
|
|
170
172
|
const isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
171
173
|
if (!isObj2(a) || !isObj2(b)) return [a, b];
|
|
@@ -916,8 +918,9 @@ function trimPolyline(pts, a, b) {
|
|
|
916
918
|
}
|
|
917
919
|
return out.length >= 2 ? out : [];
|
|
918
920
|
}
|
|
919
|
-
function trimLines(lines, a, b) {
|
|
920
|
-
const
|
|
921
|
+
function trimLines(lines, a, b, mode = "run") {
|
|
922
|
+
const each = mode === "each";
|
|
923
|
+
const key = `${a}|${b}|${each ? "each" : "run"}`;
|
|
921
924
|
let byLines = trimCache.get(lines);
|
|
922
925
|
const hit = byLines?.get(key);
|
|
923
926
|
if (hit) return hit;
|
|
@@ -928,7 +931,19 @@ function trimLines(lines, a, b) {
|
|
|
928
931
|
});
|
|
929
932
|
const total = lens.reduce((m, v) => m + v, 0);
|
|
930
933
|
const out = [];
|
|
931
|
-
if (
|
|
934
|
+
if (each && b > a) {
|
|
935
|
+
lines.forEach((l, i) => {
|
|
936
|
+
if (!(lens[i] > 0)) return;
|
|
937
|
+
if (a <= 0 && b >= 1) {
|
|
938
|
+
out.push(l);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const pts = l.points.map(([x, y]) => ({ x, y }));
|
|
942
|
+
if (l.closed && pts.length > 1) pts.push(pts[0]);
|
|
943
|
+
const cut = trimPolyline(pts, a, b);
|
|
944
|
+
if (cut.length >= 2) out.push({ points: cut.map((q) => [q.x, q.y]), closed: false });
|
|
945
|
+
});
|
|
946
|
+
} else if (total > 0 && b > a) {
|
|
932
947
|
const from = a * total;
|
|
933
948
|
const to = b * total;
|
|
934
949
|
let run = 0;
|
|
@@ -958,7 +973,7 @@ function trimLines(lines, a, b) {
|
|
|
958
973
|
var trimCache = /* @__PURE__ */ new WeakMap();
|
|
959
974
|
function strokeLines(part) {
|
|
960
975
|
const span = drawSpan(part.stroke?.draw);
|
|
961
|
-
return span ? trimLines(part.flat.lines, span[0], span[1]) : part.flat.lines;
|
|
976
|
+
return span ? trimLines(part.flat.lines, span[0], span[1], part.stroke?.draw?.mode) : part.flat.lines;
|
|
962
977
|
}
|
|
963
978
|
function strokeTaper(u) {
|
|
964
979
|
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
@@ -1058,7 +1073,7 @@ function flattenPath(d, rule = "auto") {
|
|
|
1058
1073
|
}
|
|
1059
1074
|
var spriteFlat = /* @__PURE__ */ new WeakMap();
|
|
1060
1075
|
function flattenSprite(s) {
|
|
1061
|
-
const d = s.parts ? s.parts.filter((p) => p.fill !== false).map((p) => p.path).join(" ") : s
|
|
1076
|
+
const d = s.parts ? s.parts.filter((p) => p.fill !== false).map((p) => p.path).join(" ") : spritePathD(s) ?? "";
|
|
1062
1077
|
const rule = s.parts ? "auto" : s.fillRule ?? "auto";
|
|
1063
1078
|
const hit = spriteFlat.get(s);
|
|
1064
1079
|
if (hit && hit.d === d && hit.rule === rule) return hit.flat;
|
|
@@ -1070,14 +1085,30 @@ function polygonFillRule(s) {
|
|
|
1070
1085
|
return typeof s.path === "string" && s.fillRule === "evenodd" ? "evenodd" : "nonzero";
|
|
1071
1086
|
}
|
|
1072
1087
|
function isPathPolygon(s) {
|
|
1073
|
-
return s.kind === "polygon" && (typeof s.path === "string" || Array.isArray(s.parts));
|
|
1088
|
+
return s.kind === "polygon" && (typeof s.path === "string" || Array.isArray(s.parts) || isPointsPath(s));
|
|
1089
|
+
}
|
|
1090
|
+
function isPointsPath(s) {
|
|
1091
|
+
return s.kind === "polygon" && typeof s.path !== "string" && !Array.isArray(s.parts) && Array.isArray(s.points) && s.points.length >= 2 && (s.stroke !== void 0 || s.fill === false || s.gradient !== void 0);
|
|
1092
|
+
}
|
|
1093
|
+
var pointsDs = /* @__PURE__ */ new WeakMap();
|
|
1094
|
+
function pointsPathD(points) {
|
|
1095
|
+
const hit = pointsDs.get(points);
|
|
1096
|
+
if (hit !== void 0) return hit;
|
|
1097
|
+
const d = `${points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x} ${y}`).join(" ")} Z`;
|
|
1098
|
+
pointsDs.set(points, d);
|
|
1099
|
+
return d;
|
|
1100
|
+
}
|
|
1101
|
+
function spritePathD(s) {
|
|
1102
|
+
if (typeof s.path === "string") return s.path;
|
|
1103
|
+
return isPointsPath(s) ? pointsPathD(s.points) : void 0;
|
|
1074
1104
|
}
|
|
1075
1105
|
var spriteParts = /* @__PURE__ */ new WeakMap();
|
|
1076
1106
|
function pathParts(s) {
|
|
1077
|
-
const key = s.parts ?? s.path;
|
|
1107
|
+
const key = s.parts ?? s.path ?? s.points;
|
|
1078
1108
|
const hit = spriteParts.get(s);
|
|
1079
1109
|
if (hit && hit.key === key) return hit.parts;
|
|
1080
|
-
const
|
|
1110
|
+
const d = s.parts ? void 0 : spritePathD(s);
|
|
1111
|
+
const src = s.parts ?? (d !== void 0 ? [{ path: d, fillRule: typeof s.path === "string" ? s.fillRule : void 0, fill: s.fill, gradient: s.gradient, stroke: s.stroke }] : []);
|
|
1081
1112
|
const parts = src.map((p) => {
|
|
1082
1113
|
const flat = flattenPath(p.path, p.fillRule ?? "auto");
|
|
1083
1114
|
return {
|
|
@@ -1571,11 +1602,17 @@ function pathDrawCost(s) {
|
|
|
1571
1602
|
return c;
|
|
1572
1603
|
}
|
|
1573
1604
|
|
|
1605
|
+
// src/validate-hints.ts
|
|
1606
|
+
var SCALAR_ONLY = /not a \[min,max\] range/;
|
|
1607
|
+
var SCALAR_BOUND = /^must be (?:>|an? (?:positive |single |finite |whole )?(?:number|integer)\b|between -?\d|-?\d+(?:\.\d+)?\.\.-?\d+(?:\.\d+)?(?: \(|$))/;
|
|
1608
|
+
var scalarWanted = (m) => SCALAR_ONLY.test(m) || !m.includes("[") && SCALAR_BOUND.test(m);
|
|
1609
|
+
|
|
1574
1610
|
// src/validate.ts
|
|
1575
1611
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
1576
1612
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1577
1613
|
var isStr = (v) => typeof v === "string";
|
|
1578
1614
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1615
|
+
var validPointPairs = (v) => Array.isArray(v) && v.length >= 2 && v.every((p) => Array.isArray(p) && p.length === 2 && isNum(p[0]) && isNum(p[1]));
|
|
1579
1616
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
1580
1617
|
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline", "groups"]);
|
|
1581
1618
|
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
@@ -1606,7 +1643,7 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
1606
1643
|
"transform",
|
|
1607
1644
|
"group"
|
|
1608
1645
|
]);
|
|
1609
|
-
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate", "origin"]);
|
|
1646
|
+
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "scaleY", "rotate", "origin"]);
|
|
1610
1647
|
var KNOWN_GROUP = /* @__PURE__ */ new Set(["transform", "opacity"]);
|
|
1611
1648
|
var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
1612
1649
|
var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
|
|
@@ -1624,7 +1661,7 @@ var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color",
|
|
|
1624
1661
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
1625
1662
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
1626
1663
|
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role", "number"]);
|
|
1627
|
-
var KNOWN_TEXT_NUMBER = /* @__PURE__ */ new Set(["value", "decimals", "prefix", "suffix", "grouping"]);
|
|
1664
|
+
var KNOWN_TEXT_NUMBER = /* @__PURE__ */ new Set(["value", "decimals", "prefix", "suffix", "grouping", "minDigits"]);
|
|
1628
1665
|
var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
|
|
1629
1666
|
var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
|
|
1630
1667
|
var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
|
|
@@ -1702,6 +1739,15 @@ var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
|
1702
1739
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
1703
1740
|
var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
|
|
1704
1741
|
var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1742
|
+
var KNOWN_BG_STOP = /* @__PURE__ */ new Set(["at", "color"]);
|
|
1743
|
+
var KNOWN_BAND = /* @__PURE__ */ new Set(["color", "height"]);
|
|
1744
|
+
var KNOWN_BG_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1745
|
+
var KNOWN_PATH_STROKE = /* @__PURE__ */ new Set(["color", "width", "opacity", "join", "cap", "draw"]);
|
|
1746
|
+
var KNOWN_PATH_GRADIENT = {
|
|
1747
|
+
linear: /* @__PURE__ */ new Set(["type", "from", "to", "stops", "units"]),
|
|
1748
|
+
radial: /* @__PURE__ */ new Set(["type", "center", "radius", "focus", "stops", "units"])
|
|
1749
|
+
};
|
|
1750
|
+
var KNOWN_PATH_STOP = /* @__PURE__ */ new Set(["at", "color", "opacity"]);
|
|
1705
1751
|
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate", "rock"]);
|
|
1706
1752
|
function unknownKeys(obj, known) {
|
|
1707
1753
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
@@ -1719,6 +1765,50 @@ function positiveScalar(v, path, err, msg = "must be > 0") {
|
|
|
1719
1765
|
if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
|
|
1720
1766
|
else if (!isNum(v) || v <= 0) err(path, msg);
|
|
1721
1767
|
}
|
|
1768
|
+
function valueAtPath(root, path) {
|
|
1769
|
+
let v = root;
|
|
1770
|
+
for (const seg of path.match(/[^.[\]]+/g) ?? []) {
|
|
1771
|
+
if (Array.isArray(v) && /^\d+$/.test(seg)) v = v[Number(seg)];
|
|
1772
|
+
else if (isObj(v)) v = v[seg];
|
|
1773
|
+
else return void 0;
|
|
1774
|
+
}
|
|
1775
|
+
return v;
|
|
1776
|
+
}
|
|
1777
|
+
function echo(v) {
|
|
1778
|
+
if (isNum(v)) return String(v);
|
|
1779
|
+
if (Array.isArray(v) && v.length <= 4 && v.every(isNum)) return `[${v.join(", ")}]`;
|
|
1780
|
+
const s = JSON.stringify(v) ?? String(v);
|
|
1781
|
+
return s.length > 48 ? `${s.slice(0, 45)}\u2026` : s;
|
|
1782
|
+
}
|
|
1783
|
+
var RANGE_WANTED = /^must be a \[min,max\] range/;
|
|
1784
|
+
function shapeHint(spec, path, message) {
|
|
1785
|
+
if (message.includes(" \u2014 got ")) return "";
|
|
1786
|
+
const v = valueAtPath(spec, path);
|
|
1787
|
+
if (v === void 0) return "";
|
|
1788
|
+
if (RANGE_WANTED.test(message)) {
|
|
1789
|
+
if (isNum(v)) {
|
|
1790
|
+
const fits = message.includes("within 0..1") ? v >= 0 && v <= 1 : message.includes(">= 0") ? v >= 0 : !message.includes("positive") || v > 0;
|
|
1791
|
+
return fits ? ` \u2014 got ${v}; write [${v}, ${v}]` : ` \u2014 got ${v}; a range is two numbers, [min, max]`;
|
|
1792
|
+
}
|
|
1793
|
+
if (isObj(v) && ("min" in v || "max" in v)) {
|
|
1794
|
+
return ` \u2014 got {min, max}; write [${isNum(v.min) ? v.min : "min"}, ${isNum(v.max) ? v.max : "max"}]`;
|
|
1795
|
+
}
|
|
1796
|
+
if (Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] > v[1]) {
|
|
1797
|
+
return ` \u2014 got ${echo(v)}; the smaller number comes first: write [${v[1]}, ${v[0]}]`;
|
|
1798
|
+
}
|
|
1799
|
+
return ` \u2014 got ${echo(v)}`;
|
|
1800
|
+
}
|
|
1801
|
+
if (Array.isArray(v) && scalarWanted(message)) {
|
|
1802
|
+
const segs = path.match(/[^.[\]]+/g) ?? [];
|
|
1803
|
+
const key = segs[segs.length - 1] ?? path;
|
|
1804
|
+
const parentKey = segs[segs.length - 2];
|
|
1805
|
+
const parent = valueAtPath(spec, path.slice(0, path.lastIndexOf(".")));
|
|
1806
|
+
const kind = isObj(parent) && isStr(parent.kind) ? parent.kind : void 0;
|
|
1807
|
+
const noun = kind ? `${/^[aeiou]/i.test(kind) ? "an" : "a"} ${kind}'s \`${key}\`` : parentKey && !/^\d+$/.test(parentKey) && parentKey !== "sprite" ? `\`${parentKey}.${key}\`` : `\`${key}\``;
|
|
1808
|
+
return ` \u2014 got ${echo(v)}; ${noun} is one number`;
|
|
1809
|
+
}
|
|
1810
|
+
return "";
|
|
1811
|
+
}
|
|
1722
1812
|
function normalizeColors(spec) {
|
|
1723
1813
|
if (!isObj(spec) || !Array.isArray(spec.layers)) return;
|
|
1724
1814
|
for (const layer of spec.layers) {
|
|
@@ -1787,7 +1877,7 @@ function specPathsValid(spec, paths) {
|
|
|
1787
1877
|
function validateSpecCore(spec, checkApplied = true) {
|
|
1788
1878
|
const errors = [];
|
|
1789
1879
|
const warnings = [];
|
|
1790
|
-
const err = (path, message) => void errors.push({ path, message });
|
|
1880
|
+
const err = (path, message) => void errors.push({ path, message: message + shapeHint(spec, path, message) });
|
|
1791
1881
|
const warn = (path, code, message) => void warnings.push({ path, code, message });
|
|
1792
1882
|
if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }], warnings: [] };
|
|
1793
1883
|
if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
@@ -1834,7 +1924,7 @@ function validateSpecCore(spec, checkApplied = true) {
|
|
|
1834
1924
|
for (const l of spec.layers) {
|
|
1835
1925
|
const sp = isObj(l) ? l.sprite : void 0;
|
|
1836
1926
|
if (!sp || sp.kind !== "polygon") continue;
|
|
1837
|
-
const pieces = Array.isArray(sp.parts) ? sp.parts : typeof sp.path === "string" ? [sp] : [];
|
|
1927
|
+
const pieces = Array.isArray(sp.parts) ? sp.parts : typeof sp.path === "string" ? [sp] : isPointsPath(sp) && validPointPairs(sp.points) ? [{ ...sp, path: pointsPathD(sp.points), fillRule: void 0 }] : [];
|
|
1838
1928
|
const n = typeof l.count === "number" && l.count > 0 ? l.count : 1;
|
|
1839
1929
|
for (const pc of pieces) {
|
|
1840
1930
|
if (!isObj(pc) || typeof pc.path !== "string") continue;
|
|
@@ -1857,7 +1947,7 @@ function validateSpecCore(spec, checkApplied = true) {
|
|
|
1857
1947
|
return { valid: errors.length === 0, errors, warnings };
|
|
1858
1948
|
}
|
|
1859
1949
|
function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
1860
|
-
if (!isObj(tf)) return err(path, "must be an object {x?, y?, scale?, scaleX?, rotate?, origin?}");
|
|
1950
|
+
if (!isObj(tf)) return err(path, "must be an object {x?, y?, scale?, scaleX?, scaleY?, rotate?, origin?}");
|
|
1861
1951
|
for (const k of unknownKeys(tf, KNOWN_TRANSFORM)) warn(`${path}.${k}`, "unknown-property", `unknown transform property '${k}' \u2014 will be ignored`);
|
|
1862
1952
|
const [off, unit] = offsetBound(spec);
|
|
1863
1953
|
const sc = LIMITS.maxLayerTransformScale;
|
|
@@ -1865,7 +1955,9 @@ function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
|
1865
1955
|
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
1866
1956
|
}
|
|
1867
1957
|
if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.scale`, `must be a number 0..${sc}`);
|
|
1868
|
-
|
|
1958
|
+
for (const axis of ["scaleX", "scaleY"]) {
|
|
1959
|
+
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > sc)) err(`${path}.${axis}`, `must be a number within \xB1${sc} (negative mirrors)`);
|
|
1960
|
+
}
|
|
1869
1961
|
if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
|
|
1870
1962
|
err(`${path}.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
|
|
1871
1963
|
}
|
|
@@ -2014,9 +2106,12 @@ function validateTimelineKeys(spec, errOuter, warn, checkApplied) {
|
|
|
2014
2106
|
return;
|
|
2015
2107
|
}
|
|
2016
2108
|
}
|
|
2017
|
-
function color(v, path, err) {
|
|
2018
|
-
if (
|
|
2109
|
+
function color(v, path, err, missing = "is required (a hex colour like #0a3a52)") {
|
|
2110
|
+
if (v === void 0) err(path, missing);
|
|
2111
|
+
else if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
2019
2112
|
}
|
|
2113
|
+
var SPRITE_COLOR_MISSING = "`color` is required (or a `colors` palette)";
|
|
2114
|
+
var POLYGON_COLOR_MISSING = `${SPRITE_COLOR_MISSING}; parts without their own colour inherit it, and a gradient or stroke does not stand in for it`;
|
|
2020
2115
|
function validateFinish(f, path, err, warn) {
|
|
2021
2116
|
if (!isObj(f)) return err(path, "must be an object");
|
|
2022
2117
|
for (const k of ["grain", "dither"]) {
|
|
@@ -2041,6 +2136,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2041
2136
|
bg.stops.forEach((s, i) => {
|
|
2042
2137
|
if (!isObj(s) || !isNum(s.at) || s.at < 0 || s.at > 1) err(`background.stops[${i}].at`, "must be 0..1");
|
|
2043
2138
|
color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
|
|
2139
|
+
if (isObj(s)) for (const k of unknownKeys(s, KNOWN_BG_STOP)) warn(`background.stops[${i}].${k}`, "unknown-property", `unknown stop property '${k}' \u2014 will be ignored`);
|
|
2044
2140
|
});
|
|
2045
2141
|
}
|
|
2046
2142
|
if (bg.drift !== void 0) {
|
|
@@ -2052,6 +2148,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2052
2148
|
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount <= 0 || bg.drift.amount > LIMITS.maxDriftAmount)) {
|
|
2053
2149
|
err("background.drift.amount", `must be > 0 and <= ${LIMITS.maxDriftAmount}`);
|
|
2054
2150
|
}
|
|
2151
|
+
for (const k of unknownKeys(bg.drift, KNOWN_BG_DRIFT)) warn(`background.drift.${k}`, "unknown-property", `unknown drift property '${k}' \u2014 will be ignored`);
|
|
2055
2152
|
}
|
|
2056
2153
|
}
|
|
2057
2154
|
if (bg.band !== void 0) {
|
|
@@ -2059,6 +2156,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2059
2156
|
else {
|
|
2060
2157
|
color(bg.band.color, "background.band.color", err);
|
|
2061
2158
|
positiveScalar(bg.band.height, "background.band.height", err);
|
|
2159
|
+
for (const k of unknownKeys(bg.band, KNOWN_BAND)) warn(`background.band.${k}`, "unknown-property", `unknown band property '${k}' \u2014 will be ignored`);
|
|
2062
2160
|
}
|
|
2063
2161
|
}
|
|
2064
2162
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
@@ -2180,8 +2278,8 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
2180
2278
|
if (layer.pulse !== void 0) {
|
|
2181
2279
|
if (!isObj(layer.pulse)) err(`${path}.pulse`, "must be an object");
|
|
2182
2280
|
else {
|
|
2183
|
-
if (!isNum(layer.pulse.amp) || layer.pulse.amp
|
|
2184
|
-
err(`${path}.pulse.amp`, `must be
|
|
2281
|
+
if (!isNum(layer.pulse.amp) || layer.pulse.amp < 0 || layer.pulse.amp > LIMITS.maxPulseAmp) {
|
|
2282
|
+
err(`${path}.pulse.amp`, `must be a number from 0 (no pulse) to ${LIMITS.maxPulseAmp}`);
|
|
2185
2283
|
}
|
|
2186
2284
|
if (!isNum(layer.pulse.period) || layer.pulse.period < LIMITS.minPulsePeriod) {
|
|
2187
2285
|
err(`${path}.pulse.period`, `must be >= ${LIMITS.minPulsePeriod} ms (flash-safety floor)`);
|
|
@@ -2466,19 +2564,19 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2466
2564
|
} else if (sprite.kind === "circle") {
|
|
2467
2565
|
knownSet = KNOWN_CIRCLE;
|
|
2468
2566
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2469
|
-
color(sprite.color, `${path}.color`, err);
|
|
2567
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2470
2568
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2471
2569
|
validatePalette(sprite, path, err);
|
|
2472
2570
|
} else if (sprite.kind === "ring") {
|
|
2473
2571
|
knownSet = KNOWN_RING;
|
|
2474
2572
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2475
|
-
color(sprite.color, `${path}.color`, err);
|
|
2573
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2476
2574
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2477
2575
|
validatePalette(sprite, path, err);
|
|
2478
2576
|
} else if (sprite.kind === "streak") {
|
|
2479
2577
|
knownSet = KNOWN_STREAK;
|
|
2480
2578
|
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
|
|
2481
|
-
color(sprite.color, `${path}.color`, err);
|
|
2579
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2482
2580
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2483
2581
|
validatePalette(sprite, path, err);
|
|
2484
2582
|
} else if (sprite.kind === "rect") {
|
|
@@ -2490,7 +2588,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2490
2588
|
if (sprite.feather !== void 0 && (!isNum(sprite.feather) || sprite.feather < 0 || sprite.feather > 1)) {
|
|
2491
2589
|
err(`${path}.feather`, "must be 0..1 (fraction of the half-size that fades out)");
|
|
2492
2590
|
}
|
|
2493
|
-
color(sprite.color, `${path}.color`, err);
|
|
2591
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2494
2592
|
validatePalette(sprite, path, err);
|
|
2495
2593
|
} else if (sprite.kind === "bar") {
|
|
2496
2594
|
knownSet = KNOWN_BAR;
|
|
@@ -2503,7 +2601,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2503
2601
|
if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
|
|
2504
2602
|
err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
|
|
2505
2603
|
}
|
|
2506
|
-
color(sprite.color, `${path}.color`, err);
|
|
2604
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2507
2605
|
validatePalette(sprite, path, err);
|
|
2508
2606
|
} else if (sprite.kind === "polygon") {
|
|
2509
2607
|
knownSet = KNOWN_POLYGON;
|
|
@@ -2534,8 +2632,11 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2534
2632
|
} else if (sprite.path !== void 0) {
|
|
2535
2633
|
if (sprite.points !== void 0 || sprite.sides !== void 0) err(`${path}.path`, "use path, points or sides, not more than one");
|
|
2536
2634
|
validatePathPiece(sprite, path, err, warn);
|
|
2635
|
+
} else if (sprite.points !== void 0) {
|
|
2636
|
+
validatePiecePaint(sprite, path, err, warn);
|
|
2637
|
+
if (sprite.fillRule !== void 0) warn(`${path}.fillRule`, "unknown-property", "fillRule applies only to a path \u2014 ignored");
|
|
2537
2638
|
} else {
|
|
2538
|
-
for (const k of ["gradient", "stroke", "fill"]) if (sprite[k] !== void 0) err(`${path}.${k}`, `${k} applies only to a polygon with a path
|
|
2639
|
+
for (const k of ["gradient", "stroke", "fill"]) if (sprite[k] !== void 0) err(`${path}.${k}`, `${k} applies only to a polygon with a path, points or parts`);
|
|
2539
2640
|
if (sprite.fillRule !== void 0) warn(`${path}.fillRule`, "unknown-property", "fillRule applies only to a path \u2014 ignored");
|
|
2540
2641
|
}
|
|
2541
2642
|
if (sprite.span !== void 0 || sprite.origin !== void 0) {
|
|
@@ -2544,7 +2645,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2544
2645
|
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");
|
|
2545
2646
|
}
|
|
2546
2647
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2547
|
-
color(sprite.color, `${path}.color`, err);
|
|
2648
|
+
color(sprite.color, `${path}.color`, err, POLYGON_COLOR_MISSING);
|
|
2548
2649
|
validatePalette(sprite, path, err);
|
|
2549
2650
|
} else if (sprite.kind === "stroke") {
|
|
2550
2651
|
knownSet = KNOWN_STROKE;
|
|
@@ -2555,7 +2656,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2555
2656
|
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
2556
2657
|
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
2557
2658
|
if (sprite.draw !== void 0) validateDraw(sprite.draw, `${path}.draw`, err, warn);
|
|
2558
|
-
color(sprite.color, `${path}.color`, err);
|
|
2659
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2559
2660
|
validatePalette(sprite, path, err);
|
|
2560
2661
|
} else if (sprite.kind === "textBlock") {
|
|
2561
2662
|
knownSet = KNOWN_TEXT_BLOCK;
|
|
@@ -2597,7 +2698,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2597
2698
|
validateTextRole(sprite, path, err);
|
|
2598
2699
|
if (sprite.number !== void 0) {
|
|
2599
2700
|
const n = sprite.number;
|
|
2600
|
-
if (!isObj(n)) err(`${path}.number`, "must be { value, decimals?, prefix?, suffix?, grouping? }");
|
|
2701
|
+
if (!isObj(n)) err(`${path}.number`, "must be { value, decimals?, prefix?, suffix?, grouping?, minDigits? }");
|
|
2601
2702
|
else {
|
|
2602
2703
|
if (!isNum(n.value)) err(`${path}.number.value`, "must be a finite number");
|
|
2603
2704
|
if (n.decimals !== void 0 && (!isNum(n.decimals) || !Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > LIMITS.maxTextNumberDecimals)) {
|
|
@@ -2608,6 +2709,9 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2608
2709
|
if (v !== void 0 && (!isStr(v) || v.length > LIMITS.maxTextAffix)) err(`${path}.number.${k}`, `must be a string of at most ${LIMITS.maxTextAffix} characters`);
|
|
2609
2710
|
}
|
|
2610
2711
|
if (n.grouping !== void 0 && typeof n.grouping !== "boolean") err(`${path}.number.grouping`, "must be a boolean");
|
|
2712
|
+
if (n.minDigits !== void 0 && (!isNum(n.minDigits) || !Number.isInteger(n.minDigits) || n.minDigits < 1 || n.minDigits > LIMITS.maxTextNumberMinDigits)) {
|
|
2713
|
+
err(`${path}.number.minDigits`, `must be a whole number 1..${LIMITS.maxTextNumberMinDigits} (integer digits, zero-padded)`);
|
|
2714
|
+
}
|
|
2611
2715
|
for (const k of unknownKeys(n, KNOWN_TEXT_NUMBER)) warn(`${path}.number.${k}`, "unknown-property", `unknown number property '${k}' \u2014 will be ignored`);
|
|
2612
2716
|
}
|
|
2613
2717
|
}
|
|
@@ -2857,13 +2961,16 @@ function validatePathPiece(piece, path, err, warn) {
|
|
|
2857
2961
|
const problem = pathGeometryProblem(piece.path, piece.fillRule ?? "auto", piece.fill !== false);
|
|
2858
2962
|
if (problem && !(keyedPathDepth > 0 && (problem === NO_AREA || problem === NO_LINE))) err(`${path}.path`, problem);
|
|
2859
2963
|
}
|
|
2964
|
+
validatePiecePaint(piece, path, err, warn);
|
|
2965
|
+
if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
|
|
2966
|
+
}
|
|
2967
|
+
function validatePiecePaint(piece, path, err, warn) {
|
|
2860
2968
|
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2861
2969
|
if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
|
|
2862
|
-
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
|
|
2970
|
+
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err, warn);
|
|
2863
2971
|
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
|
|
2864
|
-
if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
|
|
2865
2972
|
}
|
|
2866
|
-
function validatePathGradient(g, path, err) {
|
|
2973
|
+
function validatePathGradient(g, path, err, warn) {
|
|
2867
2974
|
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
2868
2975
|
if (g.type === "linear") {
|
|
2869
2976
|
if (!isPair(g.from)) err(`${path}.from`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
@@ -2875,6 +2982,7 @@ function validatePathGradient(g, path, err) {
|
|
|
2875
2982
|
} else {
|
|
2876
2983
|
return err(`${path}.type`, "must be 'linear' | 'radial'");
|
|
2877
2984
|
}
|
|
2985
|
+
for (const k of unknownKeys(g, KNOWN_PATH_GRADIENT[g.type])) warn(`${path}.${k}`, "unknown-property", `unknown ${g.type} gradient property '${k}' \u2014 will be ignored`);
|
|
2878
2986
|
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)");
|
|
2879
2987
|
if (!Array.isArray(g.stops) || g.stops.length < 2 || g.stops.length > 8) return err(`${path}.stops`, "must be 2..8 stops");
|
|
2880
2988
|
let last = -Infinity;
|
|
@@ -2884,16 +2992,18 @@ function validatePathGradient(g, path, err) {
|
|
|
2884
2992
|
else last = st.at;
|
|
2885
2993
|
color(st.color, `${path}.stops[${i}].color`, err);
|
|
2886
2994
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
2995
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STOP)) warn(`${path}.stops[${i}].${k}`, "unknown-property", `unknown stop property '${k}' \u2014 will be ignored`);
|
|
2887
2996
|
});
|
|
2888
2997
|
}
|
|
2889
2998
|
function validatePathStroke(st, path, err, warn) {
|
|
2890
2999
|
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap?, draw? }");
|
|
2891
|
-
color(st.color, `${path}.color`, err);
|
|
3000
|
+
color(st.color, `${path}.color`, err, "`color` is required on a stroke (a hex colour like #0a3a52); a stroke does not inherit the entity colour");
|
|
2892
3001
|
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)");
|
|
2893
3002
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2894
3003
|
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2895
3004
|
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2896
3005
|
if (st.draw !== void 0) validateDraw(st.draw, `${path}.draw`, err, warn);
|
|
3006
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STROKE)) warn(`${path}.${k}`, "unknown-property", `unknown stroke property '${k}' \u2014 will be ignored`);
|
|
2897
3007
|
}
|
|
2898
3008
|
function validateWipe(tr, path, err, warn) {
|
|
2899
3009
|
if (tr.shape !== void 0 && tr.shape !== "circle" && tr.shape !== "line") err(`${path}.shape`, "must be 'circle' | 'line'");
|
|
@@ -2912,12 +3022,13 @@ function validateWipe(tr, path, err, warn) {
|
|
|
2912
3022
|
}
|
|
2913
3023
|
}
|
|
2914
3024
|
function validateDraw(d, path, err, warn) {
|
|
2915
|
-
if (!isObj(d)) return err(path, "must be { start?, end? } \u2014 fractions of the line, 0..1");
|
|
3025
|
+
if (!isObj(d)) return err(path, "must be { start?, end?, mode? } \u2014 fractions of the line, 0..1; mode 'run' | 'each'");
|
|
2916
3026
|
for (const k of ["start", "end"]) {
|
|
2917
3027
|
const v = d[k];
|
|
2918
3028
|
if (v !== void 0 && (!isNum(v) || v < 0 || v > 1)) err(`${path}.${k}`, "must be 0..1 (a fraction of the line's length)");
|
|
2919
3029
|
}
|
|
2920
|
-
|
|
3030
|
+
if (d.mode !== void 0 && d.mode !== "run" && d.mode !== "each") err(`${path}.mode`, "must be 'run' (one pen run through every subpath, default) | 'each' (every subpath draws at once)");
|
|
3031
|
+
for (const k of Object.keys(d)) if (k !== "start" && k !== "end" && k !== "mode") warn(`${path}.${k}`, "unknown-property", `unknown draw property '${k}' \u2014 draw takes start, end and mode; will be ignored`);
|
|
2921
3032
|
}
|
|
2922
3033
|
function validateShapePoints(points, path, min, err) {
|
|
2923
3034
|
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
@@ -3436,7 +3547,9 @@ function textBlockText(s) {
|
|
|
3436
3547
|
const d = Math.max(0, Math.min(LIMITS.maxTextNumberDecimals, Math.round(n.decimals ?? 0)));
|
|
3437
3548
|
const v = Number.isFinite(n.value) ? n.value : 0;
|
|
3438
3549
|
const fixed = Math.abs(v).toFixed(d);
|
|
3439
|
-
const [
|
|
3550
|
+
const [digits, frac] = fixed.split(".");
|
|
3551
|
+
const pad = Math.max(1, Math.min(LIMITS.maxTextNumberMinDigits, Math.round(n.minDigits ?? 1)));
|
|
3552
|
+
const whole = digits.padStart(pad, "0");
|
|
3440
3553
|
const grouped = n.grouping === false ? whole : whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
3441
3554
|
const neg = v < 0 && Number(fixed) !== 0;
|
|
3442
3555
|
return `${n.prefix ?? ""}${neg ? "-" : ""}${grouped}${frac !== void 0 ? `.${frac}` : ""}${n.suffix ?? ""}`;
|
|
@@ -3520,6 +3633,7 @@ function lifeStaggerAt(life, t, i) {
|
|
|
3520
3633
|
}
|
|
3521
3634
|
function wrap(v, min, max) {
|
|
3522
3635
|
const range2 = max - min;
|
|
3636
|
+
if (range2 <= 0) return min;
|
|
3523
3637
|
return ((v - min) % range2 + range2) % range2 + min;
|
|
3524
3638
|
}
|
|
3525
3639
|
function reflect(v, min, max) {
|
|
@@ -3947,8 +4061,9 @@ function transformOrigin(tf, layer, w, h, unit) {
|
|
|
3947
4061
|
function transformPoint(tf, p, w, h, unit, origin = { x: w / 2, y: h / 2 }) {
|
|
3948
4062
|
const s = tf.scale ?? 1;
|
|
3949
4063
|
const sx = s * (tf.scaleX ?? 1);
|
|
4064
|
+
const sy = s * (tf.scaleY ?? 1);
|
|
3950
4065
|
let x = (p.x - origin.x) * sx;
|
|
3951
|
-
let y = (p.y - origin.y) *
|
|
4066
|
+
let y = (p.y - origin.y) * sy;
|
|
3952
4067
|
if (tf.rotate) {
|
|
3953
4068
|
const r = tf.rotate * Math.PI / 180;
|
|
3954
4069
|
const c = Math.cos(r);
|
|
@@ -3960,7 +4075,61 @@ function transformPoint(tf, p, w, h, unit, origin = { x: w / 2, y: h / 2 }) {
|
|
|
3960
4075
|
function transformSize(tf) {
|
|
3961
4076
|
if (!tf) return 1;
|
|
3962
4077
|
const s = tf.scale ?? 1;
|
|
3963
|
-
return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
|
|
4078
|
+
return s * Math.sqrt(Math.abs((tf.scaleX ?? 1) * (tf.scaleY ?? 1)));
|
|
4079
|
+
}
|
|
4080
|
+
var TRANSFORM_FIELDS = { x: 0, y: 0, scale: 1, scaleX: 1, scaleY: 1, rotate: 0 };
|
|
4081
|
+
function transformRangeOf(tf) {
|
|
4082
|
+
const r = {};
|
|
4083
|
+
for (const f of Object.keys(TRANSFORM_FIELDS)) {
|
|
4084
|
+
const v = tf?.[f] ?? TRANSFORM_FIELDS[f];
|
|
4085
|
+
r[f] = [v, v];
|
|
4086
|
+
}
|
|
4087
|
+
return r;
|
|
4088
|
+
}
|
|
4089
|
+
function maxAxisScale(r) {
|
|
4090
|
+
const s = Math.max(Math.abs(r.scale[0]), Math.abs(r.scale[1]));
|
|
4091
|
+
return s * Math.max(1, Math.abs(r.scaleX[0]), Math.abs(r.scaleX[1]), Math.abs(r.scaleY[0]), Math.abs(r.scaleY[1]));
|
|
4092
|
+
}
|
|
4093
|
+
function sweptBox(r, p, origin, unit) {
|
|
4094
|
+
const ux = p.x - origin.x;
|
|
4095
|
+
const uy = p.y - origin.y;
|
|
4096
|
+
const b = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
|
|
4097
|
+
const take = (x, y) => {
|
|
4098
|
+
b.x0 = Math.min(b.x0, x);
|
|
4099
|
+
b.x1 = Math.max(b.x1, x);
|
|
4100
|
+
b.y0 = Math.min(b.y0, y);
|
|
4101
|
+
b.y1 = Math.max(b.y1, y);
|
|
4102
|
+
};
|
|
4103
|
+
const r0 = r.rotate[0] * Math.PI / 180;
|
|
4104
|
+
const r1 = r.rotate[1] * Math.PI / 180;
|
|
4105
|
+
const full = r.rotate[1] - r.rotate[0] >= 360;
|
|
4106
|
+
const QUARTER = Math.PI / 2;
|
|
4107
|
+
for (const s of r.scale) {
|
|
4108
|
+
for (const sx of r.scaleX) for (const sy of r.scaleY) {
|
|
4109
|
+
const vx = ux * s * sx;
|
|
4110
|
+
const vy = uy * s * sy;
|
|
4111
|
+
const len = Math.hypot(vx, vy);
|
|
4112
|
+
if (len === 0) {
|
|
4113
|
+
take(0, 0);
|
|
4114
|
+
continue;
|
|
4115
|
+
}
|
|
4116
|
+
if (full) {
|
|
4117
|
+
take(-len, -len);
|
|
4118
|
+
take(len, len);
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
const a = Math.atan2(vy, vx);
|
|
4122
|
+
take(len * Math.cos(a + r0), len * Math.sin(a + r0));
|
|
4123
|
+
take(len * Math.cos(a + r1), len * Math.sin(a + r1));
|
|
4124
|
+
for (let phi = Math.ceil((a + r0) / QUARTER) * QUARTER; phi <= a + r1; phi += QUARTER) take(len * Math.cos(phi), len * Math.sin(phi));
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
return {
|
|
4128
|
+
x0: b.x0 + origin.x + r.x[0] * unit,
|
|
4129
|
+
x1: b.x1 + origin.x + r.x[1] * unit,
|
|
4130
|
+
y0: b.y0 + origin.y + r.y[0] * unit,
|
|
4131
|
+
y1: b.y1 + origin.y + r.y[1] * unit
|
|
4132
|
+
};
|
|
3964
4133
|
}
|
|
3965
4134
|
function groupOf(spec, layer) {
|
|
3966
4135
|
return layer.group === void 0 ? void 0 : spec.groups?.[layer.group];
|
|
@@ -5329,14 +5498,14 @@ var SpecInstance = class {
|
|
|
5329
5498
|
* entity as it was at `t` (rotation, heading, values), but at the echo's
|
|
5330
5499
|
* position, size and alpha.
|
|
5331
5500
|
*/
|
|
5332
|
-
drawEntity(built, e, t, lifeA, parentE, paint,
|
|
5501
|
+
drawEntity(built, e, t, lifeA, parentE, paint, echo2) {
|
|
5333
5502
|
const { ctx } = this;
|
|
5334
|
-
const p =
|
|
5503
|
+
const p = echo2 ? { x: echo2.x, y: echo2.y, flip: false } : this.entityPos(e, t, parentE);
|
|
5335
5504
|
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
5336
|
-
const sz =
|
|
5505
|
+
const sz = echo2 ? echo2.size : sizeAt(e, t);
|
|
5337
5506
|
const rot = rotationAt(e, t);
|
|
5338
5507
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
5339
|
-
ctx.globalAlpha =
|
|
5508
|
+
ctx.globalAlpha = echo2 ? echo2.alpha : alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
5340
5509
|
if (sprite.kind === "ring") {
|
|
5341
5510
|
const r = sz / 2;
|
|
5342
5511
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -5936,7 +6105,7 @@ var SpecInstance = class {
|
|
|
5936
6105
|
ctx.translate(cx + (tf.x ?? 0) * unit, cy + (tf.y ?? 0) * unit);
|
|
5937
6106
|
if (tf.rotate) ctx.rotate(tf.rotate * Math.PI / 180);
|
|
5938
6107
|
const s = tf.scale ?? 1;
|
|
5939
|
-
ctx.scale(s * (tf.scaleX ?? 1), s);
|
|
6108
|
+
ctx.scale(s * (tf.scaleX ?? 1), s * (tf.scaleY ?? 1));
|
|
5940
6109
|
ctx.translate(-cx, -cy);
|
|
5941
6110
|
}
|
|
5942
6111
|
/** Scene time for a timeline decision: the last painted frame's, else the loop clock's. */
|
|
@@ -7906,7 +8075,7 @@ function finishGrid(cols, rows, cells, bgRow, bgCells) {
|
|
|
7906
8075
|
colProfile
|
|
7907
8076
|
};
|
|
7908
8077
|
}
|
|
7909
|
-
function layerCellFootprints(spec, opts = {}) {
|
|
8078
|
+
function layerCellFootprints(spec, opts = {}, sweep) {
|
|
7910
8079
|
const scene = buildScene(spec, opts);
|
|
7911
8080
|
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
7912
8081
|
const rows = Math.max(8, Math.min(200, opts.rows ?? 48));
|
|
@@ -7914,14 +8083,33 @@ function layerCellFootprints(spec, opts = {}) {
|
|
|
7914
8083
|
const cellW = w / cols;
|
|
7915
8084
|
const cellH = h / rows;
|
|
7916
8085
|
const fontScale = fontScaleFor(spec, w, h);
|
|
7917
|
-
return scene.layers.map(({ layer, entities }) => {
|
|
8086
|
+
return scene.layers.map(({ layer, entities }, li) => {
|
|
7918
8087
|
if (layer.motion.type !== "static" || layer.emit || layer.links || layer.trail) return null;
|
|
7919
8088
|
const s = layer.sprite;
|
|
7920
|
-
const
|
|
8089
|
+
const sw = sweep?.get(li);
|
|
8090
|
+
const own = sw ? sw.layer ?? transformRangeOf(layer.transform) : null;
|
|
8091
|
+
const grp = sw ? sw.group ?? transformRangeOf(groupOf(spec, layer)?.transform) : null;
|
|
8092
|
+
const k = own && grp ? maxAxisScale(own) * maxAxisScale(grp) : layer.transform !== void 0 || layer.group !== void 0 ? maxAxisScale(transformRangeOf(layer.transform)) * maxAxisScale(transformRangeOf(groupOf(spec, layer)?.transform)) : 1;
|
|
8093
|
+
const pivot = own ? transformOrigin(layer.transform ?? {}, layer, w, h, scale) : null;
|
|
8094
|
+
const centre = { x: w / 2, y: h / 2 };
|
|
7921
8095
|
const cells = /* @__PURE__ */ new Set();
|
|
7922
8096
|
for (const e of entities) {
|
|
7923
8097
|
if (e.orbitParent || e.cyclePeriod > 0) return null;
|
|
7924
|
-
|
|
8098
|
+
let box;
|
|
8099
|
+
if (own && grp && pivot) {
|
|
8100
|
+
const lb = sweptBox(own, positionAt(e, 0, w, h), pivot, scale);
|
|
8101
|
+
box = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
|
|
8102
|
+
for (const x of [lb.x0, lb.x1]) {
|
|
8103
|
+
for (const y of [lb.y0, lb.y1]) {
|
|
8104
|
+
const gb = sweptBox(grp, { x, y }, centre, scale);
|
|
8105
|
+
box = { x0: Math.min(box.x0, gb.x0), y0: Math.min(box.y0, gb.y0), x1: Math.max(box.x1, gb.x1), y1: Math.max(box.y1, gb.y1) };
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
} else {
|
|
8109
|
+
const q = posOf(scene, e, 0);
|
|
8110
|
+
box = { x0: q.x, y0: q.y, x1: q.x, y1: q.y };
|
|
8111
|
+
}
|
|
8112
|
+
const p = { x: (box.x0 + box.x1) / 2, y: (box.y0 + box.y1) / 2 };
|
|
7925
8113
|
const grow = 1 + Math.abs(e.growAmp);
|
|
7926
8114
|
const sz = e.size * grow * k;
|
|
7927
8115
|
const per = e.size > 0 ? sz / e.size : 1;
|
|
@@ -7947,11 +8135,11 @@ function layerCellFootprints(spec, opts = {}) {
|
|
|
7947
8135
|
const b = s.kind === "text" ? textBox(s, e, p, fontScale, scale) : textBlockBox(s, e, p, w, h);
|
|
7948
8136
|
reach = Math.hypot(Math.abs(b.cx - p.x) + b.halfX, Math.abs(b.cy - p.y) + b.halfY) * grow * Math.max(1, k);
|
|
7949
8137
|
} else reach = sz;
|
|
7950
|
-
if (!Number.isFinite(reach) || !
|
|
7951
|
-
const c0 = Math.max(0, Math.floor((
|
|
7952
|
-
const c1 = Math.min(cols - 1, Math.floor((
|
|
7953
|
-
const r0 = Math.max(0, Math.floor((
|
|
7954
|
-
const r1 = Math.min(rows - 1, Math.floor((
|
|
8138
|
+
if (!Number.isFinite(reach) || ![box.x0, box.x1, box.y0, box.y1].every(Number.isFinite)) return null;
|
|
8139
|
+
const c0 = Math.max(0, Math.floor((box.x0 - reach - 1) / cellW));
|
|
8140
|
+
const c1 = Math.min(cols - 1, Math.floor((box.x1 + reach + 1) / cellW));
|
|
8141
|
+
const r0 = Math.max(0, Math.floor((box.y0 - reach - 1) / cellH));
|
|
8142
|
+
const r1 = Math.min(rows - 1, Math.floor((box.y1 + reach + 1) / cellH));
|
|
7955
8143
|
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) cells.add(r * cols + c);
|
|
7956
8144
|
}
|
|
7957
8145
|
return [...cells].sort((a, b) => a - b);
|
|
@@ -8360,7 +8548,7 @@ function diffScenes(a, b, opts = {}) {
|
|
|
8360
8548
|
const colB = sceneColour(b, opts);
|
|
8361
8549
|
const motA = motionStats(a, opts);
|
|
8362
8550
|
const motB = motionStats(b, opts);
|
|
8363
|
-
const
|
|
8551
|
+
const weighted2 = (stats, dom) => {
|
|
8364
8552
|
let acc = 0;
|
|
8365
8553
|
let n = 0;
|
|
8366
8554
|
for (const m of stats) {
|
|
@@ -8378,8 +8566,8 @@ function diffScenes(a, b, opts = {}) {
|
|
|
8378
8566
|
const vb = mb?.meanSpeedVp ?? 0;
|
|
8379
8567
|
return { key, a: va, b: vb, delta: vb - va, headingA: ma?.heading ?? null, headingB: mb?.heading ?? null };
|
|
8380
8568
|
}).filter((m) => Math.abs(m.delta) > 1e-9 || m.headingA !== m.headingB).sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta));
|
|
8381
|
-
const overallA =
|
|
8382
|
-
const overallB =
|
|
8569
|
+
const overallA = weighted2(motA, domA);
|
|
8570
|
+
const overallB = weighted2(motB, domB);
|
|
8383
8571
|
return {
|
|
8384
8572
|
t,
|
|
8385
8573
|
coverage: { a: gridA.coverage, b: gridB.coverage, delta: gridB.coverage - gridA.coverage },
|
|
@@ -8502,29 +8690,78 @@ function lumaTracks(spec) {
|
|
|
8502
8690
|
if (!tl) return { tracks: [], lap: 0 };
|
|
8503
8691
|
const lap = tl.loop && tl.duration ? tl.duration : 0;
|
|
8504
8692
|
const tracks = [];
|
|
8693
|
+
const laps = (ts) => lap ? [...ts, ...ts.map((b) => b + lap)] : ts;
|
|
8505
8694
|
for (const [path, keys] of timelineTracks(spec)) {
|
|
8506
8695
|
if (!LUMA_PATH.test(path)) continue;
|
|
8507
8696
|
const bounds = [];
|
|
8508
|
-
|
|
8697
|
+
const owners = /* @__PURE__ */ new Map();
|
|
8698
|
+
const jumpAt = [];
|
|
8699
|
+
keys.forEach((k, i) => {
|
|
8509
8700
|
const dur = k.dur ?? DEFAULT_KEY_DUR;
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8701
|
+
const own = (t) => {
|
|
8702
|
+
bounds.push(t);
|
|
8703
|
+
owners.set(t, (owners.get(t) ?? /* @__PURE__ */ new Set()).add(i));
|
|
8704
|
+
};
|
|
8705
|
+
own(k.t);
|
|
8706
|
+
if (dur > 0) own(k.t + dur);
|
|
8707
|
+
if (dur <= 0) jumpAt.push([k.t, i]);
|
|
8708
|
+
else if (k.ease === "step") jumpAt.push([k.t + dur, i]);
|
|
8709
|
+
});
|
|
8710
|
+
const shared = (t, i) => [...owners.get(t) ?? [], ...lap && t === 0 ? owners.get(lap) ?? [] : []].some((j) => j !== i);
|
|
8711
|
+
const jumps = [...new Set(jumpAt.filter(([t, i]) => shared(t, i)).map(([t]) => t))];
|
|
8712
|
+
tracks.push({ path, times: laps(bounds), jumps: laps(jumps) });
|
|
8514
8713
|
}
|
|
8515
8714
|
return { tracks, lap };
|
|
8516
8715
|
}
|
|
8716
|
+
function weighted(tracks) {
|
|
8717
|
+
const extra = new Set(tracks.flatMap((tr) => tr.jumps));
|
|
8718
|
+
return { times: [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b), weight: (t) => extra.has(t) ? 2 : 1 };
|
|
8719
|
+
}
|
|
8517
8720
|
function lumaKeyTimes(spec) {
|
|
8518
8721
|
const { tracks, lap } = lumaTracks(spec);
|
|
8519
|
-
return { times:
|
|
8722
|
+
return { times: weighted(tracks).times, lap };
|
|
8520
8723
|
}
|
|
8521
8724
|
var PAINT_LEAF = /(^|\.)(opacity|alpha|color|fill)(\.\d+)?$|(^|\.)gradient(\.|$)|(^|\.)draw\.(start|end)$/;
|
|
8725
|
+
var MONOTONE_EASES = /* @__PURE__ */ new Set([void 0, "step", "linear", "smooth", "in", "out", "spring"]);
|
|
8726
|
+
function widenTransform(r, base, field, keys) {
|
|
8727
|
+
const take = (f, v) => {
|
|
8728
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return false;
|
|
8729
|
+
r[f] = [Math.min(r[f][0], v), Math.max(r[f][1], v)];
|
|
8730
|
+
return true;
|
|
8731
|
+
};
|
|
8732
|
+
for (const k of keys) {
|
|
8733
|
+
if (!MONOTONE_EASES.has(k.ease)) return false;
|
|
8734
|
+
if (field !== null) {
|
|
8735
|
+
if (!(field in TRANSFORM_FIELDS) || !take(field, k.value)) return false;
|
|
8736
|
+
continue;
|
|
8737
|
+
}
|
|
8738
|
+
const v = k.value;
|
|
8739
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
8740
|
+
const o = v;
|
|
8741
|
+
for (const name of Object.keys(o)) if (name !== "origin" && !(name in TRANSFORM_FIELDS)) return false;
|
|
8742
|
+
if (o.origin !== base?.origin) return false;
|
|
8743
|
+
for (const f of Object.keys(TRANSFORM_FIELDS)) if (!take(f, o[f] ?? TRANSFORM_FIELDS[f])) return false;
|
|
8744
|
+
}
|
|
8745
|
+
return true;
|
|
8746
|
+
}
|
|
8522
8747
|
function pathCells(spec) {
|
|
8523
8748
|
const layers = spec.layers ?? [];
|
|
8524
8749
|
const volatile = /* @__PURE__ */ new Set();
|
|
8525
8750
|
const members = (g) => layers.flatMap((l, i) => l.group === g ? [i] : []);
|
|
8526
8751
|
const reach = /* @__PURE__ */ new Map();
|
|
8527
|
-
|
|
8752
|
+
const layerRanges = /* @__PURE__ */ new Map();
|
|
8753
|
+
const groupRanges = /* @__PURE__ */ new Map();
|
|
8754
|
+
const bound = (ranges, id, base, rest, keys) => {
|
|
8755
|
+
if (rest.length > 2) return false;
|
|
8756
|
+
if (!ranges.has(id)) ranges.set(id, transformRangeOf(base));
|
|
8757
|
+
const r = ranges.get(id);
|
|
8758
|
+
if (!r || !widenTransform(r, base, rest[1] ?? null, keys)) {
|
|
8759
|
+
ranges.set(id, null);
|
|
8760
|
+
return false;
|
|
8761
|
+
}
|
|
8762
|
+
return true;
|
|
8763
|
+
};
|
|
8764
|
+
for (const [path, keys] of timelineTracks(spec)) {
|
|
8528
8765
|
const [root, id, ...rest] = path.split(".");
|
|
8529
8766
|
const leaf = rest.join(".");
|
|
8530
8767
|
let to;
|
|
@@ -8532,18 +8769,27 @@ function pathCells(spec) {
|
|
|
8532
8769
|
const n = Number(id);
|
|
8533
8770
|
to = [n];
|
|
8534
8771
|
if (!PAINT_LEAF.test(leaf)) {
|
|
8535
|
-
if (rest[0] === "transform")
|
|
8536
|
-
|
|
8772
|
+
if (rest[0] === "transform") {
|
|
8773
|
+
if (!bound(layerRanges, n, layers[n]?.transform, rest, keys)) volatile.add(n);
|
|
8774
|
+
} else for (let i = n; i < layers.length; i++) volatile.add(i);
|
|
8537
8775
|
}
|
|
8538
8776
|
} else if (root === "groups" && id !== void 0) {
|
|
8539
8777
|
to = members(id);
|
|
8540
|
-
if (leaf !== "opacity") for (const i of to) volatile.add(i);
|
|
8778
|
+
if (leaf !== "opacity" && !(rest[0] === "transform" && bound(groupRanges, id, spec.groups?.[id]?.transform, rest, keys))) for (const i of to) volatile.add(i);
|
|
8541
8779
|
} else if (root === "background") to = "all";
|
|
8542
8780
|
else return null;
|
|
8543
8781
|
if (LUMA_PATH.test(path)) reach.set(path, to);
|
|
8544
8782
|
}
|
|
8545
8783
|
if (reach.size === 0) return /* @__PURE__ */ new Map();
|
|
8546
|
-
const
|
|
8784
|
+
for (const [n, r] of layerRanges) if (!r) volatile.add(n);
|
|
8785
|
+
for (const [g, r] of groupRanges) if (!r) for (const i of members(g)) volatile.add(i);
|
|
8786
|
+
const sweep = /* @__PURE__ */ new Map();
|
|
8787
|
+
layers.forEach((l, i) => {
|
|
8788
|
+
const own = layerRanges.get(i) ?? void 0;
|
|
8789
|
+
const grp = l.group !== void 0 && spec.groups?.[l.group] ? groupRanges.get(l.group) ?? void 0 : void 0;
|
|
8790
|
+
if (own || grp) sweep.set(i, { layer: own, group: grp });
|
|
8791
|
+
});
|
|
8792
|
+
const footprints = layerCellFootprints(withoutTimeline(spec), { cols: COLS, rows: ROWS }, sweep);
|
|
8547
8793
|
const out = /* @__PURE__ */ new Map();
|
|
8548
8794
|
for (const [path, to] of reach) {
|
|
8549
8795
|
if (to === "all" || to.some((i) => volatile.has(i) || !footprints[i])) out.set(path, null);
|
|
@@ -8556,23 +8802,28 @@ function cellsThatCanFlash(tracks, cellsOf, from, to) {
|
|
|
8556
8802
|
for (const tr of tracks) {
|
|
8557
8803
|
const ts = tr.times.filter((t) => t >= from && t <= to);
|
|
8558
8804
|
if (ts.length === 0) continue;
|
|
8805
|
+
const part = { times: ts, jumps: tr.jumps };
|
|
8559
8806
|
const cells = cellsOf.get(tr.path);
|
|
8560
|
-
if (cells === null || cells === void 0) for (const list of perCell) list.push(
|
|
8561
|
-
else for (const c of cells) perCell[c].push(
|
|
8807
|
+
if (cells === null || cells === void 0) for (const list of perCell) list.push(part);
|
|
8808
|
+
else for (const c of cells) perCell[c].push(part);
|
|
8562
8809
|
}
|
|
8563
8810
|
let n = 0;
|
|
8564
|
-
for (const
|
|
8565
|
-
if (
|
|
8566
|
-
const
|
|
8567
|
-
|
|
8568
|
-
if (ts[i + MIN_BOUNDARIES - 1] - ts[i] <= WINDOW + 2) {
|
|
8569
|
-
n++;
|
|
8570
|
-
break;
|
|
8571
|
-
}
|
|
8572
|
-
}
|
|
8811
|
+
for (const parts of perCell) {
|
|
8812
|
+
if (parts.length === 0) continue;
|
|
8813
|
+
const { times: ts, weight } = weighted(parts);
|
|
8814
|
+
if (denseRun(ts, weight, WINDOW + 2) !== null) n++;
|
|
8573
8815
|
}
|
|
8574
8816
|
return n;
|
|
8575
8817
|
}
|
|
8818
|
+
function denseRun(ts, weight, span) {
|
|
8819
|
+
let sum = 0;
|
|
8820
|
+
for (let i = 0, e = 0; i < ts.length; i++) {
|
|
8821
|
+
while (e < ts.length && ts[e] - ts[i] <= span) sum += weight(ts[e++]);
|
|
8822
|
+
if (sum >= MIN_BOUNDARIES) return [i, e];
|
|
8823
|
+
sum -= weight(ts[i]);
|
|
8824
|
+
}
|
|
8825
|
+
return null;
|
|
8826
|
+
}
|
|
8576
8827
|
function flashSampleTimes(boundaries) {
|
|
8577
8828
|
const ts = /* @__PURE__ */ new Set();
|
|
8578
8829
|
boundaries.forEach((t, i) => {
|
|
@@ -8583,45 +8834,37 @@ function flashSampleTimes(boundaries) {
|
|
|
8583
8834
|
});
|
|
8584
8835
|
return [...ts].sort((a, b) => a - b);
|
|
8585
8836
|
}
|
|
8586
|
-
function
|
|
8587
|
-
|
|
8588
|
-
if (!spec.timeline) return pass;
|
|
8837
|
+
function* flashSampleChunks(spec) {
|
|
8838
|
+
if (!spec.timeline) return;
|
|
8589
8839
|
const { tracks, lap } = lumaTracks(spec);
|
|
8590
|
-
const
|
|
8840
|
+
const { times, weight } = weighted(tracks);
|
|
8591
8841
|
const dense = [];
|
|
8592
|
-
for (let i = 0, e = 0; i < times.length; i++) {
|
|
8593
|
-
while (e < times.length && times[e] < times[i] + WINDOW) e
|
|
8594
|
-
if (
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8842
|
+
for (let i = 0, e = 0, sum = 0; i < times.length; i++) {
|
|
8843
|
+
while (e < times.length && times[e] < times[i] + WINDOW) sum += weight(times[e++]);
|
|
8844
|
+
if (sum >= MIN_BOUNDARIES) {
|
|
8845
|
+
const last = dense[dense.length - 1];
|
|
8846
|
+
if (last && times[i] <= last.to) {
|
|
8847
|
+
last.to = Math.max(last.to, times[e - 1]);
|
|
8848
|
+
last.n = Math.max(last.n, sum);
|
|
8849
|
+
} else dense.push({ from: times[i], to: times[e - 1], n: sum });
|
|
8850
|
+
}
|
|
8851
|
+
sum -= weight(times[i]);
|
|
8852
|
+
}
|
|
8853
|
+
if (dense.length === 0) return;
|
|
8602
8854
|
dense.sort((a, b) => b.n - a.n);
|
|
8603
8855
|
let budget = MAX_TOTAL_SAMPLES;
|
|
8604
|
-
let
|
|
8856
|
+
let over = false;
|
|
8605
8857
|
const cellsOf = pathCells(spec);
|
|
8606
8858
|
for (const w of dense) {
|
|
8607
8859
|
if (cellsOf && cellsThatCanFlash(tracks, cellsOf, w.from, w.to) < Math.ceil(AREA_THRESHOLD * COLS * ROWS)) continue;
|
|
8860
|
+
const at = lap ? w.from % lap : w.from;
|
|
8608
8861
|
const sampleTimes = flashSampleTimes(times.filter((t) => t >= w.from && t <= w.to));
|
|
8609
8862
|
for (let from = 0; from < sampleTimes.length - 1; ) {
|
|
8610
8863
|
const chunk = sampleTimes.slice(from, from + MAX_SAMPLES);
|
|
8611
8864
|
if (chunk.length < 2) break;
|
|
8612
|
-
if (chunk.length > budget)
|
|
8613
|
-
budget -= chunk.length;
|
|
8614
|
-
|
|
8615
|
-
const r = analyzeFlashes(samples);
|
|
8616
|
-
if (r.general.worstTileFlashesPerSecond > worst.flashesPerSecond || !worst.fails && r.general.fails) {
|
|
8617
|
-
worst = {
|
|
8618
|
-
fails: r.general.fails,
|
|
8619
|
-
flashesPerSecond: r.general.worstTileFlashesPerSecond,
|
|
8620
|
-
flashingArea: r.general.flashingAreaFraction,
|
|
8621
|
-
at: lap ? w.from % lap : w.from
|
|
8622
|
-
};
|
|
8623
|
-
}
|
|
8624
|
-
if (worst.fails) return worst;
|
|
8865
|
+
if (chunk.length > budget) over = true;
|
|
8866
|
+
if (!over) budget -= chunk.length;
|
|
8867
|
+
yield { times: chunk, at, n: w.n, over };
|
|
8625
8868
|
if (from + MAX_SAMPLES >= sampleTimes.length) break;
|
|
8626
8869
|
const resume = chunk[chunk.length - 1] - WINDOW;
|
|
8627
8870
|
let next = from + 1;
|
|
@@ -8629,11 +8872,44 @@ function timelineFlashReport(spec) {
|
|
|
8629
8872
|
from = next;
|
|
8630
8873
|
}
|
|
8631
8874
|
}
|
|
8875
|
+
}
|
|
8876
|
+
var PASS = { fails: false, flashesPerSecond: 0, flashingArea: 0, at: null };
|
|
8877
|
+
var failClosed = (worst, c) => ({ ...worst, fails: true, at: c.at, incomplete: true, boundariesPerSecond: c.n });
|
|
8878
|
+
function timelineFlashReport(spec) {
|
|
8879
|
+
let worst = { ...PASS };
|
|
8880
|
+
for (const c of flashSampleChunks(spec)) {
|
|
8881
|
+
if (c.over) return failClosed(worst, c);
|
|
8882
|
+
const samples = c.times.map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: COLS, rows: ROWS }).cells.flat().map((v) => srgbToLinear2(v * 255)) }));
|
|
8883
|
+
const r = analyzeFlashes(samples);
|
|
8884
|
+
if (r.general.worstTileFlashesPerSecond > worst.flashesPerSecond || !worst.fails && r.general.fails) {
|
|
8885
|
+
worst = {
|
|
8886
|
+
fails: r.general.fails,
|
|
8887
|
+
flashesPerSecond: r.general.worstTileFlashesPerSecond,
|
|
8888
|
+
flashingArea: r.general.flashingAreaFraction,
|
|
8889
|
+
at: c.at
|
|
8890
|
+
};
|
|
8891
|
+
}
|
|
8892
|
+
if (worst.fails) return worst;
|
|
8893
|
+
}
|
|
8632
8894
|
return worst;
|
|
8633
8895
|
}
|
|
8896
|
+
function flashSamplingForecast(spec) {
|
|
8897
|
+
let samples = 0;
|
|
8898
|
+
let needed = 0;
|
|
8899
|
+
let first;
|
|
8900
|
+
for (const c of flashSampleChunks(spec)) {
|
|
8901
|
+
needed += c.times.length;
|
|
8902
|
+
if (!c.over) samples += c.times.length;
|
|
8903
|
+
else first ??= c;
|
|
8904
|
+
}
|
|
8905
|
+
const base = { samples, needed, budget: MAX_TOTAL_SAMPLES };
|
|
8906
|
+
if (!first) return { exhausted: false, at: null, ...base, report: { ...PASS } };
|
|
8907
|
+
return { exhausted: true, at: first.at, boundariesPerSecond: first.n, ...base, report: failClosed({ ...PASS }, first) };
|
|
8908
|
+
}
|
|
8634
8909
|
|
|
8635
8910
|
// src/period.ts
|
|
8636
8911
|
var MAX_SCENE_PERIOD = 36e5;
|
|
8912
|
+
var SEAMLESS_PERIOD_KINDS = /* @__PURE__ */ new Set(["spin", "rock", "pulse", "grow"]);
|
|
8637
8913
|
var PERIOD_LEAF = /(^|\.)(period|rate|every|duration|spin|speed|blink|drift)$/;
|
|
8638
8914
|
var gcd = (a, b) => {
|
|
8639
8915
|
while (b) [a, b] = [b, a % b];
|
|
@@ -8651,81 +8927,180 @@ function toRatio(ms) {
|
|
|
8651
8927
|
return null;
|
|
8652
8928
|
}
|
|
8653
8929
|
var range = (r) => r === void 0 ? null : typeof r === "number" ? r : r[0] === r[1] ? r[0] : NaN;
|
|
8654
|
-
function scenePeriod(spec) {
|
|
8930
|
+
function scenePeriod(spec, skip) {
|
|
8655
8931
|
const parts = [];
|
|
8656
8932
|
const reasons = [];
|
|
8657
|
-
const add = (label, ms) => {
|
|
8658
|
-
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
8933
|
+
const add = (label, ms, kind) => {
|
|
8934
|
+
if (!(ms > 0) || !Number.isFinite(ms) || skip?.has(kind)) return;
|
|
8659
8935
|
const ratio = toRatio(ms);
|
|
8660
8936
|
if (!ratio) reasons.push(`${label} repeats every ${Number(ms.toFixed(3))} ms, which has no exact ratio to combine`);
|
|
8661
|
-
else parts.push([label, ratio[0], ratio[1]]);
|
|
8937
|
+
else parts.push([label, ratio[0], ratio[1], kind]);
|
|
8662
8938
|
};
|
|
8663
8939
|
const tl = spec.timeline;
|
|
8664
8940
|
if (tl && tl.keys.length > 0) {
|
|
8665
8941
|
const keyed = tl.keys.filter((k) => PERIOD_LEAF.test(String(k.path)));
|
|
8666
8942
|
if (keyed.length > 0) reasons.push(`timeline keys animate ${[...new Set(keyed.map((k) => String(k.path)))].join(", ")}, which sets the scene's own period`);
|
|
8667
|
-
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration);
|
|
8943
|
+
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration, "timeline");
|
|
8668
8944
|
else reasons.push("timeline does not loop (it plays once, then holds)");
|
|
8669
8945
|
}
|
|
8670
8946
|
const bg = spec.background;
|
|
8671
|
-
if (bg && bg.type !== "solid" && bg.drift) {
|
|
8947
|
+
if (bg && bg.type !== "solid" && bg.drift && !(bg.type === "field" && bg.drift.amount === 0)) {
|
|
8672
8948
|
const { period } = bg.drift;
|
|
8673
|
-
const
|
|
8674
|
-
|
|
8675
|
-
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period);
|
|
8949
|
+
const r = bg.type === "field" ? toRatio(period) : null;
|
|
8950
|
+
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period, "background");
|
|
8676
8951
|
}
|
|
8677
8952
|
if (spec.finish?.animate && ((spec.finish.grain ?? 0) > 0 || (spec.finish.dither ?? 0) > 0)) reasons.push("finish.animate steps the grain without repeating");
|
|
8678
8953
|
spec.layers.forEach((layer, i) => {
|
|
8679
8954
|
const at = `layers[${i}]${layer.key ? ` (${layer.key})` : ""}`;
|
|
8680
8955
|
const rate = layer.clock?.rate ?? 1;
|
|
8681
8956
|
const life = layer.life;
|
|
8682
|
-
|
|
8683
|
-
if (
|
|
8684
|
-
if (layer.
|
|
8685
|
-
if (layer.
|
|
8686
|
-
if (layer.
|
|
8957
|
+
const staggered = (life?.stagger ?? 0) > 0;
|
|
8958
|
+
if (life && (life.exit !== void 0 || life.enter !== void 0 && ((life.enter ?? 0) > 0 || (life.fade ?? 1e3) > 0 || staggered))) reasons.push(`${at} life builds in or out once`);
|
|
8959
|
+
if (layer.pulse && layer.pulse.amp !== 0) add(`${at} pulse`, layer.pulse.period / rate, "pulse");
|
|
8960
|
+
if (layer.grow) add(`${at} grow`, layer.grow.period / rate, "grow");
|
|
8961
|
+
if (layer.rock) add(`${at} rock`, layer.rock.period / rate, "rock");
|
|
8962
|
+
if (layer.emit) add(`${at} emit`, layer.emit.every, "emit");
|
|
8687
8963
|
const spin = range(layer.spin);
|
|
8688
|
-
|
|
8964
|
+
const fold = spin !== null && spin !== 0 ? spinSymmetry(layer.sprite) : 1;
|
|
8965
|
+
if (spin !== null && fold !== Infinity) {
|
|
8689
8966
|
if (Number.isNaN(spin)) reasons.push(`${at} spin range gives each entity its own speed`);
|
|
8690
|
-
else if (spin !== 0) add(`${at} spin`, 36e4 / Math.abs(spin));
|
|
8967
|
+
else if (spin !== 0) add(`${at} spin${fold > 1 ? ` (${fold}-fold)` : ""}`, 36e4 / Math.abs(spin) / fold, "spin");
|
|
8691
8968
|
}
|
|
8692
8969
|
const sp = layer.sprite;
|
|
8693
|
-
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate);
|
|
8970
|
+
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate, "cycle");
|
|
8694
8971
|
if (sp.kind === "textBlock" && sp.reveal) {
|
|
8695
8972
|
if ((sp.reveal.speed ?? 0) > 0) reasons.push(`${at} reveal types itself once`);
|
|
8696
8973
|
if (sp.reveal.caret) {
|
|
8697
8974
|
const hz = Math.min(3, sp.reveal.caret === true ? 1.2 : sp.reveal.caret.blink ?? 1.2);
|
|
8698
|
-
if (hz > 0) add(`${at} caret`, 1e3 / hz);
|
|
8975
|
+
if (hz > 0) add(`${at} caret`, 1e3 / hz, "caret");
|
|
8699
8976
|
}
|
|
8700
8977
|
}
|
|
8701
8978
|
const m = layer.motion;
|
|
8702
|
-
if (m.type === "path") add(`${at} path`, m.duration);
|
|
8979
|
+
if (m.type === "path") add(`${at} path`, m.duration, "path");
|
|
8703
8980
|
else if (m.type === "orbit") {
|
|
8704
8981
|
const s = range(m.speed);
|
|
8705
8982
|
if (s === null || Number.isNaN(s)) reasons.push(`${at} orbit speed range gives each entity its own lap`);
|
|
8706
|
-
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s));
|
|
8983
|
+
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s), "orbit");
|
|
8707
8984
|
} else if (m.type !== "static") {
|
|
8708
8985
|
if (m.type === "drift" && m.ease?.type === "settle" && !layer.emit) reasons.push(`${at} drift settles once`);
|
|
8709
8986
|
else reasons.push(`${at} ${m.type} motion travels at seeded per-entity speeds`);
|
|
8710
8987
|
}
|
|
8711
8988
|
});
|
|
8712
8989
|
const labelled = parts.map(([l, n, d]) => `${l}: ${Number((n / d).toFixed(3))} ms`);
|
|
8713
|
-
|
|
8714
|
-
if (
|
|
8990
|
+
const kinds = parts.map(([, , , k]) => k);
|
|
8991
|
+
if (reasons.length > 0) return { period: null, parts: labelled, kinds, reasons };
|
|
8992
|
+
const combined = combinePeriods(parts);
|
|
8993
|
+
return { ...combined, parts: labelled, kinds };
|
|
8994
|
+
}
|
|
8995
|
+
function combinePeriods(parts) {
|
|
8996
|
+
if (parts.length === 0) return { period: 0, reasons: [] };
|
|
8715
8997
|
let num = 1;
|
|
8716
8998
|
let den = 0;
|
|
8717
8999
|
for (const [, n, d] of parts) {
|
|
8718
9000
|
num = num / gcd(num, n) * n;
|
|
8719
9001
|
den = gcd(den, d);
|
|
8720
9002
|
if (num / den > MAX_SCENE_PERIOD) {
|
|
8721
|
-
return { period: null,
|
|
9003
|
+
return { period: null, reasons: [`its parts share no cycle shorter than ${MAX_SCENE_PERIOD / 6e4} min`] };
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
9006
|
+
if (num % den !== 0) return { period: null, reasons: [`its parts repeat together every ${Number((num / den).toFixed(3))} ms, not a whole number of milliseconds`] };
|
|
9007
|
+
return { period: num / den, reasons: [] };
|
|
9008
|
+
}
|
|
9009
|
+
function periodWithoutTimeline(spec, skip) {
|
|
9010
|
+
return scenePeriod(withoutTimeline(spec), skip);
|
|
9011
|
+
}
|
|
9012
|
+
var MAX_SYMMETRY = 64;
|
|
9013
|
+
var MAX_SYMMETRY_POINTS = 8192;
|
|
9014
|
+
function spinSymmetry(sp) {
|
|
9015
|
+
if (sp.kind === "circle" || sp.kind === "ring") return Infinity;
|
|
9016
|
+
if (sp.kind !== "polygon" || sp.gradient) return 1;
|
|
9017
|
+
if (!isPathPolygon(sp)) {
|
|
9018
|
+
if (sp.points && sp.points.length >= 3) return largestSymmetry(/* @__PURE__ */ new Map([["fill", closedSegs(sp.points)]]));
|
|
9019
|
+
return Math.max(3, Math.round(sp.sides ?? DEFAULT_POLYGON_SIDES));
|
|
9020
|
+
}
|
|
9021
|
+
if ((sp.span ?? "box") !== "box") return 1;
|
|
9022
|
+
const [ox, oy] = sp.origin ?? [0, 0];
|
|
9023
|
+
if (sp.soft && !sp.parts && (ox !== 0 || oy !== 0)) return 1;
|
|
9024
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9025
|
+
const add = (key, segs) => {
|
|
9026
|
+
const list = groups.get(key);
|
|
9027
|
+
if (list) list.push(...segs);
|
|
9028
|
+
else groups.set(key, segs);
|
|
9029
|
+
};
|
|
9030
|
+
const shift = (pts) => pts.map(([x, y]) => [x - ox, y - oy]);
|
|
9031
|
+
for (const part of pathParts(sp)) {
|
|
9032
|
+
if (part.gradient || part.stroke && drawSpan(part.stroke.draw) !== null) return 1;
|
|
9033
|
+
const colour = part.color ?? "*";
|
|
9034
|
+
if (part.fill && part.flat.points.length >= 3) {
|
|
9035
|
+
for (const l of part.flat.lines) if (l.points.length >= 3) add(`F|${colour}|${part.rule}|${part.opacity}`, closedSegs(shift(l.points)));
|
|
9036
|
+
}
|
|
9037
|
+
const st = part.stroke;
|
|
9038
|
+
if (st) {
|
|
9039
|
+
const key = `S|${st.color}|${st.width}|${st.opacity ?? 1}|${st.join ?? ""}|${st.cap ?? ""}|${part.opacity}`;
|
|
9040
|
+
for (const l of part.flat.lines) {
|
|
9041
|
+
const pts = shift(l.points);
|
|
9042
|
+
add(key, l.closed ? closedSegs(pts) : openSegs(pts));
|
|
9043
|
+
}
|
|
8722
9044
|
}
|
|
8723
9045
|
}
|
|
8724
|
-
|
|
8725
|
-
|
|
9046
|
+
return largestSymmetry(groups);
|
|
9047
|
+
}
|
|
9048
|
+
function closedSegs(pts) {
|
|
9049
|
+
return pts.map(([x, y], i) => {
|
|
9050
|
+
const [x2, y2] = pts[(i + 1) % pts.length];
|
|
9051
|
+
return [x, y, x2, y2];
|
|
9052
|
+
});
|
|
8726
9053
|
}
|
|
8727
|
-
function
|
|
8728
|
-
|
|
9054
|
+
function openSegs(pts) {
|
|
9055
|
+
const out = [];
|
|
9056
|
+
for (let i = 0; i + 1 < pts.length; i++) out.push([pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]]);
|
|
9057
|
+
return out;
|
|
9058
|
+
}
|
|
9059
|
+
function largestSymmetry(groups) {
|
|
9060
|
+
let radius = 0;
|
|
9061
|
+
let total = 0;
|
|
9062
|
+
for (const segs of groups.values()) {
|
|
9063
|
+
total += segs.length;
|
|
9064
|
+
for (const [ax, ay, bx, by] of segs) radius = Math.max(radius, Math.hypot(ax, ay), Math.hypot(bx, by));
|
|
9065
|
+
}
|
|
9066
|
+
if (radius === 0 || total === 0 || total > MAX_SYMMETRY_POINTS) return 1;
|
|
9067
|
+
const eps = Math.max(2e-3 * radius, 2e-3);
|
|
9068
|
+
const indexed = [...groups.values()].map((segs) => ({ grid: segGrid(segs, radius, eps), probes: segs.flatMap(([ax, ay, bx, by]) => [[ax, ay], [(ax + bx) / 2, (ay + by) / 2]]) }));
|
|
9069
|
+
for (let n = MAX_SYMMETRY; n >= 2; n--) {
|
|
9070
|
+
const a = 2 * Math.PI / n;
|
|
9071
|
+
if (indexed.every(({ grid, probes }) => [a, -a].every((t) => {
|
|
9072
|
+
const c = Math.cos(t);
|
|
9073
|
+
const s = Math.sin(t);
|
|
9074
|
+
return probes.every(([x, y]) => grid.near(x * c - y * s, x * s + y * c));
|
|
9075
|
+
}))) return n;
|
|
9076
|
+
}
|
|
9077
|
+
return 1;
|
|
9078
|
+
}
|
|
9079
|
+
function segGrid(segs, radius, eps) {
|
|
9080
|
+
const n = Math.max(1, Math.min(128, Math.ceil(Math.sqrt(segs.length) * 2)));
|
|
9081
|
+
const lo = -radius - eps;
|
|
9082
|
+
const size = 2 * (radius + eps) / n;
|
|
9083
|
+
const cellOf = (v) => Math.max(0, Math.min(n - 1, Math.floor((v - lo) / size)));
|
|
9084
|
+
const cells = Array.from({ length: n * n }, () => []);
|
|
9085
|
+
for (const sg of segs) {
|
|
9086
|
+
const [ax, ay, bx, by] = sg;
|
|
9087
|
+
for (let cy = cellOf(Math.min(ay, by) - eps); cy <= cellOf(Math.max(ay, by) + eps); cy++) {
|
|
9088
|
+
for (let cx = cellOf(Math.min(ax, bx) - eps); cx <= cellOf(Math.max(ax, bx) + eps); cx++) cells[cy * n + cx].push(sg);
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
9091
|
+
return {
|
|
9092
|
+
near: (x, y) => {
|
|
9093
|
+
if (x < lo || y < lo || x > -lo || y > -lo) return false;
|
|
9094
|
+
for (const [ax, ay, bx, by] of cells[cellOf(y) * n + cellOf(x)]) {
|
|
9095
|
+
const dx = bx - ax;
|
|
9096
|
+
const dy = by - ay;
|
|
9097
|
+
const len2 = dx * dx + dy * dy;
|
|
9098
|
+
const u = len2 > 0 ? Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len2)) : 0;
|
|
9099
|
+
if (Math.hypot(x - ax - u * dx, y - ay - u * dy) <= eps) return true;
|
|
9100
|
+
}
|
|
9101
|
+
return false;
|
|
9102
|
+
}
|
|
9103
|
+
};
|
|
8729
9104
|
}
|
|
8730
9105
|
|
|
8731
9106
|
// src/advise.ts
|
|
@@ -8969,14 +9344,15 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
8969
9344
|
if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
|
|
8970
9345
|
const lap = Math.round(authored.timeline.duration);
|
|
8971
9346
|
const rest = periodWithoutTimeline(authored);
|
|
8972
|
-
|
|
9347
|
+
const seam = rest.period !== null && rest.period > 0 && lap % rest.period !== 0 ? periodWithoutTimeline(authored, SEAMLESS_PERIOD_KINDS) : null;
|
|
9348
|
+
if (seam && seam.period !== null && seam.period > 0 && lap % seam.period !== 0) {
|
|
8973
9349
|
let a = lap;
|
|
8974
|
-
let b =
|
|
9350
|
+
let b = seam.period;
|
|
8975
9351
|
while (b) [a, b] = [b, a % b];
|
|
8976
9352
|
warnings.push({
|
|
8977
9353
|
path: "timeline.duration",
|
|
8978
9354
|
code: "loop-seam",
|
|
8979
|
-
message: `the timeline loops every ${lap} ms but
|
|
9355
|
+
message: `the timeline loops every ${lap} ms but ${seam.parts.join(", ")} repeat${seam.parts.length === 1 ? "s" : ""} every ${seam.period} ms, so the scene only comes round to its first frame every ${lap / a * seam.period} ms \u2014 make the lap a multiple of ${seam.period} ms for one lap to loop`
|
|
8980
9356
|
});
|
|
8981
9357
|
}
|
|
8982
9358
|
}
|
|
@@ -12084,6 +12460,7 @@ export {
|
|
|
12084
12460
|
SAKURA_SPEC,
|
|
12085
12461
|
SCHEMA_EXAMPLES,
|
|
12086
12462
|
SCHEMA_VERSION,
|
|
12463
|
+
SEAMLESS_PERIOD_KINDS,
|
|
12087
12464
|
SHARD_FALL_SPEC,
|
|
12088
12465
|
SIGNAL_BOARD_SPEC,
|
|
12089
12466
|
SNOWFALL_SPEC,
|
|
@@ -12132,6 +12509,7 @@ export {
|
|
|
12132
12509
|
fieldRgbAt,
|
|
12133
12510
|
fieldSampleTime,
|
|
12134
12511
|
finishStrength,
|
|
12512
|
+
flashSamplingForecast,
|
|
12135
12513
|
flattenPath,
|
|
12136
12514
|
grainOffset,
|
|
12137
12515
|
grainTilePixels,
|
|
@@ -12188,6 +12566,7 @@ export {
|
|
|
12188
12566
|
sequenceSwapCompatible,
|
|
12189
12567
|
sequenceWantsFinish,
|
|
12190
12568
|
sizeAt,
|
|
12569
|
+
spinSymmetry,
|
|
12191
12570
|
spriteIndexAt,
|
|
12192
12571
|
spriteVariants,
|
|
12193
12572
|
steerablePaths,
|