@glissade/scene 0.60.0-pre.1 → 0.61.0-pre.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/dist/describe.d.ts +9 -5
- package/dist/describe.js +23 -1
- package/dist/diagnostics.d.ts +98 -2
- package/dist/diagnostics.js +386 -1
- package/dist/motion.js +2 -121
- package/dist/shake.js +123 -0
- package/package.json +2 -2
package/dist/describe.d.ts
CHANGED
|
@@ -157,12 +157,16 @@ interface SurfaceEntry {
|
|
|
157
157
|
* `'value'` = a runtime binding on the bundle (a class / function / object);
|
|
158
158
|
* `'type'` = a TS type-only name that erases at runtime (opaque, referenced by
|
|
159
159
|
* signatures); `'diagnostic'` = a runtime AUTHORING-DIAGNOSTIC function
|
|
160
|
-
* (`critique`/`validateScene`/`resolveAt`/`instanceProps
|
|
161
|
-
* `window.glissade.<name>` callable that
|
|
162
|
-
* scene-building surface
|
|
163
|
-
*
|
|
160
|
+
* (`critique`/`validateScene`/`resolveAt`/`instanceProps`/`exportFidelity`) — a
|
|
161
|
+
* real `window.glissade.<name>` callable that reports PROBLEMS (self-check tooling),
|
|
162
|
+
* not scene-building surface; `'tool'` (0.61) = a runtime OPERATION function
|
|
163
|
+
* (`diff`) that transforms/compares scenes and returns a RESULT (a ChangeSet), not
|
|
164
|
+
* a problem list — distinct from a diagnostic so a consumer never misuses its
|
|
165
|
+
* output as a defect report. A scalable CATEGORY (diagnostics=problems /
|
|
166
|
+
* tools=operations / values=authoring surface): an agent BUILDING a scene filters
|
|
167
|
+
* `kind === 'value'`; a self-check agent filters `kind === 'diagnostic'`.
|
|
164
168
|
*/
|
|
165
|
-
kind: 'value' | 'type' | 'diagnostic';
|
|
169
|
+
kind: 'value' | 'type' | 'diagnostic' | 'tool';
|
|
166
170
|
/** `true` when it is reachable as `window.glissade.<name>` on the single-file IIFE bundle. */
|
|
167
171
|
iife: boolean;
|
|
168
172
|
/** How to consume it: `'constructor'` needs `new`, `'function'` is a plain call, `'object'` is a value namespace (e.g. `easings`), `'type'` is type-only. */
|
package/dist/describe.js
CHANGED
|
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
|
|
|
23
23
|
* never pulled onto the base embed path — a scene that never calls `describe()`
|
|
24
24
|
* pays zero bytes for it.
|
|
25
25
|
*/
|
|
26
|
-
const RAW_VERSION = "0.
|
|
26
|
+
const RAW_VERSION = "0.61.0-pre.0";
|
|
27
27
|
const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
|
|
28
28
|
/**
|
|
29
29
|
* Parse the documented positional-arg count from a helper `usage` string — the
|
|
@@ -207,9 +207,24 @@ const SURFACE_DIAGNOSTICS = [
|
|
|
207
207
|
{
|
|
208
208
|
name: "instanceProps",
|
|
209
209
|
arity: 1
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "exportFidelity",
|
|
213
|
+
arity: 1
|
|
210
214
|
}
|
|
211
215
|
];
|
|
212
216
|
/**
|
|
217
|
+
* 0.61 machine-readable TOOL functions on `window.glissade` — runtime OPERATIONS
|
|
218
|
+
* that transform/compare scenes and return a RESULT, not a problem list. `diff(a,b)`
|
|
219
|
+
* returns a ChangeSet (the blast-radius of an edit). Marked `kind:'tool'` so a
|
|
220
|
+
* consumer never treats its output as a diagnostic (no severity/defect semantics).
|
|
221
|
+
* `arity` = the documented required positional-arg count.
|
|
222
|
+
*/
|
|
223
|
+
const SURFACE_TOOLS = [{
|
|
224
|
+
name: "diff",
|
|
225
|
+
arity: 2
|
|
226
|
+
}];
|
|
227
|
+
/**
|
|
213
228
|
* The opaque, type-ONLY names the API surface references (they erase at runtime —
|
|
214
229
|
* `window.glissade.Paint` is `undefined`). `gs types --global` emits a best-effort
|
|
215
230
|
* alias per name; `gs describe --lint` guards they stay type-only (a type surfaced
|
|
@@ -270,6 +285,13 @@ function buildSurface() {
|
|
|
270
285
|
form: "function",
|
|
271
286
|
arity: d.arity
|
|
272
287
|
});
|
|
288
|
+
for (const t of SURFACE_TOOLS) out.push({
|
|
289
|
+
name: t.name,
|
|
290
|
+
kind: "tool",
|
|
291
|
+
iife: true,
|
|
292
|
+
form: "function",
|
|
293
|
+
arity: t.arity
|
|
294
|
+
});
|
|
273
295
|
for (const name of SURFACE_TYPE_ONLY) out.push({
|
|
274
296
|
name,
|
|
275
297
|
kind: "type",
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -178,7 +178,7 @@ type DiagnosticSeverity = 'error' | 'warning' | 'info';
|
|
|
178
178
|
* a built Scene structurally cannot contain a duplicate id, so validateScene
|
|
179
179
|
* never reaches this case. Kept for the shared contract / `gs parity` surface.
|
|
180
180
|
*/
|
|
181
|
-
type DiagnosticCode = 'UNKNOWN_TARGET' | 'ID_COLLISION' | 'OFF_CANVAS' | 'YOGA_CHILD_POSITION' | 'MEASURER_FALLBACK' | 'TEXT_OVERFLOW' | 'OCCLUSION';
|
|
181
|
+
type DiagnosticCode = 'UNKNOWN_TARGET' | 'ID_COLLISION' | 'OFF_CANVAS' | 'YOGA_CHILD_POSITION' | 'MEASURER_FALLBACK' | 'TEXT_OVERFLOW' | 'OCCLUSION' | 'RENDER_ONLY_EXPORT' | 'LOTTIE_DROP' | 'LOTTIE_APPROXIMATE' | 'ANCHOR_RECENTER' | 'UNEXPLAINED_RESIDUAL' | 'BACKEND_DIVERGE';
|
|
182
182
|
/**
|
|
183
183
|
* 0.60: which enforcement SURFACE produced a diagnostic — distinguishes a CERTAIN
|
|
184
184
|
* static fact (`validateScene`) from a HEURISTIC rendered judgment (`critique`,
|
|
@@ -327,4 +327,100 @@ declare function critique(scene: Scene, timeline: Timeline, opts?: CritiqueOptio
|
|
|
327
327
|
*/
|
|
328
328
|
declare function sortDiagnostics(diags: SceneDiagnostic[]): SceneDiagnostic[];
|
|
329
329
|
//#endregion
|
|
330
|
-
|
|
330
|
+
//#region src/diff.d.ts
|
|
331
|
+
/** A node reference in the added/removed lists — its id + node type. */
|
|
332
|
+
interface NodeRef {
|
|
333
|
+
/** the node id (only id-bearing nodes are diffed by identity). */
|
|
334
|
+
node: string;
|
|
335
|
+
/** the node's `describeType` (e.g. `'Rect'`, `'Text'`). */
|
|
336
|
+
type: string;
|
|
337
|
+
}
|
|
338
|
+
/** A composed-world bbox (rendered layer), same shape as critique's geometry. */
|
|
339
|
+
interface Region {
|
|
340
|
+
minX: number;
|
|
341
|
+
minY: number;
|
|
342
|
+
maxX: number;
|
|
343
|
+
maxY: number;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* One change. The `op` tags WHAT KIND of change it is:
|
|
347
|
+
* - `'moved'` — a node was RE-PARENTED (structural) or its rendered box shifted
|
|
348
|
+
* (rendered layer). `from`/`to` are the parent ids (structural) or bboxes.
|
|
349
|
+
* - `'retargeted'` — a track now points at a different target (its keys are
|
|
350
|
+
* unchanged; only `target` moved). `from`/`to` are the old/new target strings.
|
|
351
|
+
* - `'changed'` — a node prop value, a track's keys, a track's presence, or a
|
|
352
|
+
* rendered command field changed. `from`/`to` are the old/new values.
|
|
353
|
+
*/
|
|
354
|
+
type ChangeOp = 'moved' | 'retargeted' | 'changed';
|
|
355
|
+
interface Change {
|
|
356
|
+
op: ChangeOp;
|
|
357
|
+
/** the node id this change concerns (node-level changes). */
|
|
358
|
+
node?: string;
|
|
359
|
+
/** the track/prop target this change concerns (track-level + prop changes). */
|
|
360
|
+
target?: string;
|
|
361
|
+
/** the specific property that changed (e.g. `'position'`, `'keys'`, `'type'`). */
|
|
362
|
+
property?: string;
|
|
363
|
+
/** the value BEFORE (in `a`). */
|
|
364
|
+
from: unknown;
|
|
365
|
+
/** the value AFTER (in `b`). */
|
|
366
|
+
to: unknown;
|
|
367
|
+
/** rendered layer: the composed-world region the change occupies. */
|
|
368
|
+
region?: Region;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The typed change-set `diff(a, b)` returns. Programmatically checkable — an agent
|
|
372
|
+
* asserts `diff == exactly my intended change-set`, never parses a text blob.
|
|
373
|
+
* `empty` is a convenience (`added=removed=changed=[]`), so `diff(a,a).empty` is
|
|
374
|
+
* the one-liner clean check.
|
|
375
|
+
*/
|
|
376
|
+
interface ChangeSet {
|
|
377
|
+
/** nodes present in `b` but not `a` (by id). */
|
|
378
|
+
added: NodeRef[];
|
|
379
|
+
/** nodes present in `a` but not `b` (by id). */
|
|
380
|
+
removed: NodeRef[];
|
|
381
|
+
/** every moved / retargeted / changed entry, canonically sorted. */
|
|
382
|
+
changed: Change[];
|
|
383
|
+
/** true iff `added`, `removed`, and `changed` are all empty. */
|
|
384
|
+
empty: boolean;
|
|
385
|
+
}
|
|
386
|
+
/** The two sides of a diff — a bound-or-buildable scene + its optional document. */
|
|
387
|
+
interface DiffInput {
|
|
388
|
+
scene: Scene;
|
|
389
|
+
timeline?: Timeline;
|
|
390
|
+
}
|
|
391
|
+
interface DiffOptions {
|
|
392
|
+
/**
|
|
393
|
+
* ALSO run the rendered (DisplayList) layer at `at` — surfaces VISUAL deltas the
|
|
394
|
+
* semantic structural layer ignores (a paint that moved, a color that changed
|
|
395
|
+
* mid-flight). Default false (structural only).
|
|
396
|
+
*/
|
|
397
|
+
rendered?: boolean;
|
|
398
|
+
/** the time (seconds) the rendered layer samples. Default 0. */
|
|
399
|
+
at?: number;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Diff two scenes (+ optional timelines) into a typed {@link ChangeSet}. Default
|
|
403
|
+
* is the SEMANTIC structural layer (node tree + timeline); pass `{ rendered: true }`
|
|
404
|
+
* to also diff the DisplayList at `at`. Pure read; `diff(a, a)` is EMPTY.
|
|
405
|
+
*/
|
|
406
|
+
declare function diff(a: DiffInput, b: DiffInput, opts?: DiffOptions): ChangeSet;
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/fidelity.d.ts
|
|
409
|
+
/** exportFidelity result — the CLI-lint `{ hasErrors, diagnostics }` shape. */
|
|
410
|
+
interface ExportFidelityResult {
|
|
411
|
+
schemaVersion: typeof DIAGNOSTIC_SCHEMA_VERSION;
|
|
412
|
+
/** true iff any diagnostic is severity `error` — always false here (render-only
|
|
413
|
+
* use is a warning, never a build error), kept for shape-parity with the family. */
|
|
414
|
+
hasErrors: boolean;
|
|
415
|
+
/** every RENDER_ONLY_EXPORT finding, canonically sorted. */
|
|
416
|
+
diagnostics: SceneDiagnostic[];
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Statically scan `scene` (+ optional `timeline`, for reveal-track detection) for
|
|
420
|
+
* render-only features that won't survive Lottie export, aggregating one
|
|
421
|
+
* RENDER_ONLY_EXPORT warning per affected node. Pure read; a scene with no
|
|
422
|
+
* render-only feature returns an EMPTY diagnostics list.
|
|
423
|
+
*/
|
|
424
|
+
declare function exportFidelity(scene: Scene, timeline?: Timeline): ExportFidelityResult;
|
|
425
|
+
//#endregion
|
|
426
|
+
export { type CacheColdResult, type Change, type ChangeOp, type ChangeSet, type CommandDelta, type CritiqueOptions, type CritiqueResult, DIAGNOSTIC_SCHEMA_VERSION, DL_SNAPSHOT_VERSION, type DiagnosticCode, type DiagnosticSeverity, type DiagnosticSource, type DiffInput, type DiffOptions, type DisplayDiff, type DlSnapshot, DlSnapshotError, type ExportFidelityResult, type FieldChange, type FontByteLoader, type InstancePropState, type NodeRef, type Region, type SceneDiagnostic, type ValidateSceneFontsOptions, type ValidateSceneResult, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, critique, diff, diffDisplayLists, exportFidelity, formatDisplayDiff, instanceProps, levenshtein, nearestId, parseDisplaySnapshot, resolveAt, serializeDisplayList, sortDiagnostics, validateScene, validateSceneFonts };
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { I as isEstimatingMeasurer, J as collapseReplacer, P as estimatingMeasurer, R as quantize, W as createDisplayListBuilder, Y as IDENTITY, et as multiply, r as Group, s as Text } from "./nodes.js";
|
|
2
2
|
import { a as evaluate, r as bindScene } from "./scene.js";
|
|
3
3
|
import { emitWithIds } from "./identity.js";
|
|
4
|
+
import { i as shakenSpec } from "./shake.js";
|
|
4
5
|
import { buildFontRegistry, compileTimeline, evaluateAt, parseCmap, parseColor, untracked, validateFonts } from "@glissade/core";
|
|
5
6
|
//#region src/displayDiff.ts
|
|
6
7
|
/** A flat, stable JSON value for one command with its resource ids INLINED to content. */
|
|
@@ -921,4 +922,388 @@ function sortDiagnostics(diags) {
|
|
|
921
922
|
}).map(([d]) => d);
|
|
922
923
|
}
|
|
923
924
|
//#endregion
|
|
924
|
-
|
|
925
|
+
//#region src/diff.ts
|
|
926
|
+
/** Stable JSON of any value — object keys sorted so key ORDER never reads as a
|
|
927
|
+
* change. NaN/Infinity/undefined are normalized to stable sentinels. */
|
|
928
|
+
function canonical(v) {
|
|
929
|
+
return JSON.stringify(normalize(v));
|
|
930
|
+
}
|
|
931
|
+
function normalize(v) {
|
|
932
|
+
if (v === void 0) return "\0undef";
|
|
933
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : `num:${String(v)}`;
|
|
934
|
+
if (Array.isArray(v)) return v.map(normalize);
|
|
935
|
+
if (v && typeof v === "object") {
|
|
936
|
+
const out = {};
|
|
937
|
+
for (const k of Object.keys(v).sort()) out[k] = normalize(v[k]);
|
|
938
|
+
return out;
|
|
939
|
+
}
|
|
940
|
+
return v;
|
|
941
|
+
}
|
|
942
|
+
function equalValue(a, b) {
|
|
943
|
+
return canonical(a) === canonical(b);
|
|
944
|
+
}
|
|
945
|
+
/** Collect every id-bearing node's structural facts, keyed by id. */
|
|
946
|
+
function nodeFacts(scene) {
|
|
947
|
+
const out = /* @__PURE__ */ new Map();
|
|
948
|
+
for (const [id, node] of scene.nodes) out.set(id, {
|
|
949
|
+
type: node.describeType,
|
|
950
|
+
parent: nearestIdedParent(node)
|
|
951
|
+
});
|
|
952
|
+
return out;
|
|
953
|
+
}
|
|
954
|
+
/** The id of the nearest id-bearing ancestor (skipping id-less wrappers like a
|
|
955
|
+
* bare `motionBlur(...)`), or '' at the root. */
|
|
956
|
+
function nearestIdedParent(node) {
|
|
957
|
+
let p = node.parent;
|
|
958
|
+
while (p) {
|
|
959
|
+
if (p.id !== void 0 && p.id !== "__root") return p.id;
|
|
960
|
+
p = p.parent;
|
|
961
|
+
}
|
|
962
|
+
return "";
|
|
963
|
+
}
|
|
964
|
+
/** Read a node's registered animatable targets → the static (or t=0) resolved
|
|
965
|
+
* value per path. Guarded: a computed prop that throws when read outside a full
|
|
966
|
+
* render is simply omitted (favor no-spurious over a crash). */
|
|
967
|
+
function propValues(scene, node) {
|
|
968
|
+
const out = /* @__PURE__ */ new Map();
|
|
969
|
+
untracked(() => {
|
|
970
|
+
for (const { path } of node.listTargets()) {
|
|
971
|
+
const sig = node.resolveTarget(path);
|
|
972
|
+
if (typeof sig !== "function") continue;
|
|
973
|
+
try {
|
|
974
|
+
out.set(path, sig());
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
/** A track's identity-independent value signature (target excluded, so a retarget
|
|
981
|
+
* is detectable as "same keys, new target"). */
|
|
982
|
+
function trackSignature(track) {
|
|
983
|
+
return canonical({
|
|
984
|
+
keys: track.keys,
|
|
985
|
+
type: track.type,
|
|
986
|
+
expr: track.expr ?? null
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Diff two scenes (+ optional timelines) into a typed {@link ChangeSet}. Default
|
|
991
|
+
* is the SEMANTIC structural layer (node tree + timeline); pass `{ rendered: true }`
|
|
992
|
+
* to also diff the DisplayList at `at`. Pure read; `diff(a, a)` is EMPTY.
|
|
993
|
+
*/
|
|
994
|
+
function diff(a, b, opts = {}) {
|
|
995
|
+
const added = [];
|
|
996
|
+
const removed = [];
|
|
997
|
+
const changed = [];
|
|
998
|
+
const emptyDoc = {
|
|
999
|
+
version: 1,
|
|
1000
|
+
tracks: []
|
|
1001
|
+
};
|
|
1002
|
+
const docA = a.timeline ?? emptyDoc;
|
|
1003
|
+
const docB = b.timeline ?? emptyDoc;
|
|
1004
|
+
evaluate(a.scene, docA, 0);
|
|
1005
|
+
evaluate(b.scene, docB, 0);
|
|
1006
|
+
const factsA = nodeFacts(a.scene);
|
|
1007
|
+
const factsB = nodeFacts(b.scene);
|
|
1008
|
+
for (const [id, f] of factsA) if (!factsB.has(id)) removed.push({
|
|
1009
|
+
node: id,
|
|
1010
|
+
type: f.type
|
|
1011
|
+
});
|
|
1012
|
+
for (const [id, f] of factsB) if (!factsA.has(id)) added.push({
|
|
1013
|
+
node: id,
|
|
1014
|
+
type: f.type
|
|
1015
|
+
});
|
|
1016
|
+
const animated = /* @__PURE__ */ new Set();
|
|
1017
|
+
for (const tr of docA.tracks) animated.add(tr.target);
|
|
1018
|
+
for (const tr of docB.tracks) animated.add(tr.target);
|
|
1019
|
+
for (const [id, fa] of factsA) {
|
|
1020
|
+
const fb = factsB.get(id);
|
|
1021
|
+
if (!fb) continue;
|
|
1022
|
+
if (fa.type !== fb.type) {
|
|
1023
|
+
changed.push({
|
|
1024
|
+
op: "changed",
|
|
1025
|
+
node: id,
|
|
1026
|
+
property: "type",
|
|
1027
|
+
from: fa.type,
|
|
1028
|
+
to: fb.type
|
|
1029
|
+
});
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
if (fa.parent !== fb.parent) changed.push({
|
|
1033
|
+
op: "moved",
|
|
1034
|
+
node: id,
|
|
1035
|
+
from: fa.parent || null,
|
|
1036
|
+
to: fb.parent || null
|
|
1037
|
+
});
|
|
1038
|
+
const nodeA = a.scene.nodes.get(id);
|
|
1039
|
+
const nodeB = b.scene.nodes.get(id);
|
|
1040
|
+
const pvA = propValues(a.scene, nodeA);
|
|
1041
|
+
const pvB = propValues(b.scene, nodeB);
|
|
1042
|
+
for (const [path, va] of pvA) {
|
|
1043
|
+
const target = `${id}/${path}`;
|
|
1044
|
+
if (animated.has(target)) continue;
|
|
1045
|
+
if (!pvB.has(path)) continue;
|
|
1046
|
+
const vb = pvB.get(path);
|
|
1047
|
+
if (!equalValue(va, vb)) changed.push({
|
|
1048
|
+
op: "changed",
|
|
1049
|
+
node: id,
|
|
1050
|
+
property: path,
|
|
1051
|
+
from: va,
|
|
1052
|
+
to: vb
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
diffTracks(docA.tracks, docB.tracks, changed);
|
|
1057
|
+
if (opts.rendered) diffRendered(a.scene, docA, b.scene, docB, opts.at ?? 0, changed);
|
|
1058
|
+
sortChanges(changed);
|
|
1059
|
+
sortRefs(added);
|
|
1060
|
+
sortRefs(removed);
|
|
1061
|
+
return {
|
|
1062
|
+
added,
|
|
1063
|
+
removed,
|
|
1064
|
+
changed,
|
|
1065
|
+
empty: added.length === 0 && removed.length === 0 && changed.length === 0
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
/** Track diff: match by target for keys-change + add/remove, then pair an unmatched
|
|
1069
|
+
* removed target with an unmatched added target of identical signature as a RETARGET. */
|
|
1070
|
+
function diffTracks(tracksA, tracksB, changed) {
|
|
1071
|
+
const byTargetA = /* @__PURE__ */ new Map();
|
|
1072
|
+
const byTargetB = /* @__PURE__ */ new Map();
|
|
1073
|
+
for (const t of tracksA) byTargetA.set(t.target, t);
|
|
1074
|
+
for (const t of tracksB) byTargetB.set(t.target, t);
|
|
1075
|
+
const removedTargets = [];
|
|
1076
|
+
const addedTargets = [];
|
|
1077
|
+
for (const [target, ta] of byTargetA) {
|
|
1078
|
+
const tb = byTargetB.get(target);
|
|
1079
|
+
if (!tb) {
|
|
1080
|
+
removedTargets.push(ta);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (trackSignature(ta) !== trackSignature(tb)) changed.push({
|
|
1084
|
+
op: "changed",
|
|
1085
|
+
target,
|
|
1086
|
+
property: "keys",
|
|
1087
|
+
from: ta.keys,
|
|
1088
|
+
to: tb.keys
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
for (const [target, tb] of byTargetB) if (!byTargetA.has(target)) addedTargets.push(tb);
|
|
1092
|
+
removedTargets.sort((x, y) => x.target < y.target ? -1 : x.target > y.target ? 1 : 0);
|
|
1093
|
+
addedTargets.sort((x, y) => x.target < y.target ? -1 : x.target > y.target ? 1 : 0);
|
|
1094
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
1095
|
+
for (const rem of removedTargets) {
|
|
1096
|
+
const remSig = trackSignature(rem);
|
|
1097
|
+
let matched = -1;
|
|
1098
|
+
for (let i = 0; i < addedTargets.length; i++) {
|
|
1099
|
+
if (usedAdded.has(i)) continue;
|
|
1100
|
+
if (trackSignature(addedTargets[i]) === remSig) {
|
|
1101
|
+
matched = i;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (matched >= 0) {
|
|
1106
|
+
usedAdded.add(matched);
|
|
1107
|
+
changed.push({
|
|
1108
|
+
op: "retargeted",
|
|
1109
|
+
target: rem.target,
|
|
1110
|
+
from: rem.target,
|
|
1111
|
+
to: addedTargets[matched].target
|
|
1112
|
+
});
|
|
1113
|
+
} else changed.push({
|
|
1114
|
+
op: "changed",
|
|
1115
|
+
target: rem.target,
|
|
1116
|
+
property: "track",
|
|
1117
|
+
from: "present",
|
|
1118
|
+
to: "absent"
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
for (let i = 0; i < addedTargets.length; i++) {
|
|
1122
|
+
if (usedAdded.has(i)) continue;
|
|
1123
|
+
changed.push({
|
|
1124
|
+
op: "changed",
|
|
1125
|
+
target: addedTargets[i].target,
|
|
1126
|
+
property: "track",
|
|
1127
|
+
from: "absent",
|
|
1128
|
+
to: "present"
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/** Rendered layer: diff the two DisplayLists at `t` via the shipped
|
|
1133
|
+
* `diffDisplayLists`; translate each field delta into a `changed` entry. */
|
|
1134
|
+
function diffRendered(sceneA, docA, sceneB, docB, t, changed) {
|
|
1135
|
+
const d = diffDisplayLists(evaluate(sceneA, docA, t), evaluate(sceneB, docB, t));
|
|
1136
|
+
if (d.equal) return;
|
|
1137
|
+
for (const delta of d.deltas) if (delta.kind === "add") changed.push({
|
|
1138
|
+
op: "changed",
|
|
1139
|
+
property: `render:command[${delta.index}]`,
|
|
1140
|
+
from: "absent",
|
|
1141
|
+
to: delta.opB ?? "present"
|
|
1142
|
+
});
|
|
1143
|
+
else if (delta.kind === "remove") changed.push({
|
|
1144
|
+
op: "changed",
|
|
1145
|
+
property: `render:command[${delta.index}]`,
|
|
1146
|
+
from: delta.opA ?? "present",
|
|
1147
|
+
to: "absent"
|
|
1148
|
+
});
|
|
1149
|
+
else for (const fc of delta.fields) changed.push({
|
|
1150
|
+
op: "changed",
|
|
1151
|
+
property: `render:command[${delta.index}].${fc.path}`,
|
|
1152
|
+
from: fc.from,
|
|
1153
|
+
to: fc.to
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function sortRefs(refs) {
|
|
1157
|
+
refs.sort((a, b) => a.node < b.node ? -1 : a.node > b.node ? 1 : 0);
|
|
1158
|
+
}
|
|
1159
|
+
/** Canonical change order: by (target||node) key, then op, then property, then a
|
|
1160
|
+
* value tiebreak — so a shuffled emission order sorts to the same sequence
|
|
1161
|
+
* (assert sort-invariance). Sorts `changed` IN PLACE. */
|
|
1162
|
+
function sortChanges(changed) {
|
|
1163
|
+
const keyOf = (c) => c.target ?? c.node ?? "";
|
|
1164
|
+
const paired = changed.map((c, i) => [c, i]);
|
|
1165
|
+
paired.sort((A, B) => {
|
|
1166
|
+
const [a, ai] = A;
|
|
1167
|
+
const [b, bi] = B;
|
|
1168
|
+
const ka = keyOf(a);
|
|
1169
|
+
const kb = keyOf(b);
|
|
1170
|
+
if (ka !== kb) return ka < kb ? -1 : 1;
|
|
1171
|
+
if (a.op !== b.op) return a.op < b.op ? -1 : 1;
|
|
1172
|
+
const pa = a.property ?? "";
|
|
1173
|
+
const pb = b.property ?? "";
|
|
1174
|
+
if (pa !== pb) return pa < pb ? -1 : 1;
|
|
1175
|
+
const va = canonical(a.to);
|
|
1176
|
+
const vb = canonical(b.to);
|
|
1177
|
+
if (va !== vb) return va < vb ? -1 : 1;
|
|
1178
|
+
return ai - bi;
|
|
1179
|
+
});
|
|
1180
|
+
const sorted = paired.map(([c]) => c);
|
|
1181
|
+
for (let i = 0; i < sorted.length; i++) changed[i] = sorted[i];
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/fidelity.ts
|
|
1185
|
+
/** Per-feature actionable hint tail (after the "won't survive Lottie export" clause). */
|
|
1186
|
+
const HINT = {
|
|
1187
|
+
"motion-blur": "the round-trip shows the un-blurred shape; if export-bound, pre-bake the blur into keyframed copies or accept the loss",
|
|
1188
|
+
"echo-trails": "only the base shape exports (no ghost copies); if export-bound, bake the trail into real staggered layers or accept the loss",
|
|
1189
|
+
shake: "the closed-form jitter is not a keyframe track; if export-bound, bake() the shake into position/rotation tracks or accept the loss",
|
|
1190
|
+
"camera-shake": "whole-frame camera shake is not exported; if export-bound, bake it into the camera pose tracks or accept the loss",
|
|
1191
|
+
"text-cursor": "the caret sibling is not exportable and is dropped; if export-bound, drop the cursor or accept the loss",
|
|
1192
|
+
reveal: "Lottie has no range selector, so the FULL text shows on the round-trip; if export-bound, use per-word/line reveal tracks (revealWords/revealLines) or accept the loss",
|
|
1193
|
+
"mesh-fill": "a mesh fill has no Lottie gradient ramp; if export-bound, thread a PNG encoder to rasterize it (gs render does), use a solid/linear/radial fill, or accept the loss"
|
|
1194
|
+
};
|
|
1195
|
+
function label(feature) {
|
|
1196
|
+
switch (feature) {
|
|
1197
|
+
case "motion-blur": return "motionBlur";
|
|
1198
|
+
case "echo-trails": return "echo trails";
|
|
1199
|
+
case "shake": return "shake() jitter";
|
|
1200
|
+
case "camera-shake": return "camera shake";
|
|
1201
|
+
case "text-cursor": return "a text cursor";
|
|
1202
|
+
case "reveal": return "a typewriter reveal mask";
|
|
1203
|
+
case "mesh-fill": return "a mesh fill";
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Statically scan `scene` (+ optional `timeline`, for reveal-track detection) for
|
|
1208
|
+
* render-only features that won't survive Lottie export, aggregating one
|
|
1209
|
+
* RENDER_ONLY_EXPORT warning per affected node. Pure read; a scene with no
|
|
1210
|
+
* render-only feature returns an EMPTY diagnostics list.
|
|
1211
|
+
*/
|
|
1212
|
+
function exportFidelity(scene, timeline) {
|
|
1213
|
+
const diagnostics = [];
|
|
1214
|
+
const revealTargets = /* @__PURE__ */ new Set();
|
|
1215
|
+
if (timeline) {
|
|
1216
|
+
for (const tr of timeline.tracks) if (/\/reveal(Fraction)?$/.test(tr.target)) revealTargets.add(tr.target);
|
|
1217
|
+
}
|
|
1218
|
+
const push = (node, feature) => {
|
|
1219
|
+
const id = displayId(node);
|
|
1220
|
+
diagnostics.push({
|
|
1221
|
+
schemaVersion: 1,
|
|
1222
|
+
code: "RENDER_ONLY_EXPORT",
|
|
1223
|
+
severity: "warning",
|
|
1224
|
+
source: "parity",
|
|
1225
|
+
...node.id !== void 0 ? { node: node.id } : {},
|
|
1226
|
+
message: `${label(feature)} on '${id}' is render-only — won't survive Lottie export (${HINT[feature]}).`,
|
|
1227
|
+
detail: {
|
|
1228
|
+
feature,
|
|
1229
|
+
node: id,
|
|
1230
|
+
...node.id !== void 0 ? {} : { unnamed: true }
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
};
|
|
1234
|
+
untracked(() => {
|
|
1235
|
+
const visit = (node) => {
|
|
1236
|
+
const type = node.describeType;
|
|
1237
|
+
if (type === "MotionBlur") push(node, "motion-blur");
|
|
1238
|
+
else if (type === "Echo") push(node, "echo-trails");
|
|
1239
|
+
else if (type === "TextCursor") push(node, "text-cursor");
|
|
1240
|
+
else if (type === "Camera") {
|
|
1241
|
+
if (node.shakeSpec !== void 0) push(node, "camera-shake");
|
|
1242
|
+
} else if (shakenSpec(node) !== void 0) push(node, "shake");
|
|
1243
|
+
if (type === "Text") {
|
|
1244
|
+
const id = node.id;
|
|
1245
|
+
const hasRevealTrack = id !== void 0 && (revealTargets.has(`${id}/reveal`) || revealTargets.has(`${id}/revealFraction`));
|
|
1246
|
+
const rf = readNumber(node, "revealFraction");
|
|
1247
|
+
const rv = readNumber(node, "reveal");
|
|
1248
|
+
if (hasRevealTrack || rf !== void 0 && !Number.isNaN(rf) || rv !== void 0 && Number.isFinite(rv)) push(node, "reveal");
|
|
1249
|
+
}
|
|
1250
|
+
const fill = node.fill;
|
|
1251
|
+
if (typeof fill === "function") try {
|
|
1252
|
+
const v = fill();
|
|
1253
|
+
if (v && typeof v === "object" && v.kind === "mesh") push(node, "mesh-fill");
|
|
1254
|
+
} catch {}
|
|
1255
|
+
if (node instanceof Group) for (const c of node.children) visit(c);
|
|
1256
|
+
else {
|
|
1257
|
+
const children = node.children;
|
|
1258
|
+
if (Array.isArray(children)) for (const c of children) visit(c);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
visit(scene.root);
|
|
1262
|
+
});
|
|
1263
|
+
return {
|
|
1264
|
+
schemaVersion: 1,
|
|
1265
|
+
hasErrors: false,
|
|
1266
|
+
diagnostics: sortFidelity(diagnostics)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/** Canonical order — by node id, then feature (detail.feature). Shuffle-stable. */
|
|
1270
|
+
function sortFidelity(diags) {
|
|
1271
|
+
const featureOf = (d) => String(d.detail?.feature ?? "");
|
|
1272
|
+
return diags.map((d, i) => [d, i]).sort((A, B) => {
|
|
1273
|
+
const [a, ai] = A;
|
|
1274
|
+
const [b, bi] = B;
|
|
1275
|
+
const na = String(a.detail?.node ?? a.node ?? "");
|
|
1276
|
+
const nb = String(b.detail?.node ?? b.node ?? "");
|
|
1277
|
+
if (na !== nb) return na < nb ? -1 : 1;
|
|
1278
|
+
const fa = featureOf(a);
|
|
1279
|
+
const fb = featureOf(b);
|
|
1280
|
+
if (fa !== fb) return fa < fb ? -1 : 1;
|
|
1281
|
+
return ai - bi;
|
|
1282
|
+
}).map(([d]) => d);
|
|
1283
|
+
}
|
|
1284
|
+
/** A readable id for a node: its own id, else the first id-bearing descendant
|
|
1285
|
+
* (a wrapper like `motionBlur(rect)` reports the wrapped child), else its type. */
|
|
1286
|
+
function displayId(node) {
|
|
1287
|
+
if (node.id !== void 0) return node.id;
|
|
1288
|
+
const stack = [node];
|
|
1289
|
+
while (stack.length) {
|
|
1290
|
+
const n = stack.shift();
|
|
1291
|
+
if (n !== node && n.id !== void 0) return n.id;
|
|
1292
|
+
const children = n.children;
|
|
1293
|
+
if (Array.isArray(children)) stack.push(...children);
|
|
1294
|
+
}
|
|
1295
|
+
return `<${node.describeType}>`;
|
|
1296
|
+
}
|
|
1297
|
+
/** Read a node's numeric signal by prop name under untracked, or undefined. */
|
|
1298
|
+
function readNumber(node, prop) {
|
|
1299
|
+
const sig = node[prop];
|
|
1300
|
+
if (typeof sig !== "function") return void 0;
|
|
1301
|
+
try {
|
|
1302
|
+
const v = sig();
|
|
1303
|
+
return typeof v === "number" ? v : void 0;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
export { DIAGNOSTIC_SCHEMA_VERSION, DL_SNAPSHOT_VERSION, DlSnapshotError, auditCacheCold, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, critique, diff, diffDisplayLists, exportFidelity, formatDisplayDiff, instanceProps, levenshtein, nearestId, parseDisplaySnapshot, resolveAt, serializeDisplayList, sortDiagnostics, validateScene, validateSceneFonts };
|
package/dist/motion.js
CHANGED
|
@@ -1,127 +1,8 @@
|
|
|
1
1
|
import { C as Node, Z as fromTRS, _ as hashStr, et as multiply, r as Group, s as Text, t as Circle } from "./nodes.js";
|
|
2
|
+
import { i as shakenSpec, n as shakeMatrix, r as shakeOffset, t as shake } from "./shake.js";
|
|
2
3
|
import { a as FollowPath, c as pathLength, i as orientToPath, l as pointAtLength, n as OrientToPath, o as followPath, r as lookAt, s as motionPath, t as LookAt } from "./orient.js";
|
|
3
4
|
import { n as each } from "./each.js";
|
|
4
|
-
import { bake, signal,
|
|
5
|
-
//#region src/shake.ts
|
|
6
|
-
/**
|
|
7
|
-
* `shake` (0.55) — a standalone jitter driver that wobbles ANY node's pose with
|
|
8
|
-
* deterministic value noise. It subsumes the hand-rolled per-element jitters
|
|
9
|
-
* (desk-cursor jitterX/Y, glitch shakeAmp, typewriter jitterRate) behind one
|
|
10
|
-
* primitive with SEPARATE translate / rotate / frequency amplitudes.
|
|
11
|
-
*
|
|
12
|
-
* The jitter is realized AT EMIT (the Echo/MotionBlur idiom): `shake` overrides
|
|
13
|
-
* the node's `emit` to wrap it in a save → shake-transform → emit → restore, where
|
|
14
|
-
* the transform is a pure function of `ctx.time` via `valueNoise` — so it composes
|
|
15
|
-
* on top of WHATEVER already drives the node (keyframes, layout, followPath) as a
|
|
16
|
-
* parent-space offset, and stays byte-identical across two `evaluate()` passes (no
|
|
17
|
-
* cross-frame state, no `Date.now`/`Math.random`). The camera whole-frame shake
|
|
18
|
-
* reuses {@link shakeOffset} directly on its pose.
|
|
19
|
-
*
|
|
20
|
-
* Lives on `@glissade/scene/motion` (off the base embed). Note (like Echo/
|
|
21
|
-
* MotionBlur, both emit-time re-eval): the offset is not a Timeline TRACK, so it
|
|
22
|
-
* is a runtime/render effect — it is NOT emitted as animated Lottie keyframes.
|
|
23
|
-
*/
|
|
24
|
-
/** Default temporal frequency (noise cycles per second) when `frequency` is unset. */
|
|
25
|
-
const DEFAULT_FREQUENCY = 8;
|
|
26
|
-
const K_Y = 101;
|
|
27
|
-
const K_ROT = 211;
|
|
28
|
-
/** Signed value noise in [-1, 1) — the bipolar form a shake offset needs. */
|
|
29
|
-
function snoise(seed, t) {
|
|
30
|
-
return valueNoise(seed, t) * 2 - 1;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Render-INVISIBLE marker: which nodes a {@link shake} driver is applied to, and
|
|
34
|
-
* with what spec. The render path NEVER reads this (shake works purely by wrapping
|
|
35
|
-
* `emit`), so it is byte-neutral for goldens — it exists ONLY so an EXPORTER
|
|
36
|
-
* (which reads signals, not `emit`) can detect the render-only jitter and warn
|
|
37
|
-
* honestly instead of silently dropping it. A WeakMap keeps it off the Node type.
|
|
38
|
-
*/
|
|
39
|
-
const SHAKEN = /* @__PURE__ */ new WeakMap();
|
|
40
|
-
/** The shake spec applied to `node` via {@link shake}, or undefined — the seam an
|
|
41
|
-
* exporter uses to emit an honest "shake is render-only" warn (never a silent drop). */
|
|
42
|
-
function shakenSpec(node) {
|
|
43
|
-
return SHAKEN.get(node);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The pure per-time shake offset for a spec: `{ dx, dy }` px + `dr` degrees, each
|
|
47
|
-
* a deterministic function of `(seed, t)`. Both the {@link shake} node driver and
|
|
48
|
-
* the Camera whole-frame shake fold this in.
|
|
49
|
-
*/
|
|
50
|
-
function shakeOffset(spec, t) {
|
|
51
|
-
const tr = spec.translate ?? 0;
|
|
52
|
-
const rot = spec.rotate ?? 0;
|
|
53
|
-
const tf = t * (spec.frequency ?? DEFAULT_FREQUENCY);
|
|
54
|
-
return {
|
|
55
|
-
dx: tr === 0 ? 0 : tr * snoise(spec.seed, tf),
|
|
56
|
-
dy: tr === 0 ? 0 : tr * snoise(spec.seed + K_Y, tf),
|
|
57
|
-
dr: rot === 0 ? 0 : rot * snoise(spec.seed + K_ROT, tf)
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* A shake transform about the point `p` (parent space): translate by `(dx, dy)`
|
|
62
|
-
* then rotate `dr` degrees about `p`, so a rotational jitter spins the node around
|
|
63
|
-
* its own origin rather than the parent's.
|
|
64
|
-
*/
|
|
65
|
-
function shakeMatrix(p, dx, dy, dr) {
|
|
66
|
-
const translate = [
|
|
67
|
-
1,
|
|
68
|
-
0,
|
|
69
|
-
0,
|
|
70
|
-
1,
|
|
71
|
-
dx,
|
|
72
|
-
dy
|
|
73
|
-
];
|
|
74
|
-
if (dr === 0) return translate;
|
|
75
|
-
const toP = [
|
|
76
|
-
1,
|
|
77
|
-
0,
|
|
78
|
-
0,
|
|
79
|
-
1,
|
|
80
|
-
p[0],
|
|
81
|
-
p[1]
|
|
82
|
-
];
|
|
83
|
-
const fromP = [
|
|
84
|
-
1,
|
|
85
|
-
0,
|
|
86
|
-
0,
|
|
87
|
-
1,
|
|
88
|
-
-p[0],
|
|
89
|
-
-p[1]
|
|
90
|
-
];
|
|
91
|
-
return multiply(translate, multiply(toP, multiply(fromTRS([0, 0], dr, [1, 1]), fromP)));
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Jitter `node`'s pose with deterministic value noise, then return it (mutate-and-
|
|
95
|
-
* return, like Grid/orientToPath). SEPARATE `translate` (px) / `rotate` (deg) /
|
|
96
|
-
* `frequency` (Hz) amplitudes; pass at least one nonzero amplitude. The jitter is
|
|
97
|
-
* a parent-space offset applied at emit, so it composes with any existing driver.
|
|
98
|
-
*
|
|
99
|
-
* `children: [shake(cursor, { seed: 7, translate: 3 })]` — the cursor wobbles ±3px
|
|
100
|
-
* around wherever else it is (its position track, a followPath, …).
|
|
101
|
-
*/
|
|
102
|
-
function shake(node, spec) {
|
|
103
|
-
const tr = spec.translate ?? 0;
|
|
104
|
-
const rot = spec.rotate ?? 0;
|
|
105
|
-
if (tr === 0 && rot === 0) throw new Error("shake(): pass a nonzero `translate` (px) or `rotate` (deg) amplitude — both are 0/omitted, so nothing would move.");
|
|
106
|
-
SHAKEN.set(node, spec);
|
|
107
|
-
const origEmit = node.emit.bind(node);
|
|
108
|
-
node.emit = (out, ctx) => {
|
|
109
|
-
const { dx, dy, dr } = shakeOffset(spec, ctx.time);
|
|
110
|
-
if (dx === 0 && dy === 0 && dr === 0) {
|
|
111
|
-
origEmit(out, ctx);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
out.push({ op: "save" });
|
|
115
|
-
out.push({
|
|
116
|
-
op: "transform",
|
|
117
|
-
m: shakeMatrix(node.position(), dx, dy, dr)
|
|
118
|
-
});
|
|
119
|
-
origEmit(out, ctx);
|
|
120
|
-
out.push({ op: "restore" });
|
|
121
|
-
};
|
|
122
|
-
return node;
|
|
123
|
-
}
|
|
124
|
-
//#endregion
|
|
5
|
+
import { bake, signal, vec2Signal } from "@glissade/core";
|
|
125
6
|
//#region src/camera.ts
|
|
126
7
|
/**
|
|
127
8
|
* `Camera` (0.55) — a cinematic camera rig for cuts, push-ins, pans, rolls, and
|
package/dist/shake.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Z as fromTRS, et as multiply } from "./nodes.js";
|
|
2
|
+
import { valueNoise } from "@glissade/core";
|
|
3
|
+
//#region src/shake.ts
|
|
4
|
+
/**
|
|
5
|
+
* `shake` (0.55) — a standalone jitter driver that wobbles ANY node's pose with
|
|
6
|
+
* deterministic value noise. It subsumes the hand-rolled per-element jitters
|
|
7
|
+
* (desk-cursor jitterX/Y, glitch shakeAmp, typewriter jitterRate) behind one
|
|
8
|
+
* primitive with SEPARATE translate / rotate / frequency amplitudes.
|
|
9
|
+
*
|
|
10
|
+
* The jitter is realized AT EMIT (the Echo/MotionBlur idiom): `shake` overrides
|
|
11
|
+
* the node's `emit` to wrap it in a save → shake-transform → emit → restore, where
|
|
12
|
+
* the transform is a pure function of `ctx.time` via `valueNoise` — so it composes
|
|
13
|
+
* on top of WHATEVER already drives the node (keyframes, layout, followPath) as a
|
|
14
|
+
* parent-space offset, and stays byte-identical across two `evaluate()` passes (no
|
|
15
|
+
* cross-frame state, no `Date.now`/`Math.random`). The camera whole-frame shake
|
|
16
|
+
* reuses {@link shakeOffset} directly on its pose.
|
|
17
|
+
*
|
|
18
|
+
* Lives on `@glissade/scene/motion` (off the base embed). Note (like Echo/
|
|
19
|
+
* MotionBlur, both emit-time re-eval): the offset is not a Timeline TRACK, so it
|
|
20
|
+
* is a runtime/render effect — it is NOT emitted as animated Lottie keyframes.
|
|
21
|
+
*/
|
|
22
|
+
/** Default temporal frequency (noise cycles per second) when `frequency` is unset. */
|
|
23
|
+
const DEFAULT_FREQUENCY = 8;
|
|
24
|
+
const K_Y = 101;
|
|
25
|
+
const K_ROT = 211;
|
|
26
|
+
/** Signed value noise in [-1, 1) — the bipolar form a shake offset needs. */
|
|
27
|
+
function snoise(seed, t) {
|
|
28
|
+
return valueNoise(seed, t) * 2 - 1;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Render-INVISIBLE marker: which nodes a {@link shake} driver is applied to, and
|
|
32
|
+
* with what spec. The render path NEVER reads this (shake works purely by wrapping
|
|
33
|
+
* `emit`), so it is byte-neutral for goldens — it exists ONLY so an EXPORTER
|
|
34
|
+
* (which reads signals, not `emit`) can detect the render-only jitter and warn
|
|
35
|
+
* honestly instead of silently dropping it. A WeakMap keeps it off the Node type.
|
|
36
|
+
*/
|
|
37
|
+
const SHAKEN = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
/** The shake spec applied to `node` via {@link shake}, or undefined — the seam an
|
|
39
|
+
* exporter uses to emit an honest "shake is render-only" warn (never a silent drop). */
|
|
40
|
+
function shakenSpec(node) {
|
|
41
|
+
return SHAKEN.get(node);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The pure per-time shake offset for a spec: `{ dx, dy }` px + `dr` degrees, each
|
|
45
|
+
* a deterministic function of `(seed, t)`. Both the {@link shake} node driver and
|
|
46
|
+
* the Camera whole-frame shake fold this in.
|
|
47
|
+
*/
|
|
48
|
+
function shakeOffset(spec, t) {
|
|
49
|
+
const tr = spec.translate ?? 0;
|
|
50
|
+
const rot = spec.rotate ?? 0;
|
|
51
|
+
const tf = t * (spec.frequency ?? DEFAULT_FREQUENCY);
|
|
52
|
+
return {
|
|
53
|
+
dx: tr === 0 ? 0 : tr * snoise(spec.seed, tf),
|
|
54
|
+
dy: tr === 0 ? 0 : tr * snoise(spec.seed + K_Y, tf),
|
|
55
|
+
dr: rot === 0 ? 0 : rot * snoise(spec.seed + K_ROT, tf)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A shake transform about the point `p` (parent space): translate by `(dx, dy)`
|
|
60
|
+
* then rotate `dr` degrees about `p`, so a rotational jitter spins the node around
|
|
61
|
+
* its own origin rather than the parent's.
|
|
62
|
+
*/
|
|
63
|
+
function shakeMatrix(p, dx, dy, dr) {
|
|
64
|
+
const translate = [
|
|
65
|
+
1,
|
|
66
|
+
0,
|
|
67
|
+
0,
|
|
68
|
+
1,
|
|
69
|
+
dx,
|
|
70
|
+
dy
|
|
71
|
+
];
|
|
72
|
+
if (dr === 0) return translate;
|
|
73
|
+
const toP = [
|
|
74
|
+
1,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
1,
|
|
78
|
+
p[0],
|
|
79
|
+
p[1]
|
|
80
|
+
];
|
|
81
|
+
const fromP = [
|
|
82
|
+
1,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
1,
|
|
86
|
+
-p[0],
|
|
87
|
+
-p[1]
|
|
88
|
+
];
|
|
89
|
+
return multiply(translate, multiply(toP, multiply(fromTRS([0, 0], dr, [1, 1]), fromP)));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Jitter `node`'s pose with deterministic value noise, then return it (mutate-and-
|
|
93
|
+
* return, like Grid/orientToPath). SEPARATE `translate` (px) / `rotate` (deg) /
|
|
94
|
+
* `frequency` (Hz) amplitudes; pass at least one nonzero amplitude. The jitter is
|
|
95
|
+
* a parent-space offset applied at emit, so it composes with any existing driver.
|
|
96
|
+
*
|
|
97
|
+
* `children: [shake(cursor, { seed: 7, translate: 3 })]` — the cursor wobbles ±3px
|
|
98
|
+
* around wherever else it is (its position track, a followPath, …).
|
|
99
|
+
*/
|
|
100
|
+
function shake(node, spec) {
|
|
101
|
+
const tr = spec.translate ?? 0;
|
|
102
|
+
const rot = spec.rotate ?? 0;
|
|
103
|
+
if (tr === 0 && rot === 0) throw new Error("shake(): pass a nonzero `translate` (px) or `rotate` (deg) amplitude — both are 0/omitted, so nothing would move.");
|
|
104
|
+
SHAKEN.set(node, spec);
|
|
105
|
+
const origEmit = node.emit.bind(node);
|
|
106
|
+
node.emit = (out, ctx) => {
|
|
107
|
+
const { dx, dy, dr } = shakeOffset(spec, ctx.time);
|
|
108
|
+
if (dx === 0 && dy === 0 && dr === 0) {
|
|
109
|
+
origEmit(out, ctx);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
out.push({ op: "save" });
|
|
113
|
+
out.push({
|
|
114
|
+
op: "transform",
|
|
115
|
+
m: shakeMatrix(node.position(), dx, dy, dr)
|
|
116
|
+
});
|
|
117
|
+
origEmit(out, ctx);
|
|
118
|
+
out.push({ op: "restore" });
|
|
119
|
+
};
|
|
120
|
+
return node;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { shakenSpec as i, shakeMatrix as n, shakeOffset as r, shake as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0-pre.0",
|
|
4
4
|
"description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
],
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"yoga-layout": "^3.2.1",
|
|
84
|
-
"@glissade/core": "0.
|
|
84
|
+
"@glissade/core": "0.61.0-pre.0"
|
|
85
85
|
},
|
|
86
86
|
"repository": {
|
|
87
87
|
"type": "git",
|