claude-exode-mcp 7.5.7 → 7.5.8
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/MCP_DOCUMENTATION.md +1 -0
- package/SKILL.md +10 -0
- package/examples/aurum-product-page.exode +3208 -0
- package/examples/bell-ring-icon.exode +1 -0
- package/examples/drift-kinetic-type.exode +770 -0
- package/examples/earbuds-product.exode +214 -0
- package/examples/energy-flow.exode +1736 -0
- package/examples/flux-hero.exode +223 -0
- package/examples/gooey-toggle.exode +214 -0
- package/examples/gravity-well.exode +3113 -0
- package/examples/heart-icon.exode +1 -0
- package/examples/hologram-temple.exode +8100 -0
- package/examples/jarful-app.exode +1 -0
- package/examples/kip-character-rig.exode +1 -0
- package/examples/low-tide-tilt.exode +2216 -0
- package/examples/manifest.json +352 -0
- package/examples/swipe-stack-carousel.exode +4048 -0
- package/examples/tokyo-neon-weather.exode +470 -0
- package/examples/typographic-collapse.exode +1578 -0
- package/examples/void-product-ad.exode +1269 -0
- package/examples/wire-globe-card.exode +580 -0
- package/index.d.ts +41 -1
- package/index.js +2553 -2407
- package/package.json +4 -2
- package/render-runtime/exode-runtime.js +20 -20
- package/skills/creative-motion/SKILL.md +10 -0
- package/skills/creative-motion/recipes/character-rig.js +104 -0
- package/skills/creative-motion/recipes/dashboard.js +120 -0
- package/skills/creative-motion/recipes/game-arcade.js +203 -0
- package/skills/creative-motion/recipes/hero-physics.js +65 -0
- package/skills/creative-motion/recipes/pricing.js +88 -0
- package/skills/creative-motion/recipes/title-sequence.js +62 -0
- package/skills/creative-motion/references/apps.md +37 -0
- package/skills/creative-motion/references/gotchas.md +80 -0
- package/skills/creative-motion/references/native-layers.md +60 -0
- package/skills/creative-motion/references/quality.md +54 -0
- package/src/__tests__/buildKit.test.ts +127 -0
- package/src/__tests__/effectKnobs.test.ts +162 -0
- package/src/__tests__/nativeLayers.test.ts +178 -0
- package/src/buildKit.d.ts +301 -0
- package/src/buildKit.d.ts.map +1 -0
- package/src/buildKit.js +559 -0
- package/src/buildKit.js.map +1 -0
- package/src/buildKit.ts +520 -0
- package/src/compiler.d.ts +4 -0
- package/src/compiler.d.ts.map +1 -1
- package/src/compiler.js +21 -45
- package/src/compiler.js.map +1 -1
- package/src/compiler.ts +22 -51
- package/src/critique.d.ts +8 -0
- package/src/critique.d.ts.map +1 -0
- package/src/critique.js +348 -0
- package/src/critique.js.map +1 -0
- package/src/critique.ts +239 -0
- package/src/effectKnobs.d.ts +72 -0
- package/src/effectKnobs.d.ts.map +1 -0
- package/src/effectKnobs.js +541 -0
- package/src/effectKnobs.js.map +1 -0
- package/src/effectKnobs.ts +445 -0
- package/src/effectOptimizer.d.ts +18 -0
- package/src/effectOptimizer.d.ts.map +1 -0
- package/src/effectOptimizer.js +125 -0
- package/src/effectOptimizer.js.map +1 -0
- package/src/effectOptimizer.ts +104 -0
- package/src/engine/layout.js +1 -1
- package/src/engine/layout.js.map +1 -1
- package/src/engine/layout.ts +1 -1
- package/src/examples.d.ts +30 -0
- package/src/examples.d.ts.map +1 -0
- package/src/examples.js +168 -0
- package/src/examples.js.map +1 -0
- package/src/examples.ts +151 -0
- package/src/handlers/artboardHandlers.d.ts.map +1 -1
- package/src/handlers/artboardHandlers.js +45 -10
- package/src/handlers/artboardHandlers.js.map +1 -1
- package/src/handlers/artboardHandlers.ts +43 -12
- package/src/handlers/buildHandlers.d.ts +2 -0
- package/src/handlers/buildHandlers.d.ts.map +1 -0
- package/src/handlers/buildHandlers.js +86 -0
- package/src/handlers/buildHandlers.js.map +1 -0
- package/src/handlers/buildHandlers.ts +76 -0
- package/src/handlers/compilerHandlers.d.ts.map +1 -1
- package/src/handlers/compilerHandlers.js +32 -2
- package/src/handlers/compilerHandlers.js.map +1 -1
- package/src/handlers/compilerHandlers.ts +31 -2
- package/src/handlers/exampleHandlers.d.ts +2 -0
- package/src/handlers/exampleHandlers.d.ts.map +1 -0
- package/src/handlers/exampleHandlers.js +81 -0
- package/src/handlers/exampleHandlers.js.map +1 -0
- package/src/handlers/exampleHandlers.ts +76 -0
- package/src/handlers/renderHandlers.d.ts.map +1 -1
- package/src/handlers/renderHandlers.js +100 -2
- package/src/handlers/renderHandlers.js.map +1 -1
- package/src/handlers/renderHandlers.ts +90 -2
- package/src/handlers/scaffoldHandlers.d.ts.map +1 -1
- package/src/handlers/scaffoldHandlers.js +3 -0
- package/src/handlers/scaffoldHandlers.js.map +1 -1
- package/src/handlers/scaffoldHandlers.ts +2 -0
- package/src/handlers/widgetHandlers.js +1 -1
- package/src/handlers/widgetHandlers.js.map +1 -1
- package/src/handlers/widgetHandlers.ts +1 -1
- package/src/lottie/convert.d.ts +16 -0
- package/src/lottie/convert.d.ts.map +1 -0
- package/src/lottie/convert.js +1225 -0
- package/src/lottie/convert.js.map +1 -0
- package/src/lottie/convert.ts +1134 -0
- package/src/lottie/evaluate.d.ts +48 -0
- package/src/lottie/evaluate.d.ts.map +1 -0
- package/src/lottie/evaluate.js +174 -0
- package/src/lottie/evaluate.js.map +1 -0
- package/src/lottie/evaluate.ts +184 -0
- package/src/nativeLayers.d.ts +76 -0
- package/src/nativeLayers.d.ts.map +1 -0
- package/src/nativeLayers.js +404 -0
- package/src/nativeLayers.js.map +1 -0
- package/src/nativeLayers.ts +339 -0
- package/src/render/motionBrief.d.ts.map +1 -1
- package/src/render/motionBrief.js +4 -0
- package/src/render/motionBrief.js.map +1 -1
- package/src/render/motionBrief.ts +4 -0
- package/src/render/videoRenderer.d.ts.map +1 -1
- package/src/render/videoRenderer.js +5 -2
- package/src/render/videoRenderer.js.map +1 -1
- package/src/render/videoRenderer.ts +5 -2
- package/src/rive/convert.d.ts +17 -0
- package/src/rive/convert.d.ts.map +1 -0
- package/src/rive/convert.js +1779 -0
- package/src/rive/convert.js.map +1 -0
- package/src/rive/convert.ts +1492 -0
- package/src/rive/font.d.ts +13 -0
- package/src/rive/font.d.ts.map +1 -0
- package/src/rive/font.js +154 -0
- package/src/rive/font.js.map +1 -0
- package/src/rive/font.ts +132 -0
- package/src/rive/layout.d.ts +20 -0
- package/src/rive/layout.d.ts.map +1 -0
- package/src/rive/layout.js +145 -0
- package/src/rive/layout.js.map +1 -0
- package/src/rive/layout.ts +140 -0
- package/src/rive/reader.d.ts +32 -0
- package/src/rive/reader.d.ts.map +1 -0
- package/src/rive/reader.js +172 -0
- package/src/rive/reader.js.map +1 -0
- package/src/rive/reader.ts +160 -0
- package/src/rive/schema.generated.d.ts +9 -0
- package/src/rive/schema.generated.d.ts.map +1 -0
- package/src/rive/schema.generated.js +13 -0
- package/src/rive/schema.generated.js.map +1 -0
- package/src/rive/schema.generated.ts +13 -0
- package/src/schema.d.ts +5 -0
- package/src/schema.d.ts.map +1 -1
- package/src/schema.js +5 -0
- package/src/schema.js.map +1 -1
- package/src/schema.ts +6 -0
- package/src/serverInstructions.d.ts +2 -0
- package/src/serverInstructions.d.ts.map +1 -0
- package/src/serverInstructions.js +22 -0
- package/src/serverInstructions.js.map +1 -0
- package/src/serverInstructions.ts +21 -0
- package/src/sseServer.d.ts +2 -0
- package/src/sseServer.d.ts.map +1 -0
- package/src/sseServer.js +165 -0
- package/src/sseServer.js.map +1 -0
- package/src/sseServer.ts +189 -0
- package/src/utils.d.ts.map +1 -1
- package/src/utils.js +23 -1
- package/src/utils.js.map +1 -1
- package/src/utils.ts +13 -1
- package/src/widgets.d.ts +26 -0
- package/src/widgets.d.ts.map +1 -1
- package/src/widgets.js +89 -45
- package/src/widgets.js.map +1 -1
- package/src/widgets.ts +88 -45
|
@@ -0,0 +1,1779 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// GENERATED by scripts/sync-lottie-converter.mjs from editor/src/utils/rive. Edit there, then re-run.
|
|
3
|
+
// Rive (.riv) -> Exode artboard.
|
|
4
|
+
//
|
|
5
|
+
// Rive's scene graph maps closely onto Exode's: every TransformComponent
|
|
6
|
+
// (Node, Shape, Bone, …) becomes a Group with x, y, rotation (radians ->
|
|
7
|
+
// degrees), scale and opacity. Rive draws drawables in reverse stream order,
|
|
8
|
+
// which for its depth-first stream is the same as reversing children at every
|
|
9
|
+
// level, so the hierarchy is kept. A Shape's paths are merged into one
|
|
10
|
+
// compound outline in the shape's space, drawn once per fill/stroke in paint
|
|
11
|
+
// order. Written for the Exode runtime and editor as they are (see the Lottie
|
|
12
|
+
// converter for the same adaptations): Groups get clipContent: false, strokes
|
|
13
|
+
// are multiplied by their accumulated scale, even-odd fills are re-wound for
|
|
14
|
+
// the nonzero-only runtime, gradient opacity is folded into the stops, and a
|
|
15
|
+
// transition without conditions waits for its exit time via `delay`.
|
|
16
|
+
//
|
|
17
|
+
// Pure TypeScript with no editor imports.
|
|
18
|
+
import { argb, isA, readRiv } from './reader.js';
|
|
19
|
+
import { PROPS } from './schema.generated.js';
|
|
20
|
+
import { parseFont } from './font.js';
|
|
21
|
+
import { solveLayouts, isLayoutParticipant } from './layout.js';
|
|
22
|
+
const r3 = (n) => Math.round(n * 1000) / 1000;
|
|
23
|
+
const DEG = 180 / Math.PI;
|
|
24
|
+
const baseTransform = () => ({ x: 0, y: 0, rotation: 0, scale_x: 1, scale_y: 1, opacity: 1, skew_x: 0, skew_y: 0 });
|
|
25
|
+
const noStroke = () => ({ color: '#000000', width: 0, opacity: 0, isEnabled: false });
|
|
26
|
+
const CONSTRAINTS = new Set(['IKConstraint', 'TransformConstraint', 'TranslationConstraint', 'RotationConstraint', 'ScaleConstraint']);
|
|
27
|
+
function indexScene(ab) {
|
|
28
|
+
const byIndex = ab.slots;
|
|
29
|
+
const children = new Map();
|
|
30
|
+
for (const o of ab.slots) {
|
|
31
|
+
if (!o || o === ab.artboard || !isA(o.type, 'Component'))
|
|
32
|
+
continue;
|
|
33
|
+
const p = o.props.parentId ?? 0;
|
|
34
|
+
const list = children.get(p) || [];
|
|
35
|
+
list.push(o);
|
|
36
|
+
children.set(p, list);
|
|
37
|
+
}
|
|
38
|
+
// Animations: LinearAnimation > KeyedObject(objectId) > KeyedProperty(propertyKey) > KeyFrame*.
|
|
39
|
+
const animations = [];
|
|
40
|
+
const machines = [];
|
|
41
|
+
let anim = null, objId = -1, prop = null, machine = null;
|
|
42
|
+
for (const o of ab.stream) {
|
|
43
|
+
if (o.type === 'LinearAnimation') {
|
|
44
|
+
const fps = o.props.fps || 60;
|
|
45
|
+
const start = o.props.enableWorkArea ? o.props.workStart || 0 : 0;
|
|
46
|
+
const end = o.props.enableWorkArea ? o.props.workEnd || o.props.duration : o.props.duration;
|
|
47
|
+
anim = { obj: o, keyed: new Map(), fps, start, end: end || 0 };
|
|
48
|
+
animations.push(anim);
|
|
49
|
+
machine = null;
|
|
50
|
+
}
|
|
51
|
+
else if (o.type === 'StateMachine') {
|
|
52
|
+
machine = [o];
|
|
53
|
+
machines.push(machine);
|
|
54
|
+
anim = null;
|
|
55
|
+
}
|
|
56
|
+
else if (machine) {
|
|
57
|
+
machine.push(o);
|
|
58
|
+
}
|
|
59
|
+
else if (anim && o.type === 'KeyedObject') {
|
|
60
|
+
objId = o.props.objectId;
|
|
61
|
+
if (!anim.keyed.has(objId))
|
|
62
|
+
anim.keyed.set(objId, new Map());
|
|
63
|
+
}
|
|
64
|
+
else if (anim && o.type === 'KeyedProperty') {
|
|
65
|
+
prop = propName(o.props.propertyKey);
|
|
66
|
+
if (prop && !anim.keyed.get(objId).has(prop))
|
|
67
|
+
anim.keyed.get(objId).set(prop, []);
|
|
68
|
+
}
|
|
69
|
+
else if (anim && prop && isA(o.type, 'KeyFrame')) {
|
|
70
|
+
const it = o.props.interpolatorId;
|
|
71
|
+
anim.keyed.get(objId).get(prop).push({
|
|
72
|
+
frame: o.props.frame || 0,
|
|
73
|
+
value: o.props.value,
|
|
74
|
+
type: o.props.interpolationType ?? 1,
|
|
75
|
+
interpolator: it !== undefined && it !== -1 && it < byIndex.length ? byIndex[it] : null,
|
|
76
|
+
color: o.type === 'KeyFrameColor',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Joysticks re-apply their x/y animations after everything else, at a time set
|
|
81
|
+
// by the stick position (-1..1 -> start..end). Those properties are a held pose,
|
|
82
|
+
// so bake it into the base props and stop the animation from driving them.
|
|
83
|
+
for (const j of ab.slots) {
|
|
84
|
+
if (!j || j.type !== 'Joystick')
|
|
85
|
+
continue;
|
|
86
|
+
const flags = j.props.joystickFlags || 0;
|
|
87
|
+
for (const [id, v] of [[j.props.xId, flags & 1 ? -(j.props.x || 0) : j.props.x || 0], [j.props.yId, flags & 2 ? -(j.props.y || 0) : j.props.y || 0]]) {
|
|
88
|
+
const a = id !== undefined && id >= 0 ? animations[id] : undefined;
|
|
89
|
+
if (!a)
|
|
90
|
+
continue;
|
|
91
|
+
const frame = a.start + ((v + 1) / 2) * (a.end - a.start);
|
|
92
|
+
for (const [objId, props] of a.keyed) {
|
|
93
|
+
const o = byIndex[objId];
|
|
94
|
+
if (!o)
|
|
95
|
+
continue;
|
|
96
|
+
for (const [name, keys] of props)
|
|
97
|
+
if (keys.length)
|
|
98
|
+
o.props[name] = keyedAt(keys, frame);
|
|
99
|
+
}
|
|
100
|
+
a.keyed = new Map();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const constraints = [];
|
|
104
|
+
for (const c of ab.slots) {
|
|
105
|
+
if (!c || !CONSTRAINTS.has(c.type))
|
|
106
|
+
continue;
|
|
107
|
+
const owner = byIndex[c.props.parentId ?? 0];
|
|
108
|
+
const tid = c.props.targetId;
|
|
109
|
+
const target = tid !== undefined && tid >= 0 && tid < byIndex.length ? byIndex[tid] : null;
|
|
110
|
+
if (!owner || owner.index === 0 || !isA(owner.type, 'TransformComponent'))
|
|
111
|
+
continue;
|
|
112
|
+
if (c.type === 'IKConstraint') {
|
|
113
|
+
if (!target || !isA(owner.type, 'Bone'))
|
|
114
|
+
continue;
|
|
115
|
+
const chain = [owner];
|
|
116
|
+
for (let b = owner, n = c.props.parentBoneCount ?? 0; n > 0; n--) {
|
|
117
|
+
const up = byIndex[b.props.parentId ?? 0];
|
|
118
|
+
if (!up || !isA(up.type, 'Bone'))
|
|
119
|
+
break;
|
|
120
|
+
chain.unshift(up);
|
|
121
|
+
b = up;
|
|
122
|
+
}
|
|
123
|
+
constraints.push({ c, owner, target, chain });
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
constraints.push({ c, owner, target });
|
|
127
|
+
}
|
|
128
|
+
return { ab, byIndex, children, animations, machines, constraints, poseCache: new Map() };
|
|
129
|
+
}
|
|
130
|
+
const propName = (key) => (PROPS[key] ? PROPS[key][0] : null);
|
|
131
|
+
// ---------------------------------------------------------------- evaluation
|
|
132
|
+
const bezier = (x1, y1, x2, y2) => (x) => {
|
|
133
|
+
const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
|
|
134
|
+
const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
|
|
135
|
+
let t = x;
|
|
136
|
+
for (let i = 0; i < 8; i++) {
|
|
137
|
+
const v = ((ax * t + bx) * t + cx) * t - x;
|
|
138
|
+
const d = (3 * ax * t + 2 * bx) * t + cx;
|
|
139
|
+
if (Math.abs(v) < 1e-6 || Math.abs(d) < 1e-6)
|
|
140
|
+
break;
|
|
141
|
+
t -= v / d;
|
|
142
|
+
}
|
|
143
|
+
t = Math.max(0, Math.min(1, t));
|
|
144
|
+
return ((ay * t + by) * t + cy) * t;
|
|
145
|
+
};
|
|
146
|
+
const lerpColor = (a, b, f) => {
|
|
147
|
+
const ch = (c, s) => (c >>> s) & 255;
|
|
148
|
+
const mix = (s) => Math.round(ch(a, s) + (ch(b, s) - ch(a, s)) * f);
|
|
149
|
+
return ((mix(24) << 24) | (mix(16) << 16) | (mix(8) << 8) | mix(0)) >>> 0;
|
|
150
|
+
};
|
|
151
|
+
function easeOf(k) {
|
|
152
|
+
if (k.type === 0)
|
|
153
|
+
return null; // hold
|
|
154
|
+
const it = k.interpolator;
|
|
155
|
+
if (k.type === 2 && it && (it.type === 'CubicEaseInterpolator' || it.type === 'CubicValueInterpolator'))
|
|
156
|
+
return bezier(it.props.x1 ?? 0.42, it.props.y1 ?? 0, it.props.x2 ?? 0.58, it.props.y2 ?? 1);
|
|
157
|
+
if (it && it.type === 'ElasticInterpolator') {
|
|
158
|
+
const amp = it.props.amplitude ?? 1, period = it.props.period ?? 0.5, easing = it.props.easingValue ?? 1;
|
|
159
|
+
const s = period / (2 * Math.PI) * Math.asin(1 / Math.max(amp, 1));
|
|
160
|
+
const out = (t) => (t <= 0 ? 0 : t >= 1 ? 1 : Math.max(amp, 1) * Math.pow(2, -10 * t) * Math.sin((t - s) * (2 * Math.PI) / period) + 1);
|
|
161
|
+
return easing === 0 ? (t => 1 - out(1 - t)) : easing === 1 ? out : (t => (t < 0.5 ? (1 - out(1 - 2 * t)) / 2 : (out(2 * t - 1) + 1) / 2));
|
|
162
|
+
}
|
|
163
|
+
return f => f;
|
|
164
|
+
}
|
|
165
|
+
/** Value of a keyed property at a frame. */
|
|
166
|
+
function keyedAt(keys, frame) {
|
|
167
|
+
if (frame <= keys[0].frame)
|
|
168
|
+
return keys[0].value;
|
|
169
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
170
|
+
const a = keys[i], b = keys[i + 1];
|
|
171
|
+
if (frame >= b.frame)
|
|
172
|
+
continue;
|
|
173
|
+
const ease = easeOf(a);
|
|
174
|
+
if (!ease || typeof a.value !== 'number' || typeof b.value !== 'number')
|
|
175
|
+
return a.value;
|
|
176
|
+
const f = ease((frame - a.frame) / ((b.frame - a.frame) || 1));
|
|
177
|
+
return a.color ? lerpColor(a.value, b.value, Math.max(0, Math.min(1, f))) : a.value + (b.value - a.value) * f;
|
|
178
|
+
}
|
|
179
|
+
return keys[keys.length - 1].value;
|
|
180
|
+
}
|
|
181
|
+
/** Object props at a frame of an animation (static props overlaid with keyed values). */
|
|
182
|
+
const propsAt = (o, anim, frame) => {
|
|
183
|
+
const m = anim?.keyed.get(o.index);
|
|
184
|
+
if (!m)
|
|
185
|
+
return o.props;
|
|
186
|
+
const out = { ...o.props };
|
|
187
|
+
m.forEach((keys, name) => { if (keys.length)
|
|
188
|
+
out[name] = keyedAt(keys, frame); });
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
const mul = (p, q) => [p[0] * q[0] + p[2] * q[1], p[1] * q[0] + p[3] * q[1], p[0] * q[2] + p[2] * q[3], p[1] * q[2] + p[3] * q[3], p[0] * q[4] + p[2] * q[5] + p[4], p[1] * q[4] + p[3] * q[5] + p[5]];
|
|
192
|
+
const applyM = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
|
|
193
|
+
/** A child Bone sits at its parent bone's tip. */
|
|
194
|
+
const boneLocal = (s, o, p, anim, frame) => {
|
|
195
|
+
if (o.type !== 'Bone')
|
|
196
|
+
return p;
|
|
197
|
+
const parent = s.byIndex[o.props.parentId ?? 0];
|
|
198
|
+
if (!parent || (parent.type !== 'Bone' && parent.type !== 'RootBone'))
|
|
199
|
+
return p;
|
|
200
|
+
return { ...p, x: propsAt(parent, anim, frame).length ?? 0, y: 0 };
|
|
201
|
+
};
|
|
202
|
+
const localM = (p) => {
|
|
203
|
+
const c = Math.cos(p.rotation || 0), s = Math.sin(p.rotation || 0), sx = p.scaleX ?? 1, sy = p.scaleY ?? 1;
|
|
204
|
+
return [c * sx, s * sx, -s * sy, c * sy, p.x || 0, p.y || 0];
|
|
205
|
+
};
|
|
206
|
+
const CIRCLE = 0.552284749831;
|
|
207
|
+
/** A path's vertices (in path space) at a frame. */
|
|
208
|
+
function pathVertices(s, path, anim, frame) {
|
|
209
|
+
let p = propsAt(path, anim, frame);
|
|
210
|
+
const so = s.sizeOverride?.get(path.index);
|
|
211
|
+
if (so)
|
|
212
|
+
p = { ...p, width: so.w, height: so.h };
|
|
213
|
+
const w = p.width || 0, h = p.height || 0, ox0 = -(p.originX ?? 0.5) * w, oy0 = -(p.originY ?? 0.5) * h;
|
|
214
|
+
const straight = (x, y, radius = 0) => ({ x, y, ix: x, iy: y, ox: x, oy: y, radius, cubic: false });
|
|
215
|
+
if (path.type === 'Rectangle') {
|
|
216
|
+
const link = p.linkCornerRadius !== false, tl = p.cornerRadiusTL || 0;
|
|
217
|
+
return { closed: true, verts: [straight(ox0, oy0, tl), straight(ox0 + w, oy0, link ? tl : p.cornerRadiusTR || 0), straight(ox0 + w, oy0 + h, link ? tl : p.cornerRadiusBR || 0), straight(ox0, oy0 + h, link ? tl : p.cornerRadiusBL || 0)] };
|
|
218
|
+
}
|
|
219
|
+
if (path.type === 'Ellipse') {
|
|
220
|
+
const rx = w / 2, ry = h / 2, cx = ox0 + rx, cy = oy0 + ry, kx = rx * CIRCLE, ky = ry * CIRCLE;
|
|
221
|
+
const c = (x, y, ix, iy, ox, oy) => ({ x, y, ix, iy, ox, oy, radius: 0, cubic: true });
|
|
222
|
+
return { closed: true, verts: [
|
|
223
|
+
c(cx, cy - ry, cx - kx, cy - ry, cx + kx, cy - ry), c(cx + rx, cy, cx + rx, cy - ky, cx + rx, cy + ky),
|
|
224
|
+
c(cx, cy + ry, cx + kx, cy + ry, cx - kx, cy + ry), c(cx - rx, cy, cx - rx, cy + ky, cx - rx, cy - ky),
|
|
225
|
+
] };
|
|
226
|
+
}
|
|
227
|
+
if (path.type === 'Triangle')
|
|
228
|
+
return { closed: true, verts: [straight(ox0 + w / 2, oy0), straight(ox0 + w, oy0 + h), straight(ox0, oy0 + h)] };
|
|
229
|
+
if (path.type === 'Polygon' || path.type === 'Star') {
|
|
230
|
+
const n = path.type === 'Star' ? (p.points || 5) * 2 : (p.points || 5);
|
|
231
|
+
const hw = w / 2, hh = h / 2, cx = ox0 + hw, cy = oy0 + hh, inner = p.innerRadius ?? 0.5;
|
|
232
|
+
const verts = [];
|
|
233
|
+
for (let i = 0; i < n; i++) {
|
|
234
|
+
const a = -Math.PI / 2 + (i * 2 * Math.PI) / n;
|
|
235
|
+
const k = path.type === 'Star' && i % 2 ? inner : 1;
|
|
236
|
+
verts.push(straight(cx + Math.cos(a) * hw * k, cy + Math.sin(a) * hh * k, p.cornerRadius || 0));
|
|
237
|
+
}
|
|
238
|
+
return { closed: true, verts };
|
|
239
|
+
}
|
|
240
|
+
// PointsPath: vertex children.
|
|
241
|
+
const verts = [];
|
|
242
|
+
for (const v of s.children.get(path.index) || []) {
|
|
243
|
+
if (!isA(v.type, 'PathVertex'))
|
|
244
|
+
continue;
|
|
245
|
+
const q = propsAt(v, anim, frame);
|
|
246
|
+
const x = q.x || 0, y = q.y || 0;
|
|
247
|
+
if (v.type === 'StraightVertex')
|
|
248
|
+
verts.push(straight(x, y, q.radius || 0));
|
|
249
|
+
else if (v.type === 'CubicMirroredVertex') {
|
|
250
|
+
const dx = Math.cos(q.rotation || 0) * (q.distance || 0), dy = Math.sin(q.rotation || 0) * (q.distance || 0);
|
|
251
|
+
verts.push({ x, y, ix: x - dx, iy: y - dy, ox: x + dx, oy: y + dy, radius: 0, cubic: true });
|
|
252
|
+
}
|
|
253
|
+
else if (v.type === 'CubicAsymmetricVertex') {
|
|
254
|
+
const c = Math.cos(q.rotation || 0), sn = Math.sin(q.rotation || 0);
|
|
255
|
+
verts.push({ x, y, ix: x - c * (q.inDistance || 0), iy: y - sn * (q.inDistance || 0), ox: x + c * (q.outDistance || 0), oy: y + sn * (q.outDistance || 0), radius: 0, cubic: true });
|
|
256
|
+
}
|
|
257
|
+
else if (v.type === 'CubicDetachedVertex')
|
|
258
|
+
verts.push({ x, y, ix: x + Math.cos(q.inRotation || 0) * (q.inDistance || 0), iy: y + Math.sin(q.inRotation || 0) * (q.inDistance || 0), ox: x + Math.cos(q.outRotation || 0) * (q.outDistance || 0), oy: y + Math.sin(q.outRotation || 0) * (q.outDistance || 0), radius: 0, cubic: true });
|
|
259
|
+
}
|
|
260
|
+
return { verts, closed: p.isClosed !== false };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Rive skinning: each tendon contributes bone.world x inverse(bind); a vertex
|
|
264
|
+
* blends up to four of those by its 8-bit weights and applies the blend to its
|
|
265
|
+
* bind-pose world position (skin.world x vertex). Cubic handles carry their own weights.
|
|
266
|
+
*/
|
|
267
|
+
function skinVertices(s, path, skin, verts, anim, frame) {
|
|
268
|
+
const sk = skin.props;
|
|
269
|
+
const bindWorld = [sk.xx ?? 1, sk.xy ?? 0, sk.yx ?? 0, sk.yy ?? 1, sk.tx ?? 0, sk.ty ?? 0];
|
|
270
|
+
const tendons = (s.children.get(skin.index) || []).filter(k => k.type === 'Tendon');
|
|
271
|
+
const bones = [[1, 0, 0, 1, 0, 0]];
|
|
272
|
+
for (const t of tendons) {
|
|
273
|
+
const tp = t.props;
|
|
274
|
+
const bind = [tp.xx ?? 1, tp.xy ?? 0, tp.yx ?? 0, tp.yy ?? 1, tp.tx ?? 0, tp.ty ?? 0];
|
|
275
|
+
const bone = s.byIndex[tp.boneId];
|
|
276
|
+
bones.push(bone ? mul(worldOf(s, bone, anim, frame), invM(bind)) : invM(bind));
|
|
277
|
+
}
|
|
278
|
+
const deform = (x, y, indices, weights) => {
|
|
279
|
+
const m = [0, 0, 0, 0, 0, 0];
|
|
280
|
+
let total = 0;
|
|
281
|
+
for (let i = 0; i < 4; i++) {
|
|
282
|
+
const w = (weights >>> (i * 8)) & 255;
|
|
283
|
+
if (!w)
|
|
284
|
+
continue;
|
|
285
|
+
const b = bones[(indices >>> (i * 8)) & 255] || bones[0];
|
|
286
|
+
for (let k = 0; k < 6; k++)
|
|
287
|
+
m[k] += b[k] * (w / 255);
|
|
288
|
+
total += w;
|
|
289
|
+
}
|
|
290
|
+
const [wx, wy] = applyM(bindWorld, x, y);
|
|
291
|
+
return total ? applyM(m, wx, wy) : [wx, wy];
|
|
292
|
+
};
|
|
293
|
+
const vertexObjs = (s.children.get(path.index) || []).filter(k => isA(k.type, 'PathVertex'));
|
|
294
|
+
return verts.map((v, i) => {
|
|
295
|
+
const vo = vertexObjs[i];
|
|
296
|
+
const wt = vo && (s.children.get(vo.index) || []).find(k => k.type === 'Weight' || k.type === 'CubicWeight');
|
|
297
|
+
if (!wt) {
|
|
298
|
+
const [x, y] = applyM(bindWorld, v.x, v.y);
|
|
299
|
+
const [ix, iy] = applyM(bindWorld, v.ix, v.iy);
|
|
300
|
+
const [ox, oy] = applyM(bindWorld, v.ox, v.oy);
|
|
301
|
+
return { ...v, x, y, ix, iy, ox, oy };
|
|
302
|
+
}
|
|
303
|
+
const w = wt.props;
|
|
304
|
+
const [x, y] = deform(v.x, v.y, w.indices ?? 1, w.values ?? 255);
|
|
305
|
+
if (!v.cubic)
|
|
306
|
+
return { ...v, x, y, ix: x, iy: y, ox: x, oy: y };
|
|
307
|
+
const [ix, iy] = deform(v.ix, v.iy, w.inIndices ?? w.indices ?? 1, w.inValues ?? w.values ?? 255);
|
|
308
|
+
const [ox, oy] = deform(v.ox, v.oy, w.outIndices ?? w.indices ?? 1, w.outValues ?? w.values ?? 255);
|
|
309
|
+
return { ...v, x, y, ix, iy, ox, oy };
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** Vertices -> Exode points, rounding straight corners the way Rive does (natural rounding). */
|
|
313
|
+
function toPoints(verts, closed, m) {
|
|
314
|
+
const n = verts.length;
|
|
315
|
+
const out = [];
|
|
316
|
+
const push = (x, y, ix, iy, ox, oy, curved) => {
|
|
317
|
+
const [X, Y] = applyM(m, x, y), [IX, IY] = applyM(m, ix, iy), [OX, OY] = applyM(m, ox, oy);
|
|
318
|
+
out.push({ x: X, y: Y, in_x: IX, in_y: IY, out_x: OX, out_y: OY, pointType: curved ? 'smooth' : 'corner' });
|
|
319
|
+
};
|
|
320
|
+
for (let i = 0; i < n; i++) {
|
|
321
|
+
const v = verts[i];
|
|
322
|
+
const hasPrev = closed || i > 0, hasNext = closed || i < n - 1;
|
|
323
|
+
if (!v.cubic && v.radius && hasPrev && hasNext) {
|
|
324
|
+
const prev = verts[(i - 1 + n) % n], next = verts[(i + 1) % n];
|
|
325
|
+
const px = (prev.cubic ? prev.ox : prev.x) - v.x, py = (prev.cubic ? prev.oy : prev.y) - v.y;
|
|
326
|
+
const nx = (next.cubic ? next.ix : next.x) - v.x, ny = (next.cubic ? next.iy : next.y) - v.y;
|
|
327
|
+
const lp = Math.hypot(px, py) || 1, ln = Math.hypot(nx, ny) || 1;
|
|
328
|
+
const tpx = px / lp, tpy = py / lp, tnx = nx / ln, tny = ny / ln;
|
|
329
|
+
const r = Math.min(Math.abs(v.radius), lp / 2, ln / 2);
|
|
330
|
+
const angle = Math.abs(Math.atan2(tpx * tny - tpy * tnx, tpx * tnx + tpy * tny));
|
|
331
|
+
const ideal = Math.min(r, Math.tan(Math.PI / (2 * ((2 * Math.PI) / (angle || 1e-6)))) * r * (4 / 3));
|
|
332
|
+
const ex = v.x + tpx * r, ey = v.y + tpy * r, xx = v.x + tnx * r, xy = v.y + tny * r;
|
|
333
|
+
push(ex, ey, ex, ey, ex + -tpx * ideal, ey + -tpy * ideal, true);
|
|
334
|
+
push(xx, xy, xx + -tnx * ideal, xy + -tny * ideal, xx, xy, true);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
push(v.x, v.y, v.ix, v.iy, v.ox, v.oy, v.cubic);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (out.length) {
|
|
341
|
+
out[0].moveTo = true;
|
|
342
|
+
if (closed)
|
|
343
|
+
out[out.length - 1].closePath = true;
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
const area = (c) => c.reduce((s, p, i) => { const q = c[(i + 1) % c.length]; return s + p.x * q.y - q.x * p.y; }, 0) / 2;
|
|
348
|
+
const inside = (pt, poly) => { let c = false; for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
349
|
+
const a = poly[i], b = poly[j];
|
|
350
|
+
if ((a.y > pt.y) !== (b.y > pt.y) && pt.x < ((b.x - a.x) * (pt.y - a.y)) / ((b.y - a.y) || 1e-9) + a.x)
|
|
351
|
+
c = !c;
|
|
352
|
+
} return c; };
|
|
353
|
+
const reverse = (c) => {
|
|
354
|
+
const r = [...c].reverse().map(p => ({ ...p, in_x: p.out_x, in_y: p.out_y, out_x: p.in_x, out_y: p.in_y, moveTo: false, closePath: false }));
|
|
355
|
+
r[0].moveTo = true;
|
|
356
|
+
if (c[c.length - 1].closePath)
|
|
357
|
+
r[r.length - 1].closePath = true;
|
|
358
|
+
return r;
|
|
359
|
+
};
|
|
360
|
+
/** Nonzero-only runtime: alternate contour direction by nesting depth so it fills like even-odd. */
|
|
361
|
+
const evenOdd = (contours) => contours.length < 2 ? contours : contours.map((c, i) => {
|
|
362
|
+
const depth = contours.reduce((d, o, j) => (j !== i && o.length > 2 && inside(c[0], o) ? d + 1 : d), 0);
|
|
363
|
+
return (area(c) > 0) === (depth % 2 === 0) ? c : reverse(c);
|
|
364
|
+
});
|
|
365
|
+
/** A Shape's compound outline (shape space) at a frame. */
|
|
366
|
+
function shapeContours(s, shape, anim, frame, eo) {
|
|
367
|
+
const contours = [];
|
|
368
|
+
for (const c of s.children.get(shape.index) || []) {
|
|
369
|
+
if (!isA(c.type, 'Path'))
|
|
370
|
+
continue;
|
|
371
|
+
const p = propsAt(c, anim, frame);
|
|
372
|
+
if ((p.pathFlags ?? 0) & 1)
|
|
373
|
+
continue; // Path::isHidden is bit 0; bit 1 is not "hidden"
|
|
374
|
+
const { verts, closed } = pathVertices(s, c, anim, frame);
|
|
375
|
+
if (!verts.length)
|
|
376
|
+
continue;
|
|
377
|
+
const skin = (s.children.get(c.index) || []).find(k => k.type === 'Skin');
|
|
378
|
+
if (skin) {
|
|
379
|
+
// Deformed points are in artboard space; bring them into the shape's space.
|
|
380
|
+
const toShape = invM(worldOf(s, shape, anim, frame));
|
|
381
|
+
contours.push(toPoints(skinVertices(s, c, skin, verts, anim, frame), closed, toShape));
|
|
382
|
+
}
|
|
383
|
+
else
|
|
384
|
+
contours.push(toPoints(verts, closed, localM(p)));
|
|
385
|
+
}
|
|
386
|
+
return eo ? evenOdd(contours) : contours;
|
|
387
|
+
}
|
|
388
|
+
const bbox = (pts) => {
|
|
389
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
390
|
+
for (const p of pts)
|
|
391
|
+
for (const [x, y] of [[p.x, p.y], [p.in_x, p.in_y], [p.out_x, p.out_y]]) {
|
|
392
|
+
x0 = Math.min(x0, x);
|
|
393
|
+
y0 = Math.min(y0, y);
|
|
394
|
+
x1 = Math.max(x1, x);
|
|
395
|
+
y1 = Math.max(y1, y);
|
|
396
|
+
}
|
|
397
|
+
return { x0, y0, x1, y1, w: Math.max(0, x1 - x0), h: Math.max(0, y1 - y0) };
|
|
398
|
+
};
|
|
399
|
+
/** Path size; the runtime scales points by width / baseWidth, so a straight line must not be 0 wide. */
|
|
400
|
+
const dim = (v) => Math.max(1, r3(v));
|
|
401
|
+
const ptsToData = (pts) => {
|
|
402
|
+
const parts = [];
|
|
403
|
+
let start = 0;
|
|
404
|
+
pts.forEach((p, i) => {
|
|
405
|
+
if (p.moveTo || i === 0) {
|
|
406
|
+
parts.push(`M${r3(p.x)} ${r3(p.y)}`);
|
|
407
|
+
start = i;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const q = pts[i - 1];
|
|
411
|
+
parts.push(`C${r3(q.out_x)} ${r3(q.out_y)} ${r3(p.in_x)} ${r3(p.in_y)} ${r3(p.x)} ${r3(p.y)}`);
|
|
412
|
+
}
|
|
413
|
+
if (p.closePath) {
|
|
414
|
+
const st = pts[start];
|
|
415
|
+
parts.push(`C${r3(p.out_x)} ${r3(p.out_y)} ${r3(st.in_x)} ${r3(st.in_y)} ${r3(st.x)} ${r3(st.y)} Z`);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
return parts.join(' ');
|
|
419
|
+
};
|
|
420
|
+
const DRAW_TYPES = new Set(['Shape', 'Image', 'Text', 'NestedArtboard', 'NestedArtboardLeaf', 'NestedArtboardLayout']);
|
|
421
|
+
/** Artboard-space transform of a component at a frame. */
|
|
422
|
+
function worldOf(s, o, anim, frame) {
|
|
423
|
+
const chain = [];
|
|
424
|
+
for (let c = o, g = 0; c && c.index !== 0 && g < 64; c = s.byIndex[c.props.parentId ?? 0] || null, g++)
|
|
425
|
+
if (isA(c.type, 'TransformComponent'))
|
|
426
|
+
chain.unshift(c);
|
|
427
|
+
const pose = poseAt(s, anim, frame);
|
|
428
|
+
return chain.reduce((m, c) => pose.world.get(c.index) || mul(m, localM(localProps(s, c, anim, frame, pose))), [1, 0, 0, 1, 0, 0]);
|
|
429
|
+
}
|
|
430
|
+
/** A component's local transform props at a frame, with bone tips and IK solves applied. */
|
|
431
|
+
function localProps(s, c, anim, frame, pose = poseAt(s, anim, frame)) {
|
|
432
|
+
const p = boneLocal(s, c, propsAt(c, anim, frame), anim, frame);
|
|
433
|
+
return pose.rot.has(c.index) ? { ...p, rotation: pose.rot.get(c.index) } : p;
|
|
434
|
+
}
|
|
435
|
+
const decompose = (m) => {
|
|
436
|
+
const d = m[0] * m[0] + m[1] * m[1], sx = Math.sqrt(d);
|
|
437
|
+
return { x: m[4], y: m[5], rotation: Math.atan2(m[1], m[0]), scaleX: sx, scaleY: sx ? (m[0] * m[3] - m[2] * m[1]) / sx : 0, skew: Math.atan2(m[0] * m[2] + m[1] * m[3], d) };
|
|
438
|
+
};
|
|
439
|
+
const compose = (c) => {
|
|
440
|
+
const cs = Math.cos(c.rotation), sn = Math.sin(c.rotation);
|
|
441
|
+
const m = [cs * c.scaleX, sn * c.scaleX, -sn * c.scaleY, cs * c.scaleY, c.x, c.y];
|
|
442
|
+
if (c.skew) {
|
|
443
|
+
m[2] = m[0] * c.skew + m[2];
|
|
444
|
+
m[3] = m[1] * c.skew + m[3];
|
|
445
|
+
}
|
|
446
|
+
return m;
|
|
447
|
+
};
|
|
448
|
+
const wrapAngle = (from, to) => {
|
|
449
|
+
const a = from % (Math.PI * 2), b = to % (Math.PI * 2);
|
|
450
|
+
let d = b - a;
|
|
451
|
+
if (d > Math.PI)
|
|
452
|
+
d -= Math.PI * 2;
|
|
453
|
+
else if (d < -Math.PI)
|
|
454
|
+
d += Math.PI * 2;
|
|
455
|
+
return [a, d];
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* Rive IK (ik_constraint.cpp): one bone aims at the target; two bones solve the
|
|
459
|
+
* triangle by the law of cosines; longer chains solve each bone against the tip.
|
|
460
|
+
* Strength blends from the FK rotation. Solves run in file order, each seeing the
|
|
461
|
+
* ones before it, and only change chain bones' local rotations.
|
|
462
|
+
*/
|
|
463
|
+
function poseAt(s, anim, frame) {
|
|
464
|
+
let byFrame = s.poseCache.get(anim);
|
|
465
|
+
if (!byFrame) {
|
|
466
|
+
byFrame = new Map();
|
|
467
|
+
s.poseCache.set(anim, byFrame);
|
|
468
|
+
}
|
|
469
|
+
let pose = byFrame.get(frame);
|
|
470
|
+
if (pose)
|
|
471
|
+
return pose;
|
|
472
|
+
pose = { rot: new Map(), world: new Map() };
|
|
473
|
+
byFrame.set(frame, pose); // later solves read the earlier ones through this
|
|
474
|
+
if (!s.constraints.length)
|
|
475
|
+
return pose;
|
|
476
|
+
const ov = pose.rot;
|
|
477
|
+
const parentWorld = (b) => { const p = s.byIndex[b.props.parentId ?? 0]; return p && p.index !== 0 && isA(p.type, 'TransformComponent') ? worldOf(s, p, anim, frame) : [1, 0, 0, 1, 0, 0]; };
|
|
478
|
+
const pos = (m) => [m[4], m[5]];
|
|
479
|
+
const dir = (m, x, y) => [m[0] * x + m[2] * y, m[1] * x + m[3] * y];
|
|
480
|
+
const ang = (v) => Math.atan2(v[1], v[0]);
|
|
481
|
+
for (const { c, owner, target, chain } of s.constraints) {
|
|
482
|
+
if (!chain) {
|
|
483
|
+
otherConstraint(s, c, owner, target, anim, frame, pose, parentWorld);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const tw = worldOf(s, target, anim, frame);
|
|
487
|
+
const T = pos(tw);
|
|
488
|
+
const fk = chain.map(b => localProps(s, b, anim, frame, pose).rotation || 0);
|
|
489
|
+
const solved = new Map();
|
|
490
|
+
const set = (b, r) => { solved.set(b.index, r); ov.set(b.index, r); };
|
|
491
|
+
const solve2 = (i, tipIdx) => {
|
|
492
|
+
const b1 = chain[i], b2 = chain[tipIdx], first = chain[i + 1];
|
|
493
|
+
const iw = invM(parentWorld(b1));
|
|
494
|
+
const w1 = worldOf(s, b1, anim, frame), wc = worldOf(s, first, anim, frame), w2 = worldOf(s, b2, anim, frame);
|
|
495
|
+
const tipLen = propsAt(b2, anim, frame).length ?? 0;
|
|
496
|
+
const pA = applyM(iw, ...pos(w1)), pC = applyM(iw, ...pos(wc)), pB = applyM(iw, ...applyM(w2, tipLen, 0)), pBT = applyM(iw, ...T);
|
|
497
|
+
const a = Math.hypot(pB[0] - pC[0], pB[1] - pC[1]), b = Math.hypot(pC[0] - pA[0], pC[1] - pA[1]);
|
|
498
|
+
const cv = [pBT[0] - pA[0], pBT[1] - pA[1]], cl = Math.hypot(...cv);
|
|
499
|
+
const clamp = (v) => Math.max(-1, Math.min(1, v));
|
|
500
|
+
const A = Math.acos(clamp((-a * a + b * b + cl * cl) / (2 * b * cl || 1e-9)));
|
|
501
|
+
const C = Math.acos(clamp((a * a + b * b - cl * cl) / (2 * a * b || 1e-9)));
|
|
502
|
+
let corr = 0;
|
|
503
|
+
if (b2.props.parentId !== b1.index) {
|
|
504
|
+
const second = chain[i + 2];
|
|
505
|
+
const wB = applyM(w2, tipLen, 0), wC = pos(wc);
|
|
506
|
+
corr = -ang(dir(invM(parentWorld(second)), wB[0] - wC[0], wB[1] - wC[1]));
|
|
507
|
+
}
|
|
508
|
+
const inv = !!c.props.invertDirection;
|
|
509
|
+
set(b1, inv ? ang(cv) - A : A + ang(cv));
|
|
510
|
+
set(first, (inv ? -C + Math.PI : C - Math.PI) + corr);
|
|
511
|
+
};
|
|
512
|
+
if (chain.length === 1) {
|
|
513
|
+
const b = chain[0];
|
|
514
|
+
const d = dir(invM(parentWorld(b)), T[0] - worldOf(s, b, anim, frame)[4], T[1] - worldOf(s, b, anim, frame)[5]);
|
|
515
|
+
set(b, ang(d));
|
|
516
|
+
}
|
|
517
|
+
else if (chain.length === 2)
|
|
518
|
+
solve2(0, 1);
|
|
519
|
+
else
|
|
520
|
+
for (let i = 0; i < chain.length - 1; i++)
|
|
521
|
+
solve2(i, chain.length - 1);
|
|
522
|
+
const k = c.props.strength ?? 1;
|
|
523
|
+
if (k !== 1)
|
|
524
|
+
chain.forEach((b, i) => {
|
|
525
|
+
if (!solved.has(b.index))
|
|
526
|
+
return;
|
|
527
|
+
const from = fk[i] % (Math.PI * 2), to = solved.get(b.index) % (Math.PI * 2);
|
|
528
|
+
let d = to - from;
|
|
529
|
+
if (d > Math.PI)
|
|
530
|
+
d -= Math.PI * 2;
|
|
531
|
+
else if (d < -Math.PI)
|
|
532
|
+
d += Math.PI * 2;
|
|
533
|
+
ov.set(b.index, from + d * k);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return pose;
|
|
537
|
+
}
|
|
538
|
+
/** Transform / translation / rotation / scale constraints (constraints/*.cpp): set the owner's world transform. */
|
|
539
|
+
function otherConstraint(s, c, owner, tgt, anim, frame, pose, parentWorld) {
|
|
540
|
+
const p = propsAt(c, anim, frame);
|
|
541
|
+
const t = p.strength ?? 1, ti = 1 - t;
|
|
542
|
+
const A = worldOf(s, owner, anim, frame);
|
|
543
|
+
const srcLocal = (p.sourceSpaceValue ?? 0) === 1, dstLocal = (p.destSpaceValue ?? 0) === 1;
|
|
544
|
+
const targetWorld = () => { let B = worldOf(s, tgt, anim, frame); if (srcLocal)
|
|
545
|
+
B = mul(invM(parentWorld(tgt)), B); return B; };
|
|
546
|
+
if (c.type === 'TransformConstraint') {
|
|
547
|
+
if (!tgt)
|
|
548
|
+
return;
|
|
549
|
+
const b = s.bounds?.(tgt) || { x: 0, y: 0, w: 0, h: 0 };
|
|
550
|
+
let B = mul(worldOf(s, tgt, anim, frame), [1, 0, 0, 1, b.x + b.w * (p.originX ?? 0), b.y + b.h * (p.originY ?? 0)]);
|
|
551
|
+
if (srcLocal)
|
|
552
|
+
B = mul(invM(parentWorld(tgt)), B);
|
|
553
|
+
if (dstLocal)
|
|
554
|
+
B = mul(parentWorld(owner), B);
|
|
555
|
+
const ca = decompose(A), cb = decompose(B);
|
|
556
|
+
const [a0, d] = wrapAngle(ca.rotation, cb.rotation);
|
|
557
|
+
pose.world.set(owner.index, compose({ rotation: a0 + d * t, x: ca.x * ti + cb.x * t, y: ca.y * ti + cb.y * t, scaleX: ca.scaleX * ti + cb.scaleX * t, scaleY: ca.scaleY * ti + cb.scaleY * t, skew: ca.skew * ti + cb.skew * t }));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const own = propsAt(owner, anim, frame);
|
|
561
|
+
const clampLocal = (p.minMaxSpaceValue ?? 0) === 1;
|
|
562
|
+
if (c.type === 'TranslationConstraint') {
|
|
563
|
+
let bx = A[4], by = A[5];
|
|
564
|
+
if (tgt) {
|
|
565
|
+
const B = targetWorld();
|
|
566
|
+
bx = B[4];
|
|
567
|
+
by = B[5];
|
|
568
|
+
const lx = p.offset ? own.x || 0 : 0, ly = p.offset ? own.y || 0 : 0;
|
|
569
|
+
if (!p.doesCopy)
|
|
570
|
+
bx = dstLocal ? 0 : A[4];
|
|
571
|
+
else
|
|
572
|
+
bx = bx * (p.copyFactor ?? 1) + lx;
|
|
573
|
+
if (!p.doesCopyY)
|
|
574
|
+
by = dstLocal ? 0 : A[5];
|
|
575
|
+
else
|
|
576
|
+
by = by * (p.copyFactorY ?? 1) + ly;
|
|
577
|
+
if (dstLocal)
|
|
578
|
+
[bx, by] = applyM(parentWorld(owner), bx, by);
|
|
579
|
+
}
|
|
580
|
+
if (clampLocal)
|
|
581
|
+
[bx, by] = applyM(invM(parentWorld(owner)), bx, by);
|
|
582
|
+
if (p.max && bx > p.maxValue)
|
|
583
|
+
bx = p.maxValue;
|
|
584
|
+
if (p.min && bx < p.minValue)
|
|
585
|
+
bx = p.minValue;
|
|
586
|
+
if (p.maxY && by > p.maxValueY)
|
|
587
|
+
by = p.maxValueY;
|
|
588
|
+
if (p.minY && by < p.minValueY)
|
|
589
|
+
by = p.minValueY;
|
|
590
|
+
if (clampLocal)
|
|
591
|
+
[bx, by] = applyM(parentWorld(owner), bx, by);
|
|
592
|
+
const W = [...A];
|
|
593
|
+
W[4] = A[4] * ti + bx * t;
|
|
594
|
+
W[5] = A[5] * ti + by * t;
|
|
595
|
+
pose.world.set(owner.index, W);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const ca = decompose(A);
|
|
599
|
+
let cb = { ...ca };
|
|
600
|
+
if (tgt) {
|
|
601
|
+
cb = decompose(targetWorld());
|
|
602
|
+
if (c.type === 'RotationConstraint') {
|
|
603
|
+
if (!p.doesCopy)
|
|
604
|
+
cb.rotation = dstLocal ? 0 : ca.rotation;
|
|
605
|
+
else {
|
|
606
|
+
cb.rotation *= p.copyFactor ?? 1;
|
|
607
|
+
if (p.offset)
|
|
608
|
+
cb.rotation += own.rotation || 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
if (!p.doesCopy)
|
|
613
|
+
cb.scaleX = dstLocal ? 1 : ca.scaleX;
|
|
614
|
+
else {
|
|
615
|
+
cb.scaleX *= p.copyFactor ?? 1;
|
|
616
|
+
if (p.offset)
|
|
617
|
+
cb.scaleX *= own.scaleX ?? 1;
|
|
618
|
+
}
|
|
619
|
+
if (!p.doesCopyY)
|
|
620
|
+
cb.scaleY = dstLocal ? 1 : ca.scaleY;
|
|
621
|
+
else {
|
|
622
|
+
cb.scaleY *= p.copyFactorY ?? 1;
|
|
623
|
+
if (p.offset)
|
|
624
|
+
cb.scaleY *= own.scaleY ?? 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (dstLocal)
|
|
628
|
+
cb = decompose(mul(parentWorld(owner), compose(cb)));
|
|
629
|
+
}
|
|
630
|
+
if (clampLocal)
|
|
631
|
+
cb = decompose(mul(invM(parentWorld(owner)), compose(cb)));
|
|
632
|
+
if (c.type === 'RotationConstraint') {
|
|
633
|
+
if (p.min || p.max) {
|
|
634
|
+
const center = p.min && p.max ? (p.minValue + p.maxValue) / 2 : p.max ? p.maxValue : p.minValue;
|
|
635
|
+
let r = cb.rotation - center;
|
|
636
|
+
r -= Math.PI * 2 * Math.floor((r + Math.PI) / (Math.PI * 2));
|
|
637
|
+
r += center;
|
|
638
|
+
if (p.max && r > p.maxValue)
|
|
639
|
+
r = p.maxValue;
|
|
640
|
+
if (p.min && r < p.minValue)
|
|
641
|
+
r = p.minValue;
|
|
642
|
+
cb.rotation = r;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
if (p.max && cb.scaleX > p.maxValue)
|
|
647
|
+
cb.scaleX = p.maxValue;
|
|
648
|
+
if (p.min && cb.scaleX < p.minValue)
|
|
649
|
+
cb.scaleX = p.minValue;
|
|
650
|
+
if (p.maxY && cb.scaleY > p.maxValueY)
|
|
651
|
+
cb.scaleY = p.maxValueY;
|
|
652
|
+
if (p.minY && cb.scaleY < p.minValueY)
|
|
653
|
+
cb.scaleY = p.minValueY;
|
|
654
|
+
}
|
|
655
|
+
if (clampLocal)
|
|
656
|
+
cb = decompose(mul(parentWorld(owner), compose(cb)));
|
|
657
|
+
const out = { ...ca };
|
|
658
|
+
if (c.type === 'RotationConstraint') {
|
|
659
|
+
const [a0, d] = wrapAngle(ca.rotation, cb.rotation);
|
|
660
|
+
out.rotation = a0 + d * t;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
out.scaleX = ca.scaleX * ti + cb.scaleX * t;
|
|
664
|
+
out.scaleY = ca.scaleY * ti + cb.scaleY * t;
|
|
665
|
+
}
|
|
666
|
+
pose.world.set(owner.index, compose(out));
|
|
667
|
+
}
|
|
668
|
+
/** Components whose transform a constraint changes (IK chain bones and constraint owners). */
|
|
669
|
+
const constrained = (s) => new Set(s.constraints.flatMap(k => (k.chain || [k.owner]).map(b => b.index)));
|
|
670
|
+
const invM = (m) => { const d = m[0] * m[3] - m[1] * m[2] || 1e-12; return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, (m[2] * m[5] - m[3] * m[4]) / d, (m[1] * m[4] - m[0] * m[5]) / d]; };
|
|
671
|
+
const mapPts = (pts, m) => pts.map(p => { const [x, y] = applyM(m, p.x, p.y), [ix, iy] = applyM(m, p.in_x, p.in_y), [ox, oy] = applyM(m, p.out_x, p.out_y); return { ...p, x, y, in_x: ix, in_y: iy, out_x: ox, out_y: oy }; });
|
|
672
|
+
const descendants = (s, o, out = []) => { out.push(o); for (const c of s.children.get(o.index) || [])
|
|
673
|
+
descendants(s, c, out); return out; };
|
|
674
|
+
/** A clipping shape's outline in its parent node's space at a frame. */
|
|
675
|
+
function clipOutline(s, clip, node, anim, frame) {
|
|
676
|
+
const src = s.byIndex[clip.props.sourceId];
|
|
677
|
+
if (!src)
|
|
678
|
+
return [];
|
|
679
|
+
const toNode = invM(worldOf(s, node, anim, frame));
|
|
680
|
+
const pts = [];
|
|
681
|
+
for (const sh of descendants(s, src).filter(c => c.type === 'Shape')) {
|
|
682
|
+
const m = mul(toNode, worldOf(s, sh, anim, frame));
|
|
683
|
+
for (const c of shapeContours(s, sh, anim, frame, clip.props.fillRule === 1))
|
|
684
|
+
pts.push(...mapPts(c, m));
|
|
685
|
+
}
|
|
686
|
+
return pts;
|
|
687
|
+
}
|
|
688
|
+
function emitClips(ctx, node, g) {
|
|
689
|
+
const s = ctx.s;
|
|
690
|
+
const clips = (s.children.get(node.index) || []).filter(k => k.type === 'ClippingShape' && k.props.isVisible !== false);
|
|
691
|
+
clips.forEach((clip, k) => {
|
|
692
|
+
const pts = clipOutline(s, clip, node, null, 0);
|
|
693
|
+
if (!pts.length)
|
|
694
|
+
return;
|
|
695
|
+
const b = bbox(pts);
|
|
696
|
+
const id = `${ctx.pre}${node.index}_clip${k}`;
|
|
697
|
+
const o = {
|
|
698
|
+
id, name: clip.props.name || 'Clip', type: 'Shape', parentId: g.id, transform: baseTransform(),
|
|
699
|
+
geometry: { type: 'Path', points: pts, is_closed: true, width: dim(b.w), height: dim(b.h), baseWidth: dim(b.w), baseHeight: dim(b.h), isMask: true },
|
|
700
|
+
style: { fill: { type: 'Solid', color: '#000000', opacity: 0 }, stroke: noStroke() }, isVisible: true,
|
|
701
|
+
};
|
|
702
|
+
// Clips shape everything after them in the Group, so they go first.
|
|
703
|
+
g.children.unshift(id);
|
|
704
|
+
ctx.objects.push(o);
|
|
705
|
+
const src = s.byIndex[clip.props.sourceId];
|
|
706
|
+
const involved = new Set();
|
|
707
|
+
if (src)
|
|
708
|
+
descendants(s, src).forEach(c => { involved.add(c.index); for (let p = c, guard = 0; p && guard < 64; p = p.index === 0 ? null : s.byIndex[p.props.parentId ?? 0] || null, guard++)
|
|
709
|
+
involved.add(p.index); });
|
|
710
|
+
for (let p = node, guard = 0; p && guard < 64; p = p.index === 0 ? null : s.byIndex[p.props.parentId ?? 0] || null, guard++)
|
|
711
|
+
involved.add(p.index);
|
|
712
|
+
for (const a of s.animations) {
|
|
713
|
+
if (![...involved].some(i => a.keyed.has(i))) {
|
|
714
|
+
if (!src || !descendants(s, src).some(c => (s.children.get(c.index) || []).some(v => a.keyed.has(v.index))))
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const frames = [];
|
|
718
|
+
for (let f = a.start; f <= a.end + 1e-6; f++)
|
|
719
|
+
frames.push(Math.min(f, a.end));
|
|
720
|
+
const per = frames.map(f => clipOutline(s, clip, node, a, f));
|
|
721
|
+
if (!per.every(q => q.length === pts.length)) {
|
|
722
|
+
ctx.warnings.add('A clipping path changes its vertex count while animating');
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const tracks = ctx.tracksByAnim.get(a);
|
|
726
|
+
['x', 'y', 'in_x', 'in_y', 'out_x', 'out_y'].forEach(fd => pts.forEach((_, j) => {
|
|
727
|
+
const series = per.map(q => q[j][fd]);
|
|
728
|
+
if (series.every(v => Math.abs(v - series[0]) < 1e-4))
|
|
729
|
+
return;
|
|
730
|
+
tracks.push({ id: `t_${id}_${j}_${fd}`, object_id: id, property: `geometry.points.${j}.${fd}`, keyframes: frames.map((f, i) => ({ id: `k_${id}_${j}_${fd}_${i}`, time: r3((f - a.start) / a.fps), value: r3(series[i]), easing: 'linear' })) });
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const mimeOf = (b) => (b[0] === 0x89 ? 'image/png' : b[0] === 0xff ? 'image/jpeg' : b[0] === 0x52 && b[8] === 0x57 ? 'image/webp' : 'image/png');
|
|
736
|
+
const b64 = (b) => { let s = ''; for (let i = 0; i < b.length; i += 0x8000)
|
|
737
|
+
s += String.fromCharCode(...b.subarray(i, i + 0x8000)); return typeof btoa === 'function' ? btoa(s) : globalThis.Buffer.from(b).toString('base64'); };
|
|
738
|
+
function emitImage(ctx, img, host) {
|
|
739
|
+
const assets = ctx.file.assets;
|
|
740
|
+
const entry = assets[img.props.assetId];
|
|
741
|
+
if (!entry) {
|
|
742
|
+
ctx.warnings.add('An image references a missing asset');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (!entry.bytes) {
|
|
746
|
+
ctx.warnings.add(`Image "${entry.asset.props.name || 'image'}" is not embedded in the .riv (hosted or referenced assets are not downloaded)`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const w = entry.asset.props.width || 0, h = entry.asset.props.height || 0;
|
|
750
|
+
if (!(w > 0 && h > 0)) {
|
|
751
|
+
ctx.warnings.add('An image asset has no size');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if ((ctx.s.children.get(img.index) || []).some(k => k.type === 'Mesh'))
|
|
755
|
+
ctx.warnings.add('Image meshes (deformation) are not imported');
|
|
756
|
+
const src = `data:${mimeOf(entry.bytes)};base64,${b64(entry.bytes)}`;
|
|
757
|
+
const id = `${ctx.pre}${img.index}_img`;
|
|
758
|
+
const o = {
|
|
759
|
+
id, name: img.props.name || 'Image', type: 'Shape', parentId: host.id,
|
|
760
|
+
transform: { ...baseTransform(), x: r3(w / 2 - (img.props.originX ?? 0.5) * w), y: r3(h / 2 - (img.props.originY ?? 0.5) * h) },
|
|
761
|
+
geometry: { type: 'Image', src, originalSrc: src, width: w, height: h }, style: { fill: { type: 'None', opacity: 0 } }, isVisible: true,
|
|
762
|
+
};
|
|
763
|
+
host.children.push(id);
|
|
764
|
+
ctx.objects.push(o);
|
|
765
|
+
}
|
|
766
|
+
function group(ctx, id, name, parent) {
|
|
767
|
+
const g = {
|
|
768
|
+
id, name, type: 'Group', transform: baseTransform(), width: 1, height: 1,
|
|
769
|
+
geometry: { type: 'Rectangle', width: 1, height: 1, corner_radius: 0 },
|
|
770
|
+
style: { fill: { type: 'None', opacity: 0 }, stroke: noStroke() },
|
|
771
|
+
clipContent: false, isVisible: true, isLocked: false, isExpanded: false, children: [],
|
|
772
|
+
};
|
|
773
|
+
if (parent) {
|
|
774
|
+
g.parentId = parent.id;
|
|
775
|
+
parent.children.push(id);
|
|
776
|
+
}
|
|
777
|
+
ctx.objects.push(g);
|
|
778
|
+
return g;
|
|
779
|
+
}
|
|
780
|
+
/** World scale (geometric mean of |sx|, |sy|) of a component at a frame, for screen-space strokes. */
|
|
781
|
+
function worldScale(s, o, anim, frame) {
|
|
782
|
+
let k = 1;
|
|
783
|
+
for (let c = o, guard = 0; c && c.index !== 0 && guard < 64; c = s.byIndex[c.props.parentId ?? 0] || null, guard++) {
|
|
784
|
+
if (!isA(c.type, 'TransformComponent'))
|
|
785
|
+
continue;
|
|
786
|
+
const p = propsAt(c, anim, frame);
|
|
787
|
+
k *= Math.sqrt(Math.abs((p.scaleX ?? 1) * (p.scaleY ?? 1)));
|
|
788
|
+
}
|
|
789
|
+
return k;
|
|
790
|
+
}
|
|
791
|
+
/** Does a skinned path move in this animation (any tendon bone or its ancestors keyed, or the shape itself)? */
|
|
792
|
+
function skinMoves(s, a, path) {
|
|
793
|
+
const skin = (s.children.get(path.index) || []).find(k => k.type === 'Skin');
|
|
794
|
+
if (!skin)
|
|
795
|
+
return false;
|
|
796
|
+
const ids = new Set();
|
|
797
|
+
const up = (o) => { for (let c = o, g = 0; c && g < 64; c = c.index === 0 ? null : s.byIndex[c.props.parentId ?? 0] || null, g++)
|
|
798
|
+
ids.add(c.index); };
|
|
799
|
+
for (const t of (s.children.get(skin.index) || []).filter(k => k.type === 'Tendon'))
|
|
800
|
+
up(s.byIndex[t.props.boneId]);
|
|
801
|
+
up(path);
|
|
802
|
+
const ik = constrained(s);
|
|
803
|
+
if (a.keyed.size && [...ids].some(i => ik.has(i)))
|
|
804
|
+
return true;
|
|
805
|
+
return [...ids].some(i => a.keyed.has(i));
|
|
806
|
+
}
|
|
807
|
+
const ancestorsAnimated = (s, o, prop) => s.animations.some(a => {
|
|
808
|
+
for (let c = o, guard = 0; c && guard < 64; c = c.index === 0 ? null : s.byIndex[c.props.parentId ?? 0] || null, guard++) {
|
|
809
|
+
const m = a.keyed.get(c.index);
|
|
810
|
+
if (m && prop.some(p => m.has(p)))
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
});
|
|
815
|
+
function paintFill(paint, s, anim, frame, box) {
|
|
816
|
+
const kids = s.children.get(paint.index) || [];
|
|
817
|
+
const mutator = kids.find(k => k.type === 'SolidColor' || k.type === 'LinearGradient' || k.type === 'RadialGradient');
|
|
818
|
+
if (!mutator)
|
|
819
|
+
return { type: 'Solid', color: '#000000', opacity: 0 };
|
|
820
|
+
const mp = propsAt(mutator, anim, frame);
|
|
821
|
+
if (mutator.type === 'SolidColor') {
|
|
822
|
+
const [c, a] = argb(mp.colorValue ?? 0xff747474);
|
|
823
|
+
return { type: 'Solid', color: c, opacity: a };
|
|
824
|
+
}
|
|
825
|
+
const op = mp.opacity ?? 1;
|
|
826
|
+
const stops = (s.children.get(mutator.index) || []).filter(k => k.type === 'GradientStop').map(k => {
|
|
827
|
+
const q = propsAt(k, anim, frame);
|
|
828
|
+
const [c, a] = argb(q.colorValue ?? 0xffffffff);
|
|
829
|
+
return { offset: r3(q.position ?? 0), color: c, opacity: r3(a * op) };
|
|
830
|
+
}).sort((a, b) => a.offset - b.offset);
|
|
831
|
+
// Gradient points are in the shape's space; normalise around the object origin.
|
|
832
|
+
const w = box.w || 1, h = box.h || 1;
|
|
833
|
+
const nx = (x) => r3((x - box.cx + w / 2) / w), ny = (y) => r3((y - box.cy + h / 2) / h);
|
|
834
|
+
if (mutator.type === 'RadialGradient')
|
|
835
|
+
return { type: 'RadialGradient', center: [nx(mp.startX || 0), ny(mp.startY || 0)], radius: r3(Math.hypot((mp.endX || 0) - (mp.startX || 0), (mp.endY || 0) - (mp.startY || 0)) / Math.max(w, h)), stops, opacity: 1 };
|
|
836
|
+
return { type: 'LinearGradient', start: [nx(mp.startX || 0), ny(mp.startY || 0)], end: [nx(mp.endX || 0), ny(mp.endY || 0)], stops, opacity: 1 };
|
|
837
|
+
}
|
|
838
|
+
// ---------------------------------------------------------------- text
|
|
839
|
+
const fontCache = new WeakMap();
|
|
840
|
+
function fontFor(ctx, assetIndex) {
|
|
841
|
+
if (assetIndex === undefined)
|
|
842
|
+
return null;
|
|
843
|
+
let m = fontCache.get(ctx.file);
|
|
844
|
+
if (!m) {
|
|
845
|
+
m = new Map();
|
|
846
|
+
fontCache.set(ctx.file, m);
|
|
847
|
+
}
|
|
848
|
+
if (!m.has(assetIndex)) {
|
|
849
|
+
const a = ctx.file.assets[assetIndex];
|
|
850
|
+
let f = null;
|
|
851
|
+
try {
|
|
852
|
+
f = a?.bytes ? parseFont(a.bytes) : null;
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
f = null;
|
|
856
|
+
}
|
|
857
|
+
m.set(assetIndex, f);
|
|
858
|
+
}
|
|
859
|
+
return m.get(assetIndex);
|
|
860
|
+
}
|
|
861
|
+
/** One loadable family name: drop size/weight words a static instance carries in its name. */
|
|
862
|
+
const familyOf = (f) => {
|
|
863
|
+
const name = (f?.family || 'Inter').replace(/\s+\d+pt$/i, '').replace(/\s+(Thin|ExtraLight|Extra Light|UltraLight|Light|Regular|Medium|SemiBold|Semi Bold|DemiBold|Bold|ExtraBold|Extra Bold|UltraBold|Black|Heavy|Italic)$/i, '').trim();
|
|
864
|
+
return /^[a-zA-Z0-9\s_-]+$/.test(name) ? name : 'Inter';
|
|
865
|
+
};
|
|
866
|
+
/** Rive line metrics for a run: natural height is ascent + descent (no line gap); custom line height is px. */
|
|
867
|
+
const runAscent = (r) => (r.font ? r.font.ascent : 0.97) * r.size;
|
|
868
|
+
const runLineHeight = (r) => (r.lineHeight >= 0 ? r.lineHeight : ((r.font ? r.font.ascent + r.font.descent : 1.21) * r.size));
|
|
869
|
+
/** Greedy line breaking with the font's advances; returns each line's width and height. */
|
|
870
|
+
function layoutLines(runs, maxWidth, wrap) {
|
|
871
|
+
const chars = [];
|
|
872
|
+
for (const r of runs)
|
|
873
|
+
for (const c of r.text) {
|
|
874
|
+
const cp = c.codePointAt(0);
|
|
875
|
+
const adv = r.font ? r.font.advance(cp) : 0.55;
|
|
876
|
+
chars.push({ c, w: (adv || (r.font ? r.font.advance(0x20) || 0.3 : 0.55)) * r.size + r.letterSpacing, r });
|
|
877
|
+
}
|
|
878
|
+
const lines = [];
|
|
879
|
+
let start = 0;
|
|
880
|
+
const finish = (from, to) => {
|
|
881
|
+
let end = to;
|
|
882
|
+
while (end > from && /\s/.test(chars[end - 1].c))
|
|
883
|
+
end--;
|
|
884
|
+
let w = 0;
|
|
885
|
+
for (let i = from; i < end; i++)
|
|
886
|
+
w += chars[i].w;
|
|
887
|
+
let h = 0;
|
|
888
|
+
for (let i = from; i < Math.max(to, from + 1) && i < chars.length; i++)
|
|
889
|
+
h = Math.max(h, runLineHeight(chars[i].r));
|
|
890
|
+
if (!h)
|
|
891
|
+
h = runs.length ? runLineHeight(runs[runs.length - 1]) : 0;
|
|
892
|
+
lines.push({ w, h });
|
|
893
|
+
};
|
|
894
|
+
let w = 0, lastBreak = -1;
|
|
895
|
+
for (let i = 0; i < chars.length; i++) {
|
|
896
|
+
const ch = chars[i];
|
|
897
|
+
if (ch.c === '\n') {
|
|
898
|
+
finish(start, i);
|
|
899
|
+
start = i + 1;
|
|
900
|
+
w = 0;
|
|
901
|
+
lastBreak = -1;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
w += ch.w;
|
|
905
|
+
if (/\s/.test(ch.c)) {
|
|
906
|
+
lastBreak = i + 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (wrap && maxWidth > 0 && w > maxWidth && i > start) {
|
|
910
|
+
const at = lastBreak > start ? lastBreak : i;
|
|
911
|
+
finish(start, at);
|
|
912
|
+
start = at;
|
|
913
|
+
w = 0;
|
|
914
|
+
lastBreak = -1;
|
|
915
|
+
for (let k = start; k <= i; k++)
|
|
916
|
+
w += chars[k].w;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
finish(start, chars.length);
|
|
920
|
+
return lines;
|
|
921
|
+
}
|
|
922
|
+
const TEXT_LEADING = { 'Inter': 0.35, 'Roboto': 0.45, 'Montserrat': 0.40, 'Outfit': 0.35, 'Open Sans': 0.40, 'Poppins': 0.45, 'Playfair Display': 0.35, 'Lora': 0.40, 'Lato': 0.35, 'Bebas Neue': 0.45, 'Oswald': 0.40 };
|
|
923
|
+
/**
|
|
924
|
+
* Rive Text -> a native Text object inside the Text's group. Rive lays lines out
|
|
925
|
+
* from the box's top-left and shifts the box by -origin x size; the runtime centres
|
|
926
|
+
* the box on the object, so the object sits at the box centre.
|
|
927
|
+
*/
|
|
928
|
+
function textLayout(ctx, text) {
|
|
929
|
+
const s = ctx.s;
|
|
930
|
+
const p = text.props;
|
|
931
|
+
const runs = [];
|
|
932
|
+
for (const k of s.children.get(text.index) || []) {
|
|
933
|
+
if (k.type !== 'TextValueRun')
|
|
934
|
+
continue;
|
|
935
|
+
const style = s.byIndex[k.props.styleId ?? -1];
|
|
936
|
+
if (!style || !isA(style.type, 'TextStyle'))
|
|
937
|
+
continue;
|
|
938
|
+
const sp = style.props;
|
|
939
|
+
const font = fontFor(ctx, sp.fontAssetId);
|
|
940
|
+
const wght = (s.children.get(style.index) || []).find(a => a.type === 'TextStyleAxis' && a.props.tag === 0x77676874);
|
|
941
|
+
runs.push({ text: String(k.props.text ?? ''), style, size: sp.fontSize ?? 12, lineHeight: sp.lineHeight ?? -1, letterSpacing: sp.letterSpacing ?? 0, font, family: familyOf(font), weight: Math.round(wght ? wght.props.axisValue : font?.weight ?? 400), italic: !!font?.italic });
|
|
942
|
+
}
|
|
943
|
+
const full = runs.map(r => r.text).join('');
|
|
944
|
+
if (!runs.length || !full)
|
|
945
|
+
return null;
|
|
946
|
+
const sizing = p.sizingValue ?? 0; // 0 auto width, 1 auto height, 2 fixed
|
|
947
|
+
const wrap = sizing !== 0 && (p.wrapValue ?? 0) === 0;
|
|
948
|
+
const lines = layoutLines(runs, p.width || 0, wrap);
|
|
949
|
+
const contentW = Math.max(0, ...lines.map(l => l.w));
|
|
950
|
+
const contentH = lines.reduce((a, l) => a + l.h, 0);
|
|
951
|
+
const W = sizing === 0 ? contentW : p.width || contentW;
|
|
952
|
+
const H = sizing === 2 ? p.height || contentH : contentH;
|
|
953
|
+
return { runs, full, sizing, wrap, W, H };
|
|
954
|
+
}
|
|
955
|
+
function emitText(ctx, text, g) {
|
|
956
|
+
const s = ctx.s;
|
|
957
|
+
const p = text.props;
|
|
958
|
+
const lay = textLayout(ctx, text);
|
|
959
|
+
if (!lay)
|
|
960
|
+
return;
|
|
961
|
+
const { runs, full, sizing, wrap, W, H } = lay;
|
|
962
|
+
if (runs.some(r => !r.font))
|
|
963
|
+
ctx.warnings.add('Some text uses a font that is not embedded; Inter is used instead');
|
|
964
|
+
if ((s.children.get(text.index) || []).some(k => k.type === 'TextModifierGroup'))
|
|
965
|
+
ctx.warnings.add('Text modifiers (per-glyph animation) are not imported');
|
|
966
|
+
// Main style: the one covering most characters; other runs become segments.
|
|
967
|
+
const count = new Map();
|
|
968
|
+
runs.forEach(r => count.set(r.style, (count.get(r.style) || 0) + r.text.length));
|
|
969
|
+
const main = runs.reduce((a, b) => ((count.get(b.style) || 0) > (count.get(a.style) || 0) ? b : a));
|
|
970
|
+
const paints = (r) => (s.children.get(r.style.index) || []).filter(k => isA(k.type, 'ShapePaint') && k.props.isVisible !== false);
|
|
971
|
+
const box = { cx: 0, cy: 0, w: W || 1, h: H || 1 };
|
|
972
|
+
const fillOf = (r) => { const f = paints(r).find(k => k.type === 'Fill'); return f ? paintFill(f, s, null, 0, box) : { type: 'Solid', color: '#000000', opacity: 0 }; };
|
|
973
|
+
const strokePaint = paints(main).find(k => k.type === 'Stroke');
|
|
974
|
+
const align = ['left', 'right', 'center'][p.alignValue ?? 0] || 'left';
|
|
975
|
+
// Auto-width text gets headroom so the browser's own metrics don't wrap it; the box grows away from its aligned edge.
|
|
976
|
+
const slack = sizing === 0 || !wrap ? Math.ceil(main.size * 0.3) + 2 : 0;
|
|
977
|
+
const shift = align === 'center' ? 0 : align === 'right' ? -slack / 2 : slack / 2;
|
|
978
|
+
const lhPx = Math.max(...runs.map(runLineHeight));
|
|
979
|
+
const maxSize = Math.max(...runs.map(r => r.size));
|
|
980
|
+
const fam = main.family;
|
|
981
|
+
const a = main.font ? main.font.ascent : 0.97, d = main.font ? main.font.descent : 0.24;
|
|
982
|
+
const engineBaseline = (TEXT_LEADING[fam] ?? 0.5) * (lhPx - main.size) + (a / (a + d)) * main.size;
|
|
983
|
+
const drop = runAscent(main) - engineBaseline;
|
|
984
|
+
const geometry = {
|
|
985
|
+
type: 'Text', text: full, width: r3(W + slack), height: r3(H), isPointText: false,
|
|
986
|
+
fontSize: main.size, fontFamily: fam, fontWeight: main.weight, fontStyle: main.italic ? 'italic' : 'normal',
|
|
987
|
+
lineHeight: r3(lhPx / maxSize), letterSpacing: main.letterSpacing, textAlign: align,
|
|
988
|
+
};
|
|
989
|
+
if (sizing === 2) {
|
|
990
|
+
geometry.autoHeight = false;
|
|
991
|
+
geometry.textVerticalAlign = ['top', 'bottom', 'middle'][p.verticalAlignValue ?? 0] || 'top';
|
|
992
|
+
}
|
|
993
|
+
if (Math.abs(drop) >= 0.05)
|
|
994
|
+
geometry.baselineShift = r3(drop);
|
|
995
|
+
if (p.paragraphSpacing)
|
|
996
|
+
geometry.paragraphSpacing = p.paragraphSpacing;
|
|
997
|
+
const key = (r) => JSON.stringify([r.size, r.family, r.weight, r.italic, fillOf(r)]);
|
|
998
|
+
if (runs.some(r => key(r) !== key(main))) {
|
|
999
|
+
geometry.enableSegments = true;
|
|
1000
|
+
geometry.segments = runs.map(r => ({ text: r.text, fill: fillOf(r), fontSize: r.size, fontFamily: r.family, fontWeight: r.weight, fontStyle: r.italic ? 'italic' : 'normal' }));
|
|
1001
|
+
}
|
|
1002
|
+
let stroke = noStroke();
|
|
1003
|
+
if (strokePaint) {
|
|
1004
|
+
const sf = paintFill(strokePaint, s, null, 0, box);
|
|
1005
|
+
stroke = { ...noStroke(), isEnabled: true, color: sf.color || sf.stops?.[0]?.color || '#000000', opacity: sf.opacity ?? 1, width: r3(strokePaint.props.thickness ?? 1), lineJoin: ['miter', 'round', 'bevel'][strokePaint.props.join ?? 0] || 'miter', lineCap: ['butt', 'round', 'square'][strokePaint.props.cap ?? 0] || 'butt', alignment: 'center' };
|
|
1006
|
+
}
|
|
1007
|
+
const id = `${g.id}_text`;
|
|
1008
|
+
const o = {
|
|
1009
|
+
id, name: text.props.name || full.slice(0, 24), type: 'Text', parentId: g.id,
|
|
1010
|
+
transform: { ...baseTransform(), x: r3(W * (0.5 - (p.originX ?? 0)) + shift), y: r3(H * (0.5 - (p.originY ?? 0))) },
|
|
1011
|
+
geometry, style: { fill: fillOf(main), stroke }, isVisible: true,
|
|
1012
|
+
};
|
|
1013
|
+
g.children.push(id);
|
|
1014
|
+
ctx.objects.push(o);
|
|
1015
|
+
}
|
|
1016
|
+
// ---------------------------------------------------------------- nested artboards
|
|
1017
|
+
/** Wraps a frame of an animation's local time the way its loop mode does (globalToLocalSeconds). */
|
|
1018
|
+
const localFrame = (a, f) => {
|
|
1019
|
+
const len = a.end - a.start;
|
|
1020
|
+
if (len <= 0)
|
|
1021
|
+
return a.start;
|
|
1022
|
+
const loop = a.obj.props.loopValue ?? 0;
|
|
1023
|
+
let t = f - a.start;
|
|
1024
|
+
if (loop === 1)
|
|
1025
|
+
t = ((t % len) + len) % len;
|
|
1026
|
+
else if (loop === 2) {
|
|
1027
|
+
const p = ((t % (2 * len)) + 2 * len) % (2 * len);
|
|
1028
|
+
t = p > len ? 2 * len - p : p;
|
|
1029
|
+
}
|
|
1030
|
+
else
|
|
1031
|
+
t = Math.max(0, Math.min(len, t));
|
|
1032
|
+
return a.start + t;
|
|
1033
|
+
};
|
|
1034
|
+
/** Prefix every id in converted state machine layers / inputs so nested instances never collide. */
|
|
1035
|
+
function prefixMachine(m, pre, label) {
|
|
1036
|
+
const ids = new Map();
|
|
1037
|
+
const re = (id) => { if (!id)
|
|
1038
|
+
return id; if (!ids.has(id))
|
|
1039
|
+
ids.set(id, `${pre}${id}`); return ids.get(id); };
|
|
1040
|
+
for (const i of m.inputs) {
|
|
1041
|
+
i.id = re(i.id);
|
|
1042
|
+
i.name = `${label} · ${i.name}`;
|
|
1043
|
+
}
|
|
1044
|
+
for (const l of m.layers) {
|
|
1045
|
+
l.id = re(l.id);
|
|
1046
|
+
l.name = `${label} · ${l.name}`;
|
|
1047
|
+
l.entryStateId = re(l.entryStateId);
|
|
1048
|
+
for (const st of l.states) {
|
|
1049
|
+
st.id = re(st.id);
|
|
1050
|
+
for (const t of st.transitions || []) {
|
|
1051
|
+
t.id = re(t.id);
|
|
1052
|
+
t.targetStateId = re(t.targetStateId);
|
|
1053
|
+
for (const c of t.conditions || []) {
|
|
1054
|
+
c.inputId = re(c.inputId);
|
|
1055
|
+
c.inputName = `${label} · ${c.inputName}`;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return m;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* NestedArtboard: the referenced artboard drawn at the component's transform with
|
|
1064
|
+
* its origin at the component (frameOrigin off). Simple animations and state
|
|
1065
|
+
* machines become layers of their own; remap animations follow the parent timeline.
|
|
1066
|
+
*/
|
|
1067
|
+
function emitNested(ctx, o, g) {
|
|
1068
|
+
const target = ctx.file.artboards[o.props.artboardId ?? -1];
|
|
1069
|
+
if (!target) {
|
|
1070
|
+
ctx.warnings.add('A nested artboard references a missing artboard');
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (ctx.depth >= 5 || target === ctx.s.ab) {
|
|
1074
|
+
ctx.warnings.add('Nested artboards deeper than 5 levels are not imported');
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const sub = indexScene(target);
|
|
1078
|
+
const pre = `${g.id}_`;
|
|
1079
|
+
const label = o.props.name || target.artboard.props.name || 'Nested';
|
|
1080
|
+
const nestedAnims = (ctx.s.children.get(o.index) || []).filter(k => isA(k.type, 'NestedAnimation'));
|
|
1081
|
+
// Remaps: static ones hold a pose; keyed ones resample the nested timeline at each parent frame.
|
|
1082
|
+
const remapInto = new Map(); // synthetic sub anim -> parent anim
|
|
1083
|
+
const bake = (src, f) => { for (const [objId, props] of src.keyed) {
|
|
1084
|
+
const t = sub.byIndex[objId];
|
|
1085
|
+
if (t)
|
|
1086
|
+
for (const [name, keys] of props)
|
|
1087
|
+
if (keys.length)
|
|
1088
|
+
t.props[name] = keyedAt(keys, localFrame(src, f));
|
|
1089
|
+
} };
|
|
1090
|
+
for (const n of nestedAnims) {
|
|
1091
|
+
const src = sub.animations[n.props.animationId ?? -1];
|
|
1092
|
+
if (!src)
|
|
1093
|
+
continue;
|
|
1094
|
+
// A paused simple animation still applies its first frame.
|
|
1095
|
+
if (n.type === 'NestedSimpleAnimation' && !n.props.isPlaying && (n.props.mix ?? 1) > 0) {
|
|
1096
|
+
bake(src, src.start);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (n.type !== 'NestedRemapAnimation')
|
|
1100
|
+
continue;
|
|
1101
|
+
const keyedIn = ctx.s.animations.filter(a => a.keyed.get(n.index)?.get('time')?.length);
|
|
1102
|
+
if (!keyedIn.length) {
|
|
1103
|
+
bake(src, src.start + (n.props.time ?? 0) * (src.end - src.start));
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
for (const pa of keyedIn) {
|
|
1107
|
+
const timeKeys = pa.keyed.get(n.index).get('time');
|
|
1108
|
+
const keyed = new Map();
|
|
1109
|
+
for (const [objId, props] of src.keyed) {
|
|
1110
|
+
const m = new Map();
|
|
1111
|
+
for (const [name, keys] of props) {
|
|
1112
|
+
if (!keys.length)
|
|
1113
|
+
continue;
|
|
1114
|
+
const out = [];
|
|
1115
|
+
for (let f = pa.start; f <= pa.end; f++) {
|
|
1116
|
+
const lf = localFrame(src, src.start + keyedAt(timeKeys, f) * (src.end - src.start));
|
|
1117
|
+
out.push({ frame: f, value: keyedAt(keys, lf), type: typeof keys[0].value === 'number' ? 1 : 0, interpolator: null, color: keys[0].color });
|
|
1118
|
+
}
|
|
1119
|
+
m.set(name, out);
|
|
1120
|
+
}
|
|
1121
|
+
keyed.set(objId, m);
|
|
1122
|
+
}
|
|
1123
|
+
const syn = { obj: src.obj, keyed, fps: pa.fps, start: pa.start, end: pa.end, synthetic: true };
|
|
1124
|
+
sub.animations.push(syn);
|
|
1125
|
+
remapInto.set(syn, pa);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const sctx = { ...ctx, s: sub, pre, depth: ctx.depth + 1, tracksByAnim: new Map(sub.animations.map(a => [a, []])), rivToExode: new Map() };
|
|
1129
|
+
sub.bounds = boundsHook(sctx);
|
|
1130
|
+
setupLayouts(sctx);
|
|
1131
|
+
let host = g;
|
|
1132
|
+
if (o.type === 'NestedArtboardLeaf') {
|
|
1133
|
+
// Leaf: fit the artboard into its layout parent's box (computeAlignment in renderer.cpp).
|
|
1134
|
+
const par = ctx.s.byIndex[o.props.parentId ?? 0];
|
|
1135
|
+
const T = target.artboard.props, tw = T.width || 1, th = T.height || 1;
|
|
1136
|
+
const content = { x: -(T.originX || 0) * tw, y: -(T.originY || 0) * th, w: tw, h: th };
|
|
1137
|
+
let frame = content;
|
|
1138
|
+
if (par && isA(par.type, 'LayoutComponent')) {
|
|
1139
|
+
const pw = par.props.width || 0, ph = par.props.height || 0;
|
|
1140
|
+
frame = par.index === 0 ? { x: -(par.props.originX || 0) * pw, y: -(par.props.originY || 0) * ph, w: pw, h: ph } : { x: 0, y: 0, w: pw, h: ph };
|
|
1141
|
+
}
|
|
1142
|
+
const fit = o.props.fit ?? 0, ax = o.props.alignmentX ?? 0, ay = o.props.alignmentY ?? 0;
|
|
1143
|
+
let sx = 1, sy = 1;
|
|
1144
|
+
const fx = frame.w / content.w, fy = frame.h / content.h;
|
|
1145
|
+
if (fit === 0) {
|
|
1146
|
+
sx = fx;
|
|
1147
|
+
sy = fy;
|
|
1148
|
+
}
|
|
1149
|
+
else if (fit === 1)
|
|
1150
|
+
sx = sy = Math.min(fx, fy);
|
|
1151
|
+
else if (fit === 2)
|
|
1152
|
+
sx = sy = Math.max(fx, fy);
|
|
1153
|
+
else if (fit === 3)
|
|
1154
|
+
sx = sy = fx;
|
|
1155
|
+
else if (fit === 4)
|
|
1156
|
+
sx = sy = fy;
|
|
1157
|
+
else if (fit === 6)
|
|
1158
|
+
sx = sy = Math.min(1, Math.min(fx, fy));
|
|
1159
|
+
else if (fit === 7)
|
|
1160
|
+
ctx.warnings.add('Layout-fit nested artboards are imported at their authored size');
|
|
1161
|
+
const x = -content.x - content.w / 2 - (ax * content.w) / 2, y = -content.y - content.h / 2 - (ay * content.h) / 2;
|
|
1162
|
+
const tx = frame.x + frame.w / 2 + (ax * frame.w) / 2, ty = frame.y + frame.h / 2 + (ay * frame.h) / 2;
|
|
1163
|
+
host = group(sctx, `${pre}fit`, 'Fit', g);
|
|
1164
|
+
host.transform = { ...host.transform, x: r3(tx + sx * x), y: r3(ty + sy * y), scale_x: r3(sx), scale_y: r3(sy) };
|
|
1165
|
+
}
|
|
1166
|
+
const inner = group(sctx, `${pre}artboard`, target.artboard.props.name || 'Artboard', host);
|
|
1167
|
+
if (target.artboard.props.clip) {
|
|
1168
|
+
const A = target.artboard.props, W = A.width || 0, H = A.height || 0;
|
|
1169
|
+
inner.clipContent = true;
|
|
1170
|
+
inner.width = W;
|
|
1171
|
+
inner.height = H;
|
|
1172
|
+
inner.geometry = { type: 'Rectangle', width: W, height: H, corner_radius: 0 };
|
|
1173
|
+
// The clip box is centred on the group; move the group there and the content back.
|
|
1174
|
+
inner.transform.x = r3(W / 2 - (A.originX || 0) * W);
|
|
1175
|
+
inner.transform.y = r3(H / 2 - (A.originY || 0) * H);
|
|
1176
|
+
const content = group(sctx, `${pre}content`, 'Content', inner);
|
|
1177
|
+
content.transform.x = -inner.transform.x;
|
|
1178
|
+
content.transform.y = -inner.transform.y;
|
|
1179
|
+
emitArtboardContent(sctx, content, false);
|
|
1180
|
+
}
|
|
1181
|
+
else
|
|
1182
|
+
emitArtboardContent(sctx, inner, false);
|
|
1183
|
+
for (const [syn, pa] of remapInto)
|
|
1184
|
+
ctx.tracksByAnim.get(pa)?.push(...(sctx.tracksByAnim.get(syn) || []));
|
|
1185
|
+
const playing = nestedAnims.filter(k => (k.type === 'NestedSimpleAnimation' && k.props.isPlaying) || k.type === 'NestedStateMachine');
|
|
1186
|
+
if (!playing.length || o.props.isPaused)
|
|
1187
|
+
return;
|
|
1188
|
+
const { animations, ids } = buildAnimations(sctx);
|
|
1189
|
+
const speed = o.props.speed ?? 1;
|
|
1190
|
+
if (speed !== 1 && speed > 0)
|
|
1191
|
+
for (const a of animations) {
|
|
1192
|
+
a.tracks.forEach((t) => t.keyframes.forEach((k) => (k.time = r3(k.time / speed))));
|
|
1193
|
+
a.duration = r3(a.duration / speed);
|
|
1194
|
+
}
|
|
1195
|
+
const used = new Set();
|
|
1196
|
+
playing.forEach((n, k) => {
|
|
1197
|
+
if (n.type === 'NestedSimpleAnimation') {
|
|
1198
|
+
const id = ids[n.props.animationId ?? -1];
|
|
1199
|
+
const a = animations.find(x => x.id === id);
|
|
1200
|
+
if (!a)
|
|
1201
|
+
return;
|
|
1202
|
+
const sp = n.props.speed ?? 1;
|
|
1203
|
+
if (sp !== 1 && sp > 0) {
|
|
1204
|
+
a.tracks.forEach((t) => t.keyframes.forEach((q) => (q.time = r3(q.time / sp))));
|
|
1205
|
+
a.duration = r3(a.duration / sp);
|
|
1206
|
+
}
|
|
1207
|
+
used.add(id);
|
|
1208
|
+
const lid = `${pre}layer_${k}`;
|
|
1209
|
+
ctx.out.layers.push({ id: lid, name: `${label} · ${a.name}`, entryStateId: `${lid}_play`, states: [{ id: `${lid}_play`, name: a.name, animationId: id, loop: a.loop, transitions: [] }] });
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
const sm = sub.machines[n.props.animationId ?? -1];
|
|
1213
|
+
if (!sm)
|
|
1214
|
+
return;
|
|
1215
|
+
const m = prefixMachine(stateMachine(sctx, sm, ids), `${pre}${k}_`, label);
|
|
1216
|
+
m.layers.forEach((l) => l.states.forEach((st) => st.animationId && used.add(st.animationId)));
|
|
1217
|
+
ctx.out.layers.push(...m.layers);
|
|
1218
|
+
ctx.out.inputs.push(...m.inputs);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
ctx.out.animations.push(...animations.filter(a => used.has(a.id)));
|
|
1222
|
+
}
|
|
1223
|
+
// Rive feather strength -> runtime blur px (calibrated against the Rive renderer).
|
|
1224
|
+
const RIVE_FEATHER = 0.5;
|
|
1225
|
+
const BLEND = { 3: 'source-over', 14: 'screen', 15: 'overlay', 16: 'darken', 17: 'lighten', 18: 'color-dodge', 19: 'color-burn', 20: 'hard-light', 21: 'soft-light', 22: 'difference', 23: 'exclusion', 24: 'multiply', 25: 'hue', 26: 'saturation', 27: 'color', 28: 'luminosity' };
|
|
1226
|
+
function emitShape(ctx, shape, host) {
|
|
1227
|
+
const s = ctx.s;
|
|
1228
|
+
const kids = s.children.get(shape.index) || [];
|
|
1229
|
+
const paints = kids.filter(k => isA(k.type, 'ShapePaint') && k.props.isVisible !== false);
|
|
1230
|
+
const anims = s.animations;
|
|
1231
|
+
const pathMoves = (a, k) => a.keyed.has(k.index) || (s.children.get(k.index) || []).some(v => a.keyed.has(v.index)) || skinMoves(s, a, k);
|
|
1232
|
+
const geoAnimated = anims.some(a => kids.some(k => isA(k.type, 'Path') && pathMoves(a, k)));
|
|
1233
|
+
for (const paint of paints) {
|
|
1234
|
+
const isStroke = paint.type === 'Stroke';
|
|
1235
|
+
const eo = !isStroke && paint.props.fillRule === 1;
|
|
1236
|
+
const contours = shapeContours(s, shape, null, 0, eo);
|
|
1237
|
+
const pts = contours.flat();
|
|
1238
|
+
if (!pts.length)
|
|
1239
|
+
continue;
|
|
1240
|
+
const b = bbox(pts);
|
|
1241
|
+
const id = `${ctx.pre}${shape.index}_p${paint.index}`;
|
|
1242
|
+
// Width/height = point box, so the runtime draws points as authored.
|
|
1243
|
+
const geometry = { type: 'Path', points: pts, is_closed: pts.some(p => p.closePath), width: dim(b.w), height: dim(b.h), baseWidth: dim(b.w), baseHeight: dim(b.h), fillRule: 'nonzero' };
|
|
1244
|
+
if (!geoAnimated)
|
|
1245
|
+
geometry.data = ptsToData(pts);
|
|
1246
|
+
const box = { cx: 0, cy: 0, w: b.w, h: b.h };
|
|
1247
|
+
const fill = paintFill(paint, s, null, 0, box);
|
|
1248
|
+
const o = { id, name: `${shape.props.name || 'Shape'} · ${paint.props.name || (isStroke ? 'Stroke' : 'Fill')}`, type: 'Shape', parentId: host.id, transform: baseTransform(), geometry, isVisible: true, isLocked: false };
|
|
1249
|
+
if (!isStroke)
|
|
1250
|
+
o.style = { fill, stroke: noStroke() };
|
|
1251
|
+
else {
|
|
1252
|
+
const scale = paint.props.transformAffectsStroke === false ? 1 : worldScale(s, shape, null, 0);
|
|
1253
|
+
const stroke = {
|
|
1254
|
+
width: r3((paint.props.thickness ?? 1) * scale), opacity: fill.type === 'Solid' ? fill.opacity : 1, alignment: 'center',
|
|
1255
|
+
lineCap: paint.props.cap === 1 ? 'round' : paint.props.cap === 2 ? 'square' : 'butt',
|
|
1256
|
+
lineJoin: paint.props.join === 1 ? 'round' : paint.props.join === 2 ? 'bevel' : 'miter',
|
|
1257
|
+
isEnabled: true, color: fill.type === 'Solid' ? fill.color : (fill.stops?.[0]?.color || '#000000'),
|
|
1258
|
+
...(fill.type !== 'Solid' ? { fill } : {}),
|
|
1259
|
+
};
|
|
1260
|
+
const trim = (s.children.get(paint.index) || []).find(k => k.type === 'TrimPath');
|
|
1261
|
+
if (trim) {
|
|
1262
|
+
const t = trim.props;
|
|
1263
|
+
Object.assign(stroke, { trimStart: r3(Math.min(t.start ?? 0, t.end ?? 1)), trimEnd: r3(Math.max(t.start ?? 0, t.end ?? 1)), trimOffset: r3((((t.offset ?? 0) % 1) + 1) % 1) });
|
|
1264
|
+
if ((t.modeValue ?? 1) === 2)
|
|
1265
|
+
ctx.warnings.add('Sequential trim paths are imported as synchronized');
|
|
1266
|
+
}
|
|
1267
|
+
if ((s.children.get(paint.index) || []).some(k => k.type === 'DashPath' || k.type === 'Dash'))
|
|
1268
|
+
ctx.warnings.add('Dashed strokes are not imported');
|
|
1269
|
+
o.style = { fill: { type: 'Solid', color: '#000000', opacity: 0 }, stroke };
|
|
1270
|
+
}
|
|
1271
|
+
const blend = BLEND[shape.props.blendModeValue ?? 3];
|
|
1272
|
+
if (blend && blend !== 'source-over')
|
|
1273
|
+
o.style.compositeOperation = blend;
|
|
1274
|
+
const feather = (s.children.get(paint.index) || []).find(k => k.type === 'Feather');
|
|
1275
|
+
if (feather) {
|
|
1276
|
+
const fp = feather.props;
|
|
1277
|
+
const blur = (fp.strength ?? 12) * RIVE_FEATHER;
|
|
1278
|
+
if (fp.inner) {
|
|
1279
|
+
// Inner feather: the paint shows only as a soft band inside the edge.
|
|
1280
|
+
const c = o.style.fill.type === 'Solid' ? o.style.fill : { color: '#000000', opacity: 1 };
|
|
1281
|
+
o.style.effects = [{ type: 'InnerShadow', isEnabled: true, color: c.color, opacity: c.opacity, blur: r3(blur), offsetX: r3(fp.offsetX || 0), offsetY: r3(fp.offsetY || 0), spread: 0 }];
|
|
1282
|
+
o.style.fill = { type: 'Solid', color: '#000000', opacity: 0 };
|
|
1283
|
+
if (!(blur > 0))
|
|
1284
|
+
o.isVisible = false;
|
|
1285
|
+
}
|
|
1286
|
+
else if (blur > 0)
|
|
1287
|
+
o.style.blur = { amount: r3(blur) };
|
|
1288
|
+
}
|
|
1289
|
+
host.children.push(id);
|
|
1290
|
+
ctx.objects.push(o);
|
|
1291
|
+
ctx.rivToExode.set(paint.index, id);
|
|
1292
|
+
// Animation: geometry, paint colour, stroke width/trim.
|
|
1293
|
+
for (const a of anims) {
|
|
1294
|
+
const tracks = ctx.tracksByAnim.get(a);
|
|
1295
|
+
const frames = [];
|
|
1296
|
+
for (let f = a.start; f <= a.end + 1e-6; f++)
|
|
1297
|
+
frames.push(Math.min(f, a.end));
|
|
1298
|
+
const sec = (f) => r3((f - a.start) / a.fps);
|
|
1299
|
+
const sample = (prop, fn, tol = 0.01) => {
|
|
1300
|
+
const keys = frames.map(f => ({ time: sec(f), value: fn(f), easing: 'linear' }));
|
|
1301
|
+
if (keys.every(k => k.value === keys[0].value))
|
|
1302
|
+
return;
|
|
1303
|
+
const kept = keys.filter((k, i) => i === 0 || i === keys.length - 1 || typeof k.value !== 'number' || Math.abs((keys[i - 1].value + keys[i + 1].value) / 2 - k.value) > tol || k.value !== keys[i - 1].value);
|
|
1304
|
+
tracks.push({ id: `t_${id}_${prop.replace(/\W+/g, '_')}`, object_id: id, property: prop, keyframes: kept.map((k, i) => ({ id: `k_${id}_${prop.replace(/\W+/g, '_')}_${i}`, ...k, value: typeof k.value === 'number' ? r3(k.value) : k.value })) });
|
|
1305
|
+
};
|
|
1306
|
+
if (geoAnimated && kids.some(k => isA(k.type, 'Path') && pathMoves(a, k))) {
|
|
1307
|
+
const per = frames.map(f => shapeContours(s, shape, a, f, eo).flat());
|
|
1308
|
+
if (per.every(p => p.length === pts.length)) {
|
|
1309
|
+
['x', 'y', 'in_x', 'in_y', 'out_x', 'out_y'].forEach(fd => pts.forEach((_, j) => sample(`geometry.points.${j}.${fd}`, f => per[frames.indexOf(f)][j][fd], 0.05)));
|
|
1310
|
+
}
|
|
1311
|
+
else
|
|
1312
|
+
ctx.warnings.add('A path changes its vertex count while animating; it is imported at its first frame');
|
|
1313
|
+
}
|
|
1314
|
+
const mutator = (s.children.get(paint.index) || []).find(k => k.type === 'SolidColor' || k.type === 'LinearGradient' || k.type === 'RadialGradient');
|
|
1315
|
+
if (mutator && mutator.type === 'SolidColor' && a.keyed.get(mutator.index)?.has('colorValue')) {
|
|
1316
|
+
const base = isStroke ? 'style.stroke' : 'style.fill';
|
|
1317
|
+
sample(`${base}.color`, f => argb(propsAt(mutator, a, f).colorValue)[0]);
|
|
1318
|
+
sample(`${base}.opacity`, f => argb(propsAt(mutator, a, f).colorValue)[1], 0.002);
|
|
1319
|
+
}
|
|
1320
|
+
else if (mutator && mutator.type !== 'SolidColor' && (a.keyed.has(mutator.index) || (s.children.get(mutator.index) || []).some(k => a.keyed.has(k.index)))) {
|
|
1321
|
+
const base = isStroke ? 'style.stroke.fill' : 'style.fill';
|
|
1322
|
+
const first = paintFill(paint, s, a, a.start, box);
|
|
1323
|
+
first.stops.forEach((_, j) => {
|
|
1324
|
+
sample(`${base}.stops.${j}.color`, f => paintFill(paint, s, a, f, box).stops[j]?.color);
|
|
1325
|
+
sample(`${base}.stops.${j}.opacity`, f => paintFill(paint, s, a, f, box).stops[j]?.opacity, 0.002);
|
|
1326
|
+
sample(`${base}.stops.${j}.offset`, f => paintFill(paint, s, a, f, box).stops[j]?.offset, 0.002);
|
|
1327
|
+
});
|
|
1328
|
+
(first.type === 'RadialGradient' ? [['center', 0], ['center', 1]] : [['start', 0], ['start', 1], ['end', 0], ['end', 1]]).forEach(([k, d]) => sample(`${base}.${k}.${d}`, f => paintFill(paint, s, a, f, box)[k][d], 0.002));
|
|
1329
|
+
}
|
|
1330
|
+
if (isStroke) {
|
|
1331
|
+
const scaleMoves = paint.props.transformAffectsStroke !== false && ancestorsAnimated(s, shape, ['scaleX', 'scaleY']);
|
|
1332
|
+
if (a.keyed.get(paint.index)?.has('thickness') || scaleMoves)
|
|
1333
|
+
sample('style.stroke.width', f => (propsAt(paint, a, f).thickness ?? 1) * (paint.props.transformAffectsStroke === false ? 1 : worldScale(s, shape, a, f)), 0.02);
|
|
1334
|
+
const trim = (s.children.get(paint.index) || []).find(k => k.type === 'TrimPath');
|
|
1335
|
+
if (trim && a.keyed.has(trim.index)) {
|
|
1336
|
+
sample('style.stroke.trimStart', f => { const t = propsAt(trim, a, f); return Math.min(t.start ?? 0, t.end ?? 1); }, 0.002);
|
|
1337
|
+
sample('style.stroke.trimEnd', f => { const t = propsAt(trim, a, f); return Math.max(t.start ?? 0, t.end ?? 1); }, 0.002);
|
|
1338
|
+
sample('style.stroke.trimOffset', f => { const t = propsAt(trim, a, f); return (((t.offset ?? 0) % 1) + 1) % 1; }, 0.002);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (a.keyed.get(paint.index)?.has('isVisible'))
|
|
1342
|
+
sample('visible', f => propsAt(paint, a, f).isVisible !== false);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/** Local transform of a constrained component (its world under its parent's world), decomposed. */
|
|
1347
|
+
function localPose(s, o, anim, frame) {
|
|
1348
|
+
const parent = s.byIndex[o.props.parentId ?? 0];
|
|
1349
|
+
const pw = parent && parent.index !== 0 && isA(parent.type, 'TransformComponent') ? worldOf(s, parent, anim, frame) : [1, 0, 0, 1, 0, 0];
|
|
1350
|
+
return decompose(mul(invM(pw), worldOf(s, o, anim, frame)));
|
|
1351
|
+
}
|
|
1352
|
+
/** Constraint-driven transforms, sampled per frame wherever the solve moves them. */
|
|
1353
|
+
function constraintTracks(ctx, o, id) {
|
|
1354
|
+
const s = ctx.s;
|
|
1355
|
+
for (const a of s.animations) {
|
|
1356
|
+
if (!a.keyed.size)
|
|
1357
|
+
continue;
|
|
1358
|
+
const poses = [];
|
|
1359
|
+
for (let f = a.start; f <= a.end; f++)
|
|
1360
|
+
poses.push(localPose(s, o, a, f));
|
|
1361
|
+
const chans = [['x', t => t.x], ['y', t => t.y], ['rotation', t => t.rotation * DEG], ['scale_x', t => t.scaleX], ['scale_y', t => t.scaleY]];
|
|
1362
|
+
for (const [prop, get] of chans) {
|
|
1363
|
+
const vals = poses.map(get);
|
|
1364
|
+
// Unwrap so a tween never spins the long way round.
|
|
1365
|
+
if (prop === 'rotation')
|
|
1366
|
+
for (let i = 1; i < vals.length; i++)
|
|
1367
|
+
while (Math.abs(vals[i] - vals[i - 1]) > 180)
|
|
1368
|
+
vals[i] -= Math.sign(vals[i] - vals[i - 1]) * 360;
|
|
1369
|
+
if (Math.max(...vals) - Math.min(...vals) < (prop.startsWith('scale') ? 0.001 : 0.05))
|
|
1370
|
+
continue;
|
|
1371
|
+
const kf = vals.map((v, i) => ({ id: `k_${id}_c${prop}_${i}`, time: r3(i / a.fps), value: r3(v), easing: 'linear' }));
|
|
1372
|
+
ctx.tracksByAnim.get(a).push({ id: `t_${id}_${prop}`, object_id: id, property: prop, keyframes: kf });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/** Native keyframes for an affine-mapped transform property; Rive easing lives on the starting key. */
|
|
1377
|
+
function transformTracks(ctx, o, id) {
|
|
1378
|
+
if (constrained(ctx.s).has(o.index))
|
|
1379
|
+
constraintTracks(ctx, o, id);
|
|
1380
|
+
for (const a of ctx.s.animations) {
|
|
1381
|
+
const m = a.keyed.get(o.index);
|
|
1382
|
+
if (!m)
|
|
1383
|
+
continue;
|
|
1384
|
+
const tracks = ctx.tracksByAnim.get(a);
|
|
1385
|
+
const map = [['x', 'x', v => v], ['y', 'y', v => v], ['rotation', 'rotation', v => v * DEG], ['scaleX', 'scale_x', v => v], ['scaleY', 'scale_y', v => v], ['opacity', 'opacity', v => Math.max(0, Math.min(1, v))]];
|
|
1386
|
+
for (const [rive, ex, fn] of map) {
|
|
1387
|
+
if (rive !== 'opacity' && constrained(ctx.s).has(o.index))
|
|
1388
|
+
continue;
|
|
1389
|
+
const keys = m.get(rive);
|
|
1390
|
+
if (!keys?.length)
|
|
1391
|
+
continue;
|
|
1392
|
+
const easingOf = (k) => {
|
|
1393
|
+
if (k.type === 0)
|
|
1394
|
+
return 'StepEnd';
|
|
1395
|
+
const it = k.interpolator;
|
|
1396
|
+
if (k.type === 2 && it && (it.type === 'CubicEaseInterpolator' || it.type === 'CubicValueInterpolator'))
|
|
1397
|
+
return `cubic-bezier(${r3(it.props.x1 ?? 0.42)},${r3(it.props.y1 ?? 0)},${r3(it.props.x2 ?? 0.58)},${r3(it.props.y2 ?? 1)})`;
|
|
1398
|
+
if (it && it.type === 'ElasticInterpolator')
|
|
1399
|
+
return null;
|
|
1400
|
+
return 'linear';
|
|
1401
|
+
};
|
|
1402
|
+
const sec = (f) => r3((f - a.start) / a.fps);
|
|
1403
|
+
let out;
|
|
1404
|
+
if (keys.every(k => easingOf(k) !== null)) {
|
|
1405
|
+
out = keys.map((k, i) => ({ time: sec(k.frame), value: r3(fn(k.value)), easing: i < keys.length - 1 ? easingOf(k) : 'linear' }));
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
out = [];
|
|
1409
|
+
for (let f = a.start; f <= a.end; f++)
|
|
1410
|
+
out.push({ time: sec(f), value: r3(fn(keyedAt(keys, f))), easing: 'linear' });
|
|
1411
|
+
}
|
|
1412
|
+
tracks.push({ id: `t_${id}_${ex}`, object_id: id, property: ex, keyframes: out.map((k, i) => ({ id: `k_${id}_${ex}_${i}`, ...k })) });
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function emitComponent(ctx, o, parent) {
|
|
1417
|
+
const s = ctx.s;
|
|
1418
|
+
if (!isA(o.type, 'TransformComponent') && o.type !== 'Artboard')
|
|
1419
|
+
return;
|
|
1420
|
+
if (o.type === 'Artboard')
|
|
1421
|
+
return;
|
|
1422
|
+
const p = localProps(s, o, null, 0);
|
|
1423
|
+
const id = `${ctx.pre}${o.index}`;
|
|
1424
|
+
const g = group(ctx, id, o.props.name || o.type, parent);
|
|
1425
|
+
g.transform = { ...baseTransform(), x: r3(p.x || 0), y: r3(p.y || 0), rotation: r3((p.rotation || 0) * DEG), scale_x: r3(p.scaleX ?? 1), scale_y: r3(p.scaleY ?? 1), opacity: r3(Math.max(0, Math.min(1, p.opacity ?? 1))) };
|
|
1426
|
+
const box = s.layout?.get(o.index);
|
|
1427
|
+
if (box && isLayoutParticipant(o)) {
|
|
1428
|
+
// Layouts sit where the solve puts them (the artboard's own origin shifts its direct children).
|
|
1429
|
+
const par = s.byIndex[o.props.parentId ?? 0];
|
|
1430
|
+
const A = s.ab.artboard.props;
|
|
1431
|
+
let x = box.x, y = box.y;
|
|
1432
|
+
if (par && par.index === 0) {
|
|
1433
|
+
const [aw, ah] = artboardSize(s);
|
|
1434
|
+
x -= (A.originX || 0) * aw;
|
|
1435
|
+
y -= (A.originY || 0) * ah;
|
|
1436
|
+
}
|
|
1437
|
+
if (o.type === 'Text') {
|
|
1438
|
+
x += (o.props.originX ?? 0) * box.w;
|
|
1439
|
+
y += (o.props.originY ?? 0) * box.h;
|
|
1440
|
+
}
|
|
1441
|
+
if (o.type === 'NestedArtboardLayout') {
|
|
1442
|
+
const t = ctx.file.artboards[o.props.artboardId ?? -1]?.artboard.props;
|
|
1443
|
+
if (t) {
|
|
1444
|
+
x += (t.originX || 0) * (t.width || 0);
|
|
1445
|
+
y += (t.originY || 0) * (t.height || 0);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
g.transform = { ...g.transform, x: r3(x), y: r3(y) };
|
|
1449
|
+
if (o.type === 'LayoutComponent')
|
|
1450
|
+
emitLayoutPaints(ctx, o, g, box);
|
|
1451
|
+
}
|
|
1452
|
+
if (constrained(s).has(o.index)) {
|
|
1453
|
+
const t = localPose(s, o, null, 0);
|
|
1454
|
+
g.transform = { ...g.transform, x: r3(t.x), y: r3(t.y), rotation: r3(t.rotation * DEG), scale_x: r3(t.scaleX), scale_y: r3(t.scaleY) };
|
|
1455
|
+
}
|
|
1456
|
+
if (isA(o.type, 'Drawable') && ((p.drawableFlags ?? 0) & 1))
|
|
1457
|
+
g.isVisible = false;
|
|
1458
|
+
ctx.rivToExode.set(o.index, id);
|
|
1459
|
+
transformTracks(ctx, o, id);
|
|
1460
|
+
if (o.type === 'Shape')
|
|
1461
|
+
emitShape(ctx, o, g);
|
|
1462
|
+
else if (o.type === 'Image')
|
|
1463
|
+
emitImage(ctx, o, g);
|
|
1464
|
+
else if (o.type === 'Text')
|
|
1465
|
+
emitText(ctx, o, g);
|
|
1466
|
+
else if (isA(o.type, 'NestedArtboard'))
|
|
1467
|
+
emitNested(ctx, o, g);
|
|
1468
|
+
else if (DRAW_TYPES.has(o.type))
|
|
1469
|
+
ctx.warnings.add(`${o.type} layers are not imported yet`);
|
|
1470
|
+
if (o.type === 'Bone' || o.type === 'RootBone') { /* transform-only */ }
|
|
1471
|
+
// Rive draws later siblings first: reverse children for Exode's bottom-to-top order.
|
|
1472
|
+
const kids = (s.children.get(o.index) || []).filter(k => isA(k.type, 'TransformComponent') && !isA(k.type, 'Path'));
|
|
1473
|
+
for (const k of [...kids].reverse())
|
|
1474
|
+
emitComponent(ctx, k, g);
|
|
1475
|
+
if (o.type === 'Solo')
|
|
1476
|
+
soloVisibility(ctx, o, kids);
|
|
1477
|
+
emitClips(ctx, o, g);
|
|
1478
|
+
}
|
|
1479
|
+
/** A layout's own fills and strokes draw its box (rounded by the style's corner radii). */
|
|
1480
|
+
function emitLayoutPaints(ctx, o, g, box) {
|
|
1481
|
+
const s = ctx.s;
|
|
1482
|
+
const st = (o.props.styleId !== undefined ? s.byIndex[o.props.styleId] : null)?.props || {};
|
|
1483
|
+
const paints = (s.children.get(o.index) || []).filter(k => isA(k.type, 'ShapePaint') && k.props.isVisible !== false);
|
|
1484
|
+
for (const pnt of paints) {
|
|
1485
|
+
const fill = paintFill(pnt, s, null, 0, { cx: box.w / 2, cy: box.h / 2, w: box.w, h: box.h });
|
|
1486
|
+
const isStroke = pnt.type === 'Stroke';
|
|
1487
|
+
const o2 = {
|
|
1488
|
+
id: `${ctx.pre}${o.index}_bg${pnt.index}`, name: isStroke ? 'Layout stroke' : 'Layout fill', type: 'Shape', parentId: g.id,
|
|
1489
|
+
transform: { ...baseTransform(), x: r3(box.w / 2), y: r3(box.h / 2) },
|
|
1490
|
+
geometry: { type: 'Rectangle', width: r3(box.w), height: r3(box.h), corner_radius: r3(st.cornerRadiusTL || 0) },
|
|
1491
|
+
style: isStroke
|
|
1492
|
+
? { fill: { type: 'Solid', color: '#000000', opacity: 0 }, stroke: { ...noStroke(), isEnabled: true, color: fill.color || '#000000', opacity: fill.opacity ?? 1, width: r3(pnt.props.thickness ?? 1), alignment: 'center' } }
|
|
1493
|
+
: { fill, stroke: noStroke() },
|
|
1494
|
+
isVisible: true,
|
|
1495
|
+
};
|
|
1496
|
+
g.children.unshift(o2.id);
|
|
1497
|
+
ctx.objects.push(o2);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/** Solo shows only its active child; activeComponentId may be keyed (held values). */
|
|
1501
|
+
function soloVisibility(ctx, solo, kids) {
|
|
1502
|
+
const byId = new Map(ctx.objects.map(x => [x.id, x]));
|
|
1503
|
+
for (const k of kids) {
|
|
1504
|
+
const obj = byId.get(`${ctx.pre}${k.index}`);
|
|
1505
|
+
if (!obj)
|
|
1506
|
+
continue;
|
|
1507
|
+
if (k.index !== solo.props.activeComponentId)
|
|
1508
|
+
obj.isVisible = false;
|
|
1509
|
+
for (const a of ctx.s.animations) {
|
|
1510
|
+
const keys = a.keyed.get(solo.index)?.get('activeComponentId');
|
|
1511
|
+
if (!keys?.length)
|
|
1512
|
+
continue;
|
|
1513
|
+
const sec = (f) => r3((f - a.start) / a.fps);
|
|
1514
|
+
const kf = [{ time: 0, value: keyedAt(keys, a.start) === k.index }, ...keys.filter(q => q.frame > a.start).map(q => ({ time: sec(q.frame), value: q.value === k.index }))];
|
|
1515
|
+
ctx.tracksByAnim.get(a).push({ id: `t_${obj.id}_solo`, object_id: obj.id, property: 'visible', keyframes: kf.map((q, i) => ({ id: `k_${obj.id}_solo_${i}`, ...q, easing: 'StepEnd' })) });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// ---------------------------------------------------------------- state machines
|
|
1520
|
+
function stateMachine(ctx, objs, animIds) {
|
|
1521
|
+
const sm = objs[0];
|
|
1522
|
+
const inputs = [];
|
|
1523
|
+
const layers = [];
|
|
1524
|
+
const inputIds = [];
|
|
1525
|
+
let layer = null, state = null, transition = null;
|
|
1526
|
+
let stateIndex = 0;
|
|
1527
|
+
const states = [];
|
|
1528
|
+
const pendingTargets = [];
|
|
1529
|
+
for (const o of objs.slice(1)) {
|
|
1530
|
+
const p = o.props;
|
|
1531
|
+
if (o.type === 'StateMachineNumber' || o.type === 'StateMachineBool' || o.type === 'StateMachineTrigger') {
|
|
1532
|
+
const id = `in_${sm.index}_${inputs.length}_${String(p.name || 'input').replace(/\W+/g, '_')}`;
|
|
1533
|
+
const type = o.type === 'StateMachineNumber' ? 'Number' : o.type === 'StateMachineBool' ? 'Boolean' : 'Trigger';
|
|
1534
|
+
const value = type === 'Number' ? p.value ?? 0 : type === 'Boolean' ? !!p.value : false;
|
|
1535
|
+
inputs.push({ id, name: p.name || id, value: { type, value, defaultValue: value } });
|
|
1536
|
+
inputIds.push(id);
|
|
1537
|
+
}
|
|
1538
|
+
else if (o.type === 'StateMachineLayer') {
|
|
1539
|
+
layer = { id: `layer_${layers.length}_${String(p.name || 'Layer').replace(/\W+/g, '_')}`, name: p.name || `Layer ${layers.length + 1}`, entryStateId: '', states: [] };
|
|
1540
|
+
layers.push(layer);
|
|
1541
|
+
stateIndex = 0;
|
|
1542
|
+
states.length = 0;
|
|
1543
|
+
}
|
|
1544
|
+
else if (layer && isA(o.type, 'LayerState')) {
|
|
1545
|
+
state = { id: `${layer.id}_s${stateIndex++}`, name: o.type === 'AnimationState' ? `State ${stateIndex}` : o.type.replace('State', ''), kind: o.type, loop: true, transitions: [] };
|
|
1546
|
+
if (o.type === 'AnimationState' && animIds[p.animationId] !== undefined) {
|
|
1547
|
+
state.animationId = animIds[p.animationId];
|
|
1548
|
+
state.name = ctx.s.animations[p.animationId]?.obj.props.name || state.name;
|
|
1549
|
+
state.loop = (ctx.s.animations[p.animationId]?.obj.props.loopValue ?? 0) !== 0;
|
|
1550
|
+
}
|
|
1551
|
+
if (isA(o.type, 'BlendState'))
|
|
1552
|
+
ctx.warnings.add('Blend states are imported as plain states');
|
|
1553
|
+
states.push(state);
|
|
1554
|
+
layer.states.push(state);
|
|
1555
|
+
}
|
|
1556
|
+
else if (state && o.type === 'StateTransition') {
|
|
1557
|
+
transition = { id: `${state.id}_t${state.transitions.length}`, targetStateId: '', duration: 0, conditions: [] };
|
|
1558
|
+
pendingTargets.push({ t: transition, to: p.stateToId, layer });
|
|
1559
|
+
// Exit time: hold the transition until the state has played that long.
|
|
1560
|
+
const flags = p.flags ?? 0;
|
|
1561
|
+
if (flags & 4 /* enableExitTime */) {
|
|
1562
|
+
const anim = state.animationId ? ctx.s.animations[animIds.indexOf(state.animationId)] : null;
|
|
1563
|
+
const durSec = anim ? (anim.end - anim.start) / anim.fps : 0;
|
|
1564
|
+
const exit = (flags & 8) /* exitTimeIsPercentage */ ? ((p.exitTime ?? 0) / 100) * durSec : (p.exitTime ?? 0) / 1000;
|
|
1565
|
+
transition.delay = r3(exit);
|
|
1566
|
+
}
|
|
1567
|
+
state.transitions.push(transition);
|
|
1568
|
+
}
|
|
1569
|
+
else if (transition && isA(o.type, 'TransitionInputCondition')) {
|
|
1570
|
+
const inputId = inputIds[p.inputId];
|
|
1571
|
+
const input = inputs.find(i => i.id === inputId);
|
|
1572
|
+
if (!input)
|
|
1573
|
+
continue;
|
|
1574
|
+
const ops = ['Equal', 'NotEqual', 'LessEqual', 'GreaterEqual', 'LessThan', 'GreaterThan'];
|
|
1575
|
+
if (o.type === 'TransitionTriggerCondition')
|
|
1576
|
+
transition.conditions.push({ inputId, inputName: input.name, op: 'IsTrue', value: true });
|
|
1577
|
+
else if (o.type === 'TransitionBoolCondition')
|
|
1578
|
+
transition.conditions.push({ inputId, inputName: input.name, op: (p.opValue ?? 0) === 0 ? 'IsTrue' : 'IsFalse', value: (p.opValue ?? 0) === 0 });
|
|
1579
|
+
else
|
|
1580
|
+
transition.conditions.push({ inputId, inputName: input.name, op: ops[p.opValue ?? 0] || 'Equal', value: p.value ?? 0 });
|
|
1581
|
+
}
|
|
1582
|
+
else if (isA(o.type, 'StateMachineListener')) {
|
|
1583
|
+
ctx.warnings.add('State machine listeners (pointer events) are not imported yet');
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
// Resolve targets (stateToId indexes the layer's states) and wire Entry / Any.
|
|
1587
|
+
// stateToId indexes the layer's states in stream order (entry/any/exit included).
|
|
1588
|
+
for (const { t, to, layer: l } of pendingTargets)
|
|
1589
|
+
t.targetStateId = l.states[to]?.id || '';
|
|
1590
|
+
for (const l of layers) {
|
|
1591
|
+
const entry = l.states.find((st) => st.kind === 'EntryState');
|
|
1592
|
+
const first = entry?.transitions[0]?.targetStateId;
|
|
1593
|
+
// Exode has no entry/any/exit pseudo-states: enter the entry's target directly;
|
|
1594
|
+
// the runtime already checks every state's transitions globally, so Any-state
|
|
1595
|
+
// transitions keep working from wherever they sit.
|
|
1596
|
+
l.entryStateId = first || l.states.find((st) => st.animationId)?.id || l.states[0]?.id || '';
|
|
1597
|
+
const any = l.states.find((st) => st.kind === 'AnyState');
|
|
1598
|
+
const host = l.states.find((st) => st.id === l.entryStateId && st.kind === 'AnimationState') || l.states.find((st) => st.kind === 'AnimationState');
|
|
1599
|
+
if (any && host && any !== host)
|
|
1600
|
+
host.transitions.push(...any.transitions);
|
|
1601
|
+
for (const st of l.states)
|
|
1602
|
+
st.transitions = st.transitions.filter((t) => t.targetStateId && l.states.some((x) => x.id === t.targetStateId && x.kind !== 'ExitState'));
|
|
1603
|
+
l.states = l.states.filter((st) => st.kind !== 'EntryState' && st.kind !== 'ExitState' && st.kind !== 'AnyState');
|
|
1604
|
+
l.states.forEach((st) => { delete st.kind; });
|
|
1605
|
+
}
|
|
1606
|
+
return { name: sm.props.name || 'State Machine', inputs, layers: layers.filter(l => l.states.length) };
|
|
1607
|
+
}
|
|
1608
|
+
// ---------------------------------------------------------------- entry
|
|
1609
|
+
export function convertRive(data, opts = {}) {
|
|
1610
|
+
const file = data.artboards ? data : readRiv(data);
|
|
1611
|
+
if (!file.artboards.length)
|
|
1612
|
+
throw new Error('The .riv file has no artboards');
|
|
1613
|
+
const pick = typeof opts.artboard === 'number' ? file.artboards[opts.artboard]
|
|
1614
|
+
: typeof opts.artboard === 'string' ? file.artboards.find(a => a.artboard.props.name === opts.artboard) : file.artboards[0];
|
|
1615
|
+
const ab = pick || file.artboards[0];
|
|
1616
|
+
const s = indexScene(ab);
|
|
1617
|
+
const ctx = { s, file, pre: 'riv_', depth: 0, out: { animations: [], layers: [], inputs: [] }, objects: [], warnings: new Set(), tracksByAnim: new Map(s.animations.map(a => [a, []])), rivToExode: new Map() };
|
|
1618
|
+
s.bounds = boundsHook(ctx);
|
|
1619
|
+
setupLayouts(ctx);
|
|
1620
|
+
const A = ab.artboard.props;
|
|
1621
|
+
const root0 = s.layout?.get(0);
|
|
1622
|
+
const W = root0?.w || A.width || 500, H = root0?.h || A.height || 500;
|
|
1623
|
+
let background = opts.background ?? 'transparent';
|
|
1624
|
+
const root = group(ctx, 'riv_root', A.name || 'Artboard', null);
|
|
1625
|
+
root.transform.x = r3((A.originX || 0) * W);
|
|
1626
|
+
root.transform.y = r3((A.originY || 0) * H);
|
|
1627
|
+
const solid = emitArtboardContent(ctx, root, !opts.background);
|
|
1628
|
+
if (solid)
|
|
1629
|
+
background = solid;
|
|
1630
|
+
if (file.unknownTypes.size)
|
|
1631
|
+
ctx.warnings.add(`Unknown Rive object types ${[...file.unknownTypes].join(', ')} were skipped`);
|
|
1632
|
+
const { animations, ids: animIds } = buildAnimations(ctx);
|
|
1633
|
+
let machine;
|
|
1634
|
+
if (s.machines.length) {
|
|
1635
|
+
machine = stateMachine(ctx, s.machines[0], animIds);
|
|
1636
|
+
if (s.machines.length > 1)
|
|
1637
|
+
ctx.warnings.add('Only the first state machine is imported');
|
|
1638
|
+
}
|
|
1639
|
+
if (!machine || !machine.layers.length) {
|
|
1640
|
+
const first = animations[0];
|
|
1641
|
+
machine = { inputs: [], layers: first ? [{ id: 'layer_riv_timeline', name: 'Timeline', entryStateId: 'state_riv_play', states: [{ id: 'state_riv_play', name: first.name, animationId: first.id, loop: first.loop, transitions: [] }] }] : [] };
|
|
1642
|
+
}
|
|
1643
|
+
// Nested artboards play on layers of their own, next to the main ones.
|
|
1644
|
+
animations.push(...ctx.out.animations);
|
|
1645
|
+
machine.layers.push(...ctx.out.layers);
|
|
1646
|
+
machine.inputs.push(...ctx.out.inputs);
|
|
1647
|
+
// Explicit stacking in draw order (depth first).
|
|
1648
|
+
const byId = new Map(ctx.objects.map(o => [o.id, o]));
|
|
1649
|
+
let z = 0;
|
|
1650
|
+
const stack = (o) => { o.zIndex = z++; (o.children || []).forEach((cid) => { const c = byId.get(cid); if (c)
|
|
1651
|
+
stack(c); }); };
|
|
1652
|
+
ctx.objects.filter(o => !o.parentId).forEach(stack);
|
|
1653
|
+
const artboard = {
|
|
1654
|
+
id: `artboard_rive_${String(A.name || 'artboard').replace(/\W+/g, '_')}`,
|
|
1655
|
+
name: A.name || 'Rive artboard', width: W, height: H, cornerRadius: 0, fit: 'Contain',
|
|
1656
|
+
backgroundColor: background, opacity: 1, objects: ctx.objects, animations,
|
|
1657
|
+
navigation: [], animationFlow: [], stateVariables: [],
|
|
1658
|
+
stateMachine: { inputs: machine.inputs, layers: machine.layers },
|
|
1659
|
+
source: { kind: 'rive', version: `${file.major}.${file.minor}`, artboard: A.name, artboards: file.artboards.map(a => a.artboard.props.name) },
|
|
1660
|
+
};
|
|
1661
|
+
const tracks = animations.reduce((n, a) => n + a.tracks.length, 0);
|
|
1662
|
+
const keyframes = animations.reduce((n, a) => n + a.tracks.reduce((m, t) => m + t.keyframes.length, 0), 0);
|
|
1663
|
+
return { artboard, warnings: [...ctx.warnings], stats: { objects: ctx.objects.length, animations: animations.length, stateMachines: s.machines.length, tracks, keyframes } };
|
|
1664
|
+
}
|
|
1665
|
+
/** Artboard size, after a hugging artboard takes its content size. */
|
|
1666
|
+
const artboardSize = (s) => { const b = s.layout?.get(0), A = s.ab.artboard.props; return [b?.w || A.width || 500, b?.h || A.height || 500]; };
|
|
1667
|
+
/** Solve the scene's layouts and size the first parametric path of shapes directly in a layout (propagateSize). */
|
|
1668
|
+
function setupLayouts(ctx) {
|
|
1669
|
+
const s = ctx.s;
|
|
1670
|
+
if (!s.ab.stream.some(o => o.type === 'LayoutComponent' || o.type === 'NestedArtboardLayout'))
|
|
1671
|
+
return;
|
|
1672
|
+
s.layout = solveLayouts(s, s.ab.artboard, o => {
|
|
1673
|
+
if (o.type === 'Text') {
|
|
1674
|
+
const l = textLayout(ctx, o);
|
|
1675
|
+
return { w: l?.W || 0, h: l?.H || 0 };
|
|
1676
|
+
}
|
|
1677
|
+
const t = ctx.file.artboards[o.props.artboardId ?? -1]?.artboard.props;
|
|
1678
|
+
return { w: t?.width || 0, h: t?.height || 0 };
|
|
1679
|
+
});
|
|
1680
|
+
s.sizeOverride = new Map();
|
|
1681
|
+
for (const [idx, box] of s.layout) {
|
|
1682
|
+
const l = s.byIndex[idx];
|
|
1683
|
+
if (!l || l.type !== 'LayoutComponent' || l.props.styleId === undefined)
|
|
1684
|
+
continue;
|
|
1685
|
+
const sized = (c) => {
|
|
1686
|
+
for (const k of s.children.get(c.index) || []) {
|
|
1687
|
+
if (k.type === 'Shape') {
|
|
1688
|
+
const path = (s.children.get(k.index) || []).find(q => isA(q.type, 'ParametricPath'));
|
|
1689
|
+
if (path)
|
|
1690
|
+
s.sizeOverride.set(path.index, { w: box.w, h: box.h });
|
|
1691
|
+
}
|
|
1692
|
+
else if (k.type === 'Solo')
|
|
1693
|
+
sized(k);
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
sized(l);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/** Constraint bounds of a Text box or a shape's path, in its own space. */
|
|
1700
|
+
function boundsHook(ctx) {
|
|
1701
|
+
const s = ctx.s;
|
|
1702
|
+
return o => {
|
|
1703
|
+
if (o.type === 'Text') {
|
|
1704
|
+
const l = textLayout(ctx, o);
|
|
1705
|
+
return l ? { x: -l.W * (o.props.originX ?? 0), y: -l.H * (o.props.originY ?? 0), w: l.W, h: l.H } : null;
|
|
1706
|
+
}
|
|
1707
|
+
if (o.type === 'Shape') {
|
|
1708
|
+
const pts = shapeContours(s, o, null, 0, false).flat();
|
|
1709
|
+
if (!pts.length)
|
|
1710
|
+
return null;
|
|
1711
|
+
const b = bbox(pts);
|
|
1712
|
+
return { x: b.x0, y: b.y0, w: b.w, h: b.h };
|
|
1713
|
+
}
|
|
1714
|
+
return null;
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
/** An artboard's own paints (background) and its component tree under `root`. Returns a colour when the background is one opaque solid fill. */
|
|
1718
|
+
function emitArtboardContent(ctx, root, solidToBackground) {
|
|
1719
|
+
const s = ctx.s;
|
|
1720
|
+
const A = s.ab.artboard.props;
|
|
1721
|
+
const [W, H] = artboardSize(s);
|
|
1722
|
+
// Content coordinates are relative to the artboard origin; its box spans -origin x size.
|
|
1723
|
+
const ox = (A.originX || 0) * W, oy = (A.originY || 0) * H;
|
|
1724
|
+
const bgPaints = (s.children.get(0) || []).filter(k => isA(k.type, 'ShapePaint'));
|
|
1725
|
+
let solid = null;
|
|
1726
|
+
for (const pnt of bgPaints) {
|
|
1727
|
+
const fill = paintFill(pnt, s, null, 0, { cx: W / 2 - ox, cy: H / 2 - oy, w: W, h: H });
|
|
1728
|
+
if (solidToBackground && pnt.type === 'Fill' && fill.type === 'Solid' && fill.opacity >= 0.999 && bgPaints.length === 1) {
|
|
1729
|
+
solid = fill.color;
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const o = { id: `${ctx.pre}bg_${pnt.index}`, name: 'Artboard background', type: 'Shape', parentId: root.id, transform: { ...baseTransform(), x: r3(W / 2 - ox), y: r3(H / 2 - oy) }, geometry: { type: 'Rectangle', width: W, height: H, corner_radius: 0 }, style: { fill, stroke: noStroke() }, isVisible: true };
|
|
1733
|
+
root.children.push(o.id);
|
|
1734
|
+
ctx.objects.push(o);
|
|
1735
|
+
}
|
|
1736
|
+
const top = (s.children.get(0) || []).filter(k => isA(k.type, 'TransformComponent') && !isA(k.type, 'Path'));
|
|
1737
|
+
for (const k of [...top].reverse())
|
|
1738
|
+
emitComponent(ctx, k, root);
|
|
1739
|
+
const ab = s.ab;
|
|
1740
|
+
if (ab.stream.some(o => o.type === 'DrawRules' || o.type === 'DrawTarget'))
|
|
1741
|
+
ctx.warnings.add('Custom draw order (draw rules) is not imported; hierarchy order is used');
|
|
1742
|
+
if (ab.stream.some(o => o.type !== 'Artboard' && isA(o.type, 'LayoutComponent')))
|
|
1743
|
+
ctx.warnings.add('Rive layouts are solved once at import (flex rows/columns, gaps, padding, alignment); wrap, grid and layout animation are not');
|
|
1744
|
+
const unsupported = ab.stream.filter(o => isA(o.type, 'Constraint') && !CONSTRAINTS.has(o.type)).map(o => o.type);
|
|
1745
|
+
if (unsupported.length)
|
|
1746
|
+
ctx.warnings.add(`Constraints not imported: ${[...new Set(unsupported)].join(', ')}`);
|
|
1747
|
+
return solid;
|
|
1748
|
+
}
|
|
1749
|
+
/** One Exode animation per Rive timeline of this scene (ping-pong unrolled, speed applied); ids align with s.animations. */
|
|
1750
|
+
function buildAnimations(ctx) {
|
|
1751
|
+
const animations = [];
|
|
1752
|
+
const ids = [];
|
|
1753
|
+
ctx.s.animations.forEach((a, i) => {
|
|
1754
|
+
if (a.synthetic) {
|
|
1755
|
+
ids.push('');
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const id = `anim_${ctx.pre}${i}_${String(a.obj.props.name || 'Animation').replace(/\W+/g, '_')}`;
|
|
1759
|
+
let tracks = ctx.tracksByAnim.get(a);
|
|
1760
|
+
let duration = r3((a.end - a.start) / a.fps);
|
|
1761
|
+
const loopValue = a.obj.props.loopValue ?? 0;
|
|
1762
|
+
if (loopValue === 2) {
|
|
1763
|
+
tracks = tracks.map(t => {
|
|
1764
|
+
const back = [...t.keyframes].reverse().slice(1).map((k, j) => ({ ...k, id: `${k.id}_pp${j}`, time: r3(2 * duration - k.time) }));
|
|
1765
|
+
return { ...t, keyframes: [...t.keyframes, ...back] };
|
|
1766
|
+
});
|
|
1767
|
+
duration = r3(duration * 2);
|
|
1768
|
+
}
|
|
1769
|
+
const speed = a.obj.props.speed ?? 1;
|
|
1770
|
+
if (speed !== 1 && speed > 0) {
|
|
1771
|
+
tracks.forEach(t => t.keyframes.forEach((k) => (k.time = r3(k.time / speed))));
|
|
1772
|
+
duration = r3(duration / speed);
|
|
1773
|
+
}
|
|
1774
|
+
animations.push({ id, name: (ctx.depth ? `${ctx.pre.replace(/_$/, '')} · ` : '') + (a.obj.props.name || `Animation ${i + 1}`), duration, loop: loopValue !== 0, tracks });
|
|
1775
|
+
ids.push(id);
|
|
1776
|
+
});
|
|
1777
|
+
return { animations, ids };
|
|
1778
|
+
}
|
|
1779
|
+
//# sourceMappingURL=convert.js.map
|