@idle-screens/schema 3.16.0 → 3.17.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 +127 -19
- package/README.md +1 -1
- package/dist/index.d.ts +124 -7
- package/dist/index.js +944 -153
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -500,46 +500,105 @@ function easeGlide(ease2, k) {
|
|
|
500
500
|
}
|
|
501
501
|
var STEER_EASES = /* @__PURE__ */ new Set(["in", "out", "spring"]);
|
|
502
502
|
function structuralSignature(spec) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
503
|
+
const f = structuralFields(spec);
|
|
504
|
+
return JSON.stringify([...f.root.map(([, v]) => v), f.layers.map((l) => l.map(([, v]) => v))]);
|
|
505
|
+
}
|
|
506
|
+
function structuralFields(spec) {
|
|
507
|
+
return {
|
|
508
|
+
root: [
|
|
509
|
+
["units", spec.units],
|
|
510
|
+
// Normalized against the same default every renderer uses — an omitted
|
|
511
|
+
// referenceViewport and an explicit 1080 render identically, so they must
|
|
512
|
+
// hash identically or a same-sizing morph gets rejected as structural.
|
|
513
|
+
["referenceViewport", spec.referenceViewport ?? LIMITS.referenceViewport]
|
|
514
|
+
],
|
|
515
|
+
layers: spec.layers.map((l) => {
|
|
510
516
|
const s = l.sprite;
|
|
511
517
|
return [
|
|
512
|
-
l.count,
|
|
513
|
-
l.size,
|
|
514
|
-
l.region,
|
|
515
|
-
l.position,
|
|
516
|
-
l.motion,
|
|
517
|
-
l.wrap,
|
|
518
|
-
l.flip,
|
|
519
|
-
l.alpha,
|
|
520
|
-
l.pulse,
|
|
521
|
-
l.spin,
|
|
522
|
-
l.grow,
|
|
523
|
-
l.layout,
|
|
524
|
-
l.emit,
|
|
525
|
-
l.clock,
|
|
526
|
-
l.sprite.kind,
|
|
518
|
+
["count", l.count],
|
|
519
|
+
["size", l.size],
|
|
520
|
+
["region", l.region],
|
|
521
|
+
["position", l.position],
|
|
522
|
+
["motion", l.motion],
|
|
523
|
+
["wrap", l.wrap],
|
|
524
|
+
["flip", l.flip],
|
|
525
|
+
["alpha", l.alpha],
|
|
526
|
+
["pulse", l.pulse],
|
|
527
|
+
["spin", l.spin],
|
|
528
|
+
["grow", l.grow],
|
|
529
|
+
["layout", l.layout],
|
|
530
|
+
["emit", l.emit],
|
|
531
|
+
["clock", l.clock],
|
|
532
|
+
["sprite.kind", l.sprite.kind],
|
|
527
533
|
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
528
534
|
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
529
|
-
s.radius,
|
|
530
|
-
s.length,
|
|
531
|
-
l.sprite.kind === "rect" ? [s.width, s.aspect] : l.sprite.kind === "bar" ? [s.length, s.thickness] : void 0,
|
|
532
|
-
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
533
|
-
s.colorWeights,
|
|
534
|
-
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? l.sprite.anchor ? [l.sprite.fontSize, l.sprite.anchor] : [l.sprite.fontSize] : void 0,
|
|
535
|
+
["sprite.radius", s.radius],
|
|
536
|
+
["sprite.length", s.length],
|
|
537
|
+
l.sprite.kind === "rect" ? [["sprite.width", "sprite.aspect"], [s.width, s.aspect]] : l.sprite.kind === "bar" ? [["sprite.length", "sprite.thickness"], [s.length, s.thickness]] : ["sprite.dimensions", void 0],
|
|
538
|
+
["sprite.colors.length", Array.isArray(s.colors) ? s.colors.length : void 0],
|
|
539
|
+
["sprite.colorWeights", s.colorWeights],
|
|
540
|
+
l.sprite.kind === "emoji" ? [["sprite.glyphs.length", "sprite.cycle.period"], [l.sprite.glyphs.length, l.sprite.cycle?.period]] : l.sprite.kind === "text" ? [["sprite.strings.length", "sprite.cycle.period"], [l.sprite.strings.length, l.sprite.cycle?.period]] : l.sprite.kind === "textBlock" ? [["sprite.fontSize", "sprite.anchor"], l.sprite.anchor ? [l.sprite.fontSize, l.sprite.anchor] : [l.sprite.fontSize]] : ["sprite.variants", void 0],
|
|
535
541
|
// Static rotation is baked into entities (a range draws a seeded
|
|
536
542
|
// angle). Appended only when set — same rule as `anchor` above.
|
|
537
|
-
...l.rotate !== void 0 ? [l.rotate] : [],
|
|
543
|
+
...l.rotate !== void 0 ? [["rotate", l.rotate]] : [],
|
|
538
544
|
// `rock` is baked per entity like `pulse` (and draws a seeded phase).
|
|
539
|
-
...l.rock !== void 0 ? [{ rock: l.rock }] : []
|
|
545
|
+
...l.rock !== void 0 ? [["rock", { rock: l.rock }]] : []
|
|
540
546
|
];
|
|
541
547
|
})
|
|
542
|
-
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function showValue(v) {
|
|
551
|
+
if (v === void 0) return "(unset)";
|
|
552
|
+
if (typeof v === "string") return v;
|
|
553
|
+
const j = JSON.stringify(v);
|
|
554
|
+
return j.length > 40 ? `${j.slice(0, 37)}\u2026` : j;
|
|
555
|
+
}
|
|
556
|
+
function leafDifferences(path, a, b, out) {
|
|
557
|
+
if (JSON.stringify(a) === JSON.stringify(b)) return;
|
|
558
|
+
const isRec = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
559
|
+
if (isRec(a) && isRec(b)) {
|
|
560
|
+
const before = out.length;
|
|
561
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) leafDifferences(`${path}.${k}`, a[k], b[k], out);
|
|
562
|
+
if (out.length === before) out.push(`${path}: the same fields in a different key order`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
out.push(`${path}: ${showValue(a)} \u2192 ${showValue(b)}`);
|
|
566
|
+
}
|
|
567
|
+
function namedFields(fields) {
|
|
568
|
+
const m = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const [name, value] of fields) {
|
|
570
|
+
if (typeof name === "string") {
|
|
571
|
+
m.set(name, value);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const vs = Array.isArray(value) ? value : [];
|
|
575
|
+
name.forEach((n, i) => m.set(n, vs[i]));
|
|
576
|
+
}
|
|
577
|
+
return m;
|
|
578
|
+
}
|
|
579
|
+
function structuralDifferences(a, b) {
|
|
580
|
+
const fa = structuralFields(a);
|
|
581
|
+
const fb = structuralFields(b);
|
|
582
|
+
const out = [];
|
|
583
|
+
const ra = namedFields(fa.root);
|
|
584
|
+
const rb = namedFields(fb.root);
|
|
585
|
+
for (const k of /* @__PURE__ */ new Set([...ra.keys(), ...rb.keys()])) leafDifferences(k, ra.get(k), rb.get(k), out);
|
|
586
|
+
if (fa.layers.length !== fb.layers.length) out.push(`layers.length: ${fa.layers.length} \u2192 ${fb.layers.length}`);
|
|
587
|
+
for (let i = 0; i < Math.min(fa.layers.length, fb.layers.length); i++) {
|
|
588
|
+
const la = namedFields(fa.layers[i]);
|
|
589
|
+
const lb = namedFields(fb.layers[i]);
|
|
590
|
+
const kindChanged = la.get("sprite.kind") !== lb.get("sprite.kind");
|
|
591
|
+
for (const k of /* @__PURE__ */ new Set([...la.keys(), ...lb.keys()])) {
|
|
592
|
+
if (kindChanged && k.startsWith("sprite.") && k !== "sprite.kind") continue;
|
|
593
|
+
leafDifferences(`layers[${i}].${k}`, la.get(k), lb.get(k), out);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
function describeStructuralDifferences(a, b, max = 3) {
|
|
599
|
+
const d = structuralDifferences(a, b);
|
|
600
|
+
if (d.length === 0) return "";
|
|
601
|
+
return d.length <= max ? d.join("; ") : `${d.slice(0, max).join("; ")}; +${d.length - max} more`;
|
|
543
602
|
}
|
|
544
603
|
|
|
545
604
|
// src/sequence.ts
|
|
@@ -1602,6 +1661,11 @@ function pathDrawCost(s) {
|
|
|
1602
1661
|
return c;
|
|
1603
1662
|
}
|
|
1604
1663
|
|
|
1664
|
+
// src/validate-hints.ts
|
|
1665
|
+
var SCALAR_ONLY = /not a \[min,max\] range/;
|
|
1666
|
+
var SCALAR_BOUND = /^must be (?:>|an? (?:positive |single |finite |whole )?(?:number|integer)\b|between -?\d|-?\d+(?:\.\d+)?\.\.-?\d+(?:\.\d+)?(?: \(|$))/;
|
|
1667
|
+
var scalarWanted = (m) => SCALAR_ONLY.test(m) || !m.includes("[") && SCALAR_BOUND.test(m);
|
|
1668
|
+
|
|
1605
1669
|
// src/validate.ts
|
|
1606
1670
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
1607
1671
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -1734,6 +1798,15 @@ var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
|
1734
1798
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
1735
1799
|
var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
|
|
1736
1800
|
var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1801
|
+
var KNOWN_BG_STOP = /* @__PURE__ */ new Set(["at", "color"]);
|
|
1802
|
+
var KNOWN_BAND = /* @__PURE__ */ new Set(["color", "height"]);
|
|
1803
|
+
var KNOWN_BG_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1804
|
+
var KNOWN_PATH_STROKE = /* @__PURE__ */ new Set(["color", "width", "opacity", "join", "cap", "draw"]);
|
|
1805
|
+
var KNOWN_PATH_GRADIENT = {
|
|
1806
|
+
linear: /* @__PURE__ */ new Set(["type", "from", "to", "stops", "units"]),
|
|
1807
|
+
radial: /* @__PURE__ */ new Set(["type", "center", "radius", "focus", "stops", "units"])
|
|
1808
|
+
};
|
|
1809
|
+
var KNOWN_PATH_STOP = /* @__PURE__ */ new Set(["at", "color", "opacity"]);
|
|
1737
1810
|
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"]);
|
|
1738
1811
|
function unknownKeys(obj, known) {
|
|
1739
1812
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
@@ -1751,6 +1824,50 @@ function positiveScalar(v, path, err, msg = "must be > 0") {
|
|
|
1751
1824
|
if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
|
|
1752
1825
|
else if (!isNum(v) || v <= 0) err(path, msg);
|
|
1753
1826
|
}
|
|
1827
|
+
function valueAtPath(root, path) {
|
|
1828
|
+
let v = root;
|
|
1829
|
+
for (const seg of path.match(/[^.[\]]+/g) ?? []) {
|
|
1830
|
+
if (Array.isArray(v) && /^\d+$/.test(seg)) v = v[Number(seg)];
|
|
1831
|
+
else if (isObj(v)) v = v[seg];
|
|
1832
|
+
else return void 0;
|
|
1833
|
+
}
|
|
1834
|
+
return v;
|
|
1835
|
+
}
|
|
1836
|
+
function echo(v) {
|
|
1837
|
+
if (isNum(v)) return String(v);
|
|
1838
|
+
if (Array.isArray(v) && v.length <= 4 && v.every(isNum)) return `[${v.join(", ")}]`;
|
|
1839
|
+
const s = JSON.stringify(v) ?? String(v);
|
|
1840
|
+
return s.length > 48 ? `${s.slice(0, 45)}\u2026` : s;
|
|
1841
|
+
}
|
|
1842
|
+
var RANGE_WANTED = /^must be a \[min,max\] range/;
|
|
1843
|
+
function shapeHint(spec, path, message) {
|
|
1844
|
+
if (message.includes(" \u2014 got ")) return "";
|
|
1845
|
+
const v = valueAtPath(spec, path);
|
|
1846
|
+
if (v === void 0) return "";
|
|
1847
|
+
if (RANGE_WANTED.test(message)) {
|
|
1848
|
+
if (isNum(v)) {
|
|
1849
|
+
const fits = message.includes("within 0..1") ? v >= 0 && v <= 1 : message.includes(">= 0") ? v >= 0 : !message.includes("positive") || v > 0;
|
|
1850
|
+
return fits ? ` \u2014 got ${v}; write [${v}, ${v}]` : ` \u2014 got ${v}; a range is two numbers, [min, max]`;
|
|
1851
|
+
}
|
|
1852
|
+
if (isObj(v) && ("min" in v || "max" in v)) {
|
|
1853
|
+
return ` \u2014 got {min, max}; write [${isNum(v.min) ? v.min : "min"}, ${isNum(v.max) ? v.max : "max"}]`;
|
|
1854
|
+
}
|
|
1855
|
+
if (Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] > v[1]) {
|
|
1856
|
+
return ` \u2014 got ${echo(v)}; the smaller number comes first: write [${v[1]}, ${v[0]}]`;
|
|
1857
|
+
}
|
|
1858
|
+
return ` \u2014 got ${echo(v)}`;
|
|
1859
|
+
}
|
|
1860
|
+
if (Array.isArray(v) && scalarWanted(message)) {
|
|
1861
|
+
const segs = path.match(/[^.[\]]+/g) ?? [];
|
|
1862
|
+
const key = segs[segs.length - 1] ?? path;
|
|
1863
|
+
const parentKey = segs[segs.length - 2];
|
|
1864
|
+
const parent = valueAtPath(spec, path.slice(0, path.lastIndexOf(".")));
|
|
1865
|
+
const kind = isObj(parent) && isStr(parent.kind) ? parent.kind : void 0;
|
|
1866
|
+
const noun = kind ? `${/^[aeiou]/i.test(kind) ? "an" : "a"} ${kind}'s \`${key}\`` : parentKey && !/^\d+$/.test(parentKey) && parentKey !== "sprite" ? `\`${parentKey}.${key}\`` : `\`${key}\``;
|
|
1867
|
+
return ` \u2014 got ${echo(v)}; ${noun} is one number`;
|
|
1868
|
+
}
|
|
1869
|
+
return "";
|
|
1870
|
+
}
|
|
1754
1871
|
function normalizeColors(spec) {
|
|
1755
1872
|
if (!isObj(spec) || !Array.isArray(spec.layers)) return;
|
|
1756
1873
|
for (const layer of spec.layers) {
|
|
@@ -1763,9 +1880,37 @@ function normalizeColors(spec) {
|
|
|
1763
1880
|
}
|
|
1764
1881
|
}
|
|
1765
1882
|
}
|
|
1883
|
+
var LAYER_SIZED_SPRITES = /* @__PURE__ */ new Set(["emoji", "text"]);
|
|
1884
|
+
var SPRITE_SIZE_FIELD = {
|
|
1885
|
+
circle: "radius",
|
|
1886
|
+
ring: "radius",
|
|
1887
|
+
polygon: "radius",
|
|
1888
|
+
streak: "length",
|
|
1889
|
+
stroke: "length",
|
|
1890
|
+
rect: "width",
|
|
1891
|
+
bar: "length and thickness",
|
|
1892
|
+
textBlock: "fontSize"
|
|
1893
|
+
};
|
|
1894
|
+
var SPRITE_SIZE_MISPLACED = "'size' belongs on the layer, not inside sprite \u2014 move it up one level";
|
|
1895
|
+
function aliasSpriteSize(spec) {
|
|
1896
|
+
const out = [];
|
|
1897
|
+
if (!isObj(spec) || !Array.isArray(spec.layers)) return out;
|
|
1898
|
+
spec.layers.forEach((layer, i) => {
|
|
1899
|
+
if (!isObj(layer) || !isObj(layer.sprite)) return;
|
|
1900
|
+
const sp = layer.sprite;
|
|
1901
|
+
if (sp.size === void 0 || !LAYER_SIZED_SPRITES.has(sp.kind) || layer.size !== void 0) return;
|
|
1902
|
+
layer.size = sp.size;
|
|
1903
|
+
delete sp.size;
|
|
1904
|
+
out.push({ path: `layers[${i}].sprite.size`, code: "misplaced-property", message: `${SPRITE_SIZE_MISPLACED} (applied as layers[${i}].size this time)` });
|
|
1905
|
+
});
|
|
1906
|
+
return out;
|
|
1907
|
+
}
|
|
1766
1908
|
function validateSpec(spec) {
|
|
1767
1909
|
normalizeColors(spec);
|
|
1768
|
-
|
|
1910
|
+
const repaired = aliasSpriteSize(spec);
|
|
1911
|
+
const result = validateSpecCore(spec);
|
|
1912
|
+
if (repaired.length > 0) result.warnings = [...repaired, ...result.warnings ?? []];
|
|
1913
|
+
return result;
|
|
1769
1914
|
}
|
|
1770
1915
|
function ignoredPropertyPaths(spec) {
|
|
1771
1916
|
if (!isObj(spec)) return [];
|
|
@@ -1819,7 +1964,7 @@ function specPathsValid(spec, paths) {
|
|
|
1819
1964
|
function validateSpecCore(spec, checkApplied = true) {
|
|
1820
1965
|
const errors = [];
|
|
1821
1966
|
const warnings = [];
|
|
1822
|
-
const err = (path, message) => void errors.push({ path, message });
|
|
1967
|
+
const err = (path, message) => void errors.push({ path, message: message + shapeHint(spec, path, message) });
|
|
1823
1968
|
const warn = (path, code, message) => void warnings.push({ path, code, message });
|
|
1824
1969
|
if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }], warnings: [] };
|
|
1825
1970
|
if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
@@ -2048,9 +2193,12 @@ function validateTimelineKeys(spec, errOuter, warn, checkApplied) {
|
|
|
2048
2193
|
return;
|
|
2049
2194
|
}
|
|
2050
2195
|
}
|
|
2051
|
-
function color(v, path, err) {
|
|
2052
|
-
if (
|
|
2196
|
+
function color(v, path, err, missing = "is required (a hex colour like #0a3a52)") {
|
|
2197
|
+
if (v === void 0) err(path, missing);
|
|
2198
|
+
else if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
2053
2199
|
}
|
|
2200
|
+
var SPRITE_COLOR_MISSING = "`color` is required (or a `colors` palette)";
|
|
2201
|
+
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`;
|
|
2054
2202
|
function validateFinish(f, path, err, warn) {
|
|
2055
2203
|
if (!isObj(f)) return err(path, "must be an object");
|
|
2056
2204
|
for (const k of ["grain", "dither"]) {
|
|
@@ -2075,6 +2223,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2075
2223
|
bg.stops.forEach((s, i) => {
|
|
2076
2224
|
if (!isObj(s) || !isNum(s.at) || s.at < 0 || s.at > 1) err(`background.stops[${i}].at`, "must be 0..1");
|
|
2077
2225
|
color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
|
|
2226
|
+
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`);
|
|
2078
2227
|
});
|
|
2079
2228
|
}
|
|
2080
2229
|
if (bg.drift !== void 0) {
|
|
@@ -2086,6 +2235,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2086
2235
|
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount <= 0 || bg.drift.amount > LIMITS.maxDriftAmount)) {
|
|
2087
2236
|
err("background.drift.amount", `must be > 0 and <= ${LIMITS.maxDriftAmount}`);
|
|
2088
2237
|
}
|
|
2238
|
+
for (const k of unknownKeys(bg.drift, KNOWN_BG_DRIFT)) warn(`background.drift.${k}`, "unknown-property", `unknown drift property '${k}' \u2014 will be ignored`);
|
|
2089
2239
|
}
|
|
2090
2240
|
}
|
|
2091
2241
|
if (bg.band !== void 0) {
|
|
@@ -2093,6 +2243,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2093
2243
|
else {
|
|
2094
2244
|
color(bg.band.color, "background.band.color", err);
|
|
2095
2245
|
positiveScalar(bg.band.height, "background.band.height", err);
|
|
2246
|
+
for (const k of unknownKeys(bg.band, KNOWN_BAND)) warn(`background.band.${k}`, "unknown-property", `unknown band property '${k}' \u2014 will be ignored`);
|
|
2096
2247
|
}
|
|
2097
2248
|
}
|
|
2098
2249
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
@@ -2160,6 +2311,15 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
2160
2311
|
if (layer.size !== void 0 && (!isRange(layer.size) || layer.size[0] <= 0)) {
|
|
2161
2312
|
err(`${path}.size`, rangeMsg(spec));
|
|
2162
2313
|
}
|
|
2314
|
+
if (isObj(layer.sprite) && layer.sprite.size !== void 0) {
|
|
2315
|
+
const kind = layer.sprite.kind;
|
|
2316
|
+
if (LAYER_SIZED_SPRITES.has(kind)) {
|
|
2317
|
+
warn(`${path}.sprite.size`, "misplaced-property", layer.size !== void 0 ? `${SPRITE_SIZE_MISPLACED} \u2014 layer.size is also set, so the layer's value wins and sprite.size is ignored` : SPRITE_SIZE_MISPLACED);
|
|
2318
|
+
} else {
|
|
2319
|
+
const field = SPRITE_SIZE_FIELD[kind];
|
|
2320
|
+
warn(`${path}.sprite.size`, "unknown-property", field ? `unknown sprite property 'size' \u2014 will be ignored; a ${kind} sprite is sized by its ${field}` : `unknown sprite property 'size' \u2014 will be ignored`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2163
2323
|
if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
|
|
2164
2324
|
if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
|
|
2165
2325
|
if (layer.alpha !== void 0 && (!isRange(layer.alpha) || layer.alpha[0] < 0 || layer.alpha[1] > 1)) {
|
|
@@ -2331,7 +2491,8 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
2331
2491
|
const sp = layer.sprite;
|
|
2332
2492
|
if (isObj(sp) && isNum(layer.count)) {
|
|
2333
2493
|
const variants = sp.kind === "text" && Array.isArray(sp.strings) ? sp.strings.length : sp.kind === "emoji" && Array.isArray(sp.glyphs) ? sp.glyphs.length : sp.kind === "bar" && Array.isArray(sp.values) ? sp.values.length : null;
|
|
2334
|
-
|
|
2494
|
+
const atmosphere = sp.kind === "text" && sp.role === "atmosphere";
|
|
2495
|
+
if (variants !== null && variants !== layer.count && !atmosphere) {
|
|
2335
2496
|
warn(`${path}.count`, "list-length-mismatch", `count is ${layer.count} but the sprite carries ${variants} ${sp.kind === "bar" ? "values" : "variants"} \u2014 a ${lay.type} layout reads them in order, so they cycle or go unused`);
|
|
2336
2497
|
}
|
|
2337
2498
|
}
|
|
@@ -2500,19 +2661,19 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2500
2661
|
} else if (sprite.kind === "circle") {
|
|
2501
2662
|
knownSet = KNOWN_CIRCLE;
|
|
2502
2663
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2503
|
-
color(sprite.color, `${path}.color`, err);
|
|
2664
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2504
2665
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2505
2666
|
validatePalette(sprite, path, err);
|
|
2506
2667
|
} else if (sprite.kind === "ring") {
|
|
2507
2668
|
knownSet = KNOWN_RING;
|
|
2508
2669
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2509
|
-
color(sprite.color, `${path}.color`, err);
|
|
2670
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2510
2671
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2511
2672
|
validatePalette(sprite, path, err);
|
|
2512
2673
|
} else if (sprite.kind === "streak") {
|
|
2513
2674
|
knownSet = KNOWN_STREAK;
|
|
2514
2675
|
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
|
|
2515
|
-
color(sprite.color, `${path}.color`, err);
|
|
2676
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2516
2677
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2517
2678
|
validatePalette(sprite, path, err);
|
|
2518
2679
|
} else if (sprite.kind === "rect") {
|
|
@@ -2524,7 +2685,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2524
2685
|
if (sprite.feather !== void 0 && (!isNum(sprite.feather) || sprite.feather < 0 || sprite.feather > 1)) {
|
|
2525
2686
|
err(`${path}.feather`, "must be 0..1 (fraction of the half-size that fades out)");
|
|
2526
2687
|
}
|
|
2527
|
-
color(sprite.color, `${path}.color`, err);
|
|
2688
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2528
2689
|
validatePalette(sprite, path, err);
|
|
2529
2690
|
} else if (sprite.kind === "bar") {
|
|
2530
2691
|
knownSet = KNOWN_BAR;
|
|
@@ -2537,7 +2698,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2537
2698
|
if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
|
|
2538
2699
|
err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
|
|
2539
2700
|
}
|
|
2540
|
-
color(sprite.color, `${path}.color`, err);
|
|
2701
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2541
2702
|
validatePalette(sprite, path, err);
|
|
2542
2703
|
} else if (sprite.kind === "polygon") {
|
|
2543
2704
|
knownSet = KNOWN_POLYGON;
|
|
@@ -2581,7 +2742,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2581
2742
|
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");
|
|
2582
2743
|
}
|
|
2583
2744
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2584
|
-
color(sprite.color, `${path}.color`, err);
|
|
2745
|
+
color(sprite.color, `${path}.color`, err, POLYGON_COLOR_MISSING);
|
|
2585
2746
|
validatePalette(sprite, path, err);
|
|
2586
2747
|
} else if (sprite.kind === "stroke") {
|
|
2587
2748
|
knownSet = KNOWN_STROKE;
|
|
@@ -2592,7 +2753,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2592
2753
|
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
2593
2754
|
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
2594
2755
|
if (sprite.draw !== void 0) validateDraw(sprite.draw, `${path}.draw`, err, warn);
|
|
2595
|
-
color(sprite.color, `${path}.color`, err);
|
|
2756
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2596
2757
|
validatePalette(sprite, path, err);
|
|
2597
2758
|
} else if (sprite.kind === "textBlock") {
|
|
2598
2759
|
knownSet = KNOWN_TEXT_BLOCK;
|
|
@@ -2696,6 +2857,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2696
2857
|
return;
|
|
2697
2858
|
}
|
|
2698
2859
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
2860
|
+
if (k === "size") continue;
|
|
2699
2861
|
if (LAYER_PROPS_ON_SPRITE.has(k)) {
|
|
2700
2862
|
warn(`${path}.${k}`, "misplaced-property", `'${k}' belongs on the layer, not inside sprite \u2014 move it up one level`);
|
|
2701
2863
|
} else {
|
|
@@ -2903,10 +3065,10 @@ function validatePathPiece(piece, path, err, warn) {
|
|
|
2903
3065
|
function validatePiecePaint(piece, path, err, warn) {
|
|
2904
3066
|
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2905
3067
|
if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
|
|
2906
|
-
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
|
|
3068
|
+
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err, warn);
|
|
2907
3069
|
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
|
|
2908
3070
|
}
|
|
2909
|
-
function validatePathGradient(g, path, err) {
|
|
3071
|
+
function validatePathGradient(g, path, err, warn) {
|
|
2910
3072
|
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
2911
3073
|
if (g.type === "linear") {
|
|
2912
3074
|
if (!isPair(g.from)) err(`${path}.from`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
@@ -2918,6 +3080,7 @@ function validatePathGradient(g, path, err) {
|
|
|
2918
3080
|
} else {
|
|
2919
3081
|
return err(`${path}.type`, "must be 'linear' | 'radial'");
|
|
2920
3082
|
}
|
|
3083
|
+
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`);
|
|
2921
3084
|
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)");
|
|
2922
3085
|
if (!Array.isArray(g.stops) || g.stops.length < 2 || g.stops.length > 8) return err(`${path}.stops`, "must be 2..8 stops");
|
|
2923
3086
|
let last = -Infinity;
|
|
@@ -2927,16 +3090,18 @@ function validatePathGradient(g, path, err) {
|
|
|
2927
3090
|
else last = st.at;
|
|
2928
3091
|
color(st.color, `${path}.stops[${i}].color`, err);
|
|
2929
3092
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
3093
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STOP)) warn(`${path}.stops[${i}].${k}`, "unknown-property", `unknown stop property '${k}' \u2014 will be ignored`);
|
|
2930
3094
|
});
|
|
2931
3095
|
}
|
|
2932
3096
|
function validatePathStroke(st, path, err, warn) {
|
|
2933
3097
|
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap?, draw? }");
|
|
2934
|
-
color(st.color, `${path}.color`, err);
|
|
3098
|
+
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");
|
|
2935
3099
|
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)");
|
|
2936
3100
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2937
3101
|
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2938
3102
|
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2939
3103
|
if (st.draw !== void 0) validateDraw(st.draw, `${path}.draw`, err, warn);
|
|
3104
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STROKE)) warn(`${path}.${k}`, "unknown-property", `unknown stroke property '${k}' \u2014 will be ignored`);
|
|
2940
3105
|
}
|
|
2941
3106
|
function validateWipe(tr, path, err, warn) {
|
|
2942
3107
|
if (tr.shape !== void 0 && tr.shape !== "circle" && tr.shape !== "line") err(`${path}.shape`, "must be 'circle' | 'line'");
|
|
@@ -3124,7 +3289,7 @@ function validateSequence(seq) {
|
|
|
3124
3289
|
warnings.push({
|
|
3125
3290
|
path: `segments[${i}].transition`,
|
|
3126
3291
|
code: "morph-structural-mismatch",
|
|
3127
|
-
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
3292
|
+
message: `segments ${i}\u2192${i + 1} differ structurally (${describeStructuralDifferences(s.scene, next.scene)}): morph will fall back to cut`
|
|
3128
3293
|
});
|
|
3129
3294
|
} 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" })) {
|
|
3130
3295
|
warnings.push({
|
|
@@ -3143,7 +3308,7 @@ function validateSequence(seq) {
|
|
|
3143
3308
|
if (sq.loop !== true) why = "the sequence does not loop";
|
|
3144
3309
|
else if (last < 1) why = "there is only one segment";
|
|
3145
3310
|
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
3146
|
-
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
3311
|
+
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally (${describeStructuralDifferences(sq.segments[last].scene, sq.segments[0].scene)})`;
|
|
3147
3312
|
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`;
|
|
3148
3313
|
if (why || !canWrapMorph(sq)) {
|
|
3149
3314
|
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
@@ -3566,6 +3731,7 @@ function lifeStaggerAt(life, t, i) {
|
|
|
3566
3731
|
}
|
|
3567
3732
|
function wrap(v, min, max) {
|
|
3568
3733
|
const range2 = max - min;
|
|
3734
|
+
if (range2 <= 0) return min;
|
|
3569
3735
|
return ((v - min) % range2 + range2) % range2 + min;
|
|
3570
3736
|
}
|
|
3571
3737
|
function reflect(v, min, max) {
|
|
@@ -4824,17 +4990,17 @@ var SpecInstance = class {
|
|
|
4824
4990
|
const rows = Math.max(1, Math.round(this.fieldShortSide * h / short));
|
|
4825
4991
|
const ft = fieldSampleTime(bg, t);
|
|
4826
4992
|
const key = `${JSON.stringify(bg)}|${w}x${h}|${cols}x${rows}|${ft}`;
|
|
4827
|
-
let
|
|
4828
|
-
if (!
|
|
4829
|
-
if (!
|
|
4993
|
+
let raster2 = this.field;
|
|
4994
|
+
if (!raster2 || raster2.key !== key) {
|
|
4995
|
+
if (!raster2 || raster2.cols !== cols || raster2.rows !== rows) {
|
|
4830
4996
|
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(cols, rows);
|
|
4831
4997
|
canvas.width = cols;
|
|
4832
4998
|
canvas.height = rows;
|
|
4833
4999
|
const rctx = canvas.getContext("2d");
|
|
4834
5000
|
if (!rctx) return;
|
|
4835
|
-
|
|
5001
|
+
raster2 = { canvas, ctx: rctx, cols, rows, key: "" };
|
|
4836
5002
|
}
|
|
4837
|
-
const img =
|
|
5003
|
+
const img = raster2.ctx.createImageData(cols, rows);
|
|
4838
5004
|
const data = img.data;
|
|
4839
5005
|
const seed = this.seed;
|
|
4840
5006
|
let i = 0;
|
|
@@ -4849,14 +5015,14 @@ var SpecInstance = class {
|
|
|
4849
5015
|
data[i++] = 255;
|
|
4850
5016
|
}
|
|
4851
5017
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
this.field =
|
|
5018
|
+
raster2.ctx.putImageData(img, 0, 0);
|
|
5019
|
+
raster2.key = key;
|
|
5020
|
+
this.field = raster2;
|
|
4855
5021
|
}
|
|
4856
5022
|
const smooth = Math.round(bg.quantize ?? bg.bands.length) < 2;
|
|
4857
5023
|
const prev = ctx.imageSmoothingEnabled;
|
|
4858
5024
|
ctx.imageSmoothingEnabled = smooth;
|
|
4859
|
-
ctx.drawImage(
|
|
5025
|
+
ctx.drawImage(raster2.canvas, 0, 0, w, h);
|
|
4860
5026
|
ctx.imageSmoothingEnabled = prev;
|
|
4861
5027
|
}
|
|
4862
5028
|
/** Position with parent-orbit resolution: a layer-parented orbit entity's
|
|
@@ -5430,14 +5596,14 @@ var SpecInstance = class {
|
|
|
5430
5596
|
* entity as it was at `t` (rotation, heading, values), but at the echo's
|
|
5431
5597
|
* position, size and alpha.
|
|
5432
5598
|
*/
|
|
5433
|
-
drawEntity(built, e, t, lifeA, parentE, paint,
|
|
5599
|
+
drawEntity(built, e, t, lifeA, parentE, paint, echo2) {
|
|
5434
5600
|
const { ctx } = this;
|
|
5435
|
-
const p =
|
|
5601
|
+
const p = echo2 ? { x: echo2.x, y: echo2.y, flip: false } : this.entityPos(e, t, parentE);
|
|
5436
5602
|
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
5437
|
-
const sz =
|
|
5603
|
+
const sz = echo2 ? echo2.size : sizeAt(e, t);
|
|
5438
5604
|
const rot = rotationAt(e, t);
|
|
5439
5605
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
5440
|
-
ctx.globalAlpha =
|
|
5606
|
+
ctx.globalAlpha = echo2 ? echo2.alpha : alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
5441
5607
|
if (sprite.kind === "ring") {
|
|
5442
5608
|
const r = sz / 2;
|
|
5443
5609
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -7551,6 +7717,7 @@ function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1, evenodd = false)
|
|
|
7551
7717
|
}
|
|
7552
7718
|
return cov;
|
|
7553
7719
|
}
|
|
7720
|
+
var MAX_GRID_SIDE = 400;
|
|
7554
7721
|
function softProfile(u) {
|
|
7555
7722
|
if (u <= 0) return 1;
|
|
7556
7723
|
if (u >= 1) return 0;
|
|
@@ -7586,12 +7753,13 @@ function discCellInk(cx, cy, rOuter, rInner, soft, x0, y0, cw, ch) {
|
|
|
7586
7753
|
}
|
|
7587
7754
|
return ink / (ni * nj) * sx * sy / (cw * ch);
|
|
7588
7755
|
}
|
|
7589
|
-
function luminanceGrid(spec, opts = {}) {
|
|
7756
|
+
function luminanceGrid(spec, opts = {}, maxSide = 200) {
|
|
7590
7757
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
7591
7758
|
const scene = buildScene(spec, opts);
|
|
7592
7759
|
const t = opts.t ?? 5e3;
|
|
7593
|
-
const
|
|
7594
|
-
const
|
|
7760
|
+
const cap = Math.max(8, Math.min(MAX_GRID_SIDE, maxSide));
|
|
7761
|
+
const cols = Math.max(8, Math.min(cap, opts.cols ?? 80));
|
|
7762
|
+
const rows = Math.max(8, Math.min(cap, opts.rows ?? 48));
|
|
7595
7763
|
const { w, h, scale } = scene;
|
|
7596
7764
|
const cellW = w / cols;
|
|
7597
7765
|
const cellH = h / rows;
|
|
@@ -7605,10 +7773,22 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7605
7773
|
const short = Math.max(1, Math.min(w, h));
|
|
7606
7774
|
const seed = scene.seed;
|
|
7607
7775
|
const ft = fieldSampleTime(bg, t);
|
|
7776
|
+
const fCols = Math.max(1, Math.round(FIELD_RASTER_SHORT_SIDE * w / short));
|
|
7777
|
+
const fRows = Math.max(1, Math.round(FIELD_RASTER_SHORT_SIDE * h / short));
|
|
7778
|
+
const viaRaster = cols > fCols || rows > fRows;
|
|
7779
|
+
const rasterLum = viaRaster ? new Float64Array(fCols * fRows).fill(-1) : null;
|
|
7780
|
+
const fieldLum = (c, r) => {
|
|
7781
|
+
if (!rasterLum) return rgb255Luma(fieldRgbAt((c + 0.5) * cellW / short, (r + 0.5) * cellH / short, ft, bg, seed));
|
|
7782
|
+
const fc = Math.min(fCols - 1, Math.floor((c + 0.5) / cols * fCols));
|
|
7783
|
+
const fr = Math.min(fRows - 1, Math.floor((r + 0.5) / rows * fRows));
|
|
7784
|
+
const k = fr * fCols + fc;
|
|
7785
|
+
if (rasterLum[k] < 0) rasterLum[k] = rgb255Luma(fieldRgbAt((fc + 0.5) * w / fCols / short, (fr + 0.5) * h / fRows / short, ft, bg, seed));
|
|
7786
|
+
return rasterLum[k];
|
|
7787
|
+
};
|
|
7608
7788
|
for (let r = 0; r < rows; r++) {
|
|
7609
7789
|
let rowSum = 0;
|
|
7610
7790
|
for (let c = 0; c < cols; c++) {
|
|
7611
|
-
const lum =
|
|
7791
|
+
const lum = fieldLum(c, r);
|
|
7612
7792
|
bgCells[r * cols + c] = lum;
|
|
7613
7793
|
rowSum += lum;
|
|
7614
7794
|
}
|
|
@@ -8841,6 +9021,7 @@ function flashSamplingForecast(spec) {
|
|
|
8841
9021
|
|
|
8842
9022
|
// src/period.ts
|
|
8843
9023
|
var MAX_SCENE_PERIOD = 36e5;
|
|
9024
|
+
var SEAMLESS_PERIOD_KINDS = /* @__PURE__ */ new Set(["spin", "rock", "pulse", "grow"]);
|
|
8844
9025
|
var PERIOD_LEAF = /(^|\.)(period|rate|every|duration|spin|speed|blink|drift)$/;
|
|
8845
9026
|
var gcd = (a, b) => {
|
|
8846
9027
|
while (b) [a, b] = [b, a % b];
|
|
@@ -8858,27 +9039,27 @@ function toRatio(ms) {
|
|
|
8858
9039
|
return null;
|
|
8859
9040
|
}
|
|
8860
9041
|
var range = (r) => r === void 0 ? null : typeof r === "number" ? r : r[0] === r[1] ? r[0] : NaN;
|
|
8861
|
-
function scenePeriod(spec) {
|
|
9042
|
+
function scenePeriod(spec, skip) {
|
|
8862
9043
|
const parts = [];
|
|
8863
9044
|
const reasons = [];
|
|
8864
|
-
const add = (label, ms) => {
|
|
8865
|
-
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
9045
|
+
const add = (label, ms, kind) => {
|
|
9046
|
+
if (!(ms > 0) || !Number.isFinite(ms) || skip?.has(kind)) return;
|
|
8866
9047
|
const ratio = toRatio(ms);
|
|
8867
9048
|
if (!ratio) reasons.push(`${label} repeats every ${Number(ms.toFixed(3))} ms, which has no exact ratio to combine`);
|
|
8868
|
-
else parts.push([label, ratio[0], ratio[1]]);
|
|
9049
|
+
else parts.push([label, ratio[0], ratio[1], kind]);
|
|
8869
9050
|
};
|
|
8870
9051
|
const tl = spec.timeline;
|
|
8871
9052
|
if (tl && tl.keys.length > 0) {
|
|
8872
9053
|
const keyed = tl.keys.filter((k) => PERIOD_LEAF.test(String(k.path)));
|
|
8873
9054
|
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`);
|
|
8874
|
-
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration);
|
|
9055
|
+
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration, "timeline");
|
|
8875
9056
|
else reasons.push("timeline does not loop (it plays once, then holds)");
|
|
8876
9057
|
}
|
|
8877
9058
|
const bg = spec.background;
|
|
8878
9059
|
if (bg && bg.type !== "solid" && bg.drift && !(bg.type === "field" && bg.drift.amount === 0)) {
|
|
8879
9060
|
const { period } = bg.drift;
|
|
8880
9061
|
const r = bg.type === "field" ? toRatio(period) : null;
|
|
8881
|
-
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period);
|
|
9062
|
+
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period, "background");
|
|
8882
9063
|
}
|
|
8883
9064
|
if (spec.finish?.animate && ((spec.finish.grain ?? 0) > 0 || (spec.finish.dither ?? 0) > 0)) reasons.push("finish.animate steps the grain without repeating");
|
|
8884
9065
|
spec.layers.forEach((layer, i) => {
|
|
@@ -8887,53 +9068,58 @@ function scenePeriod(spec) {
|
|
|
8887
9068
|
const life = layer.life;
|
|
8888
9069
|
const staggered = (life?.stagger ?? 0) > 0;
|
|
8889
9070
|
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`);
|
|
8890
|
-
if (layer.pulse && layer.pulse.amp !== 0) add(`${at} pulse`, layer.pulse.period / rate);
|
|
8891
|
-
if (layer.grow) add(`${at} grow`, layer.grow.period / rate);
|
|
8892
|
-
if (layer.rock) add(`${at} rock`, layer.rock.period / rate);
|
|
8893
|
-
if (layer.emit) add(`${at} emit`, layer.emit.every);
|
|
9071
|
+
if (layer.pulse && layer.pulse.amp !== 0) add(`${at} pulse`, layer.pulse.period / rate, "pulse");
|
|
9072
|
+
if (layer.grow) add(`${at} grow`, layer.grow.period / rate, "grow");
|
|
9073
|
+
if (layer.rock) add(`${at} rock`, layer.rock.period / rate, "rock");
|
|
9074
|
+
if (layer.emit) add(`${at} emit`, layer.emit.every, "emit");
|
|
8894
9075
|
const spin = range(layer.spin);
|
|
8895
9076
|
const fold = spin !== null && spin !== 0 ? spinSymmetry(layer.sprite) : 1;
|
|
8896
9077
|
if (spin !== null && fold !== Infinity) {
|
|
8897
9078
|
if (Number.isNaN(spin)) reasons.push(`${at} spin range gives each entity its own speed`);
|
|
8898
|
-
else if (spin !== 0) add(`${at} spin${fold > 1 ? ` (${fold}-fold)` : ""}`, 36e4 / Math.abs(spin) / fold);
|
|
9079
|
+
else if (spin !== 0) add(`${at} spin${fold > 1 ? ` (${fold}-fold)` : ""}`, 36e4 / Math.abs(spin) / fold, "spin");
|
|
8899
9080
|
}
|
|
8900
9081
|
const sp = layer.sprite;
|
|
8901
|
-
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate);
|
|
9082
|
+
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate, "cycle");
|
|
8902
9083
|
if (sp.kind === "textBlock" && sp.reveal) {
|
|
8903
9084
|
if ((sp.reveal.speed ?? 0) > 0) reasons.push(`${at} reveal types itself once`);
|
|
8904
9085
|
if (sp.reveal.caret) {
|
|
8905
9086
|
const hz = Math.min(3, sp.reveal.caret === true ? 1.2 : sp.reveal.caret.blink ?? 1.2);
|
|
8906
|
-
if (hz > 0) add(`${at} caret`, 1e3 / hz);
|
|
9087
|
+
if (hz > 0) add(`${at} caret`, 1e3 / hz, "caret");
|
|
8907
9088
|
}
|
|
8908
9089
|
}
|
|
8909
9090
|
const m = layer.motion;
|
|
8910
|
-
if (m.type === "path") add(`${at} path`, m.duration);
|
|
9091
|
+
if (m.type === "path") add(`${at} path`, m.duration, "path");
|
|
8911
9092
|
else if (m.type === "orbit") {
|
|
8912
9093
|
const s = range(m.speed);
|
|
8913
9094
|
if (s === null || Number.isNaN(s)) reasons.push(`${at} orbit speed range gives each entity its own lap`);
|
|
8914
|
-
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s));
|
|
9095
|
+
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s), "orbit");
|
|
8915
9096
|
} else if (m.type !== "static") {
|
|
8916
9097
|
if (m.type === "drift" && m.ease?.type === "settle" && !layer.emit) reasons.push(`${at} drift settles once`);
|
|
8917
9098
|
else reasons.push(`${at} ${m.type} motion travels at seeded per-entity speeds`);
|
|
8918
9099
|
}
|
|
8919
9100
|
});
|
|
8920
9101
|
const labelled = parts.map(([l, n, d]) => `${l}: ${Number((n / d).toFixed(3))} ms`);
|
|
8921
|
-
|
|
8922
|
-
if (
|
|
9102
|
+
const kinds = parts.map(([, , , k]) => k);
|
|
9103
|
+
if (reasons.length > 0) return { period: null, parts: labelled, kinds, reasons };
|
|
9104
|
+
const combined = combinePeriods(parts);
|
|
9105
|
+
return { ...combined, parts: labelled, kinds };
|
|
9106
|
+
}
|
|
9107
|
+
function combinePeriods(parts) {
|
|
9108
|
+
if (parts.length === 0) return { period: 0, reasons: [] };
|
|
8923
9109
|
let num = 1;
|
|
8924
9110
|
let den = 0;
|
|
8925
9111
|
for (const [, n, d] of parts) {
|
|
8926
9112
|
num = num / gcd(num, n) * n;
|
|
8927
9113
|
den = gcd(den, d);
|
|
8928
9114
|
if (num / den > MAX_SCENE_PERIOD) {
|
|
8929
|
-
return { period: null,
|
|
9115
|
+
return { period: null, reasons: [`its parts share no cycle shorter than ${MAX_SCENE_PERIOD / 6e4} min`] };
|
|
8930
9116
|
}
|
|
8931
9117
|
}
|
|
8932
|
-
if (num % den !== 0) return { period: null,
|
|
8933
|
-
return { period: num / den,
|
|
9118
|
+
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`] };
|
|
9119
|
+
return { period: num / den, reasons: [] };
|
|
8934
9120
|
}
|
|
8935
|
-
function periodWithoutTimeline(spec) {
|
|
8936
|
-
return scenePeriod(withoutTimeline(spec));
|
|
9121
|
+
function periodWithoutTimeline(spec, skip) {
|
|
9122
|
+
return scenePeriod(withoutTimeline(spec), skip);
|
|
8937
9123
|
}
|
|
8938
9124
|
var MAX_SYMMETRY = 64;
|
|
8939
9125
|
var MAX_SYMMETRY_POINTS = 8192;
|
|
@@ -9045,6 +9231,7 @@ function edgeAnchored(range2) {
|
|
|
9045
9231
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
9046
9232
|
const hidden = hiddenLayers(spec);
|
|
9047
9233
|
const authored = spec;
|
|
9234
|
+
const keyed = keyedLayers(authored);
|
|
9048
9235
|
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
9049
9236
|
const warnings = [];
|
|
9050
9237
|
const w = viewport.width;
|
|
@@ -9058,18 +9245,19 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9058
9245
|
w,
|
|
9059
9246
|
h
|
|
9060
9247
|
);
|
|
9061
|
-
let
|
|
9248
|
+
let declaredEntities = 0;
|
|
9062
9249
|
let textLayerCount = 0;
|
|
9063
9250
|
let motionLayerCount = 0;
|
|
9064
9251
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
9065
9252
|
if (hidden?.[li]) continue;
|
|
9066
9253
|
const layer = spec.layers[li];
|
|
9067
9254
|
const entities = allEntities[li];
|
|
9068
|
-
|
|
9069
|
-
const
|
|
9070
|
-
const
|
|
9255
|
+
declaredEntities += layer.count;
|
|
9256
|
+
const isText2 = layer.sprite.kind === "text" || layer.sprite.kind === "textBlock";
|
|
9257
|
+
const isKeyed = !!keyed?.[li];
|
|
9258
|
+
const isStaticText = isText2 && layer.motion.type === "static" && !isKeyed;
|
|
9071
9259
|
if (isStaticText) textLayerCount++;
|
|
9072
|
-
if (layer.motion.type !== "static") motionLayerCount++;
|
|
9260
|
+
if (layer.motion.type !== "static" || isKeyed) motionLayerCount++;
|
|
9073
9261
|
const coh = cohesion[li];
|
|
9074
9262
|
if (seamsWorthWarning(layer, coh, entities.length, () => buriedAreaOf(layer, entities, opts.t ?? COHESION_T, w, h))) {
|
|
9075
9263
|
const s = layer.sprite;
|
|
@@ -9094,13 +9282,26 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9094
9282
|
message: `${layer.sprite.kind === "streak" ? "streak sprites orient" : "an oriented stroke turns"} along the motion heading \u2014 static entities have none, so the heading contributes nothing and it renders at angle 0`
|
|
9095
9283
|
});
|
|
9096
9284
|
}
|
|
9097
|
-
if (layer.motion.type === "wander" && layer.motion.coherence === 1 &&
|
|
9285
|
+
if (layer.motion.type === "wander" && layer.motion.coherence === 1 && layer.count >= 20) {
|
|
9098
9286
|
warnings.push({
|
|
9099
9287
|
path: `layers[${li}].motion.coherence`,
|
|
9100
9288
|
code: "full-coherence",
|
|
9101
9289
|
message: "coherence 1 makes every entity share identical harmonics \u2014 the layer moves as a rigid sheet. 0.5\u20130.8 reads as schooling"
|
|
9102
9290
|
});
|
|
9103
9291
|
}
|
|
9292
|
+
if (layer.sprite.kind === "emoji" || layer.sprite.kind === "text") {
|
|
9293
|
+
const vpMin = Math.min(w, h);
|
|
9294
|
+
const fontPx = layer.sprite.kind === "text" && layer.sprite.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(layer.sprite.font) : null;
|
|
9295
|
+
const maxGlyph = fontPx ? Number(fontPx[1]) * (spec.units === "px" ? 1 : vpMin / (spec.referenceViewport ?? LIMITS.referenceViewport)) : entities.reduce((m, e) => Math.max(m, e.size), 0);
|
|
9296
|
+
if (maxGlyph > vpMin) {
|
|
9297
|
+
const unitHint = spec.units === "px" ? 'sizes are px under units: "px"' : `sizes are fractions of min(width, height) \u2014 ${fontPx ? "the font's px size is rescaled from the reference viewport" : "write e.g. [0.03, 0.06], not px"}`;
|
|
9298
|
+
warnings.push({
|
|
9299
|
+
path: `layers[${li}]`,
|
|
9300
|
+
code: "giant-entity",
|
|
9301
|
+
message: `layer ${layer.key ? `'${layer.key}'` : li} renders ${layer.sprite.kind} glyphs up to ${Math.round(maxGlyph)}px \u2014 taller than the ${vpMin}px viewport (its min dimension), so they blanket the frame as blurred blobs; ${unitHint}`
|
|
9302
|
+
});
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9104
9305
|
if (layer.sprite.kind === "circle") {
|
|
9105
9306
|
const maxR = entities.reduce((m, e) => Math.max(m, e.size / 2), 0);
|
|
9106
9307
|
const maxAlpha = entities.reduce((m, e) => Math.max(m, e.alpha), 0);
|
|
@@ -9213,11 +9414,11 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9213
9414
|
message: `"lighter"/"screen" layers stack ${additiveInk.toFixed(1)}\xD7 the frame's area in alpha-weighted light \u2014 overlapping additive sprites clip to white on a real canvas, and the analytic perception model sums them without a ceiling so its coverage/luminance/dominance numbers cannot see it. Thin the count, lower alpha (summed additive alpha at any point \u2272 0.5), or drop blend on the densest layer`
|
|
9214
9415
|
});
|
|
9215
9416
|
}
|
|
9216
|
-
if (
|
|
9417
|
+
if (declaredEntities > 500 && density !== "dense") {
|
|
9217
9418
|
warnings.push({
|
|
9218
9419
|
path: "layers",
|
|
9219
9420
|
code: "dense-scene",
|
|
9220
|
-
message: `${
|
|
9421
|
+
message: `${declaredEntities} entities \u2014 scene may feel crowded and hurt performance on low-end devices`
|
|
9221
9422
|
});
|
|
9222
9423
|
}
|
|
9223
9424
|
if (density === "sparse" && totalCoverage > SPARSE_DECLARED_MAX_COVERAGE) {
|
|
@@ -9259,25 +9460,41 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9259
9460
|
});
|
|
9260
9461
|
}
|
|
9261
9462
|
}
|
|
9262
|
-
const
|
|
9263
|
-
if (
|
|
9463
|
+
const flicker2 = nearLimitFlash(authored);
|
|
9464
|
+
if (flicker2) {
|
|
9264
9465
|
warnings.push({
|
|
9265
9466
|
path: "timeline",
|
|
9266
9467
|
code: "flash-near-limit",
|
|
9267
|
-
message: `the timeline flickers ${
|
|
9468
|
+
message: `the timeline flickers ${flicker2.flashesPerSecond} times a second over ${Math.round(flicker2.area * 100)}% of the frame around ${Math.round(flicker2.at)} ms \u2014 at or near WCAG 2.3.1's limit of 3; space opposing luminance changes \u2265 500 ms apart, or lower the contrast between the two states`
|
|
9268
9469
|
});
|
|
9269
9470
|
}
|
|
9270
9471
|
if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
|
|
9271
9472
|
const lap = Math.round(authored.timeline.duration);
|
|
9272
9473
|
const rest = periodWithoutTimeline(authored);
|
|
9273
|
-
|
|
9474
|
+
const seam = rest.period !== null && rest.period > 0 && lap % rest.period !== 0 ? periodWithoutTimeline(authored, SEAMLESS_PERIOD_KINDS) : null;
|
|
9475
|
+
if (seam && seam.period !== null && seam.period > 0 && lap % seam.period !== 0) {
|
|
9274
9476
|
let a = lap;
|
|
9275
|
-
let b =
|
|
9477
|
+
let b = seam.period;
|
|
9276
9478
|
while (b) [a, b] = [b, a % b];
|
|
9277
9479
|
warnings.push({
|
|
9278
9480
|
path: "timeline.duration",
|
|
9279
9481
|
code: "loop-seam",
|
|
9280
|
-
message: `the timeline loops every ${lap} ms but
|
|
9482
|
+
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`
|
|
9483
|
+
});
|
|
9484
|
+
}
|
|
9485
|
+
}
|
|
9486
|
+
if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
|
|
9487
|
+
const lap = Math.round(authored.timeline.duration);
|
|
9488
|
+
for (let li = 0; li < authored.layers.length; li++) {
|
|
9489
|
+
if (hidden?.[li]) continue;
|
|
9490
|
+
const layer = authored.layers[li];
|
|
9491
|
+
const life = layer.life;
|
|
9492
|
+
if (!life || life.enter === void 0 && life.exit === void 0 && !life.stagger) continue;
|
|
9493
|
+
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
9494
|
+
warnings.push({
|
|
9495
|
+
path: `layers[${li}].life`,
|
|
9496
|
+
code: "life-runs-once",
|
|
9497
|
+
message: `informational: ${label}'s \`life\` runs once on scene time, from when the scene (or its sequence segment) starts \u2014 it does not replay when the timeline loops every ${lap} ms${life.exit !== void 0 ? ", so after its exit the layer stays gone for every later lap" : ""}; to build it on every lap, key its \`opacity\` in the timeline instead`
|
|
9281
9498
|
});
|
|
9282
9499
|
}
|
|
9283
9500
|
}
|
|
@@ -9304,7 +9521,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9304
9521
|
if (layer.motion.type === "static") continue;
|
|
9305
9522
|
const entities = allEntities[li];
|
|
9306
9523
|
if (entities.length < 3) continue;
|
|
9307
|
-
const speeds = entities.map((e) => Math.sqrt(e.vx * e.vx + e.vy * e.vy));
|
|
9524
|
+
const speeds = layer.motion.type === "orbit" ? entities.map((e) => Math.abs(e.vx) * (Math.PI / 180) * e.orbitR) : entities.map((e) => Math.sqrt(e.vx * e.vx + e.vy * e.vy));
|
|
9308
9525
|
const mean = speeds.reduce((a, b) => a + b, 0) / speeds.length;
|
|
9309
9526
|
if (mean === 0) continue;
|
|
9310
9527
|
const maxDev = Math.max(...speeds.map((s) => Math.abs(s - mean)));
|
|
@@ -9338,29 +9555,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9338
9555
|
}
|
|
9339
9556
|
}
|
|
9340
9557
|
}
|
|
9341
|
-
const textBoxes =
|
|
9342
|
-
for (let li = 0; li < spec.layers.length; li++) {
|
|
9343
|
-
if (hidden?.[li]) continue;
|
|
9344
|
-
const layer = spec.layers[li];
|
|
9345
|
-
const s = layer.sprite;
|
|
9346
|
-
if (layer.motion.type !== "static") continue;
|
|
9347
|
-
if (s.kind !== "text" && s.kind !== "textBlock") continue;
|
|
9348
|
-
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
9349
|
-
for (const e of allEntities[li]) {
|
|
9350
|
-
const p = positionAt(e, 0, w, h);
|
|
9351
|
-
const unturned = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
9352
|
-
const rot = rotationAt(e, 0);
|
|
9353
|
-
const turn = (q) => ({ x: p.x + (q.x - p.x) * Math.cos(rot) - (q.y - p.y) * Math.sin(rot), y: p.y + (q.x - p.x) * Math.sin(rot) + (q.y - p.y) * Math.cos(rot) });
|
|
9354
|
-
const map = paintMap(spec, layer, w, h, scale);
|
|
9355
|
-
const both = rot && map ? (q) => map(turn(q)) : rot ? turn : map;
|
|
9356
|
-
const box = both ? { ...unturned, ...mapBox(both, unturned) } : unturned;
|
|
9357
|
-
const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
|
|
9358
|
-
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
9359
|
-
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
9360
|
-
const str = s.kind === "textBlock" ? textBlockText(s) : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
9361
|
-
textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
|
|
9362
|
-
}
|
|
9363
|
-
}
|
|
9558
|
+
const textBoxes = staticTextBoxes(spec, allEntities, hidden, w, h, scale);
|
|
9364
9559
|
const asFraction = (b) => ({
|
|
9365
9560
|
x: +(b.x0 / w).toFixed(4),
|
|
9366
9561
|
y: +(b.y0 / h).toFixed(4),
|
|
@@ -9385,34 +9580,46 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9385
9580
|
message: `${b.label} text runs off the ${edges.join(" and the ")} at ${w}\xD7${h} \u2014 move position, or shrink fontSize / maxWidth`
|
|
9386
9581
|
});
|
|
9387
9582
|
}
|
|
9388
|
-
const
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it`,
|
|
9410
|
-
// The two boxes, so an agent can move one without re-deriving the
|
|
9411
|
-
// layout. Additive: the message is unchanged.
|
|
9412
|
-
boxes: [asFraction(first), asFraction(second)]
|
|
9583
|
+
const overlapSamples = textOverlapSamples(authored, spec, allEntities, keyed, hidden, opts.t ?? COHESION_T, opts.until, opts.seed ?? spec.seed ?? 42, w, h);
|
|
9584
|
+
const overlapAt = /* @__PURE__ */ new Map();
|
|
9585
|
+
if (overlapSamples) {
|
|
9586
|
+
const pairsIn = (boxes, each) => {
|
|
9587
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
9588
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
9589
|
+
const a = boxes[i];
|
|
9590
|
+
const b = boxes[j];
|
|
9591
|
+
if (a.li === b.li) continue;
|
|
9592
|
+
const share = overlapShare(a, b);
|
|
9593
|
+
if (share <= 0.1 || isOutlineStack(a, b)) continue;
|
|
9594
|
+
each(`${Math.min(a.li, b.li)}:${Math.max(a.li, b.li)}`, a, b, share);
|
|
9595
|
+
}
|
|
9596
|
+
}
|
|
9597
|
+
};
|
|
9598
|
+
const raised = /* @__PURE__ */ new Set();
|
|
9599
|
+
pairsIn(overlapSamples.now, (pair) => raised.add(pair));
|
|
9600
|
+
for (const sample of overlapSamples.visible) {
|
|
9601
|
+
pairsIn(sample.boxes, (pair, a, b, share) => {
|
|
9602
|
+
if (!raised.has(pair) || overlapAt.has(pair)) return;
|
|
9603
|
+
overlapAt.set(pair, { share, a, b, t: sample.t });
|
|
9413
9604
|
});
|
|
9414
9605
|
}
|
|
9415
9606
|
}
|
|
9607
|
+
for (const pair of [...overlapAt.keys()].sort((x, y) => {
|
|
9608
|
+
const [x0, x1] = x.split(":").map(Number);
|
|
9609
|
+
const [y0, y1] = y.split(":").map(Number);
|
|
9610
|
+
return x0 - y0 || x1 - y1;
|
|
9611
|
+
})) {
|
|
9612
|
+
const { share, a, b, t } = overlapAt.get(pair);
|
|
9613
|
+
const [first, second] = a.li < b.li ? [a, b] : [b, a];
|
|
9614
|
+
warnings.push({
|
|
9615
|
+
path: `layers[${first.li}]`,
|
|
9616
|
+
code: "text-overlap",
|
|
9617
|
+
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one)${t === null ? "" : `, both visible at ${Math.round(t)} ms`} \u2014 they will paint over each other; move one or shrink it`,
|
|
9618
|
+
// The two boxes, so an agent can move one without re-deriving the
|
|
9619
|
+
// layout. Additive: the message is unchanged.
|
|
9620
|
+
boxes: [asFraction(first), asFraction(second)]
|
|
9621
|
+
});
|
|
9622
|
+
}
|
|
9416
9623
|
const cellFlagged = /* @__PURE__ */ new Set();
|
|
9417
9624
|
for (let i = 0; i < textBoxes.length; i++) {
|
|
9418
9625
|
const a = textBoxes[i];
|
|
@@ -9420,6 +9627,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9420
9627
|
const layer = spec.layers[a.li];
|
|
9421
9628
|
const layout = layer.layout;
|
|
9422
9629
|
if (layout?.type !== "list" && layout?.type !== "table") continue;
|
|
9630
|
+
if (isAtmosphere(layer)) continue;
|
|
9423
9631
|
for (let j = i + 1; j < textBoxes.length; j++) {
|
|
9424
9632
|
const b = textBoxes[j];
|
|
9425
9633
|
if (b.li !== a.li) continue;
|
|
@@ -9526,6 +9734,120 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9526
9734
|
}
|
|
9527
9735
|
return warnings;
|
|
9528
9736
|
}
|
|
9737
|
+
function isAtmosphere(layer) {
|
|
9738
|
+
const s = layer.sprite;
|
|
9739
|
+
return (s.kind === "text" || s.kind === "textBlock") && s.role === "atmosphere";
|
|
9740
|
+
}
|
|
9741
|
+
function staticTextBoxes(spec, allEntities, hidden, w, h, scale, only) {
|
|
9742
|
+
const textBoxes = [];
|
|
9743
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
9744
|
+
if (hidden?.[li]) continue;
|
|
9745
|
+
if (only && !only.has(li)) continue;
|
|
9746
|
+
const layer = spec.layers[li];
|
|
9747
|
+
const s = layer.sprite;
|
|
9748
|
+
if (layer.motion.type !== "static") continue;
|
|
9749
|
+
if (s.kind !== "text" && s.kind !== "textBlock") continue;
|
|
9750
|
+
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
9751
|
+
for (const e of allEntities[li]) {
|
|
9752
|
+
const p = positionAt(e, 0, w, h);
|
|
9753
|
+
const unturned = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
9754
|
+
const rot = rotationAt(e, 0);
|
|
9755
|
+
const turn = (q) => ({ x: p.x + (q.x - p.x) * Math.cos(rot) - (q.y - p.y) * Math.sin(rot), y: p.y + (q.x - p.x) * Math.sin(rot) + (q.y - p.y) * Math.cos(rot) });
|
|
9756
|
+
const map = paintMap(spec, layer, w, h, scale);
|
|
9757
|
+
const both = rot && map ? (q) => map(turn(q)) : rot ? turn : map;
|
|
9758
|
+
const box = both ? { ...unturned, ...mapBox(both, unturned) } : unturned;
|
|
9759
|
+
const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
|
|
9760
|
+
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
9761
|
+
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
9762
|
+
const str = s.kind === "textBlock" ? textBlockText(s) : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
9763
|
+
textBoxes.push({ li, label, alpha, peak: Math.min(1, e.alpha + e.pulseAmp), str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
return textBoxes;
|
|
9767
|
+
}
|
|
9768
|
+
function keyedLayers(spec) {
|
|
9769
|
+
if (!spec.timeline) return null;
|
|
9770
|
+
const paths = [...timelineTracks(spec).keys()];
|
|
9771
|
+
if (paths.length === 0) return null;
|
|
9772
|
+
return spec.layers.map((l, i) => paths.some((p) => keyTouchesLayer(p, i, l)));
|
|
9773
|
+
}
|
|
9774
|
+
function keyTouchesLayer(path, i, layer) {
|
|
9775
|
+
const lp = `layers.${i}`;
|
|
9776
|
+
if (path === "layers" || path === lp || path.startsWith(`${lp}.`)) return true;
|
|
9777
|
+
if (path === "referenceViewport" || path === "units") return true;
|
|
9778
|
+
if (layer.group === void 0) return false;
|
|
9779
|
+
const gp = `groups.${layer.group}`;
|
|
9780
|
+
return path === "groups" || path === gp || path.startsWith(`${gp}.`);
|
|
9781
|
+
}
|
|
9782
|
+
var TEXT_VISIBLE_FLOOR = 0.02;
|
|
9783
|
+
function paintAxisScale(spec, layer) {
|
|
9784
|
+
const axes = (tf) => {
|
|
9785
|
+
const s = Math.abs(tf?.scale ?? 1);
|
|
9786
|
+
return [s * Math.abs(tf?.scaleX ?? 1), s * Math.abs(tf?.scaleY ?? 1)];
|
|
9787
|
+
};
|
|
9788
|
+
const [lx, ly] = axes(layer.transform);
|
|
9789
|
+
const [gx, gy] = axes(groupOf(spec, layer)?.transform);
|
|
9790
|
+
return Math.min(lx * gx, ly * gy);
|
|
9791
|
+
}
|
|
9792
|
+
var TEXT_OVERLAP_MAX_SAMPLES = 48;
|
|
9793
|
+
function textOverlapSamples(authored, spec, allEntities, keyed, hidden, tNow, until, seed, w, h) {
|
|
9794
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
9795
|
+
spec.layers.forEach((l, li) => {
|
|
9796
|
+
const k = l.sprite.kind;
|
|
9797
|
+
if (!hidden?.[li] && (k === "text" || k === "textBlock") && !isAtmosphere(l)) candidates.add(li);
|
|
9798
|
+
});
|
|
9799
|
+
if (candidates.size < 2) return null;
|
|
9800
|
+
const visibleAt = (t, at, entities, boxes) => {
|
|
9801
|
+
const { scale } = entityScales(at, w, h);
|
|
9802
|
+
return (boxes ?? staticTextBoxes(at, entities, hidden, w, h, scale, candidates)).filter((b) => textBoxVisible(at, at.layers[b.li], b, t, w, h, scale));
|
|
9803
|
+
};
|
|
9804
|
+
const now = staticTextBoxes(spec, allEntities, hidden, w, h, entityScales(spec, w, h).scale, candidates);
|
|
9805
|
+
const visible = [{ t: null, boxes: visibleAt(tNow, spec, allEntities, now) }];
|
|
9806
|
+
if (keyed && [...candidates].some((li) => keyed[li])) {
|
|
9807
|
+
const touches = (path) => [...candidates].some((li) => keyTouchesLayer(path, li, authored.layers[li]));
|
|
9808
|
+
let times = timelineSampleTimes(authored, false, 120, touches).filter((t) => t !== tNow && (until === void 0 || t <= until));
|
|
9809
|
+
if (times.length > TEXT_OVERLAP_MAX_SAMPLES) {
|
|
9810
|
+
const step = times.length / TEXT_OVERLAP_MAX_SAMPLES;
|
|
9811
|
+
times = Array.from({ length: TEXT_OVERLAP_MAX_SAMPLES }, (_, i) => times[Math.floor(i * step)]);
|
|
9812
|
+
}
|
|
9813
|
+
const built = /* @__PURE__ */ new Map([[structuralSignature(spec), allEntities]]);
|
|
9814
|
+
for (const t of times) {
|
|
9815
|
+
const at = resolveTimelineAt(authored, t, true);
|
|
9816
|
+
const sig = structuralSignature(at);
|
|
9817
|
+
let entities = built.get(sig);
|
|
9818
|
+
if (!entities) {
|
|
9819
|
+
const sc = entityScales(at, w, h);
|
|
9820
|
+
const rng = createRng3(seed);
|
|
9821
|
+
entities = at.layers.map((l) => buildEntities(l, rng, w, h, sc.scale, sc.countScale));
|
|
9822
|
+
built.set(sig, entities);
|
|
9823
|
+
}
|
|
9824
|
+
visible.push({ t, boxes: visibleAt(t, at, entities) });
|
|
9825
|
+
}
|
|
9826
|
+
}
|
|
9827
|
+
return { now, visible };
|
|
9828
|
+
}
|
|
9829
|
+
function textBoxVisible(spec, layer, b, t, w, h, scale) {
|
|
9830
|
+
if (b.x1 <= 0 || b.x0 >= w || b.y1 <= 0 || b.y0 >= h) return false;
|
|
9831
|
+
if (paintAxisScale(spec, layer) <= TEXT_VISIBLE_FLOOR) return false;
|
|
9832
|
+
const s = layer.sprite;
|
|
9833
|
+
let a = paintOpacity(spec, layer) * b.peak;
|
|
9834
|
+
if (s.kind === "textBlock") {
|
|
9835
|
+
a *= s.opacity ?? 1;
|
|
9836
|
+
if (s.reveal) {
|
|
9837
|
+
const fsPx = s.fontSize * scale;
|
|
9838
|
+
const lines = breakTextBlock(textBlockText(s), s.maxWidth * scale / fsPx, textMetricsClassFor(s.font));
|
|
9839
|
+
if (revealState(lines, s.reveal, t).progress <= 0) return false;
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
return a > TEXT_VISIBLE_FLOOR;
|
|
9843
|
+
}
|
|
9844
|
+
function overlapShare(a, b) {
|
|
9845
|
+
const ix = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
9846
|
+
const iy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
9847
|
+
if (ix <= 0 || iy <= 0) return 0;
|
|
9848
|
+
const smaller = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
|
|
9849
|
+
return smaller > 0 ? ix * iy / smaller : 0;
|
|
9850
|
+
}
|
|
9529
9851
|
function nearLimitFlash(spec) {
|
|
9530
9852
|
if (!spec.timeline) return null;
|
|
9531
9853
|
const { times, lap } = lumaKeyTimes(spec);
|
|
@@ -9607,7 +9929,7 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundR
|
|
|
9607
9929
|
if (layer.life !== void 0 || layer.emit !== void 0) continue;
|
|
9608
9930
|
if ((sp.kind === "circle" || sp.kind === "polygon") && sp.soft) continue;
|
|
9609
9931
|
if (sp.kind !== "circle" && sp.kind !== "rect" && sp.kind !== "polygon") continue;
|
|
9610
|
-
|
|
9932
|
+
const drawing = sp.kind === "polygon" && isPathPolygon(sp);
|
|
9611
9933
|
const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
9612
9934
|
for (const e of allEntities[lj]) {
|
|
9613
9935
|
if (e.spinSpeed !== 0 || e.pulseAmp > 0 || e.growAmp > 0 || e.alpha * a <= 0) continue;
|
|
@@ -9616,6 +9938,14 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundR
|
|
|
9616
9938
|
const c = positionAt(e, 0, w, h);
|
|
9617
9939
|
const dx = pt.x - c.x;
|
|
9618
9940
|
const dy = pt.y - c.y;
|
|
9941
|
+
if (drawing) {
|
|
9942
|
+
if (e.rotate) continue;
|
|
9943
|
+
const ink = pathGroundAt(sp, e, dx, dy, w, h);
|
|
9944
|
+
if (!ink) continue;
|
|
9945
|
+
plate = sourceOverPlate(plate, ink.rgb, Math.min(1, e.alpha * a * ink.opacity));
|
|
9946
|
+
covered = true;
|
|
9947
|
+
continue;
|
|
9948
|
+
}
|
|
9619
9949
|
let inside2 = false;
|
|
9620
9950
|
if (sp.kind === "circle") inside2 = Math.hypot(dx, dy) <= e.size / 2;
|
|
9621
9951
|
else if (sp.kind === "rect") {
|
|
@@ -9636,6 +9966,41 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundR
|
|
|
9636
9966
|
}
|
|
9637
9967
|
return covered ? { rgb: plate, luma: 0.2126 * plate.r + 0.7152 * plate.g + 0.0722 * plate.b } : null;
|
|
9638
9968
|
}
|
|
9969
|
+
function pathGroundAt(sp, e, dx, dy, w, h) {
|
|
9970
|
+
const base = sp.colors?.[e.colorIndex] ?? sp.color;
|
|
9971
|
+
const fr = pathFrame(sp, e.size / 2, w, h);
|
|
9972
|
+
const u = dx / fr.sx + fr.ox;
|
|
9973
|
+
const v = dy / fr.sy + fr.oy;
|
|
9974
|
+
let r = 0, g = 0, b = 0, wsum = 0, area = 0;
|
|
9975
|
+
for (const part of pathParts(sp)) {
|
|
9976
|
+
if (!part.fill || part.opacity <= 0 || part.flat.points.length < 3) continue;
|
|
9977
|
+
const box = part.box;
|
|
9978
|
+
if (Math.abs(u - box.cx) > box.hw || Math.abs(v - box.cy) > box.hh) continue;
|
|
9979
|
+
const ring = part.flat.points.map(([x, y]) => ({ x, y }));
|
|
9980
|
+
if (!pointInPolygon(u, v, ring)) continue;
|
|
9981
|
+
const pc = hexRgb(part.color ?? base);
|
|
9982
|
+
let rgb = pc;
|
|
9983
|
+
if (part.gradient) {
|
|
9984
|
+
const weights = gradientStopWeights(part.gradient.stops);
|
|
9985
|
+
rgb = { r: 0, g: 0, b: 0 };
|
|
9986
|
+
part.gradient.stops.forEach((st, i) => {
|
|
9987
|
+
const o = st.opacity ?? 1;
|
|
9988
|
+
const c = hexRgb(st.color);
|
|
9989
|
+
const k = weights[i];
|
|
9990
|
+
rgb = { r: rgb.r + (c.r * o + pc.r * (1 - o)) * k, g: rgb.g + (c.g * o + pc.g * (1 - o)) * k, b: rgb.b + (c.b * o + pc.b * (1 - o)) * k };
|
|
9991
|
+
});
|
|
9992
|
+
}
|
|
9993
|
+
const pa = Math.max(1e-6, polygonArea(ring));
|
|
9994
|
+
const wt = pa * part.opacity;
|
|
9995
|
+
r += rgb.r * wt;
|
|
9996
|
+
g += rgb.g * wt;
|
|
9997
|
+
b += rgb.b * wt;
|
|
9998
|
+
wsum += wt;
|
|
9999
|
+
area += pa;
|
|
10000
|
+
}
|
|
10001
|
+
if (wsum <= 0 || area <= 0) return null;
|
|
10002
|
+
return { rgb: { r: r / wsum, g: g / wsum, b: b / wsum }, opacity: Math.min(1, wsum / area) };
|
|
10003
|
+
}
|
|
9639
10004
|
function pointInPolygon(x, y, pts) {
|
|
9640
10005
|
let inside2 = false;
|
|
9641
10006
|
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
@@ -9717,7 +10082,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
9717
10082
|
const seg = seq.segments[i];
|
|
9718
10083
|
const timed = !!seg.scene.timeline && seg.duration !== void 0;
|
|
9719
10084
|
const tSample = timed ? Math.min(COHESION_T, seg.duration) : void 0;
|
|
9720
|
-
const segWarnings = adviseSpec(seg.scene, viewport, tSample === void 0 ? {} : { t: tSample });
|
|
10085
|
+
const segWarnings = adviseSpec(seg.scene, viewport, tSample === void 0 ? {} : { t: tSample, until: seg.duration });
|
|
9721
10086
|
const at = tSample === void 0 ? "" : ` (judged ${(tSample / 1e3).toFixed(1)} s into this ${(seg.duration / 1e3).toFixed(1)} s segment)`;
|
|
9722
10087
|
for (const w of segWarnings) {
|
|
9723
10088
|
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message + at, boxes: w.boxes });
|
|
@@ -9754,7 +10119,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
9754
10119
|
warnings.push({
|
|
9755
10120
|
path: `segments[${i}].transition`,
|
|
9756
10121
|
code: "morph-structural-mismatch",
|
|
9757
|
-
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
10122
|
+
message: `segments ${i}\u2192${i + 1} differ structurally (${describeStructuralDifferences(seq.segments[i].scene, seq.segments[i + 1].scene)}): morph will fall back to cut`
|
|
9758
10123
|
});
|
|
9759
10124
|
} else if (!seq.segments[i].scene.timeline && !seq.segments[i + 1].scene.timeline && morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" || tr.text === "dip" })) {
|
|
9760
10125
|
warnings.push({
|
|
@@ -9859,6 +10224,418 @@ function describeScene(spec, options = {}) {
|
|
|
9859
10224
|
};
|
|
9860
10225
|
}
|
|
9861
10226
|
|
|
10227
|
+
// src/pattern-hazard.ts
|
|
10228
|
+
import { FLASH_DARK_MAX, FLASH_MAX_PER_SECOND, FLASH_SWING_THRESHOLD, srgbToLinear as srgbToLinear4 } from "@idle-screens/validator";
|
|
10229
|
+
var PATTERN_GRID_SHORT_SIDE = 216;
|
|
10230
|
+
var PATTERN_GRID_MAX_SIDE = 400;
|
|
10231
|
+
var PATTERN_MIN_PAIRS = 6;
|
|
10232
|
+
var PATTERN_STATIONARY_AREA = 0.4;
|
|
10233
|
+
var PATTERN_MOVING_AREA = 0.25;
|
|
10234
|
+
var PATTERN_FLICKER_AREA = 0.25;
|
|
10235
|
+
var PATTERN_MAX_SAMPLE_TIMES = 6;
|
|
10236
|
+
var PERIOD_RATIO = 4 / 3;
|
|
10237
|
+
var PATTERN_MAX_PITCH = 0.15;
|
|
10238
|
+
var PATTERN_MIN_EXTENT = 2;
|
|
10239
|
+
var NEIGHBOUR_PITCH = 0.25;
|
|
10240
|
+
var BLOCK = 4;
|
|
10241
|
+
var DELTA_MS = 1e3 / 60;
|
|
10242
|
+
var FLICKER_PERIODS = 4;
|
|
10243
|
+
var BASE_T = 5e3;
|
|
10244
|
+
var isText = (l) => l.sprite.kind === "text" || l.sprite.kind === "textBlock";
|
|
10245
|
+
function prepared(spec, t, keep, flat) {
|
|
10246
|
+
const resolved = resolveTimelineAt(spec, t);
|
|
10247
|
+
const out = { ...resolved, layers: resolved.layers.map((l, i) => keep(l, i) && !isText(l) ? l : { ...l, opacity: 0 }) };
|
|
10248
|
+
delete out.ghosting;
|
|
10249
|
+
if (flat !== void 0) {
|
|
10250
|
+
const v = Math.round(Math.max(0, Math.min(1, flat)) * 255).toString(16).padStart(2, "0");
|
|
10251
|
+
out.background = { type: "solid", color: `#${v}${v}${v}` };
|
|
10252
|
+
}
|
|
10253
|
+
return out;
|
|
10254
|
+
}
|
|
10255
|
+
function gridSize(viewport) {
|
|
10256
|
+
const { width: w, height: h } = viewport;
|
|
10257
|
+
const short = Math.max(1, Math.min(w, h));
|
|
10258
|
+
const fit = (side) => Math.max(8, Math.min(PATTERN_GRID_MAX_SIDE, Math.round(side / short * PATTERN_GRID_SHORT_SIDE)));
|
|
10259
|
+
return { cols: fit(w), rows: fit(h) };
|
|
10260
|
+
}
|
|
10261
|
+
function raster(spec, t, opts) {
|
|
10262
|
+
const { cols, rows } = gridSize(opts.viewport);
|
|
10263
|
+
const g = luminanceGrid(spec, { viewport: opts.viewport, seed: opts.seed, t, cols, rows }, PATTERN_GRID_MAX_SIDE);
|
|
10264
|
+
const lum = new Float64Array(g.cells.length);
|
|
10265
|
+
for (let i = 0; i < lum.length; i++) lum[i] = srgbToLinear4(g.cells[i] * 255);
|
|
10266
|
+
const short = Math.min(opts.viewport.width, opts.viewport.height);
|
|
10267
|
+
const bg = g.backgroundCells ?? g.background;
|
|
10268
|
+
return {
|
|
10269
|
+
cols: g.cols,
|
|
10270
|
+
rows: g.rows,
|
|
10271
|
+
cellX: opts.viewport.width / g.cols / short,
|
|
10272
|
+
cellY: opts.viewport.height / g.rows / short,
|
|
10273
|
+
lum,
|
|
10274
|
+
meanGround: bg.reduce((a, b) => a + b, 0) / Math.max(1, bg.length)
|
|
10275
|
+
};
|
|
10276
|
+
}
|
|
10277
|
+
function stripeExtrema(line) {
|
|
10278
|
+
const n = line.length;
|
|
10279
|
+
const out = [];
|
|
10280
|
+
if (n < 3) return out;
|
|
10281
|
+
let lo = line[0], loAt = 0, hi = line[0], hiAt = 0;
|
|
10282
|
+
let dir = 0;
|
|
10283
|
+
let cand = 0, candFrom = 0, candTo = 0;
|
|
10284
|
+
for (let i = 1; i < n; i++) {
|
|
10285
|
+
const v = line[i];
|
|
10286
|
+
if (dir === 0) {
|
|
10287
|
+
if (v > hi) {
|
|
10288
|
+
hi = v;
|
|
10289
|
+
hiAt = i;
|
|
10290
|
+
}
|
|
10291
|
+
if (v < lo) {
|
|
10292
|
+
lo = v;
|
|
10293
|
+
loAt = i;
|
|
10294
|
+
}
|
|
10295
|
+
if (hi - lo >= FLASH_SWING_THRESHOLD && lo < FLASH_DARK_MAX) {
|
|
10296
|
+
if (loAt < hiAt) {
|
|
10297
|
+
out.push({ x: loAt, v: lo, light: false });
|
|
10298
|
+
dir = 1;
|
|
10299
|
+
cand = hi;
|
|
10300
|
+
candFrom = candTo = hiAt;
|
|
10301
|
+
} else {
|
|
10302
|
+
out.push({ x: hiAt, v: hi, light: true });
|
|
10303
|
+
dir = -1;
|
|
10304
|
+
cand = lo;
|
|
10305
|
+
candFrom = candTo = loAt;
|
|
10306
|
+
}
|
|
10307
|
+
}
|
|
10308
|
+
continue;
|
|
10309
|
+
}
|
|
10310
|
+
if (dir === 1 ? v > cand : v < cand) {
|
|
10311
|
+
cand = v;
|
|
10312
|
+
candFrom = candTo = i;
|
|
10313
|
+
continue;
|
|
10314
|
+
}
|
|
10315
|
+
if (v === cand) {
|
|
10316
|
+
if (candTo === i - 1) candTo = i;
|
|
10317
|
+
continue;
|
|
10318
|
+
}
|
|
10319
|
+
const swing = dir === 1 ? cand - v : v - cand;
|
|
10320
|
+
if (swing >= FLASH_SWING_THRESHOLD && Math.min(cand, v) < FLASH_DARK_MAX) {
|
|
10321
|
+
out.push({ x: (candFrom + candTo) / 2, v: cand, light: dir === 1 });
|
|
10322
|
+
dir = -dir;
|
|
10323
|
+
cand = v;
|
|
10324
|
+
candFrom = candTo = i;
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
return out;
|
|
10328
|
+
}
|
|
10329
|
+
function similarSpacing(a, b, frac) {
|
|
10330
|
+
return Math.abs(a - b) <= 1 + frac * Math.min(a, b);
|
|
10331
|
+
}
|
|
10332
|
+
function stripeRuns(ext, line, axis) {
|
|
10333
|
+
const runs = [];
|
|
10334
|
+
for (const light of [true, false]) {
|
|
10335
|
+
const idx = [];
|
|
10336
|
+
ext.forEach((e, i) => {
|
|
10337
|
+
if (e.light === light) idx.push(i);
|
|
10338
|
+
});
|
|
10339
|
+
let start = 0;
|
|
10340
|
+
const close = (end) => {
|
|
10341
|
+
const count = end - start + 1;
|
|
10342
|
+
if (count >= PATTERN_MIN_PAIRS) {
|
|
10343
|
+
const xs = idx.slice(start, end + 1).map((i) => ext[i].x);
|
|
10344
|
+
const periods = xs.slice(1).map((x, k) => x - xs[k]).sort((a, b) => a - b);
|
|
10345
|
+
const first = idx[start], last = idx[end];
|
|
10346
|
+
let swing = 0, pairs = 0;
|
|
10347
|
+
for (let k = Math.max(0, first - 1); k < Math.min(ext.length - 1, last + 1); k++) {
|
|
10348
|
+
swing += Math.abs(ext[k + 1].v - ext[k].v);
|
|
10349
|
+
pairs++;
|
|
10350
|
+
}
|
|
10351
|
+
runs.push({ line, axis, from: xs[0], to: xs[xs.length - 1], period: periods[periods.length >> 1], count, light, contrast: pairs ? swing / pairs : 0 });
|
|
10352
|
+
}
|
|
10353
|
+
};
|
|
10354
|
+
for (let k = 1; k < idx.length; k++) {
|
|
10355
|
+
if (k >= 2) {
|
|
10356
|
+
const p0 = ext[idx[k - 1]].x - ext[idx[k - 2]].x;
|
|
10357
|
+
const p1 = ext[idx[k]].x - ext[idx[k - 1]].x;
|
|
10358
|
+
if (!similarSpacing(p0, p1, PERIOD_RATIO - 1)) {
|
|
10359
|
+
close(k - 1);
|
|
10360
|
+
start = k - 1;
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
}
|
|
10364
|
+
if (idx.length) close(idx.length - 1);
|
|
10365
|
+
}
|
|
10366
|
+
return runs;
|
|
10367
|
+
}
|
|
10368
|
+
function scanRaster(r) {
|
|
10369
|
+
const rowLine = new Float64Array(r.cols);
|
|
10370
|
+
const colLine = new Float64Array(r.rows);
|
|
10371
|
+
const perLine = (axis, n) => {
|
|
10372
|
+
const lines = [];
|
|
10373
|
+
for (let k = 0; k < n; k++) {
|
|
10374
|
+
if (axis === "x") {
|
|
10375
|
+
for (let c = 0; c < r.cols; c++) rowLine[c] = r.lum[k * r.cols + c];
|
|
10376
|
+
} else {
|
|
10377
|
+
for (let q = 0; q < r.rows; q++) colLine[q] = r.lum[q * r.cols + k];
|
|
10378
|
+
}
|
|
10379
|
+
const line = axis === "x" ? rowLine : colLine;
|
|
10380
|
+
lines.push(stripeRuns(stripeExtrema(line), k, axis));
|
|
10381
|
+
}
|
|
10382
|
+
return lines;
|
|
10383
|
+
};
|
|
10384
|
+
const all = [perLine("x", r.rows), perLine("y", r.cols)];
|
|
10385
|
+
const kept = [];
|
|
10386
|
+
for (const lines of all) {
|
|
10387
|
+
const flat = lines.flat();
|
|
10388
|
+
const at = /* @__PURE__ */ new Map();
|
|
10389
|
+
flat.forEach((run, i) => at.set(run, i));
|
|
10390
|
+
const parent = flat.map((_, i) => i);
|
|
10391
|
+
const find = (i) => {
|
|
10392
|
+
while (parent[i] !== i) i = parent[i] = parent[parent[i]];
|
|
10393
|
+
return i;
|
|
10394
|
+
};
|
|
10395
|
+
const across = (run) => run.period * (run.axis === "x" ? r.cellX / r.cellY : r.cellY / r.cellX);
|
|
10396
|
+
for (const run of flat) {
|
|
10397
|
+
const reach = Math.max(1, Math.ceil(across(run)));
|
|
10398
|
+
for (let k = run.line + 1; k <= run.line + reach && k < lines.length; k++) {
|
|
10399
|
+
for (const q of lines[k]) {
|
|
10400
|
+
const overlap = Math.min(run.to, q.to) - Math.max(run.from, q.from);
|
|
10401
|
+
if (overlap >= 0.5 * Math.min(run.to - run.from, q.to - q.from) && similarSpacing(run.period, q.period, NEIGHBOUR_PITCH)) {
|
|
10402
|
+
parent[find(at.get(q))] = find(at.get(run));
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
const span = /* @__PURE__ */ new Map();
|
|
10408
|
+
for (const run of flat) {
|
|
10409
|
+
const root = find(at.get(run));
|
|
10410
|
+
const s0 = span.get(root);
|
|
10411
|
+
const a = across(run);
|
|
10412
|
+
if (!s0) span.set(root, { lo: run.line, hi: run.line, across: a, periods: [run.period], slants: [] });
|
|
10413
|
+
else {
|
|
10414
|
+
s0.lo = Math.min(s0.lo, run.line);
|
|
10415
|
+
s0.hi = Math.max(s0.hi, run.line);
|
|
10416
|
+
s0.across = Math.max(s0.across, a);
|
|
10417
|
+
s0.periods.push(run.period);
|
|
10418
|
+
}
|
|
10419
|
+
}
|
|
10420
|
+
for (const run of flat) {
|
|
10421
|
+
for (const q of lines[run.line + 1] ?? []) {
|
|
10422
|
+
if (q.light !== run.light || find(at.get(q)) !== find(at.get(run))) continue;
|
|
10423
|
+
const d = q.from - run.from;
|
|
10424
|
+
span.get(find(at.get(run))).slants.push(Math.abs(d - run.period * Math.round(d / run.period)));
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
const median = (xs) => xs.length ? [...xs].sort((a, b) => a - b)[xs.length >> 1] : 0;
|
|
10428
|
+
for (const run of flat) {
|
|
10429
|
+
const sp = span.get(find(at.get(run)));
|
|
10430
|
+
if (sp.hi - sp.lo + 1 < PATTERN_MIN_EXTENT * sp.across) continue;
|
|
10431
|
+
const [along, step] = run.axis === "x" ? [r.cellX, r.cellY] : [r.cellY, r.cellX];
|
|
10432
|
+
const slant = median(sp.slants) * along;
|
|
10433
|
+
run.pitch = median(sp.periods) * along * (step / Math.hypot(slant, step));
|
|
10434
|
+
if (run.pitch <= PATTERN_MAX_PITCH) kept.push(run);
|
|
10435
|
+
}
|
|
10436
|
+
}
|
|
10437
|
+
return kept;
|
|
10438
|
+
}
|
|
10439
|
+
function runBlocks(r, run, from = run.from, to = run.to) {
|
|
10440
|
+
const half = run.period / 2;
|
|
10441
|
+
const across = run.axis === "x" ? half * (r.cellX / r.cellY) : half * (r.cellY / r.cellX);
|
|
10442
|
+
const [x0, x1, y0, y1] = run.axis === "x" ? [from - half, to + half, run.line - across, run.line + across] : [run.line - across, run.line + across, from - half, to + half];
|
|
10443
|
+
const bw = Math.ceil(r.cols / BLOCK), bh = Math.ceil(r.rows / BLOCK);
|
|
10444
|
+
const clampB = (v, blocks) => Math.max(0, Math.min(blocks - 1, Math.floor(v / BLOCK)));
|
|
10445
|
+
return { bx0: clampB(x0, bw), bx1: clampB(x1, bw), by0: clampB(y0, bh), by1: clampB(y1, bh) };
|
|
10446
|
+
}
|
|
10447
|
+
function markedArea(r, marked) {
|
|
10448
|
+
const bw = Math.ceil(r.cols / BLOCK);
|
|
10449
|
+
let cells = 0;
|
|
10450
|
+
marked.forEach((m, i) => {
|
|
10451
|
+
if (!m) return;
|
|
10452
|
+
const bx = i % bw, by = Math.floor(i / bw);
|
|
10453
|
+
cells += (Math.min(r.cols, (bx + 1) * BLOCK) - bx * BLOCK) * (Math.min(r.rows, (by + 1) * BLOCK) - by * BLOCK);
|
|
10454
|
+
});
|
|
10455
|
+
return cells / (r.cols * r.rows);
|
|
10456
|
+
}
|
|
10457
|
+
function patternArea(r, runs) {
|
|
10458
|
+
const bw = Math.ceil(r.cols / BLOCK), bh = Math.ceil(r.rows / BLOCK);
|
|
10459
|
+
const marked = new Uint8Array(bw * bh);
|
|
10460
|
+
for (const run of runs) {
|
|
10461
|
+
const b = runBlocks(r, run);
|
|
10462
|
+
for (let y = b.by0; y <= b.by1; y++) for (let x = b.bx0; x <= b.bx1; x++) marked[y * bw + x] = 1;
|
|
10463
|
+
}
|
|
10464
|
+
return markedArea(r, marked);
|
|
10465
|
+
}
|
|
10466
|
+
function flicker(a, b, runs, dt) {
|
|
10467
|
+
const bw = Math.ceil(a.cols / BLOCK), bh = Math.ceil(a.rows / BLOCK);
|
|
10468
|
+
const marked = new Uint8Array(bw * bh);
|
|
10469
|
+
let worst = 0;
|
|
10470
|
+
for (const run of runs) {
|
|
10471
|
+
if (run.contrast <= 0) continue;
|
|
10472
|
+
const span = Math.max(8, FLICKER_PERIODS * run.period);
|
|
10473
|
+
for (let s = run.from; s < run.to; s += span) {
|
|
10474
|
+
const e = Math.min(run.to, s + span);
|
|
10475
|
+
const c0 = Math.max(0, Math.floor(s)), c1 = Math.min((run.axis === "x" ? a.cols : a.rows) - 1, Math.ceil(e));
|
|
10476
|
+
let sum = 0;
|
|
10477
|
+
for (let c = c0; c <= c1; c++) {
|
|
10478
|
+
const i = run.axis === "x" ? run.line * a.cols + c : c * a.cols + run.line;
|
|
10479
|
+
sum += Math.abs(b.lum[i] - a.lum[i]);
|
|
10480
|
+
}
|
|
10481
|
+
const hz = sum / (c1 - c0 + 1) / (dt / 1e3) / (2 * run.contrast);
|
|
10482
|
+
worst = Math.max(worst, hz);
|
|
10483
|
+
if (hz > FLASH_MAX_PER_SECOND) {
|
|
10484
|
+
const bl = runBlocks(a, run, s, e);
|
|
10485
|
+
for (let y = bl.by0; y <= bl.by1; y++) for (let x = bl.bx0; x <= bl.bx1; x++) marked[y * bw + x] = 1;
|
|
10486
|
+
}
|
|
10487
|
+
}
|
|
10488
|
+
}
|
|
10489
|
+
return { area: markedArea(a, marked), hz: worst };
|
|
10490
|
+
}
|
|
10491
|
+
var nonZero = (v) => v !== void 0 && (Array.isArray(v) ? v[0] !== 0 || v[1] !== 0 : v !== 0);
|
|
10492
|
+
function layerPatternMotion(spec, index) {
|
|
10493
|
+
const l = spec.layers[index];
|
|
10494
|
+
if (!l) return "static";
|
|
10495
|
+
const keyed = keyedLayers2(spec);
|
|
10496
|
+
if (keyed.has(index)) return "moving";
|
|
10497
|
+
const sprite = l.sprite;
|
|
10498
|
+
if (nonZero(l.spin) || l.rock || l.pulse || l.grow || l.emit || sprite.cycle) return "moving";
|
|
10499
|
+
const m = l.motion;
|
|
10500
|
+
const moves = !!m.speed && nonZero(m.speed);
|
|
10501
|
+
switch (m.type) {
|
|
10502
|
+
case "static":
|
|
10503
|
+
return "static";
|
|
10504
|
+
case "drift":
|
|
10505
|
+
if (!moves) return (m.bob ?? 0) !== 0 ? "moving" : "static";
|
|
10506
|
+
return (m.bob ?? 0) !== 0 || m.bidirectional ? "moving" : "flow";
|
|
10507
|
+
case "rise":
|
|
10508
|
+
if (!moves) return (m.sway ?? 0) !== 0 ? "moving" : "static";
|
|
10509
|
+
return (m.sway ?? 0) !== 0 ? "moving" : "flow";
|
|
10510
|
+
case "warp":
|
|
10511
|
+
return moves ? "flow" : "static";
|
|
10512
|
+
default:
|
|
10513
|
+
return "moving";
|
|
10514
|
+
}
|
|
10515
|
+
}
|
|
10516
|
+
function keyedLayers2(spec) {
|
|
10517
|
+
const out = /* @__PURE__ */ new Set();
|
|
10518
|
+
if (!spec.timeline) return out;
|
|
10519
|
+
for (const path of timelineTracks(spec).keys()) {
|
|
10520
|
+
const [root, id] = path.split(".");
|
|
10521
|
+
if (root === "layers" && /^\d+$/.test(id ?? "")) out.add(Number(id));
|
|
10522
|
+
else if (root === "groups" && id !== void 0) spec.layers.forEach((l, i) => {
|
|
10523
|
+
if (l.group === id) out.add(i);
|
|
10524
|
+
});
|
|
10525
|
+
else out.add(-1);
|
|
10526
|
+
}
|
|
10527
|
+
return out;
|
|
10528
|
+
}
|
|
10529
|
+
function backgroundMotion(spec) {
|
|
10530
|
+
if (keyedLayers2(spec).has(-1)) return "moving";
|
|
10531
|
+
const bg = spec.background;
|
|
10532
|
+
if (!bg) return "static";
|
|
10533
|
+
const drift = bg.drift;
|
|
10534
|
+
if (bg.type === "field") return drift && drift.amount !== 0 ? "flow" : "static";
|
|
10535
|
+
return drift ? "flow" : "static";
|
|
10536
|
+
}
|
|
10537
|
+
function patternSampleTimes(spec) {
|
|
10538
|
+
const tl = spec.timeline;
|
|
10539
|
+
if (!tl) return [BASE_T];
|
|
10540
|
+
const lap = tl.loop && tl.duration ? tl.duration : 0;
|
|
10541
|
+
const settle = /* @__PURE__ */ new Set();
|
|
10542
|
+
for (const keys of timelineTracks(spec).values()) {
|
|
10543
|
+
for (const k of keys) {
|
|
10544
|
+
const end = k.t + Math.max(0, k.dur ?? DEFAULT_KEY_DUR);
|
|
10545
|
+
settle.add(lap ? end % lap : end);
|
|
10546
|
+
}
|
|
10547
|
+
}
|
|
10548
|
+
settle.delete(BASE_T);
|
|
10549
|
+
const ts = [...settle].sort((a, b) => a - b);
|
|
10550
|
+
const room = PATTERN_MAX_SAMPLE_TIMES - 1;
|
|
10551
|
+
const picked = ts.length <= room ? ts : Array.from({ length: room }, (_, i) => ts[Math.round((i + 1) * (ts.length - 1) / room)]);
|
|
10552
|
+
return [BASE_T, ...new Set(picked)];
|
|
10553
|
+
}
|
|
10554
|
+
var pct = (f) => `${Math.round(f * 100)}%`;
|
|
10555
|
+
function describeRun(run) {
|
|
10556
|
+
const p = run.pitch ?? 0;
|
|
10557
|
+
return `${run.count} light-dark pairs of stripes at a pitch of ${p.toFixed(3)} of the short side (about ${Math.round(p * 1080)} px at 1080p), luminance swing ${run.contrast.toFixed(2)}`;
|
|
10558
|
+
}
|
|
10559
|
+
var REMEDY = "To pass: lower the stripes' contrast (keep light and dark within 0.1 relative luminance of each other), use five or fewer stripes, break the regular spacing, or confine the pattern to a smaller part of the frame.";
|
|
10560
|
+
function patternHazardReport(spec, opts = {}) {
|
|
10561
|
+
const viewport = opts.viewport ?? { width: 1920, height: 1080 };
|
|
10562
|
+
const ro = { viewport, seed: opts.seed };
|
|
10563
|
+
const motion = spec.layers.map((_, i) => layerPatternMotion(spec, i));
|
|
10564
|
+
const bgMotion = backgroundMotion(spec);
|
|
10565
|
+
const anyMotion = bgMotion !== "static" || motion.some((m, i) => m !== "static" && !isText(spec.layers[i]));
|
|
10566
|
+
const best = { fails: false, kind: null, at: null, area: 0, movingArea: 0, flickerArea: 0, flickerHz: 0, pairs: 0, pitch: null, contrast: 0, rasters: 0 };
|
|
10567
|
+
let rasters = 0;
|
|
10568
|
+
for (const t of opts.times ?? patternSampleTimes(spec)) {
|
|
10569
|
+
const full = raster(prepared(spec, t, () => true), t, ro);
|
|
10570
|
+
rasters++;
|
|
10571
|
+
const runs = scanRaster(full);
|
|
10572
|
+
const area = patternArea(full, runs);
|
|
10573
|
+
const top = runs.reduce((a, b) => !a || b.count > a.count ? b : a, null);
|
|
10574
|
+
const found = {
|
|
10575
|
+
fails: false,
|
|
10576
|
+
kind: null,
|
|
10577
|
+
at: runs.length ? t : null,
|
|
10578
|
+
area,
|
|
10579
|
+
movingArea: 0,
|
|
10580
|
+
flickerArea: 0,
|
|
10581
|
+
flickerHz: 0,
|
|
10582
|
+
pairs: top?.count ?? 0,
|
|
10583
|
+
pitch: top?.pitch ?? null,
|
|
10584
|
+
contrast: top?.contrast ?? 0,
|
|
10585
|
+
rasters: 0
|
|
10586
|
+
};
|
|
10587
|
+
const where = `at t = ${Math.round(t)} ms`;
|
|
10588
|
+
if (top && area > PATTERN_STATIONARY_AREA) {
|
|
10589
|
+
return {
|
|
10590
|
+
...found,
|
|
10591
|
+
fails: true,
|
|
10592
|
+
kind: "pattern",
|
|
10593
|
+
rasters,
|
|
10594
|
+
message: `pattern hazard (photosensitivity): ${where} the frame shows a regular stripe pattern (${describeRun(top)}) over ${pct(area)} of the frame. ITU-R BT.1702 treats more than five light-dark pairs of such stripes as harmful above 40% of the screen when still, and above 25% when they move, turn or oscillate. ${REMEDY}`
|
|
10595
|
+
};
|
|
10596
|
+
}
|
|
10597
|
+
if (top && area > PATTERN_MOVING_AREA) {
|
|
10598
|
+
const movers = motion.map((m) => m === "moving");
|
|
10599
|
+
if (movers.some((m, i) => m && !isText(spec.layers[i])) || bgMotion === "moving") {
|
|
10600
|
+
const flat = bgMotion === "moving" ? void 0 : full.meanGround;
|
|
10601
|
+
const own = raster(prepared(spec, t, (_, i) => movers[i], flat), t, ro);
|
|
10602
|
+
rasters++;
|
|
10603
|
+
const ownRuns = scanRaster(own);
|
|
10604
|
+
found.movingArea = patternArea(own, ownRuns);
|
|
10605
|
+
if (found.movingArea > PATTERN_MOVING_AREA) {
|
|
10606
|
+
const ownTop = ownRuns.reduce((a, b) => !a || b.count > a.count ? b : a, null) ?? top;
|
|
10607
|
+
return {
|
|
10608
|
+
...found,
|
|
10609
|
+
fails: true,
|
|
10610
|
+
kind: "moving-pattern",
|
|
10611
|
+
rasters,
|
|
10612
|
+
message: `pattern hazard (photosensitivity): ${where} a regular stripe pattern (${describeRun(ownTop)}) that moves, turns or oscillates covers ${pct(found.movingArea)} of the frame. ITU-R BT.1702 limits a moving pattern of more than five light-dark pairs to 25% of the screen (40% when still). ${REMEDY} Or hold it still.`
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
if (anyMotion) {
|
|
10617
|
+
const later = raster(prepared(spec, t + DELTA_MS, () => true), t + DELTA_MS, ro);
|
|
10618
|
+
rasters++;
|
|
10619
|
+
const f = flicker(full, later, runs, DELTA_MS);
|
|
10620
|
+
found.flickerArea = f.area;
|
|
10621
|
+
found.flickerHz = f.hz;
|
|
10622
|
+
if (f.area > PATTERN_FLICKER_AREA) {
|
|
10623
|
+
return {
|
|
10624
|
+
...found,
|
|
10625
|
+
fails: true,
|
|
10626
|
+
kind: "flicker",
|
|
10627
|
+
rasters,
|
|
10628
|
+
message: `pattern hazard (photosensitivity): ${where} a regular stripe pattern (${describeRun(top)}) moves across its stripes fast enough that ${pct(f.area)} of the frame flickers more than ${FLASH_MAX_PER_SECOND} times a second (up to ${Math.round(f.hz)}): each point turns light and dark as the stripes pass, at speed \xF7 pitch. More than 3 a second over 25% of the frame is a flash hazard. Slow it (under 3 stripe pitches a second), widen the pitch, lower the contrast, or shrink the patterned area.`
|
|
10629
|
+
};
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
}
|
|
10633
|
+
if (area > best.area || best.at === null && found.at !== null) Object.assign(best, found);
|
|
10634
|
+
}
|
|
10635
|
+
best.rasters = rasters;
|
|
10636
|
+
return best;
|
|
10637
|
+
}
|
|
10638
|
+
|
|
9862
10639
|
// src/examples/aquarium.ts
|
|
9863
10640
|
var AQUARIUM_SPEC = {
|
|
9864
10641
|
schemaVersion: 1,
|
|
@@ -12376,6 +13153,14 @@ export {
|
|
|
12376
13153
|
MURMUR_SPEC,
|
|
12377
13154
|
NOSTALGHIA_CANDLE_SPEC,
|
|
12378
13155
|
ORRERY_SPEC,
|
|
13156
|
+
PATTERN_FLICKER_AREA,
|
|
13157
|
+
PATTERN_GRID_SHORT_SIDE,
|
|
13158
|
+
PATTERN_MAX_PITCH,
|
|
13159
|
+
PATTERN_MAX_SAMPLE_TIMES,
|
|
13160
|
+
PATTERN_MIN_EXTENT,
|
|
13161
|
+
PATTERN_MIN_PAIRS,
|
|
13162
|
+
PATTERN_MOVING_AREA,
|
|
13163
|
+
PATTERN_STATIONARY_AREA,
|
|
12379
13164
|
PHASE_DUET_SPEC,
|
|
12380
13165
|
PINGS_SPEC,
|
|
12381
13166
|
POLYGONS_SPEC,
|
|
@@ -12385,6 +13170,7 @@ export {
|
|
|
12385
13170
|
SAKURA_SPEC,
|
|
12386
13171
|
SCHEMA_EXAMPLES,
|
|
12387
13172
|
SCHEMA_VERSION,
|
|
13173
|
+
SEAMLESS_PERIOD_KINDS,
|
|
12388
13174
|
SHARD_FALL_SPEC,
|
|
12389
13175
|
SIGNAL_BOARD_SPEC,
|
|
12390
13176
|
SNOWFALL_SPEC,
|
|
@@ -12419,6 +13205,7 @@ export {
|
|
|
12419
13205
|
createFinishPass,
|
|
12420
13206
|
createSceneCanvas,
|
|
12421
13207
|
describeScene,
|
|
13208
|
+
describeStructuralDifferences,
|
|
12422
13209
|
diffScenes,
|
|
12423
13210
|
dominanceRanking,
|
|
12424
13211
|
drawSpan,
|
|
@@ -12448,6 +13235,7 @@ export {
|
|
|
12448
13235
|
latticeHash,
|
|
12449
13236
|
layerCellFootprints,
|
|
12450
13237
|
layerCohesion,
|
|
13238
|
+
layerPatternMotion,
|
|
12451
13239
|
lerpPathD,
|
|
12452
13240
|
lerpSpec,
|
|
12453
13241
|
lerpValue,
|
|
@@ -12465,6 +13253,8 @@ export {
|
|
|
12465
13253
|
normalizeSeed,
|
|
12466
13254
|
pathFrame,
|
|
12467
13255
|
pathParts,
|
|
13256
|
+
patternHazardReport,
|
|
13257
|
+
patternSampleTimes,
|
|
12468
13258
|
perceiveScene,
|
|
12469
13259
|
perceiveSequenceFrame,
|
|
12470
13260
|
periodWithoutTimeline,
|
|
@@ -12496,6 +13286,7 @@ export {
|
|
|
12496
13286
|
steerablePaths,
|
|
12497
13287
|
strokeDrawnSamples,
|
|
12498
13288
|
strokeLines,
|
|
13289
|
+
structuralDifferences,
|
|
12499
13290
|
structuralSignature,
|
|
12500
13291
|
textBlockAnchorOffset,
|
|
12501
13292
|
textBlockText,
|