@pinepaper.studio/mcp-server 1.6.23 → 1.6.24
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/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +84 -5
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +46 -0
- package/dist/agent/types.d.ts +114 -106
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/browser/puppeteer-controller.js +2 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +5 -1
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +1 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-macros.d.ts +27 -0
- package/dist/tools/batch-macros.d.ts.map +1 -0
- package/dist/tools/batch-macros.js +157 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +2 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.js +3 -3
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +87 -5
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +2 -2
- package/dist/tools/engine-lists.js +2 -2
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +1 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +52 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-check.d.ts +31 -0
- package/dist/tools/export-file-check.d.ts.map +1 -0
- package/dist/tools/export-file-check.js +103 -0
- package/dist/tools/export-file-check.js.map +1 -0
- package/dist/tools/export-file-receipt.d.ts +56 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +151 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +7 -44
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +31 -6
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +41 -270
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +67 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +304 -67
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +34 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +210 -0
- package/dist/tools/lane.js.map +1 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/nested-ignored.d.ts +16 -0
- package/dist/tools/nested-ignored.d.ts.map +1 -0
- package/dist/tools/nested-ignored.js +83 -0
- package/dist/tools/nested-ignored.js.map +1 -0
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +9 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +4 -4
- package/dist/tools/stick-vocabulary.d.ts.map +1 -1
- package/dist/tools/stick-vocabulary.js +8 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/tools/timeline.d.ts +136 -0
- package/dist/tools/timeline.d.ts.map +1 -0
- package/dist/tools/timeline.js +1036 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +12 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +30 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +277 -53
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5276 -25048
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +78 -40
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +8 -7
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TIMELINE OP: animation written the way an agent already knows it from GSAP, compiled onto the engine's own
|
|
3
|
+
* keyframe tracks.
|
|
4
|
+
*
|
|
5
|
+
* It is a JSON shape, never code: nothing here is evaluated. The compiler is pure (no studio, no I/O) and runs on the
|
|
6
|
+
* server before anything is sent, so a tween it cannot honour is refused by name with the reason, before the canvas is
|
|
7
|
+
* touched. What it produces is the same thing a keyframe_animate op produces: one track per item, written with
|
|
8
|
+
* app.addAnimation, so a timeline call is exported, saved and edited exactly as hand-written keys are.
|
|
9
|
+
*
|
|
10
|
+
* { "type": "timeline", "defaults": { "ease": "power2.out" }, "tweens": [
|
|
11
|
+
* { "to": "bar-*", "from": { "scaleY": 0 }, "vars": { "scaleY": 1 }, "duration": 0.8, "at": 0.5, "stagger": 0.15 },
|
|
12
|
+
* { "to": "ring", "vars": { "rotation": 480 }, "duration": 8, "ease": "none", "at": 0 } ] }
|
|
13
|
+
*
|
|
14
|
+
* SEMANTICS (GSAP's, where the engine can carry them):
|
|
15
|
+
* - to + vars (end values), from (start values), both = fromTo. A value missing on one side is the value the item has
|
|
16
|
+
* at the tween's start: the end of the previous tween on that property, else the item's rest value, read in the studio.
|
|
17
|
+
* - x and y are OFFSETS from where the item was created, as GSAP's x/y are offsets from layout (x: 0 is home).
|
|
18
|
+
* - at: absolute seconds, "<" / ">" (start / end of the previous tween, optionally "<0.2", ">-0.1"), "+=0.2" / "-=0.2"
|
|
19
|
+
* (from the timeline's end), a label, or "label+=0.2". Omitted: the timeline's end. delay adds to it.
|
|
20
|
+
* - stagger: seconds between targets, or { each | amount, from: start | end | center | edges | index }.
|
|
21
|
+
* - repeat / yoyo / repeatDelay: a repeat that never ends on an item with nothing else is the engine's loop; anything
|
|
22
|
+
* else is expanded into keys (repeat -1 up to the timeline's duration).
|
|
23
|
+
* - ease: GSAP names mapped onto the engine's easing table; a name with no equivalent is refused, never made linear.
|
|
24
|
+
* - Every compiled track is as long as the timeline (its end, or `duration`), so no key can outrun its clip.
|
|
25
|
+
*
|
|
26
|
+
* KNOWN GAPS (the engine cannot carry them, so they are refused rather than approximated): a scale pivot left or right
|
|
27
|
+
* of centre, a percentage x/y, stagger from "random" or a grid, eased parameters (back(3), elastic(1.2, 0.4)),
|
|
28
|
+
* elastic/bounce inOut, steps(), callbacks, and two tweens on one property that overlap in time.
|
|
29
|
+
*/
|
|
30
|
+
import { ENGINE_EASING_NAMES } from './engine-lists.js';
|
|
31
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
32
|
+
// Eases
|
|
33
|
+
/** GSAP family → the engine's suffix. */
|
|
34
|
+
const EASE_FAMILY = {
|
|
35
|
+
power1: 'Quad', quad: 'Quad',
|
|
36
|
+
power2: 'Cubic', cubic: 'Cubic',
|
|
37
|
+
power3: 'Quart', quart: 'Quart',
|
|
38
|
+
power4: 'Quint', quint: 'Quint', strong: 'Quint',
|
|
39
|
+
sine: 'Sine', expo: 'Expo', circ: 'Circ',
|
|
40
|
+
back: 'Back', elastic: 'Elastic', bounce: 'Bounce',
|
|
41
|
+
};
|
|
42
|
+
/** GSAP's own default, used when neither the tween nor the timeline's defaults name one. */
|
|
43
|
+
export const GSAP_DEFAULT_EASE = 'power1.out';
|
|
44
|
+
/** The names this op accepts, for the refusal message. */
|
|
45
|
+
export const GSAP_EASE_NAMES = [
|
|
46
|
+
'none', 'linear', 'power0',
|
|
47
|
+
...['power1', 'power2', 'power3', 'power4', 'sine', 'expo', 'circ', 'back'].flatMap((f) => [`${f}.in`, `${f}.out`, `${f}.inOut`]),
|
|
48
|
+
'elastic.in', 'elastic.out', 'bounce.in', 'bounce.out',
|
|
49
|
+
];
|
|
50
|
+
const ENGINE_EASES = new Set(ENGINE_EASING_NAMES);
|
|
51
|
+
/** A GSAP ease name → the engine's, or the reason it has none. Also takes the engine's own names. */
|
|
52
|
+
export function mapGsapEase(name) {
|
|
53
|
+
const raw = String(name).trim();
|
|
54
|
+
if (ENGINE_EASES.has(raw))
|
|
55
|
+
return { engine: raw };
|
|
56
|
+
const refuse = (why) => ({
|
|
57
|
+
error: `ease "${raw}" ${why}. Accepted: ${GSAP_EASE_NAMES.join(', ')} (a family alone, e.g. "power2", is its .out), or an engine name: ${ENGINE_EASING_NAMES.join(', ')}.`,
|
|
58
|
+
});
|
|
59
|
+
// Old syntax too: Power2.easeOut, Linear.easeNone, Expo.easeInOut.
|
|
60
|
+
const m = /^([a-z]+[0-4]?)(?:\.(in|out|inout|easein|easeout|easeinout|easenone))?\s*(?:\((.*)\))?$/i.exec(raw);
|
|
61
|
+
if (!m)
|
|
62
|
+
return refuse('is not an ease this op knows');
|
|
63
|
+
const family = m[1].toLowerCase();
|
|
64
|
+
const dir = (m[2] || 'out').toLowerCase().replace(/^ease/, '');
|
|
65
|
+
const params = m[3] !== undefined ? m[3].split(',').map((s) => s.trim()).filter(Boolean).map(Number) : [];
|
|
66
|
+
if (family === 'none' || family === 'linear' || family === 'power0' || dir === 'none') {
|
|
67
|
+
return params.length ? refuse('takes no parameters') : { engine: 'linear' };
|
|
68
|
+
}
|
|
69
|
+
if (family === 'steps')
|
|
70
|
+
return refuse('has no equivalent: the engine steps only as "hold" (one jump at the next key)');
|
|
71
|
+
const suffix = EASE_FAMILY[family];
|
|
72
|
+
if (!suffix)
|
|
73
|
+
return refuse('has no equivalent in the engine');
|
|
74
|
+
// A parameter changes the curve; the engine's curves are fixed. Only GSAP's own defaults are the same curve.
|
|
75
|
+
if (params.length) {
|
|
76
|
+
const same = (family === 'back' && params.length === 1 && Math.abs(params[0] - 1.70158) < 0.01)
|
|
77
|
+
|| (family === 'elastic' && Math.abs(params[0] - 1) < 1e-9 && (params.length === 1 || Math.abs(params[1] - 0.3) < 1e-9));
|
|
78
|
+
if (!same)
|
|
79
|
+
return refuse(`has parameters (${m[3]}) the engine's ${family} cannot take: its curve is fixed (back 1.70158, elastic 1, 0.3); drop them`);
|
|
80
|
+
}
|
|
81
|
+
if ((suffix === 'Elastic' || suffix === 'Bounce') && dir === 'inout') {
|
|
82
|
+
return refuse(`has no equivalent: the engine has ${family}.in and ${family}.out only`);
|
|
83
|
+
}
|
|
84
|
+
const engine = dir === 'in' ? `easeIn${suffix}` : dir === 'inout' ? `easeInOut${suffix}` : `easeOut${suffix}`;
|
|
85
|
+
return ENGINE_EASES.has(engine) ? { engine } : refuse('has no equivalent in the engine');
|
|
86
|
+
}
|
|
87
|
+
/** The ease of the same move played backwards (a yoyo's return), or null when the engine has none. */
|
|
88
|
+
function reverseEase(engine) {
|
|
89
|
+
if (engine === 'linear' || engine === 'hold' || engine === 'step' || engine.startsWith('easeInOut'))
|
|
90
|
+
return engine;
|
|
91
|
+
if (engine === 'bounce')
|
|
92
|
+
return 'easeInBounce';
|
|
93
|
+
if (engine === 'elastic')
|
|
94
|
+
return 'easeInElastic';
|
|
95
|
+
if (engine.startsWith('easeIn')) {
|
|
96
|
+
const out = 'easeOut' + engine.slice(6);
|
|
97
|
+
return ENGINE_EASES.has(out) ? out : null;
|
|
98
|
+
}
|
|
99
|
+
if (engine.startsWith('easeOut')) {
|
|
100
|
+
const inn = 'easeIn' + engine.slice(7);
|
|
101
|
+
return ENGINE_EASES.has(inn) ? inn : null;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** GSAP property → [engine property, kind, how it was converted]. */
|
|
106
|
+
const PROPS = {
|
|
107
|
+
x: { engine: 'x', kind: 'offset' },
|
|
108
|
+
y: { engine: 'y', kind: 'offset' },
|
|
109
|
+
scale: { engine: 'scale', kind: 'number' },
|
|
110
|
+
scaleX: { engine: 'scaleX', kind: 'number' },
|
|
111
|
+
scaleY: { engine: 'scaleY', kind: 'number' },
|
|
112
|
+
rotation: { engine: 'rotation', kind: 'number' },
|
|
113
|
+
rotate: { engine: 'rotation', kind: 'number', note: 'rotate → rotation (degrees)' },
|
|
114
|
+
opacity: { engine: 'opacity', kind: 'number' },
|
|
115
|
+
autoAlpha: { engine: 'opacity', kind: 'number', note: 'autoAlpha → opacity (the item stays in the scene at 0; it is not hidden)' },
|
|
116
|
+
fill: { engine: 'fillColor', kind: 'color', note: 'fill → fillColor' },
|
|
117
|
+
color: { engine: 'fillColor', kind: 'color', note: 'color → fillColor' },
|
|
118
|
+
fillColor: { engine: 'fillColor', kind: 'color' },
|
|
119
|
+
backgroundColor: { engine: 'fillColor', kind: 'color', note: 'backgroundColor → fillColor (the item\'s own fill)' },
|
|
120
|
+
stroke: { engine: 'strokeColor', kind: 'color', note: 'stroke → strokeColor' },
|
|
121
|
+
strokeColor: { engine: 'strokeColor', kind: 'color' },
|
|
122
|
+
strokeWidth: { engine: 'strokeWidth', kind: 'number' },
|
|
123
|
+
width: { engine: 'width', kind: 'number' },
|
|
124
|
+
height: { engine: 'height', kind: 'number' },
|
|
125
|
+
radius: { engine: 'radius', kind: 'number' },
|
|
126
|
+
fontSize: { engine: 'fontSize', kind: 'number' },
|
|
127
|
+
drawSVG: { engine: 'trimEnd', kind: 'draw', note: 'drawSVG → trimStart/trimEnd (the engine\'s draw-on, 0..1 of the path)' },
|
|
128
|
+
trimEnd: { engine: 'trimEnd', kind: 'number' },
|
|
129
|
+
trimStart: { engine: 'trimStart', kind: 'number' },
|
|
130
|
+
strokeDashoffset: { engine: 'dashOffset', kind: 'number', note: 'strokeDashoffset → dashOffset (px along the dash pattern; drawSVG is the length-free draw-on)' },
|
|
131
|
+
strokeDasharray: { engine: 'dashArray', kind: 'dash', note: 'strokeDasharray → dashArray' },
|
|
132
|
+
};
|
|
133
|
+
/** GSAP's special keys, read from vars (or from) as well as the tween itself. */
|
|
134
|
+
const SPECIAL = ['duration', 'delay', 'ease', 'stagger', 'repeat', 'yoyo', 'repeatDelay', 'transformOrigin'];
|
|
135
|
+
/** Keys GSAP has that this op cannot carry, with the reason, so the refusal says more than "unknown". */
|
|
136
|
+
const REFUSED_KEYS = {
|
|
137
|
+
xPercent: 'percentages of the item\'s size are not an engine property; give x in px',
|
|
138
|
+
yPercent: 'percentages of the item\'s size are not an engine property; give y in px',
|
|
139
|
+
rotationX: 'the canvas is 2D; 3D rotation is the camera director\'s',
|
|
140
|
+
rotationY: 'the canvas is 2D; 3D rotation is the camera director\'s',
|
|
141
|
+
skewX: 'skew is not keyable in the engine',
|
|
142
|
+
skewY: 'skew is not keyable in the engine',
|
|
143
|
+
onComplete: 'callbacks cannot cross a JSON call; nothing here runs code',
|
|
144
|
+
onStart: 'callbacks cannot cross a JSON call; nothing here runs code',
|
|
145
|
+
onUpdate: 'callbacks cannot cross a JSON call; nothing here runs code',
|
|
146
|
+
onRepeat: 'callbacks cannot cross a JSON call; nothing here runs code',
|
|
147
|
+
motionPath: 'a path to follow is a moves_along_path relation op, not a tween',
|
|
148
|
+
morphSVG: 'a shape morph is a morphs_to relation op, not a tween',
|
|
149
|
+
text: 'text content is discrete: key properties.content with keyframe_animate',
|
|
150
|
+
immediateRender: 'the engine always shows a track\'s first key from the start (immediateRender true)',
|
|
151
|
+
overwrite: 'overlapping tweens on one property are refused instead of overwritten',
|
|
152
|
+
yoyoEase: 'the return of a yoyo uses the reverse of its ease; another ease is not supported',
|
|
153
|
+
};
|
|
154
|
+
const TWEEN_KEYS = new Set(['to', 'targets', 'target', 'vars', 'from', 'at', 'position', ...SPECIAL]);
|
|
155
|
+
const TIMELINE_KEYS = new Set(['type', 'defaults', 'tweens', 'duration', 'labels', 'repeat', 'yoyo']);
|
|
156
|
+
const EPS = 0.001;
|
|
157
|
+
const round = (t) => Math.round(t * 1e4) / 1e4;
|
|
158
|
+
const REST0 = { k: 'rest', add: 0 };
|
|
159
|
+
function sameVal(a, b) {
|
|
160
|
+
if (a.k !== b.k)
|
|
161
|
+
return false;
|
|
162
|
+
if (a.k === 'abs' && b.k === 'abs')
|
|
163
|
+
return JSON.stringify(a.v) === JSON.stringify(b.v);
|
|
164
|
+
const ra = a, rb = b;
|
|
165
|
+
return ra.add === rb.add && ra.pivot === rb.pivot && ra.s === rb.s && ra.sAdd === rb.sAdd;
|
|
166
|
+
}
|
|
167
|
+
function describe(v, prop) {
|
|
168
|
+
if (v.k === 'abs')
|
|
169
|
+
return JSON.stringify(v.v);
|
|
170
|
+
if (prop === 'x' || prop === 'y') {
|
|
171
|
+
const base = v.add === 0 ? 'home' : `home${v.add > 0 ? '+' : ''}${v.add}`;
|
|
172
|
+
return v.pivot ? `${base} (edge held at scale ${v.s ?? 'rest'})` : base;
|
|
173
|
+
}
|
|
174
|
+
return v.add === 0 ? 'its own value' : `its own value ${v.add > 0 ? '+' : ''}${v.add}`;
|
|
175
|
+
}
|
|
176
|
+
function toKeyValue(v, engineProp) {
|
|
177
|
+
if (v.k === 'abs')
|
|
178
|
+
return v.v;
|
|
179
|
+
const out = { $rest: engineProp };
|
|
180
|
+
if (v.add)
|
|
181
|
+
out.add = v.add;
|
|
182
|
+
if (v.pivot) {
|
|
183
|
+
out.pivot = v.pivot;
|
|
184
|
+
if (v.s !== undefined)
|
|
185
|
+
out.s = v.s;
|
|
186
|
+
if (v.sAdd)
|
|
187
|
+
out.sAdd = v.sAdd;
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/** Parse one asked value for a property kind. `start` is what a relative "+=" is measured from. */
|
|
192
|
+
function parseValue(raw, kind, start) {
|
|
193
|
+
if (kind === 'color') {
|
|
194
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
195
|
+
return { error: 'a colour is a string such as "#22d3ee" or "rgba(0,0,0,0.5)"' };
|
|
196
|
+
if (/^[+-]=/.test(raw))
|
|
197
|
+
return { error: 'a colour cannot be relative (+= / -=)' };
|
|
198
|
+
return { val: { k: 'abs', v: raw.trim() } };
|
|
199
|
+
}
|
|
200
|
+
if (kind === 'dash') {
|
|
201
|
+
if (typeof raw === 'number' && raw >= 0)
|
|
202
|
+
return { val: { k: 'abs', v: [raw, raw] } };
|
|
203
|
+
if (Array.isArray(raw) && raw.length >= 2 && raw.every((n) => typeof n === 'number'))
|
|
204
|
+
return { val: { k: 'abs', v: raw } };
|
|
205
|
+
if (typeof raw === 'string' && /^\s*[\d.]+(?:[\s,]+[\d.]+)+\s*$/.test(raw))
|
|
206
|
+
return { val: { k: 'abs', v: raw.trim().split(/[\s,]+/).map(Number) } };
|
|
207
|
+
return { error: 'a dash array is a number, [dash, gap] or "dash gap" in px' };
|
|
208
|
+
}
|
|
209
|
+
let note;
|
|
210
|
+
let n;
|
|
211
|
+
let rel = 0;
|
|
212
|
+
if (typeof raw === 'number')
|
|
213
|
+
n = raw;
|
|
214
|
+
else if (typeof raw === 'string') {
|
|
215
|
+
const m = /^\s*([+-]=)?\s*(-?\d*\.?\d+(?:e-?\d+)?)\s*(px|deg)?\s*$/i.exec(raw);
|
|
216
|
+
if (!m) {
|
|
217
|
+
return { error: /%/.test(raw) ? 'a percentage is not an engine value here; give px (x, y) or a number' : `"${raw}" is not a number, "+=N" or "-=N"` };
|
|
218
|
+
}
|
|
219
|
+
n = Number(m[2]);
|
|
220
|
+
if (m[1])
|
|
221
|
+
rel = m[1] === '+=' ? 1 : -1;
|
|
222
|
+
if (m[3])
|
|
223
|
+
note = `"${raw}" → ${rel ? m[1] : ''}${n}`;
|
|
224
|
+
}
|
|
225
|
+
else
|
|
226
|
+
return { error: 'expected a number, "+=N" or "-=N"' };
|
|
227
|
+
if (!Number.isFinite(n))
|
|
228
|
+
return { error: 'not a finite number' };
|
|
229
|
+
if (rel) {
|
|
230
|
+
const s = start();
|
|
231
|
+
if (s.k === 'abs') {
|
|
232
|
+
if (typeof s.v !== 'number')
|
|
233
|
+
return { error: 'a relative value needs a numeric start' };
|
|
234
|
+
return { val: { k: 'abs', v: s.v + rel * n }, note };
|
|
235
|
+
}
|
|
236
|
+
return { val: { k: 'rest', add: s.add + rel * n }, note };
|
|
237
|
+
}
|
|
238
|
+
return { val: kind === 'offset' ? { k: 'rest', add: n } : { k: 'abs', v: n }, note };
|
|
239
|
+
}
|
|
240
|
+
/** drawSVG "a% b%" / "b%" / true / 0 → [trimStart, trimEnd] in 0..1. */
|
|
241
|
+
function parseDraw(raw) {
|
|
242
|
+
if (raw === true)
|
|
243
|
+
return [0, 1];
|
|
244
|
+
if (raw === 0 || raw === '0')
|
|
245
|
+
return [0, 0];
|
|
246
|
+
if (typeof raw === 'string') {
|
|
247
|
+
const parts = raw.trim().split(/\s+/);
|
|
248
|
+
const pct = parts.map((p) => /^(-?\d*\.?\d+)%$/.exec(p));
|
|
249
|
+
if (pct.every(Boolean) && (parts.length === 1 || parts.length === 2)) {
|
|
250
|
+
const nums = pct.map((m) => Number(m[1]) / 100);
|
|
251
|
+
return parts.length === 1 ? [0, nums[0]] : [nums[0], nums[1]];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { error: 'drawSVG takes "N%" or "A% B%" (or true / 0); lengths in px need the path length, which this op does not know' };
|
|
255
|
+
}
|
|
256
|
+
function resolveAt(at, cur, labels) {
|
|
257
|
+
if (at === undefined || at === null)
|
|
258
|
+
return cur.end;
|
|
259
|
+
if (typeof at === 'number')
|
|
260
|
+
return Number.isFinite(at) && at >= 0 ? at : { error: 'an absolute position is seconds ≥ 0' };
|
|
261
|
+
if (typeof at !== 'string')
|
|
262
|
+
return { error: 'at is seconds, "<", ">", "+=N", "-=N" or a label' };
|
|
263
|
+
const s = at.trim();
|
|
264
|
+
let m = /^([<>])\s*(?:([+-]=?)?\s*(\d*\.?\d+))?$/.exec(s);
|
|
265
|
+
if (m) {
|
|
266
|
+
const base = m[1] === '<' ? cur.prevStart : cur.prevEnd;
|
|
267
|
+
const off = m[3] ? Number(m[3]) * (m[2] && m[2].startsWith('-') ? -1 : 1) : 0;
|
|
268
|
+
return Math.max(0, base + off);
|
|
269
|
+
}
|
|
270
|
+
m = /^([+-])=\s*(\d*\.?\d+)$/.exec(s);
|
|
271
|
+
if (m)
|
|
272
|
+
return Math.max(0, cur.end + Number(m[2]) * (m[1] === '-' ? -1 : 1));
|
|
273
|
+
m = /^([A-Za-z_][\w-]*)\s*(?:([+-])=\s*(\d*\.?\d+))?$/.exec(s);
|
|
274
|
+
if (m) {
|
|
275
|
+
if (!(m[1] in labels))
|
|
276
|
+
return { error: `no label "${m[1]}"; labels: ${Object.keys(labels).join(', ') || 'none (give the timeline labels: {"name": seconds})'}` };
|
|
277
|
+
return Math.max(0, labels[m[1]] + (m[3] ? Number(m[3]) * (m[2] === '-' ? -1 : 1) : 0));
|
|
278
|
+
}
|
|
279
|
+
if (/%/.test(s))
|
|
280
|
+
return { error: `"${s}": percentage positions are not supported; give seconds` };
|
|
281
|
+
return { error: `"${s}" is not a position: seconds, "<", ">", "<0.2", "+=0.2", "-=0.2", or a label` };
|
|
282
|
+
}
|
|
283
|
+
function staggerDelays(n, stagger) {
|
|
284
|
+
if (stagger === undefined || n <= 1)
|
|
285
|
+
return new Array(n).fill(0);
|
|
286
|
+
let each = 0, amount, from = 'start';
|
|
287
|
+
if (typeof stagger === 'number')
|
|
288
|
+
each = stagger;
|
|
289
|
+
else if (stagger && typeof stagger === 'object' && !Array.isArray(stagger)) {
|
|
290
|
+
const o = stagger;
|
|
291
|
+
for (const k of Object.keys(o)) {
|
|
292
|
+
if (!['each', 'amount', 'from'].includes(k)) {
|
|
293
|
+
return { error: `stagger.${k} is not supported (${k === 'grid' || k === 'axis' ? 'a grid stagger needs a layout the op does not know' : k === 'ease' ? 'an eased stagger is not supported' : 'accepted: each, amount, from'})` };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (o.each !== undefined)
|
|
297
|
+
each = Number(o.each);
|
|
298
|
+
if (o.amount !== undefined)
|
|
299
|
+
amount = Number(o.amount);
|
|
300
|
+
if (o.from !== undefined)
|
|
301
|
+
from = o.from;
|
|
302
|
+
}
|
|
303
|
+
else
|
|
304
|
+
return { error: 'stagger is seconds, or { each | amount, from }' };
|
|
305
|
+
if (!Number.isFinite(each) || (amount !== undefined && !Number.isFinite(amount)))
|
|
306
|
+
return { error: 'stagger each / amount must be numbers' };
|
|
307
|
+
const c = (n - 1) / 2;
|
|
308
|
+
let d;
|
|
309
|
+
if (typeof from === 'number' && Number.isInteger(from) && from >= 0 && from < n)
|
|
310
|
+
d = [...Array(n)].map((_, i) => Math.abs(i - from));
|
|
311
|
+
else if (from === 'start')
|
|
312
|
+
d = [...Array(n)].map((_, i) => i);
|
|
313
|
+
else if (from === 'end')
|
|
314
|
+
d = [...Array(n)].map((_, i) => n - 1 - i);
|
|
315
|
+
else if (from === 'center')
|
|
316
|
+
d = [...Array(n)].map((_, i) => Math.abs(i - c));
|
|
317
|
+
else if (from === 'edges')
|
|
318
|
+
d = [...Array(n)].map((_, i) => c - Math.abs(i - c));
|
|
319
|
+
else
|
|
320
|
+
return { error: `stagger.from ${JSON.stringify(from)} is not supported: start, end, center, edges, or an index 0..${n - 1}${from === 'random' ? ' (a random order would differ between renders)' : ''}` };
|
|
321
|
+
const min = Math.min(...d), max = Math.max(...d);
|
|
322
|
+
if (amount !== undefined)
|
|
323
|
+
each = max > min ? amount / (max - min) : 0;
|
|
324
|
+
return d.map((x) => (x - min) * each);
|
|
325
|
+
}
|
|
326
|
+
/** The resolved [start, end] of each segment, filled while channels are walked (in time order). */
|
|
327
|
+
const resolvedEnds = new WeakMap();
|
|
328
|
+
/** One segment's total length including its repeats (Infinity for repeat -1). */
|
|
329
|
+
const segLength = (s) => s.repeat < 0 ? Infinity : s.dur * (s.repeat + 1) + s.repeatDelay * s.repeat;
|
|
330
|
+
function parseOrigin(raw) {
|
|
331
|
+
if (typeof raw !== 'string')
|
|
332
|
+
return { error: 'transformOrigin is a string such as "50% 100%" or "center bottom"' };
|
|
333
|
+
const words = raw.trim().toLowerCase().split(/\s+/);
|
|
334
|
+
let h = 'center', v = 'center';
|
|
335
|
+
const asH = (w) => (w === 'left' || w === '0%' ? 'left' : w === 'right' || w === '100%' ? 'right' : w === 'center' || w === '50%' ? 'center' : null);
|
|
336
|
+
const asV = (w) => (w === 'top' || w === '0%' ? 'top' : w === 'bottom' || w === '100%' ? 'bottom' : w === 'center' || w === '50%' ? 'center' : null);
|
|
337
|
+
if (words.length === 1) {
|
|
338
|
+
const w = words[0];
|
|
339
|
+
if (w === 'top' || w === 'bottom')
|
|
340
|
+
v = w;
|
|
341
|
+
else if (w === 'left' || w === 'right')
|
|
342
|
+
h = w;
|
|
343
|
+
else if (!(w === 'center' || w === '50%'))
|
|
344
|
+
return { error: `transformOrigin "${raw}" is not understood` };
|
|
345
|
+
}
|
|
346
|
+
else if (words.length === 2) {
|
|
347
|
+
// GSAP reads "x y", but keywords may come in either order ("bottom center").
|
|
348
|
+
const [a, b] = words;
|
|
349
|
+
if ((a === 'top' || a === 'bottom') && asH(b)) {
|
|
350
|
+
v = a;
|
|
351
|
+
h = asH(b);
|
|
352
|
+
}
|
|
353
|
+
else if (asH(a) && asV(b)) {
|
|
354
|
+
h = asH(a);
|
|
355
|
+
v = asV(b);
|
|
356
|
+
}
|
|
357
|
+
else
|
|
358
|
+
return { error: `transformOrigin "${raw}" is not understood` };
|
|
359
|
+
}
|
|
360
|
+
else
|
|
361
|
+
return { error: `transformOrigin "${raw}" is not understood (2D origins only)` };
|
|
362
|
+
if (h !== 'center')
|
|
363
|
+
return { error: `transformOrigin "${raw}": the engine scales x about the item's centre only; a ${h} pivot is not possible` };
|
|
364
|
+
return v === 'bottom' ? 1 : v === 'top' ? -1 : 0;
|
|
365
|
+
}
|
|
366
|
+
/** Glob "bar-*" over names; "*" matches any run. */
|
|
367
|
+
function globToRegExp(glob) {
|
|
368
|
+
return new RegExp('^' + glob.split('*').map((s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$');
|
|
369
|
+
}
|
|
370
|
+
export function compileTimeline(op, ctx = { creates: [] }) {
|
|
371
|
+
const errors = [];
|
|
372
|
+
const err = (path, message) => { errors.push({ path, message }); };
|
|
373
|
+
for (const k of Object.keys(op)) {
|
|
374
|
+
if (!TIMELINE_KEYS.has(k))
|
|
375
|
+
err(k, `"${k}" is not a timeline field (accepted: ${[...TIMELINE_KEYS].filter((x) => x !== 'type').join(', ')})`);
|
|
376
|
+
}
|
|
377
|
+
const defaults = (op.defaults ?? {});
|
|
378
|
+
if (typeof defaults !== 'object' || Array.isArray(defaults))
|
|
379
|
+
err('defaults', 'defaults is an object: { ease, duration }');
|
|
380
|
+
else
|
|
381
|
+
for (const k of Object.keys(defaults))
|
|
382
|
+
if (k !== 'ease' && k !== 'duration')
|
|
383
|
+
err(`defaults.${k}`, `defaults takes ease and duration; put "${k}" on the tween`);
|
|
384
|
+
const labels = {};
|
|
385
|
+
if (op.labels !== undefined) {
|
|
386
|
+
if (!op.labels || typeof op.labels !== 'object' || Array.isArray(op.labels))
|
|
387
|
+
err('labels', 'labels is { "name": seconds }');
|
|
388
|
+
else
|
|
389
|
+
for (const [k, v] of Object.entries(op.labels)) {
|
|
390
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0)
|
|
391
|
+
err(`labels.${k}`, 'a label is seconds ≥ 0');
|
|
392
|
+
else
|
|
393
|
+
labels[k] = v;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const tlDuration = op.duration;
|
|
397
|
+
if (tlDuration !== undefined && (typeof tlDuration !== 'number' || !(tlDuration > 0)))
|
|
398
|
+
err('duration', 'the timeline duration is seconds > 0');
|
|
399
|
+
const tlLoop = op.repeat === -1;
|
|
400
|
+
if (op.repeat !== undefined && op.repeat !== -1 && op.repeat !== 0)
|
|
401
|
+
err('repeat', 'a timeline repeats forever (-1, every track loops at the timeline\'s length) or not at all; repeat a tween instead');
|
|
402
|
+
if (op.yoyo === true)
|
|
403
|
+
err('yoyo', 'a whole-timeline yoyo is not supported; yoyo a tween instead');
|
|
404
|
+
const tweens = op.tweens;
|
|
405
|
+
if (!Array.isArray(tweens) || tweens.length === 0) {
|
|
406
|
+
err('tweens', 'tweens is a non-empty array of { to, vars, from, duration, at, ... }');
|
|
407
|
+
return { ok: false, errors };
|
|
408
|
+
}
|
|
409
|
+
const defEaseName = typeof defaults.ease === 'string' ? defaults.ease : undefined;
|
|
410
|
+
const defDuration = typeof defaults.duration === 'number' ? defaults.duration : undefined;
|
|
411
|
+
const notes = [];
|
|
412
|
+
const segments = [];
|
|
413
|
+
const receipts = [];
|
|
414
|
+
const cur = { end: 0, prevStart: 0, prevEnd: 0 };
|
|
415
|
+
const pivotByTarget = new Map();
|
|
416
|
+
const userYByTarget = new Map();
|
|
417
|
+
const refByTarget = new Map();
|
|
418
|
+
const createByName = new Map();
|
|
419
|
+
ctx.creates.forEach((c, i) => { if (c.name)
|
|
420
|
+
createByName.set(c.name, [...(createByName.get(c.name) || []), i]); });
|
|
421
|
+
const anchorOf = (target) => {
|
|
422
|
+
const m = /^\$(\d+)$/.exec(target);
|
|
423
|
+
return m ? ctx.creates[Number(m[1])]?.anchor : undefined;
|
|
424
|
+
};
|
|
425
|
+
tweens.forEach((rawTween, ti) => {
|
|
426
|
+
const P = `tweens[${ti}]`;
|
|
427
|
+
if (!rawTween || typeof rawTween !== 'object' || Array.isArray(rawTween)) {
|
|
428
|
+
err(P, 'a tween is an object');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const tw = rawTween;
|
|
432
|
+
const before = errors.length;
|
|
433
|
+
const converted = [];
|
|
434
|
+
const dropped = [];
|
|
435
|
+
for (const k of Object.keys(tw)) {
|
|
436
|
+
if (!TWEEN_KEYS.has(k))
|
|
437
|
+
err(`${P}.${k}`, REFUSED_KEYS[k] ? `${k} is refused: ${REFUSED_KEYS[k]}` : `"${k}" is not a tween field (accepted: to, vars, from, duration, delay, at, stagger, repeat, yoyo, repeatDelay, ease)`);
|
|
438
|
+
}
|
|
439
|
+
const vars = { ...(tw.vars ?? {}) };
|
|
440
|
+
const fromVars = { ...(tw.from ?? {}) };
|
|
441
|
+
if (typeof vars !== 'object' || Array.isArray(tw.vars))
|
|
442
|
+
err(`${P}.vars`, 'vars is an object of end values');
|
|
443
|
+
if (typeof fromVars !== 'object' || Array.isArray(tw.from))
|
|
444
|
+
err(`${P}.from`, 'from is an object of start values');
|
|
445
|
+
// Special keys may sit on the tween or inside vars / from, as GSAP writes them.
|
|
446
|
+
const special = {};
|
|
447
|
+
for (const key of SPECIAL) {
|
|
448
|
+
const places = [];
|
|
449
|
+
if (tw[key] !== undefined)
|
|
450
|
+
places.push([`${P}.${key}`, tw[key]]);
|
|
451
|
+
if (vars[key] !== undefined) {
|
|
452
|
+
places.push([`${P}.vars.${key}`, vars[key]]);
|
|
453
|
+
delete vars[key];
|
|
454
|
+
}
|
|
455
|
+
if (fromVars[key] !== undefined) {
|
|
456
|
+
places.push([`${P}.from.${key}`, fromVars[key]]);
|
|
457
|
+
delete fromVars[key];
|
|
458
|
+
}
|
|
459
|
+
if (places.length > 1 && places.some(([, v]) => JSON.stringify(v) !== JSON.stringify(places[0][1]))) {
|
|
460
|
+
err(places[1][0], `${key} is given twice with different values (${places.map(([p]) => p).join(', ')})`);
|
|
461
|
+
}
|
|
462
|
+
if (places.length)
|
|
463
|
+
special[key] = places[0][1];
|
|
464
|
+
}
|
|
465
|
+
if (tw.at !== undefined && tw.position !== undefined)
|
|
466
|
+
err(`${P}.position`, 'give at or position, not both');
|
|
467
|
+
const at = tw.at ?? tw.position;
|
|
468
|
+
// Targets.
|
|
469
|
+
const rawTo = tw.to ?? tw.targets ?? tw.target;
|
|
470
|
+
if ([tw.to, tw.targets, tw.target].filter((v) => v !== undefined).length > 1)
|
|
471
|
+
err(`${P}.to`, 'name the targets once (to)');
|
|
472
|
+
const refs = Array.isArray(rawTo) ? rawTo : rawTo === undefined ? [] : [rawTo];
|
|
473
|
+
if (!refs.length)
|
|
474
|
+
err(`${P}.to`, 'to names the item(s): a name, an id, "$N", or an array of them');
|
|
475
|
+
const targets = [];
|
|
476
|
+
refs.forEach((r, ri) => {
|
|
477
|
+
if (typeof r !== 'string' || !r.trim()) {
|
|
478
|
+
err(`${P}.to[${ri}]`, 'a target is a string');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
let ref = r.trim();
|
|
482
|
+
if (ref.startsWith('.')) {
|
|
483
|
+
err(`${P}.to`, `"${ref}" is a CSS class; items have names: use the name, or a name pattern like "bar-*"`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (ref.startsWith('#')) {
|
|
487
|
+
converted.push(`target "${ref}" → name "${ref.slice(1)}"`);
|
|
488
|
+
ref = ref.slice(1);
|
|
489
|
+
}
|
|
490
|
+
if (/^\$\d+$/.test(ref)) {
|
|
491
|
+
if (Number(ref.slice(1)) >= ctx.creates.length) {
|
|
492
|
+
err(`${P}.to`, `${ref} is an item this call creates, and it creates ${ctx.creates.length} before this op`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
targets.push({ ref, target: ref });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (ref.includes('*')) {
|
|
499
|
+
const re = globToRegExp(ref);
|
|
500
|
+
const hits = ctx.creates.map((c, i) => (c.name && re.test(c.name) ? i : -1)).filter((i) => i >= 0);
|
|
501
|
+
if (!hits.length) {
|
|
502
|
+
err(`${P}.to`, `"${ref}" matches no item this call creates before the timeline (a name pattern is resolved against this batch's creates)`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (const i of hits)
|
|
506
|
+
targets.push({ ref: ctx.creates[i].name, target: `$${i}` });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const named = createByName.get(ref);
|
|
510
|
+
if (named && named.length > 1) {
|
|
511
|
+
err(`${P}.to`, `${named.length} items this call creates are named "${ref}"; give each its own name, or use "$N"`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
targets.push({ ref, target: named ? `$${named[0]}` : ref });
|
|
515
|
+
});
|
|
516
|
+
const seen = new Set();
|
|
517
|
+
for (const t of targets) {
|
|
518
|
+
if (seen.has(t.target))
|
|
519
|
+
err(`${P}.to`, `"${t.ref}" is named twice in one tween`);
|
|
520
|
+
seen.add(t.target);
|
|
521
|
+
}
|
|
522
|
+
// Timing.
|
|
523
|
+
let dur = special.duration !== undefined ? special.duration : defDuration;
|
|
524
|
+
if (dur === undefined) {
|
|
525
|
+
dur = 0.5;
|
|
526
|
+
converted.push('duration not given → 0.5 s (GSAP\'s default)');
|
|
527
|
+
}
|
|
528
|
+
if (typeof dur !== 'number' || !Number.isFinite(dur) || dur < 0) {
|
|
529
|
+
err(`${P}.duration`, 'duration is seconds ≥ 0');
|
|
530
|
+
dur = 0;
|
|
531
|
+
}
|
|
532
|
+
const delay = special.delay === undefined ? 0 : special.delay;
|
|
533
|
+
if (typeof delay !== 'number' || !Number.isFinite(delay) || delay < 0)
|
|
534
|
+
err(`${P}.delay`, 'delay is seconds ≥ 0');
|
|
535
|
+
const repeat = special.repeat === undefined ? 0 : special.repeat;
|
|
536
|
+
if (typeof repeat !== 'number' || !Number.isInteger(repeat) || repeat < -1)
|
|
537
|
+
err(`${P}.repeat`, 'repeat is a whole number of extra plays, or -1 for forever');
|
|
538
|
+
const yoyo = special.yoyo === true;
|
|
539
|
+
if (special.yoyo !== undefined && typeof special.yoyo !== 'boolean')
|
|
540
|
+
err(`${P}.yoyo`, 'yoyo is true or false');
|
|
541
|
+
const repeatDelay = special.repeatDelay === undefined ? 0 : special.repeatDelay;
|
|
542
|
+
if (typeof repeatDelay !== 'number' || repeatDelay < 0)
|
|
543
|
+
err(`${P}.repeatDelay`, 'repeatDelay is seconds ≥ 0');
|
|
544
|
+
const askedEase = typeof special.ease === 'string' ? special.ease : defEaseName ?? GSAP_DEFAULT_EASE;
|
|
545
|
+
if (special.ease !== undefined && typeof special.ease !== 'string')
|
|
546
|
+
err(`${P}.ease`, 'ease is a name such as "power2.out"');
|
|
547
|
+
const mappedEase = mapGsapEase(askedEase);
|
|
548
|
+
if ('error' in mappedEase)
|
|
549
|
+
err(special.ease !== undefined ? `${P}.ease` : 'defaults.ease', mappedEase.error);
|
|
550
|
+
const ease = 'engine' in mappedEase ? mappedEase.engine : 'linear';
|
|
551
|
+
if (special.ease === undefined && defEaseName === undefined)
|
|
552
|
+
converted.push(`ease not given → ${GSAP_DEFAULT_EASE} (GSAP's default) → ${ease}`);
|
|
553
|
+
if (yoyo && repeat !== 0 && !reverseEase(ease))
|
|
554
|
+
err(`${P}.yoyo`, `ease ${ease} has no reverse in the engine, so a yoyo cannot play it back`);
|
|
555
|
+
const delays = staggerDelays(targets.length, special.stagger);
|
|
556
|
+
if (!Array.isArray(delays))
|
|
557
|
+
err(`${P}.stagger`, delays.error);
|
|
558
|
+
const start = resolveAt(at, cur, labels);
|
|
559
|
+
if (typeof start !== 'number')
|
|
560
|
+
err(`${P}.at`, start.error);
|
|
561
|
+
// Pivot for scale.
|
|
562
|
+
let originPivot;
|
|
563
|
+
if (special.transformOrigin !== undefined) {
|
|
564
|
+
const o = parseOrigin(special.transformOrigin);
|
|
565
|
+
if (typeof o === 'object')
|
|
566
|
+
err(`${P}.vars.transformOrigin`, o.error);
|
|
567
|
+
else
|
|
568
|
+
originPivot = o;
|
|
569
|
+
}
|
|
570
|
+
// Properties.
|
|
571
|
+
const propKeys = [...new Set([...Object.keys(vars), ...Object.keys(fromVars)])];
|
|
572
|
+
if (!propKeys.length)
|
|
573
|
+
err(P, 'the tween animates nothing: give vars (end values) and/or from (start values)');
|
|
574
|
+
const accepted = [];
|
|
575
|
+
for (const k of propKeys) {
|
|
576
|
+
const spec = PROPS[k];
|
|
577
|
+
if (!spec) {
|
|
578
|
+
const where = k in vars ? 'vars' : 'from';
|
|
579
|
+
err(`${P}.${where}.${k}`, REFUSED_KEYS[k] ? `${k} is refused: ${REFUSED_KEYS[k]}` : `"${k}" is not a property this op can animate. Accepted: ${Object.keys(PROPS).join(', ')}`);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (spec.note)
|
|
583
|
+
converted.push(spec.note);
|
|
584
|
+
accepted.push({ asked: k, spec });
|
|
585
|
+
}
|
|
586
|
+
const enginesUsed = new Map();
|
|
587
|
+
for (const a of accepted) {
|
|
588
|
+
const engines = a.spec.kind === 'draw' ? ['trimStart', 'trimEnd'] : [a.spec.engine];
|
|
589
|
+
for (const e of engines) {
|
|
590
|
+
if (enginesUsed.has(e))
|
|
591
|
+
err(P, `${enginesUsed.get(e)} and ${a.asked} both animate the engine's ${e}; give one`);
|
|
592
|
+
enginesUsed.set(e, a.asked);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (enginesUsed.has('scale') && (enginesUsed.has('scaleX') || enginesUsed.has('scaleY')))
|
|
596
|
+
err(P, 'scale and scaleX/scaleY in one tween: give one form');
|
|
597
|
+
if (errors.length > before || typeof start !== 'number' || !Array.isArray(delays) || typeof dur !== 'number')
|
|
598
|
+
return;
|
|
599
|
+
const tStart = start + delay;
|
|
600
|
+
const one = segLength({ dur, repeat: repeat, repeatDelay: repeatDelay });
|
|
601
|
+
const receiptProps = [];
|
|
602
|
+
const receiptTargets = [];
|
|
603
|
+
targets.forEach((t, i) => {
|
|
604
|
+
refByTarget.set(t.target, t.ref);
|
|
605
|
+
const t0 = round(tStart + delays[i]);
|
|
606
|
+
const base = { tween: ti, target: t.target, t0, dur, ease, repeat: repeat, yoyo, repeatDelay: repeatDelay };
|
|
607
|
+
for (const a of accepted) {
|
|
608
|
+
const hasFrom = a.asked in fromVars, hasTo = a.asked in vars;
|
|
609
|
+
if (a.spec.kind === 'draw') {
|
|
610
|
+
const f = hasFrom ? parseDraw(fromVars[a.asked]) : undefined;
|
|
611
|
+
const v = hasTo ? parseDraw(vars[a.asked]) : undefined;
|
|
612
|
+
if (f && !Array.isArray(f)) {
|
|
613
|
+
err(`${P}.from.${a.asked}`, f.error);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (v && !Array.isArray(v)) {
|
|
617
|
+
err(`${P}.vars.${a.asked}`, v.error);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const fa = f, va = v;
|
|
621
|
+
const needsStart = (fa && fa[0] !== 0) || (va && va[0] !== 0);
|
|
622
|
+
if (needsStart)
|
|
623
|
+
segments.push({ ...base, prop: 'trimStart', from: fa ? { k: 'abs', v: fa[0] } : undefined, to: va ? { k: 'abs', v: va[0] } : undefined });
|
|
624
|
+
segments.push({ ...base, prop: 'trimEnd', from: fa ? { k: 'abs', v: fa[1] } : undefined, to: va ? { k: 'abs', v: va[1] } : undefined });
|
|
625
|
+
if (i === 0)
|
|
626
|
+
receiptProps.push({ asked: a.asked, engine: needsStart ? 'trimStart, trimEnd' : 'trimEnd', from: fa ? JSON.stringify(fa) : 'its own value', to: va ? JSON.stringify(va) : 'its own value' });
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
// A relative value is measured from the value the item has at the tween's start: resolved per channel below,
|
|
630
|
+
// so it is parsed against a marker here and rebased later.
|
|
631
|
+
const parse = (raw, where) => {
|
|
632
|
+
const r = parseValue(raw, a.spec.kind, () => REST0);
|
|
633
|
+
if ('error' in r) {
|
|
634
|
+
err(`${P}.${where}.${a.asked}`, r.error);
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
if (r.note && i === 0)
|
|
638
|
+
converted.push(r.note);
|
|
639
|
+
return r.val;
|
|
640
|
+
};
|
|
641
|
+
const relFrom = hasFrom && typeof fromVars[a.asked] === 'string' && /^\s*[+-]=/.test(fromVars[a.asked]);
|
|
642
|
+
const relTo = hasTo && typeof vars[a.asked] === 'string' && /^\s*[+-]=/.test(vars[a.asked]);
|
|
643
|
+
const from = hasFrom ? parse(fromVars[a.asked], 'from') : undefined;
|
|
644
|
+
const to = hasTo ? parse(vars[a.asked], 'vars') : undefined;
|
|
645
|
+
if ((hasFrom && !from) || (hasTo && !to))
|
|
646
|
+
continue;
|
|
647
|
+
const seg = { ...base, prop: a.spec.engine, from, to, relFrom, relTo };
|
|
648
|
+
segments.push(seg);
|
|
649
|
+
if (a.spec.engine === 'y')
|
|
650
|
+
userYByTarget.set(t.target, ti);
|
|
651
|
+
if (i === 0)
|
|
652
|
+
receiptProps.push({ asked: a.asked, engine: a.spec.engine, from: '', to: '' });
|
|
653
|
+
}
|
|
654
|
+
// Baseline pivot: transformOrigin, else the item's create anchor (bottom-* / top-*), else its centre. An item this
|
|
655
|
+
// batch does not create has its anchor read in the studio ('anchor').
|
|
656
|
+
const scaleProp = enginesUsed.has('scaleY') ? 'scaleY' : enginesUsed.has('scale') ? 'scale' : null;
|
|
657
|
+
if (scaleProp) {
|
|
658
|
+
const created = /^\$\d+$/.test(t.target);
|
|
659
|
+
const anchor = anchorOf(t.target);
|
|
660
|
+
const pivot = originPivot !== undefined ? originPivot
|
|
661
|
+
: !created ? 'anchor'
|
|
662
|
+
: anchor && /^bottom/.test(anchor) ? 1 : anchor && /^top/.test(anchor) ? -1 : 0;
|
|
663
|
+
if (pivot === 'anchor') {
|
|
664
|
+
if (i === 0)
|
|
665
|
+
converted.push(`${scaleProp} holds the item's bottom or top edge when its anchor names one (read in the studio), with matching y keys; else it scales about the centre`);
|
|
666
|
+
segments.push({ ...base, prop: 'y', pivotOf: { scaleProp, pivot } });
|
|
667
|
+
}
|
|
668
|
+
else if (pivot) {
|
|
669
|
+
if (i === 0)
|
|
670
|
+
converted.push(`${scaleProp} pivots on the item's ${pivot > 0 ? 'bottom' : 'top'} edge (${originPivot !== undefined ? 'transformOrigin' : `its anchor "${anchor}"`}): compiled as ${scaleProp} keys plus matching y keys that hold the edge`);
|
|
671
|
+
pivotByTarget.set(t.target, { pivot, tween: ti });
|
|
672
|
+
segments.push({ ...base, prop: 'y', pivotOf: { scaleProp, pivot } });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
receiptTargets.push({ ref: t.ref, item: t.target, start: t0, end: Number.isFinite(one) ? round(t0 + one) : 'forever' });
|
|
676
|
+
});
|
|
677
|
+
const tEnd = round(tStart + Math.max(0, ...delays) + one);
|
|
678
|
+
receipts.push({
|
|
679
|
+
tween: ti, compiledTo: 'track', targets: receiptTargets, start: round(tStart),
|
|
680
|
+
end: Number.isFinite(tEnd) ? tEnd : 'forever', ease: { asked: askedEase, engine: ease }, properties: receiptProps,
|
|
681
|
+
converted: [...new Set(converted)], dropped,
|
|
682
|
+
});
|
|
683
|
+
cur.prevStart = round(tStart);
|
|
684
|
+
cur.prevEnd = Number.isFinite(tEnd) ? tEnd : round(tStart + dur);
|
|
685
|
+
if (Number.isFinite(tEnd))
|
|
686
|
+
cur.end = Math.max(cur.end, tEnd);
|
|
687
|
+
});
|
|
688
|
+
for (const [target, { tween }] of pivotByTarget) {
|
|
689
|
+
if (userYByTarget.has(target)) {
|
|
690
|
+
err(`tweens[${userYByTarget.get(target)}].vars.y`, `"${refByTarget.get(target)}" scales from its edge (tween ${tween}) and also tweens y; the edge is held with y keys, so a y tween on the same item cannot be compiled. Move it with a parent group, or scale about its centre (transformOrigin "center").`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (errors.length)
|
|
694
|
+
return { ok: false, errors };
|
|
695
|
+
// An item whose edge would be read in the studio, and that also tweens y: its y is the caller's, so it scales about
|
|
696
|
+
// its centre (said in the notes).
|
|
697
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
698
|
+
const s = segments[i];
|
|
699
|
+
if (s.pivotOf?.pivot === 'anchor' && userYByTarget.has(s.target)) {
|
|
700
|
+
notes.push(`"${refByTarget.get(s.target)}" tweens y, so its ${s.pivotOf.scaleProp} scales about its centre whatever its anchor`);
|
|
701
|
+
segments.splice(i, 1);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// ---- channels: (target, prop) → ordered segments → keys
|
|
705
|
+
const timelineEnd = round(cur.end);
|
|
706
|
+
let clip = timelineEnd;
|
|
707
|
+
if (typeof tlDuration === 'number') {
|
|
708
|
+
if (tlDuration + 1e-9 < timelineEnd) {
|
|
709
|
+
return { ok: false, errors: [{ path: 'duration', message: `the tweens run to ${timelineEnd} s, past the timeline's duration ${tlDuration} s; a key past its clip is never played. Make duration ≥ ${timelineEnd}, or leave it out.` }] };
|
|
710
|
+
}
|
|
711
|
+
clip = tlDuration;
|
|
712
|
+
}
|
|
713
|
+
const byChannel = new Map();
|
|
714
|
+
for (const s of segments) {
|
|
715
|
+
const key = `${s.target}\u0000${s.prop}`;
|
|
716
|
+
byChannel.set(key, [...(byChannel.get(key) || []), s]);
|
|
717
|
+
}
|
|
718
|
+
const segsByTarget = new Map();
|
|
719
|
+
for (const s of segments)
|
|
720
|
+
segsByTarget.set(s.target, [...(segsByTarget.get(s.target) || []), s]);
|
|
721
|
+
const channelKeys = new Map();
|
|
722
|
+
const loopTargets = new Map(); // target → loop period
|
|
723
|
+
// A forever-repeat is the engine's loop only when it is the item's whole story and starts at 0.
|
|
724
|
+
for (const [target, segs] of segsByTarget) {
|
|
725
|
+
const forever = segs.filter((s) => s.repeat < 0);
|
|
726
|
+
if (!forever.length)
|
|
727
|
+
continue;
|
|
728
|
+
const tweensOn = new Set(segs.map((s) => s.tween));
|
|
729
|
+
const f = forever[0];
|
|
730
|
+
if (tweensOn.size === 1 && f.t0 === 0 && !tlLoop) {
|
|
731
|
+
const period = f.yoyo ? 2 * (f.dur + f.repeatDelay) : f.dur + f.repeatDelay;
|
|
732
|
+
if (period > 0) {
|
|
733
|
+
loopTargets.set(target, round(period));
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (typeof tlDuration !== 'number') {
|
|
738
|
+
return { ok: false, errors: [{ path: `tweens[${f.tween}].repeat`, message: `repeat -1 on "${refByTarget.get(target)}", which ${tweensOn.size > 1 ? 'has other tweens' : 'does not start at 0'}: the engine loops a whole track, so this is expanded into keys, and that needs the timeline's duration to know where to stop. Give the timeline duration.` }] };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const ordered = (a, b) => a.t0 - b.t0;
|
|
742
|
+
for (const [key, segsRaw] of byChannel) {
|
|
743
|
+
const segs = [...segsRaw].sort(ordered);
|
|
744
|
+
const target = segs[0].target;
|
|
745
|
+
const keys = [];
|
|
746
|
+
let current = REST0;
|
|
747
|
+
const push = (time, value, e) => {
|
|
748
|
+
let t = round(time);
|
|
749
|
+
const last = keys[keys.length - 1];
|
|
750
|
+
if (last && t <= last.time + 1e-9) {
|
|
751
|
+
if (sameVal(last.value, value))
|
|
752
|
+
return;
|
|
753
|
+
t = round(last.time + EPS); // one property cannot take two values at one time: the jump lands 1 ms later
|
|
754
|
+
}
|
|
755
|
+
keys.push({ time: t, value, ease: e });
|
|
756
|
+
};
|
|
757
|
+
for (let si = 0; si < segs.length; si++) {
|
|
758
|
+
const s = segs[si];
|
|
759
|
+
const prev = segs[si - 1];
|
|
760
|
+
if (prev) {
|
|
761
|
+
const prevEnd = prev.t0 + segLength(prev);
|
|
762
|
+
if (s.t0 + 1e-9 < prevEnd) {
|
|
763
|
+
return { ok: false, errors: [{ path: `tweens[${s.tween}]`, message: `on "${refByTarget.get(target)}", ${s.prop} is tweened by tween ${prev.tween} until ${round(prevEnd)} s and by tween ${s.tween} from ${s.t0} s; two tweens on one property cannot overlap (GSAP would let the later one fight the earlier). Start it at ">" or later.` }] };
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
let a, b;
|
|
767
|
+
if (s.pivotOf) {
|
|
768
|
+
const scaleSeg = (byChannel.get(`${target}\u0000${s.pivotOf.scaleProp}`) || []).find((x) => x.tween === s.tween);
|
|
769
|
+
const sa = resolvedEnds.get(scaleSeg);
|
|
770
|
+
// The scale at each end: a number, or the item's rest scale (+ a relative offset), resolved in the studio.
|
|
771
|
+
const at = (v) => (v.k === 'abs'
|
|
772
|
+
? (typeof v.v === 'number' ? { k: 'rest', add: 0, pivot: s.pivotOf.pivot, s: v.v } : null)
|
|
773
|
+
: { k: 'rest', add: 0, pivot: s.pivotOf.pivot, ...(v.add ? { sAdd: v.add } : {}) });
|
|
774
|
+
const ea = sa && at(sa[0]), eb = sa && at(sa[1]);
|
|
775
|
+
if (!ea || !eb) {
|
|
776
|
+
return { ok: false, errors: [{ path: `tweens[${s.tween}]`, message: `"${refByTarget.get(target)}" scales from its edge, which needs ${s.pivotOf.scaleProp} as numbers` }] };
|
|
777
|
+
}
|
|
778
|
+
a = ea;
|
|
779
|
+
b = eb;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
const rebase = (v, rel, base) => {
|
|
783
|
+
if (!v)
|
|
784
|
+
return base;
|
|
785
|
+
if (!rel)
|
|
786
|
+
return v;
|
|
787
|
+
// v was parsed relative to REST0: shift it onto the real base.
|
|
788
|
+
const delta = v.k === 'rest' ? v.add : 0;
|
|
789
|
+
if (base.k === 'abs' && typeof base.v === 'number')
|
|
790
|
+
return { k: 'abs', v: base.v + delta };
|
|
791
|
+
if (base.k === 'rest')
|
|
792
|
+
return { k: 'rest', add: base.add + delta };
|
|
793
|
+
return v;
|
|
794
|
+
};
|
|
795
|
+
// A side the tween leaves out is the value the property has when the tween starts (gsap.to / gsap.from).
|
|
796
|
+
a = s.from ? rebase(s.from, s.relFrom, current) : current;
|
|
797
|
+
b = s.to ? rebase(s.to, s.relTo, current) : current;
|
|
798
|
+
resolvedEnds.set(s, [a, b]);
|
|
799
|
+
}
|
|
800
|
+
const jump = !sameVal(a, current) && keys.length > 0;
|
|
801
|
+
if (!keys.length)
|
|
802
|
+
push(s.t0, a, null);
|
|
803
|
+
else
|
|
804
|
+
push(s.t0, a, jump ? 'hold' : null);
|
|
805
|
+
const back = s.yoyo ? reverseEase(s.ease) : s.ease;
|
|
806
|
+
const loops = loopTargets.has(target);
|
|
807
|
+
let cycles = s.repeat < 0 ? (loops ? 1 : Infinity) : s.repeat + 1;
|
|
808
|
+
if (s.repeat < 0 && loops && s.yoyo)
|
|
809
|
+
cycles = 2;
|
|
810
|
+
let t = s.t0;
|
|
811
|
+
let from = a, to = b;
|
|
812
|
+
for (let c = 0; c < cycles; c++) {
|
|
813
|
+
if (c > 0) {
|
|
814
|
+
if (s.yoyo) {
|
|
815
|
+
[from, to] = [to, from];
|
|
816
|
+
}
|
|
817
|
+
else
|
|
818
|
+
push(t, a, 'hold');
|
|
819
|
+
}
|
|
820
|
+
if (!loops && s.repeat < 0 && t + s.dur > clip + 1e-9) {
|
|
821
|
+
notes.push(`tween ${s.tween} (repeat -1) on "${refByTarget.get(target)}": ${c} cycle(s) fit before ${clip} s; it holds after the last whole one`);
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
t = round(t + s.dur);
|
|
825
|
+
push(t, to, c > 0 && s.yoyo && c % 2 === 1 ? back : s.ease);
|
|
826
|
+
if (s.repeatDelay > 0 && (c < cycles - 1 || loops)) {
|
|
827
|
+
t = round(t + s.repeatDelay);
|
|
828
|
+
push(t, to, null);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
current = to;
|
|
832
|
+
}
|
|
833
|
+
channelKeys.set(key, keys);
|
|
834
|
+
}
|
|
835
|
+
// ---- items: merge their channels into one track each
|
|
836
|
+
const tracks = [];
|
|
837
|
+
for (const [target] of segsByTarget) {
|
|
838
|
+
const chans = [...channelKeys.entries()].filter(([k]) => k.startsWith(`${target}\u0000`)).map(([k, keys]) => [k.split('\u0000')[1], keys]);
|
|
839
|
+
const firstAt = Math.min(...chans.map(([, keys]) => keys[0].time));
|
|
840
|
+
const byTime = new Map();
|
|
841
|
+
for (const [prop, keys0] of chans) {
|
|
842
|
+
// Every property holds a key at the item's first key time, so the engine never tweens it from its rest value
|
|
843
|
+
// across a span it was not asked to move in.
|
|
844
|
+
const keys = keys0[0].time > firstAt ? [{ time: firstAt, value: keys0[0].value, ease: null }, ...keys0] : keys0;
|
|
845
|
+
for (const k of keys) {
|
|
846
|
+
let t = k.time;
|
|
847
|
+
for (;;) {
|
|
848
|
+
const slot = byTime.get(t);
|
|
849
|
+
if (!slot)
|
|
850
|
+
break;
|
|
851
|
+
const free = slot.props[prop] === undefined;
|
|
852
|
+
if (free && (k.ease === null || slot.ease === null || slot.ease === k.ease))
|
|
853
|
+
break;
|
|
854
|
+
// One key carries one easing: a property arriving here on another ease lands 1 ms later.
|
|
855
|
+
if (free)
|
|
856
|
+
notes.push(`on "${refByTarget.get(target)}", ${prop} arrives at ${k.time} s on ${k.ease} while another property arrives there on ${slot.ease}; one key carries one ease, so ${prop}'s key is ${round(t + EPS - k.time) * 1000} ms later`);
|
|
857
|
+
t = round(t + EPS);
|
|
858
|
+
}
|
|
859
|
+
const slot = byTime.get(t) || { props: {}, ease: null };
|
|
860
|
+
slot.props[prop] = toKeyValue(k.value, prop);
|
|
861
|
+
if (k.ease !== null)
|
|
862
|
+
slot.ease = k.ease;
|
|
863
|
+
byTime.set(t, slot);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const keyframes = [...byTime.entries()].sort((a, b) => a[0] - b[0])
|
|
867
|
+
.map(([time, s]) => ({ time, properties: s.props, easing: s.ease ?? 'linear' }));
|
|
868
|
+
const last = keyframes[keyframes.length - 1].time;
|
|
869
|
+
const period = loopTargets.get(target);
|
|
870
|
+
const loop = period !== undefined || tlLoop;
|
|
871
|
+
const duration = period !== undefined ? period : Math.max(clip, last);
|
|
872
|
+
tracks.push({ target, ref: refByTarget.get(target) || target, keyframes, duration, loop });
|
|
873
|
+
}
|
|
874
|
+
// Fill in the receipt's from/to per property, now the channels are resolved.
|
|
875
|
+
for (const r of receipts) {
|
|
876
|
+
for (const p of r.properties) {
|
|
877
|
+
if (p.from)
|
|
878
|
+
continue;
|
|
879
|
+
const seg = segments.find((s) => s.tween === r.tween && s.prop === p.engine && !s.pivotOf);
|
|
880
|
+
const ends = seg && resolvedEnds.get(seg);
|
|
881
|
+
if (ends) {
|
|
882
|
+
p.from = describe(ends[0], p.engine);
|
|
883
|
+
p.to = describe(ends[1], p.engine);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (tlLoop)
|
|
888
|
+
notes.push(`repeat -1: every track loops at ${clip} s`);
|
|
889
|
+
return { ok: true, plan: { tracks, tweens: receipts, duration: clip, notes: [...new Set(notes)] } };
|
|
890
|
+
}
|
|
891
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
892
|
+
// Batch integration
|
|
893
|
+
/** What the batch creates, in create order, for name resolution and anchors. */
|
|
894
|
+
export function batchCreatesBefore(operations, index) {
|
|
895
|
+
const out = [];
|
|
896
|
+
for (let i = 0; i < index; i++) {
|
|
897
|
+
const op = operations[i];
|
|
898
|
+
if (op.type !== 'create')
|
|
899
|
+
continue;
|
|
900
|
+
const p = (op.properties || {});
|
|
901
|
+
const name = typeof p.name === 'string' ? p.name : typeof op.name === 'string' ? op.name : undefined;
|
|
902
|
+
const anchor = p.anchor ?? p.origin ?? op.anchor;
|
|
903
|
+
out.push({ name, anchor: typeof anchor === 'string' ? anchor.toLowerCase().replace(/[\s_]+/g, '-') : undefined });
|
|
904
|
+
}
|
|
905
|
+
return out;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Compile every timeline op in a batch. Errors come back with batch paths (operations[i].tweens[j]…). Also refused: a
|
|
909
|
+
* timeline track and a keyframe_animate op on the same item in one batch, or two timelines on one item — each writes
|
|
910
|
+
* the item's whole track, so one would silently replace the other.
|
|
911
|
+
*/
|
|
912
|
+
export function compileBatchTimelines(operations) {
|
|
913
|
+
const plans = new Map();
|
|
914
|
+
const errors = [];
|
|
915
|
+
const owner = new Map();
|
|
916
|
+
operations.forEach((op, i) => {
|
|
917
|
+
if (op.type !== 'timeline')
|
|
918
|
+
return;
|
|
919
|
+
const r = compileTimeline(op, { creates: batchCreatesBefore(operations, i) });
|
|
920
|
+
if (!r.ok) {
|
|
921
|
+
for (const e of r.errors)
|
|
922
|
+
errors.push({ path: `operations[${i}].${e.path}`, message: e.message });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
for (const t of r.plan.tracks) {
|
|
926
|
+
const was = owner.get(t.target);
|
|
927
|
+
if (was)
|
|
928
|
+
errors.push({ path: `operations[${i}]`, message: `"${t.ref}" is also animated by ${was}; one batch gives an item one timeline (put all its tweens in one)` });
|
|
929
|
+
else
|
|
930
|
+
owner.set(t.target, `operations[${i}] (timeline)`);
|
|
931
|
+
}
|
|
932
|
+
plans.set(i, r.plan);
|
|
933
|
+
});
|
|
934
|
+
operations.forEach((op, i) => {
|
|
935
|
+
if (op.type !== 'keyframe_animate' || typeof op.itemId !== 'string')
|
|
936
|
+
return;
|
|
937
|
+
const was = owner.get(op.itemId);
|
|
938
|
+
if (was)
|
|
939
|
+
errors.push({ path: `operations[${i}].itemId`, message: `${op.itemId} is animated by ${was}, and a keyframe_animate op would replace that track (or be replaced by it). Put its keys in the timeline as tweens.` });
|
|
940
|
+
});
|
|
941
|
+
return { plans, errors };
|
|
942
|
+
}
|
|
943
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
944
|
+
// The emitted op
|
|
945
|
+
/**
|
|
946
|
+
* The body of a batch op (a function body that returns the op's result) that writes a compiled timeline: one
|
|
947
|
+
* app.addAnimation per item, the same call keyframe_animate makes. Values the compiler could not know (an item's rest
|
|
948
|
+
* position, rotation, opacity, colour) are read from the item here, from its rest state when it has one.
|
|
949
|
+
* `refExpr` turns a "$N" target into the batch's runtime reference.
|
|
950
|
+
*/
|
|
951
|
+
export function emitTimelineOpBody(plan, refExpr) {
|
|
952
|
+
const J = (v) => JSON.stringify(v);
|
|
953
|
+
const tracks = plan.tracks.map((t) => `
|
|
954
|
+
{
|
|
955
|
+
const __tr = ${refExpr(t.target)};
|
|
956
|
+
const __hit = (app.getItemById && app.getItemById(__tr))
|
|
957
|
+
|| (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(__tr))
|
|
958
|
+
|| (typeof app.resolveRef === 'function' ? app.resolveRef(__tr) : null);
|
|
959
|
+
if (!__hit) return { success: false, itemId: __tr, error: 'timeline: no item has the id or name ' + ${J(J(t.ref))} + (typeof app.resolveRef === 'function' ? '' : ' (this studio resolves ids only; it predates names)'), timeline: { tracks: __ppTl } };
|
|
960
|
+
const __it = __hit.item || __hit;
|
|
961
|
+
const __id = (__it.data && (__it.data.registryId || __it.data.id)) || __tr;
|
|
962
|
+
const __prev = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.length : 0;
|
|
963
|
+
const __kfs = __ppResolve(__it, ${J(t.keyframes)});
|
|
964
|
+
app.addAnimation(__id, __kfs, { duration: ${t.duration}, loop: ${t.loop}, timeUnits: 'seconds' });
|
|
965
|
+
__ppTl.push(Object.assign({ ref: ${J(t.ref)}, itemId: __id, keys: __kfs.length, duration: ${t.duration}, loop: ${t.loop} }, __prev ? { replaced: __prev } : {}));
|
|
966
|
+
}`).join('');
|
|
967
|
+
return `
|
|
968
|
+
if (typeof app.addAnimation !== 'function') return { success: false, error: 'timeline: app.addAnimation unavailable in this studio' };
|
|
969
|
+
const __ppNum = function (v) { return (typeof v === 'number' && isFinite(v)) ? v : undefined; };
|
|
970
|
+
const __ppFirst = function () { for (let i = 0; i < arguments.length; i++) if (arguments[i] !== undefined) return arguments[i]; return undefined; };
|
|
971
|
+
// An item's rest value: its authored state when it has a track, else what it is now.
|
|
972
|
+
const __ppRest = function (it, p) {
|
|
973
|
+
const o = it.data && it.data.originalKeyframeState;
|
|
974
|
+
const b = it.bounds;
|
|
975
|
+
const css = function (c) { return c && typeof c.toCSS === 'function' ? c.toCSS(true) : undefined; };
|
|
976
|
+
switch (p) {
|
|
977
|
+
case 'x': return __ppFirst(__ppNum(o && o.position && o.position.x), __ppNum(it.position && it.position.x));
|
|
978
|
+
case 'y': return __ppFirst(__ppNum(o && o.position && o.position.y), __ppNum(it.position && it.position.y));
|
|
979
|
+
case 'scale': case 'scaleX': return __ppFirst(__ppNum(o && o.scaling && o.scaling.x), __ppNum(it.scaling && it.scaling.x), 1);
|
|
980
|
+
case 'scaleY': return __ppFirst(__ppNum(o && o.scaling && o.scaling.y), __ppNum(it.scaling && it.scaling.y), 1);
|
|
981
|
+
case 'rotation': return __ppFirst(__ppNum(o && o.rotation), __ppNum(it.rotation), 0);
|
|
982
|
+
case 'opacity': return __ppFirst(__ppNum(o && o.opacity), __ppNum(it.opacity), 1);
|
|
983
|
+
case 'fillColor': return css(it.fillColor);
|
|
984
|
+
case 'strokeColor': return css(it.strokeColor);
|
|
985
|
+
case 'strokeWidth': return __ppNum(it.strokeWidth);
|
|
986
|
+
case 'width': return __ppNum(b && b.width);
|
|
987
|
+
case 'height': return __ppNum(b && b.height);
|
|
988
|
+
case 'radius': return b ? __ppNum(b.width / 2) : undefined;
|
|
989
|
+
case 'fontSize': return __ppNum(it.fontSize);
|
|
990
|
+
case 'trimStart': return __ppFirst(__ppNum(it.data && it.data.trimStart), 0);
|
|
991
|
+
case 'trimEnd': return __ppFirst(__ppNum(it.data && it.data.trimEnd), 1);
|
|
992
|
+
case 'dashOffset': return __ppFirst(__ppNum(it.dashOffset), 0);
|
|
993
|
+
case 'dashArray': return Array.isArray(it.dashArray) && it.dashArray.length ? it.dashArray.slice() : undefined;
|
|
994
|
+
default: return undefined;
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
// The item's height at scale 1, for a scale that holds its bottom or top edge.
|
|
998
|
+
const __ppHeight = function (it) {
|
|
999
|
+
const o = it.data && it.data.originalKeyframeState;
|
|
1000
|
+
const base = o && (o.localBounds || o.bounds);
|
|
1001
|
+
if (base && __ppNum(base.height) !== undefined) return base.height;
|
|
1002
|
+
return it.bounds ? it.bounds.height / (Math.abs(it.scaling ? it.scaling.y : 1) || 1) : 0;
|
|
1003
|
+
};
|
|
1004
|
+
const __ppResolve = function (it, kfs) {
|
|
1005
|
+
for (const k of kfs) {
|
|
1006
|
+
for (const p of Object.keys(k.properties)) {
|
|
1007
|
+
const v = k.properties[p];
|
|
1008
|
+
if (!v || typeof v !== 'object' || Array.isArray(v) || typeof v.$rest !== 'string') continue;
|
|
1009
|
+
let r = __ppRest(it, v.$rest);
|
|
1010
|
+
if (r === undefined || r === null) { delete k.properties[p]; continue; }
|
|
1011
|
+
if (typeof r === 'number') {
|
|
1012
|
+
r += v.add || 0;
|
|
1013
|
+
if (v.pivot) {
|
|
1014
|
+
// A bottom / top anchor is held by the engine itself (it moves y as scaleY changes): adding y keys too would
|
|
1015
|
+
// move the item twice. Any other bottom-* / top-* anchor, or a transformOrigin, is held here.
|
|
1016
|
+
const an = String((it.data && it.data.anchor) || it.anchor || '');
|
|
1017
|
+
let pv = v.pivot;
|
|
1018
|
+
if (an === 'bottom' || an === 'top') pv = 0;
|
|
1019
|
+
else if (pv === 'anchor') pv = /^bottom/.test(an) ? 1 : /^top/.test(an) ? -1 : 0;
|
|
1020
|
+
if (pv) {
|
|
1021
|
+
const s0 = __ppRest(it, 'scaleY');
|
|
1022
|
+
const s = typeof v.s === 'number' ? v.s : s0 + (v.sAdd || 0);
|
|
1023
|
+
r += pv * (__ppHeight(it) / 2) * (s0 - s);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
k.properties[p] = r;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return kfs.filter(function (k) { return Object.keys(k.properties).length > 0; });
|
|
1031
|
+
};
|
|
1032
|
+
const __ppTl = [];${tracks}
|
|
1033
|
+
return { success: true, timeline: { duration: ${plan.duration}, tracks: __ppTl, tweens: ${J(plan.tweens)}${plan.notes.length ? `, notes: ${J(plan.notes)}` : ''} } };
|
|
1034
|
+
`;
|
|
1035
|
+
}
|
|
1036
|
+
//# sourceMappingURL=timeline.js.map
|