@idle-screens/schema 3.16.0 → 3.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +12 -3
- package/dist/index.d.ts +19 -5
- package/dist/index.js +118 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/FORMAT.md
CHANGED
|
@@ -394,7 +394,8 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
|
|
|
394
394
|
`"evenodd"` are SVG's own rules — use them for pasted SVG, for overlap
|
|
395
395
|
patterns (a rosette of overlapping circles wants `evenodd`), and when a
|
|
396
396
|
same-colour shape sits inside another on purpose (`auto` would cut it).
|
|
397
|
-
- **`gradient`** replaces the flat colour of the fill
|
|
397
|
+
- **`gradient`** replaces the flat colour of the fill (the sprite still
|
|
398
|
+
needs a `color` or `colors`; a gradient does not stand in for it):
|
|
398
399
|
`{ "type": "linear", "from": [0, -1], "to": [0, 1], "stops": [{ "at": 0, "color": "#2c6a3a" }, { "at": 1, "color": "#7ab070", "opacity": 0.6 }] }`
|
|
399
400
|
or `{ "type": "radial", "center": [0, 0], "radius": 1, "focus": [-0.3, -0.4], "stops": [...] }`
|
|
400
401
|
(2..8 stops, `at` 0..1 non-decreasing, optional per-stop `opacity`).
|
|
@@ -424,7 +425,9 @@ for `"nearest"` and `"random"`; a `"chain"` may omit both, except that
|
|
|
424
425
|
(1..256), drawn back to front, each with its own `path`, `color`
|
|
425
426
|
(default: the entity's colour, so `colors` palettes still pick per
|
|
426
427
|
entity), `fillRule`, `fill`, `gradient`, `stroke` and `opacity`. With
|
|
427
|
-
`parts`, put that paint on the parts, never on the sprite.
|
|
428
|
+
`parts`, put that paint on the parts, never on the sprite. The sprite
|
|
429
|
+
still needs its own `color` (or a `colors` palette): parts without a
|
|
430
|
+
colour inherit it, and it is required even when every part sets one. One layer
|
|
428
431
|
holds a whole illustration, and a counted layer reuses it: a balloon with
|
|
429
432
|
gores, shading, ropes and basket × 7 drifting entities, all parts moving
|
|
430
433
|
as one.
|
|
@@ -1143,7 +1146,13 @@ moves and scales its positions and sizes (sprite rotation is not modelled).
|
|
|
1143
1146
|
`loop-seam` fires when a scene declares a looping timeline and the rest
|
|
1144
1147
|
of the scene repeats too, but on a period the lap does not divide — so
|
|
1145
1148
|
one lap can never come round to its own first frame (it names the period
|
|
1146
|
-
to make the lap a multiple of).
|
|
1149
|
+
to make the lap a multiple of). `spin`, `rock`, `pulse` and `grow` are left
|
|
1150
|
+
out of that period: they run on scene time and change smoothly, so nothing
|
|
1151
|
+
jumps at the wrap, and a lap off their period still loops seamlessly on
|
|
1152
|
+
screen (only an exact one-lap export would differ). Keyed paths are
|
|
1153
|
+
continuous at the wrap by construction — each lap enters from the last
|
|
1154
|
+
key's value — unless a key cuts there on purpose (`t: 0, dur: 0`) or a
|
|
1155
|
+
glide overruns the lap (`timeline-glide-overruns-lap` warns). A scene whose other motion never repeats
|
|
1147
1156
|
(drifting stars, wandering motes) is not judged: that is ambient by
|
|
1148
1157
|
design. `scenePeriod(spec)` is the analysis behind it — the least common
|
|
1149
1158
|
multiple of every periodic part (pulse, grow, rock, spin, cycle, orbit,
|
package/dist/index.d.ts
CHANGED
|
@@ -2757,17 +2757,31 @@ interface ScenePeriod {
|
|
|
2757
2757
|
period: number | null;
|
|
2758
2758
|
/** One line per part that sets or breaks the period, e.g. `layers[2] pulse: 4000 ms`. */
|
|
2759
2759
|
parts: string[];
|
|
2760
|
+
/** What drives each of `parts`, index for index: `pulse`, `grow`, `rock`, `spin`, `path`, `orbit`, `emit`, `cycle`, `caret`, `background`, `timeline`. */
|
|
2761
|
+
kinds: PeriodKind[];
|
|
2760
2762
|
/** Why it is null (empty otherwise). */
|
|
2761
2763
|
reasons: string[];
|
|
2762
2764
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
+
type PeriodKind = 'timeline' | 'background' | 'pulse' | 'grow' | 'rock' | 'emit' | 'spin' | 'cycle' | 'caret' | 'path' | 'orbit';
|
|
2766
|
+
/**
|
|
2767
|
+
* Periodic parts that run on scene time and change smoothly, so a looping
|
|
2768
|
+
* timeline's wrap never makes them jump: they carry on through it. A lap
|
|
2769
|
+
* that does not divide their period still loops seamlessly on screen — only
|
|
2770
|
+
* an exact export of one lap would differ (F145, X725, X823).
|
|
2771
|
+
*/
|
|
2772
|
+
declare const SEAMLESS_PERIOD_KINDS: ReadonlySet<PeriodKind>;
|
|
2773
|
+
/**
|
|
2774
|
+
* The repeat period of `spec` — see the module comment. `skip` leaves out the
|
|
2775
|
+
* periodic parts of those kinds, as if they were static (the loop-seam
|
|
2776
|
+
* advisory passes `SEAMLESS_PERIOD_KINDS`).
|
|
2777
|
+
*/
|
|
2778
|
+
declare function scenePeriod(spec: SaverSpec, skip?: ReadonlySet<PeriodKind>): ScenePeriod;
|
|
2765
2779
|
/**
|
|
2766
2780
|
* The period of everything in the scene except its timeline — what a
|
|
2767
2781
|
* looping timeline's lap has to be a multiple of for one lap to loop
|
|
2768
2782
|
* seamlessly.
|
|
2769
2783
|
*/
|
|
2770
|
-
declare function periodWithoutTimeline(spec: SaverSpec): ScenePeriod;
|
|
2784
|
+
declare function periodWithoutTimeline(spec: SaverSpec, skip?: ReadonlySet<PeriodKind>): ScenePeriod;
|
|
2771
2785
|
/**
|
|
2772
2786
|
* How many-fold rotationally symmetric a sprite's drawing is about the point
|
|
2773
2787
|
* `spin` turns it on: a spinning layer looks the same again after 360°/n,
|
|
@@ -3300,5 +3314,5 @@ declare function pathFrame(s: PolygonSprite, r: number, w: number, h: number): {
|
|
|
3300
3314
|
uniform: boolean;
|
|
3301
3315
|
};
|
|
3302
3316
|
|
|
3303
|
-
export { AQUARIUM_SPEC, AURORA_SPEC, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, DASHBOARD_SPEC, DEFAULT_KEY_DUR, DITHER_ALPHA, DITHER_TILE, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, GRAIN_ALPHA, GRAIN_TILE, HAIKU_SPEC, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, MATRIX_RAIN_SPEC, MAX_PATH_POINTS, MAX_SCENE_PERIOD, MOTION_FIELDS, MURMUR_SPEC, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, RAIN_SPEC, RELAY_BOARD_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, STEER_EASES, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, adviseSequence, adviseSpec, alphaAt, applyDeltasShared, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, buriedAreaOf, canMorph, canWrapMorph, canonicalSpecPath, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, drawSpan, easeGlide, easeSmooth, emitEnvelope, emitWindow, entityScales, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, flashSamplingForecast, flattenPath, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, ignoredPropertyPaths, isCombiningKana, isFullWidth, isPathPolygon, latticeHash, layerCellFootprints, layerCohesion, lerpPathD, lerpSpec, lerpValue, lifeAlphaAt, lifeStaggerAt, linkEdges, linkMaxDistPx, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, nextKeyAfter, normalizeSeed, pathFrame, pathParts, perceiveScene, perceiveSequenceFrame, periodWithoutTimeline, pickScreenOp, positionAt, presentWithFinish, readSpecPath, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, resolveTimelineAt, revealState, rgb255Luma, rotationAt, sceneColour, scenePeriod, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spinSymmetry, spriteIndexAt, spriteVariants, steerablePaths, strokeDrawnSamples, strokeLines, structuralSignature, textBlockAnchorOffset, textBlockText, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, timelineFlashReport, timelineLocalTime, timelineSampleTimes, timelineSampleTimesAfter, timelineTracks, trimLines, trimPolyline, validateSequence, validateSpec, validateSpecPaths, wipeMask, withoutTimeline };
|
|
3304
|
-
export type { BackgroundDrift, BackgroundSpec, BandSpec, CapabilityTier, CohesionRead, CycleSpec, DominanceEntry, Entity, FieldBackground, FieldDrift, FinishPass, FinishSpec, FlashSamplingForecast, FlattenedPath, GlideEase, GradientStop, IdleSequence, LayerCohesion, LayerGroup, LayerMotionStats, LayerSnapshot, LayerSpec, LayerTransform, LinkEdge, LuminanceGrid, LuminanceGridOptions, MotionEase, MotionSpec, PathGradient, PathGradientStop, PathPart, PathStroke, PerceiveOptions, PerceiveSequenceOptions, Placed, ResolveOptions, ResolvedPart, ResolvedSegment, RevealState, SaverSpec, SceneColour, SceneDescription, SceneDiff, ScenePerception, ScenePeriod, SceneSnapshot, SchemaExample, ScreenOp, SequenceExample, SequenceFramePerception, SequenceMountContext, SequenceSaverInstance, SequenceSaverPlugin, SequenceSegment, SequenceTransition, SpecError, SpecMountContext, SpecWarning, SpriteSpec, SteerDelta, StrokeDraw, TextBlockAnchor, TextBlockLine, TextMetricsClass, TextNumber, TextRevealInput, TextRevealSpec, TextRole, TextSpriteInfo, Timeline, TimelineFlashReport, TimelineKey, ValidationResult, WanderOsc, WarningBox, WipeTransition };
|
|
3317
|
+
export { AQUARIUM_SPEC, AURORA_SPEC, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, DASHBOARD_SPEC, DEFAULT_KEY_DUR, DITHER_ALPHA, DITHER_TILE, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, GRAIN_ALPHA, GRAIN_TILE, HAIKU_SPEC, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, MATRIX_RAIN_SPEC, MAX_PATH_POINTS, MAX_SCENE_PERIOD, MOTION_FIELDS, MURMUR_SPEC, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, RAIN_SPEC, RELAY_BOARD_SPEC, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SEAMLESS_PERIOD_KINDS, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, STEER_EASES, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, adviseSequence, adviseSpec, alphaAt, applyDeltasShared, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, buriedAreaOf, canMorph, canWrapMorph, canonicalSpecPath, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, drawSpan, easeGlide, easeSmooth, emitEnvelope, emitWindow, entityScales, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, flashSamplingForecast, flattenPath, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, ignoredPropertyPaths, isCombiningKana, isFullWidth, isPathPolygon, latticeHash, layerCellFootprints, layerCohesion, lerpPathD, lerpSpec, lerpValue, lifeAlphaAt, lifeStaggerAt, linkEdges, linkMaxDistPx, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, nextKeyAfter, normalizeSeed, pathFrame, pathParts, perceiveScene, perceiveSequenceFrame, periodWithoutTimeline, pickScreenOp, positionAt, presentWithFinish, readSpecPath, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, resolveTimelineAt, revealState, rgb255Luma, rotationAt, sceneColour, scenePeriod, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spinSymmetry, spriteIndexAt, spriteVariants, steerablePaths, strokeDrawnSamples, strokeLines, structuralSignature, textBlockAnchorOffset, textBlockText, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, timelineFlashReport, timelineLocalTime, timelineSampleTimes, timelineSampleTimesAfter, timelineTracks, trimLines, trimPolyline, validateSequence, validateSpec, validateSpecPaths, wipeMask, withoutTimeline };
|
|
3318
|
+
export type { BackgroundDrift, BackgroundSpec, BandSpec, CapabilityTier, CohesionRead, CycleSpec, DominanceEntry, Entity, FieldBackground, FieldDrift, FinishPass, FinishSpec, FlashSamplingForecast, FlattenedPath, GlideEase, GradientStop, IdleSequence, LayerCohesion, LayerGroup, LayerMotionStats, LayerSnapshot, LayerSpec, LayerTransform, LinkEdge, LuminanceGrid, LuminanceGridOptions, MotionEase, MotionSpec, PathGradient, PathGradientStop, PathPart, PathStroke, PerceiveOptions, PerceiveSequenceOptions, PeriodKind, Placed, ResolveOptions, ResolvedPart, ResolvedSegment, RevealState, SaverSpec, SceneColour, SceneDescription, SceneDiff, ScenePerception, ScenePeriod, SceneSnapshot, SchemaExample, ScreenOp, SequenceExample, SequenceFramePerception, SequenceMountContext, SequenceSaverInstance, SequenceSaverPlugin, SequenceSegment, SequenceTransition, SpecError, SpecMountContext, SpecWarning, SpriteSpec, SteerDelta, StrokeDraw, TextBlockAnchor, TextBlockLine, TextMetricsClass, TextNumber, TextRevealInput, TextRevealSpec, TextRole, TextSpriteInfo, Timeline, TimelineFlashReport, TimelineKey, ValidationResult, WanderOsc, WarningBox, WipeTransition };
|
package/dist/index.js
CHANGED
|
@@ -1602,6 +1602,11 @@ function pathDrawCost(s) {
|
|
|
1602
1602
|
return c;
|
|
1603
1603
|
}
|
|
1604
1604
|
|
|
1605
|
+
// src/validate-hints.ts
|
|
1606
|
+
var SCALAR_ONLY = /not a \[min,max\] range/;
|
|
1607
|
+
var SCALAR_BOUND = /^must be (?:>|an? (?:positive |single |finite |whole )?(?:number|integer)\b|between -?\d|-?\d+(?:\.\d+)?\.\.-?\d+(?:\.\d+)?(?: \(|$))/;
|
|
1608
|
+
var scalarWanted = (m) => SCALAR_ONLY.test(m) || !m.includes("[") && SCALAR_BOUND.test(m);
|
|
1609
|
+
|
|
1605
1610
|
// src/validate.ts
|
|
1606
1611
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
1607
1612
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -1734,6 +1739,15 @@ var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
|
1734
1739
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
1735
1740
|
var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
|
|
1736
1741
|
var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1742
|
+
var KNOWN_BG_STOP = /* @__PURE__ */ new Set(["at", "color"]);
|
|
1743
|
+
var KNOWN_BAND = /* @__PURE__ */ new Set(["color", "height"]);
|
|
1744
|
+
var KNOWN_BG_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1745
|
+
var KNOWN_PATH_STROKE = /* @__PURE__ */ new Set(["color", "width", "opacity", "join", "cap", "draw"]);
|
|
1746
|
+
var KNOWN_PATH_GRADIENT = {
|
|
1747
|
+
linear: /* @__PURE__ */ new Set(["type", "from", "to", "stops", "units"]),
|
|
1748
|
+
radial: /* @__PURE__ */ new Set(["type", "center", "radius", "focus", "stops", "units"])
|
|
1749
|
+
};
|
|
1750
|
+
var KNOWN_PATH_STOP = /* @__PURE__ */ new Set(["at", "color", "opacity"]);
|
|
1737
1751
|
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate", "rock"]);
|
|
1738
1752
|
function unknownKeys(obj, known) {
|
|
1739
1753
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
@@ -1751,6 +1765,50 @@ function positiveScalar(v, path, err, msg = "must be > 0") {
|
|
|
1751
1765
|
if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
|
|
1752
1766
|
else if (!isNum(v) || v <= 0) err(path, msg);
|
|
1753
1767
|
}
|
|
1768
|
+
function valueAtPath(root, path) {
|
|
1769
|
+
let v = root;
|
|
1770
|
+
for (const seg of path.match(/[^.[\]]+/g) ?? []) {
|
|
1771
|
+
if (Array.isArray(v) && /^\d+$/.test(seg)) v = v[Number(seg)];
|
|
1772
|
+
else if (isObj(v)) v = v[seg];
|
|
1773
|
+
else return void 0;
|
|
1774
|
+
}
|
|
1775
|
+
return v;
|
|
1776
|
+
}
|
|
1777
|
+
function echo(v) {
|
|
1778
|
+
if (isNum(v)) return String(v);
|
|
1779
|
+
if (Array.isArray(v) && v.length <= 4 && v.every(isNum)) return `[${v.join(", ")}]`;
|
|
1780
|
+
const s = JSON.stringify(v) ?? String(v);
|
|
1781
|
+
return s.length > 48 ? `${s.slice(0, 45)}\u2026` : s;
|
|
1782
|
+
}
|
|
1783
|
+
var RANGE_WANTED = /^must be a \[min,max\] range/;
|
|
1784
|
+
function shapeHint(spec, path, message) {
|
|
1785
|
+
if (message.includes(" \u2014 got ")) return "";
|
|
1786
|
+
const v = valueAtPath(spec, path);
|
|
1787
|
+
if (v === void 0) return "";
|
|
1788
|
+
if (RANGE_WANTED.test(message)) {
|
|
1789
|
+
if (isNum(v)) {
|
|
1790
|
+
const fits = message.includes("within 0..1") ? v >= 0 && v <= 1 : message.includes(">= 0") ? v >= 0 : !message.includes("positive") || v > 0;
|
|
1791
|
+
return fits ? ` \u2014 got ${v}; write [${v}, ${v}]` : ` \u2014 got ${v}; a range is two numbers, [min, max]`;
|
|
1792
|
+
}
|
|
1793
|
+
if (isObj(v) && ("min" in v || "max" in v)) {
|
|
1794
|
+
return ` \u2014 got {min, max}; write [${isNum(v.min) ? v.min : "min"}, ${isNum(v.max) ? v.max : "max"}]`;
|
|
1795
|
+
}
|
|
1796
|
+
if (Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] > v[1]) {
|
|
1797
|
+
return ` \u2014 got ${echo(v)}; the smaller number comes first: write [${v[1]}, ${v[0]}]`;
|
|
1798
|
+
}
|
|
1799
|
+
return ` \u2014 got ${echo(v)}`;
|
|
1800
|
+
}
|
|
1801
|
+
if (Array.isArray(v) && scalarWanted(message)) {
|
|
1802
|
+
const segs = path.match(/[^.[\]]+/g) ?? [];
|
|
1803
|
+
const key = segs[segs.length - 1] ?? path;
|
|
1804
|
+
const parentKey = segs[segs.length - 2];
|
|
1805
|
+
const parent = valueAtPath(spec, path.slice(0, path.lastIndexOf(".")));
|
|
1806
|
+
const kind = isObj(parent) && isStr(parent.kind) ? parent.kind : void 0;
|
|
1807
|
+
const noun = kind ? `${/^[aeiou]/i.test(kind) ? "an" : "a"} ${kind}'s \`${key}\`` : parentKey && !/^\d+$/.test(parentKey) && parentKey !== "sprite" ? `\`${parentKey}.${key}\`` : `\`${key}\``;
|
|
1808
|
+
return ` \u2014 got ${echo(v)}; ${noun} is one number`;
|
|
1809
|
+
}
|
|
1810
|
+
return "";
|
|
1811
|
+
}
|
|
1754
1812
|
function normalizeColors(spec) {
|
|
1755
1813
|
if (!isObj(spec) || !Array.isArray(spec.layers)) return;
|
|
1756
1814
|
for (const layer of spec.layers) {
|
|
@@ -1819,7 +1877,7 @@ function specPathsValid(spec, paths) {
|
|
|
1819
1877
|
function validateSpecCore(spec, checkApplied = true) {
|
|
1820
1878
|
const errors = [];
|
|
1821
1879
|
const warnings = [];
|
|
1822
|
-
const err = (path, message) => void errors.push({ path, message });
|
|
1880
|
+
const err = (path, message) => void errors.push({ path, message: message + shapeHint(spec, path, message) });
|
|
1823
1881
|
const warn = (path, code, message) => void warnings.push({ path, code, message });
|
|
1824
1882
|
if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }], warnings: [] };
|
|
1825
1883
|
if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
@@ -2048,9 +2106,12 @@ function validateTimelineKeys(spec, errOuter, warn, checkApplied) {
|
|
|
2048
2106
|
return;
|
|
2049
2107
|
}
|
|
2050
2108
|
}
|
|
2051
|
-
function color(v, path, err) {
|
|
2052
|
-
if (
|
|
2109
|
+
function color(v, path, err, missing = "is required (a hex colour like #0a3a52)") {
|
|
2110
|
+
if (v === void 0) err(path, missing);
|
|
2111
|
+
else if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
2053
2112
|
}
|
|
2113
|
+
var SPRITE_COLOR_MISSING = "`color` is required (or a `colors` palette)";
|
|
2114
|
+
var POLYGON_COLOR_MISSING = `${SPRITE_COLOR_MISSING}; parts without their own colour inherit it, and a gradient or stroke does not stand in for it`;
|
|
2054
2115
|
function validateFinish(f, path, err, warn) {
|
|
2055
2116
|
if (!isObj(f)) return err(path, "must be an object");
|
|
2056
2117
|
for (const k of ["grain", "dither"]) {
|
|
@@ -2075,6 +2136,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2075
2136
|
bg.stops.forEach((s, i) => {
|
|
2076
2137
|
if (!isObj(s) || !isNum(s.at) || s.at < 0 || s.at > 1) err(`background.stops[${i}].at`, "must be 0..1");
|
|
2077
2138
|
color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
|
|
2139
|
+
if (isObj(s)) for (const k of unknownKeys(s, KNOWN_BG_STOP)) warn(`background.stops[${i}].${k}`, "unknown-property", `unknown stop property '${k}' \u2014 will be ignored`);
|
|
2078
2140
|
});
|
|
2079
2141
|
}
|
|
2080
2142
|
if (bg.drift !== void 0) {
|
|
@@ -2086,6 +2148,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2086
2148
|
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount <= 0 || bg.drift.amount > LIMITS.maxDriftAmount)) {
|
|
2087
2149
|
err("background.drift.amount", `must be > 0 and <= ${LIMITS.maxDriftAmount}`);
|
|
2088
2150
|
}
|
|
2151
|
+
for (const k of unknownKeys(bg.drift, KNOWN_BG_DRIFT)) warn(`background.drift.${k}`, "unknown-property", `unknown drift property '${k}' \u2014 will be ignored`);
|
|
2089
2152
|
}
|
|
2090
2153
|
}
|
|
2091
2154
|
if (bg.band !== void 0) {
|
|
@@ -2093,6 +2156,7 @@ function validateBackground(bg, err, warn) {
|
|
|
2093
2156
|
else {
|
|
2094
2157
|
color(bg.band.color, "background.band.color", err);
|
|
2095
2158
|
positiveScalar(bg.band.height, "background.band.height", err);
|
|
2159
|
+
for (const k of unknownKeys(bg.band, KNOWN_BAND)) warn(`background.band.${k}`, "unknown-property", `unknown band property '${k}' \u2014 will be ignored`);
|
|
2096
2160
|
}
|
|
2097
2161
|
}
|
|
2098
2162
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
@@ -2500,19 +2564,19 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2500
2564
|
} else if (sprite.kind === "circle") {
|
|
2501
2565
|
knownSet = KNOWN_CIRCLE;
|
|
2502
2566
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2503
|
-
color(sprite.color, `${path}.color`, err);
|
|
2567
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2504
2568
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2505
2569
|
validatePalette(sprite, path, err);
|
|
2506
2570
|
} else if (sprite.kind === "ring") {
|
|
2507
2571
|
knownSet = KNOWN_RING;
|
|
2508
2572
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
2509
|
-
color(sprite.color, `${path}.color`, err);
|
|
2573
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2510
2574
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2511
2575
|
validatePalette(sprite, path, err);
|
|
2512
2576
|
} else if (sprite.kind === "streak") {
|
|
2513
2577
|
knownSet = KNOWN_STREAK;
|
|
2514
2578
|
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
|
|
2515
|
-
color(sprite.color, `${path}.color`, err);
|
|
2579
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2516
2580
|
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
2517
2581
|
validatePalette(sprite, path, err);
|
|
2518
2582
|
} else if (sprite.kind === "rect") {
|
|
@@ -2524,7 +2588,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2524
2588
|
if (sprite.feather !== void 0 && (!isNum(sprite.feather) || sprite.feather < 0 || sprite.feather > 1)) {
|
|
2525
2589
|
err(`${path}.feather`, "must be 0..1 (fraction of the half-size that fades out)");
|
|
2526
2590
|
}
|
|
2527
|
-
color(sprite.color, `${path}.color`, err);
|
|
2591
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2528
2592
|
validatePalette(sprite, path, err);
|
|
2529
2593
|
} else if (sprite.kind === "bar") {
|
|
2530
2594
|
knownSet = KNOWN_BAR;
|
|
@@ -2537,7 +2601,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2537
2601
|
if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
|
|
2538
2602
|
err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
|
|
2539
2603
|
}
|
|
2540
|
-
color(sprite.color, `${path}.color`, err);
|
|
2604
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2541
2605
|
validatePalette(sprite, path, err);
|
|
2542
2606
|
} else if (sprite.kind === "polygon") {
|
|
2543
2607
|
knownSet = KNOWN_POLYGON;
|
|
@@ -2581,7 +2645,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2581
2645
|
if (sprite.origin !== void 0 && !(Array.isArray(sprite.origin) && sprite.origin.length === 2 && sprite.origin.every((v) => isNum(v) && Math.abs(v) <= 1))) err(`${path}.origin`, "must be [x, y] in the unit box (-1..1): the point placed at the position");
|
|
2582
2646
|
}
|
|
2583
2647
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
2584
|
-
color(sprite.color, `${path}.color`, err);
|
|
2648
|
+
color(sprite.color, `${path}.color`, err, POLYGON_COLOR_MISSING);
|
|
2585
2649
|
validatePalette(sprite, path, err);
|
|
2586
2650
|
} else if (sprite.kind === "stroke") {
|
|
2587
2651
|
knownSet = KNOWN_STROKE;
|
|
@@ -2592,7 +2656,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2592
2656
|
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
2593
2657
|
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
2594
2658
|
if (sprite.draw !== void 0) validateDraw(sprite.draw, `${path}.draw`, err, warn);
|
|
2595
|
-
color(sprite.color, `${path}.color`, err);
|
|
2659
|
+
color(sprite.color, `${path}.color`, err, SPRITE_COLOR_MISSING);
|
|
2596
2660
|
validatePalette(sprite, path, err);
|
|
2597
2661
|
} else if (sprite.kind === "textBlock") {
|
|
2598
2662
|
knownSet = KNOWN_TEXT_BLOCK;
|
|
@@ -2903,10 +2967,10 @@ function validatePathPiece(piece, path, err, warn) {
|
|
|
2903
2967
|
function validatePiecePaint(piece, path, err, warn) {
|
|
2904
2968
|
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2905
2969
|
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);
|
|
2970
|
+
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err, warn);
|
|
2907
2971
|
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
|
|
2908
2972
|
}
|
|
2909
|
-
function validatePathGradient(g, path, err) {
|
|
2973
|
+
function validatePathGradient(g, path, err, warn) {
|
|
2910
2974
|
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
2911
2975
|
if (g.type === "linear") {
|
|
2912
2976
|
if (!isPair(g.from)) err(`${path}.from`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
@@ -2918,6 +2982,7 @@ function validatePathGradient(g, path, err) {
|
|
|
2918
2982
|
} else {
|
|
2919
2983
|
return err(`${path}.type`, "must be 'linear' | 'radial'");
|
|
2920
2984
|
}
|
|
2985
|
+
for (const k of unknownKeys(g, KNOWN_PATH_GRADIENT[g.type])) warn(`${path}.${k}`, "unknown-property", `unknown ${g.type} gradient property '${k}' \u2014 will be ignored`);
|
|
2921
2986
|
if (g.units !== void 0 && g.units !== "box" && g.units !== "part") err(`${path}.units`, "must be 'box' (the drawing's unit box, default) | 'part' (the part's own box, -1..1)");
|
|
2922
2987
|
if (!Array.isArray(g.stops) || g.stops.length < 2 || g.stops.length > 8) return err(`${path}.stops`, "must be 2..8 stops");
|
|
2923
2988
|
let last = -Infinity;
|
|
@@ -2927,16 +2992,18 @@ function validatePathGradient(g, path, err) {
|
|
|
2927
2992
|
else last = st.at;
|
|
2928
2993
|
color(st.color, `${path}.stops[${i}].color`, err);
|
|
2929
2994
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
2995
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STOP)) warn(`${path}.stops[${i}].${k}`, "unknown-property", `unknown stop property '${k}' \u2014 will be ignored`);
|
|
2930
2996
|
});
|
|
2931
2997
|
}
|
|
2932
2998
|
function validatePathStroke(st, path, err, warn) {
|
|
2933
2999
|
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap?, draw? }");
|
|
2934
|
-
color(st.color, `${path}.color`, err);
|
|
3000
|
+
color(st.color, `${path}.color`, err, "`color` is required on a stroke (a hex colour like #0a3a52); a stroke does not inherit the entity colour");
|
|
2935
3001
|
if (!isNum(st.width) || st.width <= 0 || st.width > 0.5) err(`${path}.width`, "must be > 0 and <= 0.5 (unit-box units: 0.01 is 1% of the drawing's half-size)");
|
|
2936
3002
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2937
3003
|
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2938
3004
|
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2939
3005
|
if (st.draw !== void 0) validateDraw(st.draw, `${path}.draw`, err, warn);
|
|
3006
|
+
for (const k of unknownKeys(st, KNOWN_PATH_STROKE)) warn(`${path}.${k}`, "unknown-property", `unknown stroke property '${k}' \u2014 will be ignored`);
|
|
2940
3007
|
}
|
|
2941
3008
|
function validateWipe(tr, path, err, warn) {
|
|
2942
3009
|
if (tr.shape !== void 0 && tr.shape !== "circle" && tr.shape !== "line") err(`${path}.shape`, "must be 'circle' | 'line'");
|
|
@@ -3566,6 +3633,7 @@ function lifeStaggerAt(life, t, i) {
|
|
|
3566
3633
|
}
|
|
3567
3634
|
function wrap(v, min, max) {
|
|
3568
3635
|
const range2 = max - min;
|
|
3636
|
+
if (range2 <= 0) return min;
|
|
3569
3637
|
return ((v - min) % range2 + range2) % range2 + min;
|
|
3570
3638
|
}
|
|
3571
3639
|
function reflect(v, min, max) {
|
|
@@ -5430,14 +5498,14 @@ var SpecInstance = class {
|
|
|
5430
5498
|
* entity as it was at `t` (rotation, heading, values), but at the echo's
|
|
5431
5499
|
* position, size and alpha.
|
|
5432
5500
|
*/
|
|
5433
|
-
drawEntity(built, e, t, lifeA, parentE, paint,
|
|
5501
|
+
drawEntity(built, e, t, lifeA, parentE, paint, echo2) {
|
|
5434
5502
|
const { ctx } = this;
|
|
5435
|
-
const p =
|
|
5503
|
+
const p = echo2 ? { x: echo2.x, y: echo2.y, flip: false } : this.entityPos(e, t, parentE);
|
|
5436
5504
|
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
5437
|
-
const sz =
|
|
5505
|
+
const sz = echo2 ? echo2.size : sizeAt(e, t);
|
|
5438
5506
|
const rot = rotationAt(e, t);
|
|
5439
5507
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
5440
|
-
ctx.globalAlpha =
|
|
5508
|
+
ctx.globalAlpha = echo2 ? echo2.alpha : alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
5441
5509
|
if (sprite.kind === "ring") {
|
|
5442
5510
|
const r = sz / 2;
|
|
5443
5511
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -8841,6 +8909,7 @@ function flashSamplingForecast(spec) {
|
|
|
8841
8909
|
|
|
8842
8910
|
// src/period.ts
|
|
8843
8911
|
var MAX_SCENE_PERIOD = 36e5;
|
|
8912
|
+
var SEAMLESS_PERIOD_KINDS = /* @__PURE__ */ new Set(["spin", "rock", "pulse", "grow"]);
|
|
8844
8913
|
var PERIOD_LEAF = /(^|\.)(period|rate|every|duration|spin|speed|blink|drift)$/;
|
|
8845
8914
|
var gcd = (a, b) => {
|
|
8846
8915
|
while (b) [a, b] = [b, a % b];
|
|
@@ -8858,27 +8927,27 @@ function toRatio(ms) {
|
|
|
8858
8927
|
return null;
|
|
8859
8928
|
}
|
|
8860
8929
|
var range = (r) => r === void 0 ? null : typeof r === "number" ? r : r[0] === r[1] ? r[0] : NaN;
|
|
8861
|
-
function scenePeriod(spec) {
|
|
8930
|
+
function scenePeriod(spec, skip) {
|
|
8862
8931
|
const parts = [];
|
|
8863
8932
|
const reasons = [];
|
|
8864
|
-
const add = (label, ms) => {
|
|
8865
|
-
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
8933
|
+
const add = (label, ms, kind) => {
|
|
8934
|
+
if (!(ms > 0) || !Number.isFinite(ms) || skip?.has(kind)) return;
|
|
8866
8935
|
const ratio = toRatio(ms);
|
|
8867
8936
|
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]]);
|
|
8937
|
+
else parts.push([label, ratio[0], ratio[1], kind]);
|
|
8869
8938
|
};
|
|
8870
8939
|
const tl = spec.timeline;
|
|
8871
8940
|
if (tl && tl.keys.length > 0) {
|
|
8872
8941
|
const keyed = tl.keys.filter((k) => PERIOD_LEAF.test(String(k.path)));
|
|
8873
8942
|
if (keyed.length > 0) reasons.push(`timeline keys animate ${[...new Set(keyed.map((k) => String(k.path)))].join(", ")}, which sets the scene's own period`);
|
|
8874
|
-
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration);
|
|
8943
|
+
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration, "timeline");
|
|
8875
8944
|
else reasons.push("timeline does not loop (it plays once, then holds)");
|
|
8876
8945
|
}
|
|
8877
8946
|
const bg = spec.background;
|
|
8878
8947
|
if (bg && bg.type !== "solid" && bg.drift && !(bg.type === "field" && bg.drift.amount === 0)) {
|
|
8879
8948
|
const { period } = bg.drift;
|
|
8880
8949
|
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);
|
|
8950
|
+
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period, "background");
|
|
8882
8951
|
}
|
|
8883
8952
|
if (spec.finish?.animate && ((spec.finish.grain ?? 0) > 0 || (spec.finish.dither ?? 0) > 0)) reasons.push("finish.animate steps the grain without repeating");
|
|
8884
8953
|
spec.layers.forEach((layer, i) => {
|
|
@@ -8887,53 +8956,58 @@ function scenePeriod(spec) {
|
|
|
8887
8956
|
const life = layer.life;
|
|
8888
8957
|
const staggered = (life?.stagger ?? 0) > 0;
|
|
8889
8958
|
if (life && (life.exit !== void 0 || life.enter !== void 0 && ((life.enter ?? 0) > 0 || (life.fade ?? 1e3) > 0 || staggered))) reasons.push(`${at} life builds in or out once`);
|
|
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);
|
|
8959
|
+
if (layer.pulse && layer.pulse.amp !== 0) add(`${at} pulse`, layer.pulse.period / rate, "pulse");
|
|
8960
|
+
if (layer.grow) add(`${at} grow`, layer.grow.period / rate, "grow");
|
|
8961
|
+
if (layer.rock) add(`${at} rock`, layer.rock.period / rate, "rock");
|
|
8962
|
+
if (layer.emit) add(`${at} emit`, layer.emit.every, "emit");
|
|
8894
8963
|
const spin = range(layer.spin);
|
|
8895
8964
|
const fold = spin !== null && spin !== 0 ? spinSymmetry(layer.sprite) : 1;
|
|
8896
8965
|
if (spin !== null && fold !== Infinity) {
|
|
8897
8966
|
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);
|
|
8967
|
+
else if (spin !== 0) add(`${at} spin${fold > 1 ? ` (${fold}-fold)` : ""}`, 36e4 / Math.abs(spin) / fold, "spin");
|
|
8899
8968
|
}
|
|
8900
8969
|
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);
|
|
8970
|
+
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate, "cycle");
|
|
8902
8971
|
if (sp.kind === "textBlock" && sp.reveal) {
|
|
8903
8972
|
if ((sp.reveal.speed ?? 0) > 0) reasons.push(`${at} reveal types itself once`);
|
|
8904
8973
|
if (sp.reveal.caret) {
|
|
8905
8974
|
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);
|
|
8975
|
+
if (hz > 0) add(`${at} caret`, 1e3 / hz, "caret");
|
|
8907
8976
|
}
|
|
8908
8977
|
}
|
|
8909
8978
|
const m = layer.motion;
|
|
8910
|
-
if (m.type === "path") add(`${at} path`, m.duration);
|
|
8979
|
+
if (m.type === "path") add(`${at} path`, m.duration, "path");
|
|
8911
8980
|
else if (m.type === "orbit") {
|
|
8912
8981
|
const s = range(m.speed);
|
|
8913
8982
|
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));
|
|
8983
|
+
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s), "orbit");
|
|
8915
8984
|
} else if (m.type !== "static") {
|
|
8916
8985
|
if (m.type === "drift" && m.ease?.type === "settle" && !layer.emit) reasons.push(`${at} drift settles once`);
|
|
8917
8986
|
else reasons.push(`${at} ${m.type} motion travels at seeded per-entity speeds`);
|
|
8918
8987
|
}
|
|
8919
8988
|
});
|
|
8920
8989
|
const labelled = parts.map(([l, n, d]) => `${l}: ${Number((n / d).toFixed(3))} ms`);
|
|
8921
|
-
|
|
8922
|
-
if (
|
|
8990
|
+
const kinds = parts.map(([, , , k]) => k);
|
|
8991
|
+
if (reasons.length > 0) return { period: null, parts: labelled, kinds, reasons };
|
|
8992
|
+
const combined = combinePeriods(parts);
|
|
8993
|
+
return { ...combined, parts: labelled, kinds };
|
|
8994
|
+
}
|
|
8995
|
+
function combinePeriods(parts) {
|
|
8996
|
+
if (parts.length === 0) return { period: 0, reasons: [] };
|
|
8923
8997
|
let num = 1;
|
|
8924
8998
|
let den = 0;
|
|
8925
8999
|
for (const [, n, d] of parts) {
|
|
8926
9000
|
num = num / gcd(num, n) * n;
|
|
8927
9001
|
den = gcd(den, d);
|
|
8928
9002
|
if (num / den > MAX_SCENE_PERIOD) {
|
|
8929
|
-
return { period: null,
|
|
9003
|
+
return { period: null, reasons: [`its parts share no cycle shorter than ${MAX_SCENE_PERIOD / 6e4} min`] };
|
|
8930
9004
|
}
|
|
8931
9005
|
}
|
|
8932
|
-
if (num % den !== 0) return { period: null,
|
|
8933
|
-
return { period: num / den,
|
|
9006
|
+
if (num % den !== 0) return { period: null, reasons: [`its parts repeat together every ${Number((num / den).toFixed(3))} ms, not a whole number of milliseconds`] };
|
|
9007
|
+
return { period: num / den, reasons: [] };
|
|
8934
9008
|
}
|
|
8935
|
-
function periodWithoutTimeline(spec) {
|
|
8936
|
-
return scenePeriod(withoutTimeline(spec));
|
|
9009
|
+
function periodWithoutTimeline(spec, skip) {
|
|
9010
|
+
return scenePeriod(withoutTimeline(spec), skip);
|
|
8937
9011
|
}
|
|
8938
9012
|
var MAX_SYMMETRY = 64;
|
|
8939
9013
|
var MAX_SYMMETRY_POINTS = 8192;
|
|
@@ -9270,14 +9344,15 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
9270
9344
|
if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
|
|
9271
9345
|
const lap = Math.round(authored.timeline.duration);
|
|
9272
9346
|
const rest = periodWithoutTimeline(authored);
|
|
9273
|
-
|
|
9347
|
+
const seam = rest.period !== null && rest.period > 0 && lap % rest.period !== 0 ? periodWithoutTimeline(authored, SEAMLESS_PERIOD_KINDS) : null;
|
|
9348
|
+
if (seam && seam.period !== null && seam.period > 0 && lap % seam.period !== 0) {
|
|
9274
9349
|
let a = lap;
|
|
9275
|
-
let b =
|
|
9350
|
+
let b = seam.period;
|
|
9276
9351
|
while (b) [a, b] = [b, a % b];
|
|
9277
9352
|
warnings.push({
|
|
9278
9353
|
path: "timeline.duration",
|
|
9279
9354
|
code: "loop-seam",
|
|
9280
|
-
message: `the timeline loops every ${lap} ms but
|
|
9355
|
+
message: `the timeline loops every ${lap} ms but ${seam.parts.join(", ")} repeat${seam.parts.length === 1 ? "s" : ""} every ${seam.period} ms, so the scene only comes round to its first frame every ${lap / a * seam.period} ms \u2014 make the lap a multiple of ${seam.period} ms for one lap to loop`
|
|
9281
9356
|
});
|
|
9282
9357
|
}
|
|
9283
9358
|
}
|
|
@@ -12385,6 +12460,7 @@ export {
|
|
|
12385
12460
|
SAKURA_SPEC,
|
|
12386
12461
|
SCHEMA_EXAMPLES,
|
|
12387
12462
|
SCHEMA_VERSION,
|
|
12463
|
+
SEAMLESS_PERIOD_KINDS,
|
|
12388
12464
|
SHARD_FALL_SPEC,
|
|
12389
12465
|
SIGNAL_BOARD_SPEC,
|
|
12390
12466
|
SNOWFALL_SPEC,
|