@sebastianwessel/isostate 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/animation-engine.d.ts +3 -0
- package/dist/animation/animation-engine.d.ts.map +1 -1
- package/dist/animation/controller.d.ts +36 -2
- package/dist/animation/controller.d.ts.map +1 -1
- package/dist/assets/software-architecture/IMAGEGEN-PROMPTS.md +66 -0
- package/dist/assets/software-architecture/LICENSE +21 -0
- package/dist/assets/software-architecture/NOTICE.md +17 -0
- package/dist/assets/software-architecture/README.md +105 -0
- package/dist/assets/software-architecture/agentic/AGENT-VISUAL-PROMPTS.md +42 -0
- package/dist/assets/software-architecture/agentic/HANDOFF-PROMPTS.md +19 -0
- package/dist/assets/software-architecture/agentic/PROCESS-VISUAL-PROMPTS.md +109 -0
- package/dist/assets/software-architecture/agentic/agents-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/ai-capabilities-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/ai-to-human-handoff.png +0 -0
- package/dist/assets/software-architecture/agentic/channels-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/human-to-ai-handoff.png +0 -0
- package/dist/assets/software-architecture/agentic/humans-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/infrastructure-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/lifecycle-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/orchestration-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/providers-sprites.png +0 -0
- package/dist/assets/software-architecture/agentic/servicenow-sprites.png +0 -0
- package/dist/assets/software-architecture/architecture/architecture-sprites.png +0 -0
- package/dist/assets/software-architecture/manifest.json +2095 -0
- package/dist/assets/software-architecture/workflow/workflow-process-sprites.png +0 -0
- package/dist/browser/isostate.runtime.js +4 -4
- package/dist/browser/isostate.runtime.js.map +12 -10
- package/dist/chunks/errors-2yA81Vfb.js +23 -0
- package/dist/chunks/{errors-DyqEkrm5.js.map → errors-2yA81Vfb.js.map} +1 -1
- package/dist/chunks/index-WmsNtuaZ.js +1685 -0
- package/dist/chunks/index-WmsNtuaZ.js.map +1 -0
- package/dist/chunks/rendering-engine-DLaM1XbU.js +1425 -0
- package/dist/chunks/rendering-engine-DLaM1XbU.js.map +1 -0
- package/dist/chunks/scene-parser-BNXXIAqj.js +3033 -0
- package/dist/chunks/scene-parser-BNXXIAqj.js.map +1 -0
- package/dist/chunks/sha256-BkgWUdh1.js +123 -0
- package/dist/chunks/sha256-BkgWUdh1.js.map +1 -0
- package/dist/dsl/browser.js +3 -3
- package/dist/dsl/index.js +3 -3
- package/dist/dsl/scene-parser.d.ts.map +1 -1
- package/dist/dsl/scene-validator.d.ts.map +1 -1
- package/dist/dsl/workflow-effects-validation.d.ts +8 -0
- package/dist/dsl/workflow-effects-validation.d.ts.map +1 -0
- package/dist/editor-support/adapter.d.ts.map +1 -1
- package/dist/editor-support/index.js +347 -394
- package/dist/editor-support/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -40
- package/dist/index.js.map +1 -1
- package/dist/rendering/animation-css.d.ts.map +1 -1
- package/dist/rendering/rendering-engine.d.ts +11 -0
- package/dist/rendering/rendering-engine.d.ts.map +1 -1
- package/dist/rendering/scene-effects.d.ts +15 -0
- package/dist/rendering/scene-effects.d.ts.map +1 -0
- package/dist/rendering/theme.d.ts +4 -0
- package/dist/rendering/theme.d.ts.map +1 -1
- package/dist/runtime/diagnostics-overlay.d.ts +29 -0
- package/dist/runtime/diagnostics-overlay.d.ts.map +1 -0
- package/dist/runtime/export.d.ts +27 -0
- package/dist/runtime/export.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -4
- package/dist/runtime/interactivity.d.ts +56 -0
- package/dist/runtime/interactivity.d.ts.map +1 -0
- package/dist/runtime/mount-scene.d.ts +11 -10
- package/dist/runtime/mount-scene.d.ts.map +1 -1
- package/dist/types/asset-registry.d.ts +1 -0
- package/dist/types/asset-registry.d.ts.map +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/node.d.ts +54 -1
- package/dist/types/node.d.ts.map +1 -1
- package/dist/types/runtime-bundle.d.ts +35 -0
- package/dist/types/runtime-bundle.d.ts.map +1 -1
- package/dist/types/scene.d.ts +2 -0
- package/dist/types/scene.d.ts.map +1 -1
- package/package.json +9 -8
- package/dist/chunks/errors-DyqEkrm5.js +0 -29
- package/dist/chunks/index-C3qcXbPI.js +0 -1542
- package/dist/chunks/index-C3qcXbPI.js.map +0 -1
- package/dist/chunks/rendering-engine-BUUV3Qq-.js +0 -1124
- package/dist/chunks/rendering-engine-BUUV3Qq-.js.map +0 -1
- package/dist/chunks/scene-parser-B9SPDLYs.js +0 -2688
- package/dist/chunks/scene-parser-B9SPDLYs.js.map +0 -1
- package/dist/chunks/sha256-4JuurR-b.js +0 -69
- package/dist/chunks/sha256-4JuurR-b.js.map +0 -1
|
@@ -0,0 +1,1685 @@
|
|
|
1
|
+
import { A as AnimationError, C as ControllerError, R as RenderError } from './errors-2yA81Vfb.js';
|
|
2
|
+
import { a as getResolvedViewBox, c as getCurrentElementBounds, b as getElementState, q as getGridAreaBounds, e as updateElementTransforms, g as getResolvedProjectionLayout, f as projectToScreen, l as calculateVisualSize, s as applySceneViewBox, u as unhideElementOnReadd, h as hideElementAfterExit, d as getConnectorState, t as animateElement, j as buildSceneDOM, r as resolveTheme } from './rendering-engine-DLaM1XbU.js';
|
|
3
|
+
import { s as sha256 } from './sha256-BkgWUdh1.js';
|
|
4
|
+
|
|
5
|
+
class AnimationEngine {
|
|
6
|
+
_bundle = null;
|
|
7
|
+
_progress = 0;
|
|
8
|
+
_paused = false;
|
|
9
|
+
_prevFrameMap = /* @__PURE__ */ new Map();
|
|
10
|
+
_elementFrameMap = /* @__PURE__ */ new Map();
|
|
11
|
+
_prevConnectorFrameMap = /* @__PURE__ */ new Map();
|
|
12
|
+
_connectorFrameMap = /* @__PURE__ */ new Map();
|
|
13
|
+
get bundle() {
|
|
14
|
+
return this._bundle;
|
|
15
|
+
}
|
|
16
|
+
get progress() {
|
|
17
|
+
return this._progress;
|
|
18
|
+
}
|
|
19
|
+
getProgress() {
|
|
20
|
+
return this._progress;
|
|
21
|
+
}
|
|
22
|
+
get paused() {
|
|
23
|
+
return this._paused;
|
|
24
|
+
}
|
|
25
|
+
isPaused() {
|
|
26
|
+
return this._paused;
|
|
27
|
+
}
|
|
28
|
+
get elementsCount() {
|
|
29
|
+
return this._elementFrameMap.size;
|
|
30
|
+
}
|
|
31
|
+
get connectorsCount() {
|
|
32
|
+
return this._connectorFrameMap.size;
|
|
33
|
+
}
|
|
34
|
+
/** Initialize with a compiled runtime bundle. */
|
|
35
|
+
init(bundle) {
|
|
36
|
+
this._bundle = bundle;
|
|
37
|
+
this._progress = 0;
|
|
38
|
+
this._paused = false;
|
|
39
|
+
this._prevFrameMap.clear();
|
|
40
|
+
this._elementFrameMap.clear();
|
|
41
|
+
this._prevConnectorFrameMap.clear();
|
|
42
|
+
this._connectorFrameMap.clear();
|
|
43
|
+
const initial = resolveFrameMap(bundle, 0);
|
|
44
|
+
this._elementFrameMap = initial;
|
|
45
|
+
this._prevFrameMap = cloneFrameMap(initial);
|
|
46
|
+
const initialConnectors = resolveConnectorFrameMap(bundle, 0);
|
|
47
|
+
this._connectorFrameMap = initialConnectors;
|
|
48
|
+
this._prevConnectorFrameMap = cloneConnectorFrameMap(initialConnectors);
|
|
49
|
+
}
|
|
50
|
+
/** Set current scroll progress (0-1) and compute frame update. */
|
|
51
|
+
setProgress(progress) {
|
|
52
|
+
if (!Number.isFinite(progress)) {
|
|
53
|
+
throw new AnimationError("INVALID_PROGRESS", "setProgress() requires a finite progress value");
|
|
54
|
+
}
|
|
55
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
56
|
+
if (this._paused) return;
|
|
57
|
+
this._progress = clamped;
|
|
58
|
+
if (!this._bundle) return;
|
|
59
|
+
this._prevFrameMap = cloneFrameMap(this._elementFrameMap);
|
|
60
|
+
this._elementFrameMap = resolveFrameMap(this._bundle, clamped);
|
|
61
|
+
this._prevConnectorFrameMap = cloneConnectorFrameMap(this._connectorFrameMap);
|
|
62
|
+
this._connectorFrameMap = resolveConnectorFrameMap(this._bundle, clamped);
|
|
63
|
+
}
|
|
64
|
+
/** Get interpolated FrameUpdate for an element id or runtime element. */
|
|
65
|
+
getElementUpdate(element) {
|
|
66
|
+
const id = typeof element === "string" ? element : element.id;
|
|
67
|
+
const frame = this._elementFrameMap.get(id);
|
|
68
|
+
if (!frame) {
|
|
69
|
+
return {
|
|
70
|
+
id,
|
|
71
|
+
asset: "",
|
|
72
|
+
lifecycle: "removed",
|
|
73
|
+
ambient: [],
|
|
74
|
+
pos: [0, 0],
|
|
75
|
+
size: 1,
|
|
76
|
+
layer: ""
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return frameToUpdate(frame);
|
|
80
|
+
}
|
|
81
|
+
getFrameUpdates() {
|
|
82
|
+
return [...this._elementFrameMap.values()].map(frameToUpdate);
|
|
83
|
+
}
|
|
84
|
+
/** Get interpolated ConnectorFrameUpdate for a connector id or runtime connector. */
|
|
85
|
+
getConnectorUpdate(connector) {
|
|
86
|
+
const id = typeof connector === "string" ? connector : connector.id;
|
|
87
|
+
const frame = this._connectorFrameMap.get(id);
|
|
88
|
+
if (!frame) return connectorFrameToUpdate(removedConnectorFrame(id));
|
|
89
|
+
return connectorFrameToUpdate(frame);
|
|
90
|
+
}
|
|
91
|
+
getConnectorFrameUpdates() {
|
|
92
|
+
return [...this._connectorFrameMap.values()].map(connectorFrameToUpdate);
|
|
93
|
+
}
|
|
94
|
+
/** Compute lifecycle transition between previous and current frame. */
|
|
95
|
+
getLifecycleTransition(elId) {
|
|
96
|
+
const prev = this._prevFrameMap.get(elId);
|
|
97
|
+
const current = this._elementFrameMap.get(elId);
|
|
98
|
+
const from = prev?.lifecycle ?? "removed";
|
|
99
|
+
const to = current?.lifecycle ?? "removed";
|
|
100
|
+
if (from === to) return null;
|
|
101
|
+
return {
|
|
102
|
+
from,
|
|
103
|
+
to
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Compute lifecycle transition between previous and current connector frame. */
|
|
107
|
+
getConnectorLifecycleTransition(connectorId) {
|
|
108
|
+
const prev = this._prevConnectorFrameMap.get(connectorId);
|
|
109
|
+
const current = this._connectorFrameMap.get(connectorId);
|
|
110
|
+
const from = prev?.lifecycle ?? "removed";
|
|
111
|
+
const to = current?.lifecycle ?? "removed";
|
|
112
|
+
if (from === to) return null;
|
|
113
|
+
return {
|
|
114
|
+
from,
|
|
115
|
+
to
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
getCurrentState() {
|
|
119
|
+
const bundle = this._bundle;
|
|
120
|
+
if (!bundle) return null;
|
|
121
|
+
const pair = findSurroundingStops(bundle.scenes, this._progress);
|
|
122
|
+
return pair?.nextStop ?? bundle.scenes[0] ?? null;
|
|
123
|
+
}
|
|
124
|
+
pause() {
|
|
125
|
+
this._paused = true;
|
|
126
|
+
}
|
|
127
|
+
resume() {
|
|
128
|
+
this._paused = false;
|
|
129
|
+
}
|
|
130
|
+
destroy() {
|
|
131
|
+
this._bundle = null;
|
|
132
|
+
this._progress = 0;
|
|
133
|
+
this._paused = false;
|
|
134
|
+
this._elementFrameMap.clear();
|
|
135
|
+
this._prevFrameMap.clear();
|
|
136
|
+
this._connectorFrameMap.clear();
|
|
137
|
+
this._prevConnectorFrameMap.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function resolveFrameMap(bundle, progress) {
|
|
141
|
+
const pair = findSurroundingStops(bundle.scenes, progress);
|
|
142
|
+
const result = /* @__PURE__ */ new Map();
|
|
143
|
+
if (!pair) return result;
|
|
144
|
+
const ids = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const stop of bundle.scenes) {
|
|
146
|
+
for (const element of stop.elements ?? []) ids.add(element.id);
|
|
147
|
+
}
|
|
148
|
+
for (const element of pair.prevStop.elements ?? []) ids.add(element.id);
|
|
149
|
+
for (const element of pair.nextStop.elements ?? []) ids.add(element.id);
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
const frame = withRemovedElementGeometry(
|
|
152
|
+
interpolateElement(id, pair.prevStop, pair.nextStop, pair.t),
|
|
153
|
+
bundle.scenes,
|
|
154
|
+
id,
|
|
155
|
+
progress
|
|
156
|
+
);
|
|
157
|
+
result.set(id, frame);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function resolveConnectorFrameMap(bundle, progress) {
|
|
162
|
+
const pair = findSurroundingStops(bundle.scenes, progress);
|
|
163
|
+
const result = /* @__PURE__ */ new Map();
|
|
164
|
+
if (!pair) return result;
|
|
165
|
+
const ids = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const stop of bundle.scenes) {
|
|
167
|
+
for (const connector of stop.connectors ?? []) ids.add(connector.id);
|
|
168
|
+
}
|
|
169
|
+
for (const connector of pair.prevStop.connectors ?? []) ids.add(connector.id);
|
|
170
|
+
for (const connector of pair.nextStop.connectors ?? []) ids.add(connector.id);
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
const frame = interpolateConnector(id, pair.prevStop, pair.nextStop, pair.t);
|
|
173
|
+
const reference = frame.lifecycle === "removed" ? findNearestGeometry(bundle.scenes, id, progress, (stop) => stop.connectors ?? []) : void 0;
|
|
174
|
+
result.set(id, reference ? frameFromConnector(reference, "removed") : frame);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
function findSurroundingStops(stops, progress) {
|
|
179
|
+
if (stops.length === 0) return null;
|
|
180
|
+
const sorted = [...stops].sort((a, b) => a.progress - b.progress);
|
|
181
|
+
if (sorted.length === 1 || progress <= sorted[0].progress) {
|
|
182
|
+
return { prevStop: sorted[0], nextStop: sorted[0], t: 0, nextIndex: 0 };
|
|
183
|
+
}
|
|
184
|
+
const lastIndex = sorted.length - 1;
|
|
185
|
+
if (progress >= sorted[lastIndex].progress) {
|
|
186
|
+
return {
|
|
187
|
+
prevStop: sorted[lastIndex - 1] ?? sorted[lastIndex],
|
|
188
|
+
nextStop: sorted[lastIndex],
|
|
189
|
+
t: 1,
|
|
190
|
+
nextIndex: lastIndex
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
194
|
+
const nextStop = sorted[i];
|
|
195
|
+
if (nextStop.progress >= progress) {
|
|
196
|
+
const prevStop = sorted[i - 1];
|
|
197
|
+
const range = nextStop.progress - prevStop.progress;
|
|
198
|
+
const t = range > 0 ? (progress - prevStop.progress) / range : 0;
|
|
199
|
+
return { prevStop, nextStop, t, nextIndex: i };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
function interpolateElement(id, prevStop, nextStop, t) {
|
|
205
|
+
const prev = findElement(prevStop, id);
|
|
206
|
+
const next = findElement(nextStop, id);
|
|
207
|
+
const source = next ?? prev;
|
|
208
|
+
if (!source) {
|
|
209
|
+
return removedFrame(id);
|
|
210
|
+
}
|
|
211
|
+
if (!prev || prev.presence === "removed") {
|
|
212
|
+
return frameFromElement(source, source.presence === "removed" || t < 1 ? "removed" : source.presence);
|
|
213
|
+
}
|
|
214
|
+
if (!next || next.presence === "removed") {
|
|
215
|
+
return frameFromElement(prev, t < 1 ? prev.presence : "removed");
|
|
216
|
+
}
|
|
217
|
+
const lifecycle = t < 1 ? prev.presence : next.presence;
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
asset: t < 1 ? prev.asset : next.asset,
|
|
221
|
+
pos: interpolatePos(prev.pos, next.pos, t),
|
|
222
|
+
size: prev.size + (next.size - prev.size) * t,
|
|
223
|
+
lifecycle,
|
|
224
|
+
ambient: cloneAmbient(t < 1 ? prev.ambient : next.ambient),
|
|
225
|
+
layer: t < 1 ? prev.layer : next.layer,
|
|
226
|
+
entry: next.enter ?? prev.enter,
|
|
227
|
+
exit: next.exit ?? prev.exit,
|
|
228
|
+
text: cloneText(next.text ?? prev.text),
|
|
229
|
+
primitive: clonePrimitive(next.primitive ?? prev.primitive),
|
|
230
|
+
activity: cloneOptional(t < 1 ? prev.activity : next.activity)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function interpolateConnector(id, prevStop, nextStop, t) {
|
|
234
|
+
const prev = findConnector(prevStop, id);
|
|
235
|
+
const next = findConnector(nextStop, id);
|
|
236
|
+
const source = next ?? prev;
|
|
237
|
+
if (!source) return removedConnectorFrame(id);
|
|
238
|
+
if (!prev || prev.presence === "removed") {
|
|
239
|
+
return frameFromConnector(source, source.presence === "removed" || t < 1 ? "removed" : source.presence);
|
|
240
|
+
}
|
|
241
|
+
if (!next || next.presence === "removed") {
|
|
242
|
+
return frameFromConnector(prev, t < 1 ? prev.presence : "removed");
|
|
243
|
+
}
|
|
244
|
+
const lifecycle = t < 1 ? prev.presence : next.presence;
|
|
245
|
+
return {
|
|
246
|
+
id,
|
|
247
|
+
route: interpolateRoute(prev.route, next.route, t),
|
|
248
|
+
layer: t < 1 ? prev.layer : next.layer,
|
|
249
|
+
lifecycle,
|
|
250
|
+
style: cloneConnectorStyle(t < 1 ? prev.style : next.style),
|
|
251
|
+
start: t < 1 ? prev.start : next.start,
|
|
252
|
+
end: t < 1 ? prev.end : next.end,
|
|
253
|
+
direction: t < 1 ? prev.direction : next.direction,
|
|
254
|
+
message: cloneOptional(t < 1 ? prev.message : next.message),
|
|
255
|
+
ambient: cloneAmbient(t < 1 ? prev.ambient : next.ambient),
|
|
256
|
+
entry: next.enter ?? prev.enter,
|
|
257
|
+
exit: next.exit ?? prev.exit
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function findElement(stop, id) {
|
|
261
|
+
return (stop.elements ?? []).find((element) => element.id === id);
|
|
262
|
+
}
|
|
263
|
+
function findConnector(stop, id) {
|
|
264
|
+
return (stop.connectors ?? []).find((connector) => connector.id === id);
|
|
265
|
+
}
|
|
266
|
+
function withRemovedElementGeometry(frame, stops, id, progress) {
|
|
267
|
+
if (frame.lifecycle !== "removed") return frame;
|
|
268
|
+
const reference = findNearestGeometry(stops, id, progress, (stop) => stop.elements ?? []);
|
|
269
|
+
if (!reference) return frame;
|
|
270
|
+
return {
|
|
271
|
+
...frame,
|
|
272
|
+
asset: reference.asset,
|
|
273
|
+
pos: [...reference.pos],
|
|
274
|
+
size: reference.size,
|
|
275
|
+
ambient: cloneAmbient(reference.ambient),
|
|
276
|
+
layer: reference.layer,
|
|
277
|
+
entry: reference.enter,
|
|
278
|
+
exit: reference.exit,
|
|
279
|
+
text: cloneText(reference.text),
|
|
280
|
+
primitive: clonePrimitive(reference.primitive),
|
|
281
|
+
activity: cloneOptional(reference.activity)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function findNearestGeometry(stops, id, progress, states) {
|
|
285
|
+
const sorted = [...stops].sort((a, b) => a.progress - b.progress);
|
|
286
|
+
const next = sorted.filter((stop) => stop.progress >= progress).flatMap(states).find((element) => element.id === id && element.presence !== "removed");
|
|
287
|
+
if (next) return next;
|
|
288
|
+
for (let index = sorted.length - 1; index >= 0; index -= 1) {
|
|
289
|
+
if (sorted[index].progress > progress) continue;
|
|
290
|
+
const previous = states(sorted[index]).find((element) => element.id === id && element.presence !== "removed");
|
|
291
|
+
if (previous) return previous;
|
|
292
|
+
}
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
function frameFromElement(element, lifecycle = element.presence) {
|
|
296
|
+
return {
|
|
297
|
+
id: element.id,
|
|
298
|
+
asset: element.asset,
|
|
299
|
+
pos: [...element.pos],
|
|
300
|
+
size: element.size,
|
|
301
|
+
lifecycle,
|
|
302
|
+
ambient: cloneAmbient(element.ambient),
|
|
303
|
+
layer: element.layer,
|
|
304
|
+
entry: element.enter,
|
|
305
|
+
exit: element.exit,
|
|
306
|
+
text: cloneText(element.text),
|
|
307
|
+
primitive: clonePrimitive(element.primitive),
|
|
308
|
+
activity: cloneOptional(element.activity)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function frameFromConnector(connector, lifecycle = connector.presence) {
|
|
312
|
+
return {
|
|
313
|
+
id: connector.id,
|
|
314
|
+
route: cloneRoute(connector.route),
|
|
315
|
+
layer: connector.layer,
|
|
316
|
+
lifecycle,
|
|
317
|
+
style: cloneConnectorStyle(connector.style),
|
|
318
|
+
start: connector.start,
|
|
319
|
+
end: connector.end,
|
|
320
|
+
direction: connector.direction,
|
|
321
|
+
message: cloneOptional(connector.message),
|
|
322
|
+
ambient: cloneAmbient(connector.ambient),
|
|
323
|
+
entry: connector.enter,
|
|
324
|
+
exit: connector.exit
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function removedFrame(id) {
|
|
328
|
+
return {
|
|
329
|
+
id,
|
|
330
|
+
asset: "",
|
|
331
|
+
pos: [0, 0],
|
|
332
|
+
size: 1,
|
|
333
|
+
lifecycle: "removed",
|
|
334
|
+
ambient: [],
|
|
335
|
+
layer: ""
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function removedConnectorFrame(id) {
|
|
339
|
+
return {
|
|
340
|
+
id,
|
|
341
|
+
route: [],
|
|
342
|
+
layer: "",
|
|
343
|
+
lifecycle: "removed",
|
|
344
|
+
style: {
|
|
345
|
+
variant: "line",
|
|
346
|
+
pattern: "solid",
|
|
347
|
+
stroke: "#2563eb",
|
|
348
|
+
strokeWidth: 3,
|
|
349
|
+
opacity: 1,
|
|
350
|
+
outlineWidth: 0,
|
|
351
|
+
lane: "none"
|
|
352
|
+
},
|
|
353
|
+
start: "none",
|
|
354
|
+
end: "none",
|
|
355
|
+
direction: "route",
|
|
356
|
+
ambient: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function interpolatePos(prev, next, t) {
|
|
360
|
+
return [prev[0] + (next[0] - prev[0]) * t, prev[1] + (next[1] - prev[1]) * t];
|
|
361
|
+
}
|
|
362
|
+
function interpolateRoute(prev, next, t) {
|
|
363
|
+
if (prev.length !== next.length) return cloneRoute(t < 1 ? prev : next);
|
|
364
|
+
return prev.map((point, index) => interpolatePos(point, next[index], t));
|
|
365
|
+
}
|
|
366
|
+
function cloneRoute(route) {
|
|
367
|
+
return route.map((point) => [point[0], point[1]]);
|
|
368
|
+
}
|
|
369
|
+
function cloneConnectorStyle(style) {
|
|
370
|
+
return {
|
|
371
|
+
...style,
|
|
372
|
+
...style.dash ? { dash: [...style.dash] } : {}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function cloneAmbient(ambient) {
|
|
376
|
+
return (ambient ?? []).map((item) => ({ ...item }));
|
|
377
|
+
}
|
|
378
|
+
function frameToUpdate(frame) {
|
|
379
|
+
return {
|
|
380
|
+
id: frame.id,
|
|
381
|
+
asset: frame.asset,
|
|
382
|
+
lifecycle: frame.lifecycle,
|
|
383
|
+
ambient: cloneAmbient(frame.ambient),
|
|
384
|
+
pos: [...frame.pos],
|
|
385
|
+
size: frame.size,
|
|
386
|
+
layer: frame.layer,
|
|
387
|
+
entry: frame.entry,
|
|
388
|
+
exit: frame.exit,
|
|
389
|
+
text: cloneText(frame.text),
|
|
390
|
+
primitive: clonePrimitive(frame.primitive),
|
|
391
|
+
activity: cloneOptional(frame.activity)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function connectorFrameToUpdate(frame) {
|
|
395
|
+
return {
|
|
396
|
+
id: frame.id,
|
|
397
|
+
route: cloneRoute(frame.route),
|
|
398
|
+
layer: frame.layer,
|
|
399
|
+
lifecycle: frame.lifecycle,
|
|
400
|
+
style: cloneConnectorStyle(frame.style),
|
|
401
|
+
start: frame.start,
|
|
402
|
+
end: frame.end,
|
|
403
|
+
direction: frame.direction,
|
|
404
|
+
message: cloneOptional(frame.message),
|
|
405
|
+
ambient: cloneAmbient(frame.ambient),
|
|
406
|
+
entry: frame.entry,
|
|
407
|
+
exit: frame.exit
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function cloneFrameMap(map) {
|
|
411
|
+
const clone = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const [id, frame] of map) {
|
|
413
|
+
clone.set(id, {
|
|
414
|
+
...frame,
|
|
415
|
+
pos: [...frame.pos],
|
|
416
|
+
ambient: cloneAmbient(frame.ambient),
|
|
417
|
+
text: cloneText(frame.text),
|
|
418
|
+
primitive: clonePrimitive(frame.primitive),
|
|
419
|
+
activity: cloneOptional(frame.activity)
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return clone;
|
|
423
|
+
}
|
|
424
|
+
function cloneText(text) {
|
|
425
|
+
return text ? { ...text } : void 0;
|
|
426
|
+
}
|
|
427
|
+
function clonePrimitive(primitive) {
|
|
428
|
+
if (!primitive) return void 0;
|
|
429
|
+
return {
|
|
430
|
+
...primitive.rectangle ? {
|
|
431
|
+
rectangle: {
|
|
432
|
+
...primitive.rectangle,
|
|
433
|
+
dash: cloneDash(primitive.rectangle.dash)
|
|
434
|
+
}
|
|
435
|
+
} : {},
|
|
436
|
+
...primitive.circle ? {
|
|
437
|
+
circle: {
|
|
438
|
+
...primitive.circle,
|
|
439
|
+
dash: cloneDash(primitive.circle.dash)
|
|
440
|
+
}
|
|
441
|
+
} : {},
|
|
442
|
+
...primitive.polygon ? {
|
|
443
|
+
polygon: {
|
|
444
|
+
...primitive.polygon,
|
|
445
|
+
dash: cloneDash(primitive.polygon.dash),
|
|
446
|
+
points: cloneRoute(primitive.polygon.points)
|
|
447
|
+
}
|
|
448
|
+
} : {},
|
|
449
|
+
...primitive.line ? {
|
|
450
|
+
line: {
|
|
451
|
+
...primitive.line,
|
|
452
|
+
dash: cloneDash(primitive.line.dash),
|
|
453
|
+
points: cloneRoute(primitive.line.points)
|
|
454
|
+
}
|
|
455
|
+
} : {}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function cloneDash(dash) {
|
|
459
|
+
return dash ? [dash[0], dash[1]] : void 0;
|
|
460
|
+
}
|
|
461
|
+
function cloneConnectorFrameMap(map) {
|
|
462
|
+
const clone = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const [id, frame] of map) {
|
|
464
|
+
clone.set(id, {
|
|
465
|
+
...frame,
|
|
466
|
+
route: cloneRoute(frame.route),
|
|
467
|
+
style: cloneConnectorStyle(frame.style),
|
|
468
|
+
ambient: cloneAmbient(frame.ambient)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return clone;
|
|
472
|
+
}
|
|
473
|
+
function cloneOptional(value) {
|
|
474
|
+
return value ? { ...value } : void 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function linear(t) {
|
|
478
|
+
return t;
|
|
479
|
+
}
|
|
480
|
+
function easeInCubic(t) {
|
|
481
|
+
return t * t * t;
|
|
482
|
+
}
|
|
483
|
+
function easeOutCubic(t) {
|
|
484
|
+
return 1 - (1 - t) ** 3;
|
|
485
|
+
}
|
|
486
|
+
function easeInOutCubic(t) {
|
|
487
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
488
|
+
}
|
|
489
|
+
function resolveEasing(type) {
|
|
490
|
+
switch (type) {
|
|
491
|
+
case "linear":
|
|
492
|
+
return linear;
|
|
493
|
+
case "easeInCubic":
|
|
494
|
+
return easeInCubic;
|
|
495
|
+
case "easeOutCubic":
|
|
496
|
+
return easeOutCubic;
|
|
497
|
+
case "easeInOutCubic":
|
|
498
|
+
return easeInOutCubic;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const DEFAULT_CONFIG = {
|
|
503
|
+
scrollDirection: "vertical",
|
|
504
|
+
scrollOffset: {},
|
|
505
|
+
minProgress: 0,
|
|
506
|
+
maxProgress: 1,
|
|
507
|
+
keyboardControls: false,
|
|
508
|
+
touchControls: false,
|
|
509
|
+
scrollSensitivity: 1,
|
|
510
|
+
transitionDuration: 600,
|
|
511
|
+
transitionEasing: "ease-in-out"
|
|
512
|
+
};
|
|
513
|
+
class AnimationController {
|
|
514
|
+
_engine = new AnimationEngine();
|
|
515
|
+
_bundle = null;
|
|
516
|
+
_sceneIndex = 0;
|
|
517
|
+
_progress = 0;
|
|
518
|
+
_paused = false;
|
|
519
|
+
_config = DEFAULT_CONFIG;
|
|
520
|
+
_container = null;
|
|
521
|
+
_sceneElement = null;
|
|
522
|
+
_ownsEngine = true;
|
|
523
|
+
_rafId = null;
|
|
524
|
+
_pendingProgress = null;
|
|
525
|
+
_destroyed = false;
|
|
526
|
+
_listeners = /* @__PURE__ */ new Map();
|
|
527
|
+
_cameraAnim = null;
|
|
528
|
+
_cameraState = null;
|
|
529
|
+
// Scroll tracking state
|
|
530
|
+
_minScroll = 0;
|
|
531
|
+
_maxScroll = 0;
|
|
532
|
+
_lastTouchY = 0;
|
|
533
|
+
_lastTouchX = 0;
|
|
534
|
+
_isDragging = false;
|
|
535
|
+
// Transition animation state
|
|
536
|
+
_transitionAnim = null;
|
|
537
|
+
_backward = false;
|
|
538
|
+
get engine() {
|
|
539
|
+
return this._engine;
|
|
540
|
+
}
|
|
541
|
+
get progress() {
|
|
542
|
+
return this._progress;
|
|
543
|
+
}
|
|
544
|
+
getProgress() {
|
|
545
|
+
this._assertNotDestroyed();
|
|
546
|
+
return this._progress;
|
|
547
|
+
}
|
|
548
|
+
get sceneIndex() {
|
|
549
|
+
return this._sceneIndex;
|
|
550
|
+
}
|
|
551
|
+
getSceneIndex() {
|
|
552
|
+
this._assertNotDestroyed();
|
|
553
|
+
return this._sceneIndex;
|
|
554
|
+
}
|
|
555
|
+
get scenes() {
|
|
556
|
+
return this._bundle?.scenes ?? [];
|
|
557
|
+
}
|
|
558
|
+
get paused() {
|
|
559
|
+
return this._paused;
|
|
560
|
+
}
|
|
561
|
+
get currentScene() {
|
|
562
|
+
return this.scenes[this._sceneIndex];
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Initialize the controller with a compiled bundle and optional runtime resources.
|
|
566
|
+
*/
|
|
567
|
+
init(bundle, config = {}, runtime = {}) {
|
|
568
|
+
this._assertNotDestroyed();
|
|
569
|
+
if (!bundle.scenes || bundle.scenes.length === 0) {
|
|
570
|
+
throw new ControllerError("CONTROLLER_NO_SCENES", "init() requires at least one compiled scene stop");
|
|
571
|
+
}
|
|
572
|
+
this._cancelFrame();
|
|
573
|
+
this._bundle = bundle;
|
|
574
|
+
this._engine = runtime.engine ?? new AnimationEngine();
|
|
575
|
+
this._ownsEngine = !runtime.engine;
|
|
576
|
+
this._sceneIndex = 0;
|
|
577
|
+
this._progress = 0;
|
|
578
|
+
this._paused = false;
|
|
579
|
+
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
580
|
+
this._sceneElement = runtime.sceneElement ?? config.sceneElement ?? null;
|
|
581
|
+
this._cameraState = {
|
|
582
|
+
viewBox: getResolvedViewBox(bundle),
|
|
583
|
+
target: { type: "reset" },
|
|
584
|
+
isZoomed: false
|
|
585
|
+
};
|
|
586
|
+
this._engine.init(bundle);
|
|
587
|
+
this._bindScroll();
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Set scroll progress (0–1, clamped) and trigger frame update.
|
|
591
|
+
*/
|
|
592
|
+
setProgress(progress) {
|
|
593
|
+
this._assertNotDestroyed();
|
|
594
|
+
if (!Number.isFinite(progress)) {
|
|
595
|
+
throw new ControllerError("CONTROLLER_PROGRESS_OUT_OF_RANGE", "setProgress() requires a finite progress value");
|
|
596
|
+
}
|
|
597
|
+
this._cancelTransition();
|
|
598
|
+
this._setProgress(progress);
|
|
599
|
+
}
|
|
600
|
+
_setProgress(progress) {
|
|
601
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
602
|
+
if (clamped === this._progress && !this._paused && this._rafId !== null) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this._progress = clamped;
|
|
606
|
+
this._cancelCameraAnimation();
|
|
607
|
+
if (this._paused) return;
|
|
608
|
+
this._scheduleProgressForward(clamped);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Navigate to next scene (wraps to 0 if at end).
|
|
612
|
+
*/
|
|
613
|
+
nextScene() {
|
|
614
|
+
this._assertNotDestroyed();
|
|
615
|
+
if (this.scenes.length <= 1) return;
|
|
616
|
+
const nextIndex = (this._sceneIndex + 1) % this.scenes.length;
|
|
617
|
+
this._transitionToScene(nextIndex);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Navigate to previous scene (wraps to last if at beginning).
|
|
621
|
+
*/
|
|
622
|
+
prevScene() {
|
|
623
|
+
this._assertNotDestroyed();
|
|
624
|
+
if (this.scenes.length <= 1) return;
|
|
625
|
+
const prevIndex = (this._sceneIndex - 1 + this.scenes.length) % this.scenes.length;
|
|
626
|
+
this._transitionToScene(prevIndex);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Set scene index directly.
|
|
630
|
+
*/
|
|
631
|
+
setSceneIndex(index) {
|
|
632
|
+
this._assertNotDestroyed();
|
|
633
|
+
if (index < 0 || index >= this.scenes.length) {
|
|
634
|
+
throw new ControllerError(
|
|
635
|
+
"CONTROLLER_SCENE_INDEX_OUT_OF_RANGE",
|
|
636
|
+
`Scene index ${index} is out of bounds [0, ${this.scenes.length - 1}]`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
this._transitionToScene(index);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Pause all animations.
|
|
643
|
+
*/
|
|
644
|
+
pause() {
|
|
645
|
+
this._assertNotDestroyed();
|
|
646
|
+
if (this._paused) return;
|
|
647
|
+
this._paused = true;
|
|
648
|
+
this._cancelFrame();
|
|
649
|
+
this._cancelTransition();
|
|
650
|
+
this._engine.pause();
|
|
651
|
+
this._applyPauseState(true);
|
|
652
|
+
this._emit("paused");
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Resume from paused state.
|
|
656
|
+
*/
|
|
657
|
+
resume() {
|
|
658
|
+
this._assertNotDestroyed();
|
|
659
|
+
if (!this._paused) return;
|
|
660
|
+
this._paused = false;
|
|
661
|
+
this._engine.resume();
|
|
662
|
+
this._applyPauseState(false);
|
|
663
|
+
this._scheduleProgressForward(this._progress);
|
|
664
|
+
this._emit("resumed");
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Check if controller is paused.
|
|
668
|
+
*/
|
|
669
|
+
isPaused() {
|
|
670
|
+
this._assertNotDestroyed();
|
|
671
|
+
return this._paused;
|
|
672
|
+
}
|
|
673
|
+
zoomToElement(id, options = {}) {
|
|
674
|
+
this._assertNotDestroyed();
|
|
675
|
+
const svg = this._getSceneSvg();
|
|
676
|
+
if (!this._bundle || !svg) {
|
|
677
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToElement() requires an initialized scene SVG");
|
|
678
|
+
}
|
|
679
|
+
const elementBounds = getCurrentElementBounds(svg, id);
|
|
680
|
+
if (!elementBounds) {
|
|
681
|
+
if (getElementState(svg, id)) {
|
|
682
|
+
throw new ControllerError("CAMERA_TARGET_NOT_VISIBLE", `Camera target element "${id}" is currently removed`, {
|
|
683
|
+
elementId: id
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${id}" was not found`, {
|
|
687
|
+
elementId: id
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
this._applyCameraDestination(
|
|
691
|
+
expandViewBox(elementBounds, this._resolveCameraPadding(options)),
|
|
692
|
+
{ type: "element", id },
|
|
693
|
+
options
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
zoomToArea(area, options = {}) {
|
|
697
|
+
this._assertNotDestroyed();
|
|
698
|
+
if (!this._bundle || !this._getSceneSvg()) {
|
|
699
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToArea() requires an initialized scene SVG");
|
|
700
|
+
}
|
|
701
|
+
this._assertValidCameraArea(area);
|
|
702
|
+
const bounds = getGridAreaBounds(this._bundle, area);
|
|
703
|
+
this._applyCameraDestination(
|
|
704
|
+
expandViewBox(bounds, this._resolveCameraPadding(options)),
|
|
705
|
+
{
|
|
706
|
+
type: "area",
|
|
707
|
+
at: [...area.at],
|
|
708
|
+
size: [...area.size]
|
|
709
|
+
},
|
|
710
|
+
options
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
resetZoom(options = {}) {
|
|
714
|
+
this._assertNotDestroyed();
|
|
715
|
+
if (!this._bundle || !this._getSceneSvg()) {
|
|
716
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "resetZoom() requires an initialized scene SVG");
|
|
717
|
+
}
|
|
718
|
+
this._applyCameraDestination(getResolvedViewBox(this._bundle), { type: "reset" }, options);
|
|
719
|
+
}
|
|
720
|
+
getCameraState() {
|
|
721
|
+
this._assertNotDestroyed();
|
|
722
|
+
if (!this._cameraState) {
|
|
723
|
+
throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera state is not initialized");
|
|
724
|
+
}
|
|
725
|
+
return copyCameraState(this._cameraState);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Destroy controller and clean up all listeners and resources.
|
|
729
|
+
*/
|
|
730
|
+
destroy() {
|
|
731
|
+
this._assertNotDestroyed();
|
|
732
|
+
this._unbindScroll();
|
|
733
|
+
this._cancelFrame();
|
|
734
|
+
this._cancelTransition();
|
|
735
|
+
this._cancelCameraAnimation();
|
|
736
|
+
this._getSceneSvg()?.classList.add("iso-motion-stopped");
|
|
737
|
+
if (this._ownsEngine) this._engine.destroy();
|
|
738
|
+
this._listeners.clear();
|
|
739
|
+
this._bundle = null;
|
|
740
|
+
this._container = null;
|
|
741
|
+
this._pendingProgress = null;
|
|
742
|
+
this._cameraState = null;
|
|
743
|
+
this._destroyed = true;
|
|
744
|
+
}
|
|
745
|
+
// ── Event system ───────────────────────────────────────────────────────
|
|
746
|
+
on(event, listener) {
|
|
747
|
+
this._assertNotDestroyed();
|
|
748
|
+
const set = this._listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
749
|
+
set.add(listener);
|
|
750
|
+
this._listeners.set(event, set);
|
|
751
|
+
}
|
|
752
|
+
off(event, listener) {
|
|
753
|
+
this._assertNotDestroyed();
|
|
754
|
+
const set = this._listeners.get(event);
|
|
755
|
+
if (set) {
|
|
756
|
+
set.delete(listener);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
_emit(event, ...args) {
|
|
760
|
+
const set = this._listeners.get(event);
|
|
761
|
+
if (set) {
|
|
762
|
+
for (const listener of set) {
|
|
763
|
+
try {
|
|
764
|
+
listener(...args);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
queueMicrotask(() => {
|
|
767
|
+
throw error;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// ── Frame update ───────────────────────────────────────────────────────
|
|
774
|
+
_scheduleProgressForward(progress) {
|
|
775
|
+
this._pendingProgress = progress;
|
|
776
|
+
if (this._rafId !== null) return;
|
|
777
|
+
this._rafId = requestAnimationFrame(() => {
|
|
778
|
+
this._rafId = null;
|
|
779
|
+
const pending = this._pendingProgress;
|
|
780
|
+
this._pendingProgress = null;
|
|
781
|
+
if (pending === null || this._paused || this._destroyed) return;
|
|
782
|
+
this._backward = pending < this._engine.getProgress();
|
|
783
|
+
this._engine.setProgress(pending);
|
|
784
|
+
this._applyFrameUpdate();
|
|
785
|
+
this._emit("progress-change", pending);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
_cancelFrame() {
|
|
789
|
+
if (this._rafId !== null) {
|
|
790
|
+
cancelAnimationFrame(this._rafId);
|
|
791
|
+
this._rafId = null;
|
|
792
|
+
}
|
|
793
|
+
this._pendingProgress = null;
|
|
794
|
+
}
|
|
795
|
+
_applyFrameUpdate() {
|
|
796
|
+
if (!this._bundle) return;
|
|
797
|
+
const svg = this._sceneElement ?? this._container?.querySelector("svg") ?? null;
|
|
798
|
+
if (!svg) return;
|
|
799
|
+
const updates = this._engine.getFrameUpdates().map((update) => ({
|
|
800
|
+
id: update.id,
|
|
801
|
+
asset: update.asset,
|
|
802
|
+
pos: update.pos,
|
|
803
|
+
size: update.size,
|
|
804
|
+
layer: update.layer,
|
|
805
|
+
presence: update.lifecycle,
|
|
806
|
+
enter: update.entry,
|
|
807
|
+
exit: update.exit,
|
|
808
|
+
ambient: update.ambient,
|
|
809
|
+
text: update.text,
|
|
810
|
+
primitive: update.primitive,
|
|
811
|
+
activity: update.activity
|
|
812
|
+
}));
|
|
813
|
+
const connectors = this._engine.getConnectorFrameUpdates().map((update) => ({
|
|
814
|
+
id: update.id,
|
|
815
|
+
route: update.route,
|
|
816
|
+
layer: update.layer,
|
|
817
|
+
presence: update.lifecycle,
|
|
818
|
+
style: update.style,
|
|
819
|
+
start: update.start,
|
|
820
|
+
end: update.end,
|
|
821
|
+
direction: update.direction,
|
|
822
|
+
message: update.message,
|
|
823
|
+
enter: update.entry,
|
|
824
|
+
exit: update.exit,
|
|
825
|
+
ambient: update.ambient
|
|
826
|
+
}));
|
|
827
|
+
updateElementTransforms(svg, updates, connectors);
|
|
828
|
+
this._applyLifecycleChanges(updates);
|
|
829
|
+
this._applyConnectorLifecycleChanges(connectors);
|
|
830
|
+
this._applyCameraForProgress(this._progress);
|
|
831
|
+
}
|
|
832
|
+
_applyCameraForProgress(progress) {
|
|
833
|
+
const svg = this._getSceneSvg();
|
|
834
|
+
if (!this._bundle || !svg) return;
|
|
835
|
+
const sceneStops = this.scenes;
|
|
836
|
+
if (sceneStops.length === 0) return;
|
|
837
|
+
const { previous, next } = surroundingStops(sceneStops, progress);
|
|
838
|
+
const previousCamera = this._effectiveCameraAt(previous.index);
|
|
839
|
+
const nextCamera = this._effectiveCameraAt(next.index);
|
|
840
|
+
const previousViewBox = this._resolveCameraViewBox(previousCamera, { forInterpolation: true });
|
|
841
|
+
const nextViewBox = this._resolveCameraViewBox(nextCamera, { forInterpolation: true });
|
|
842
|
+
const range = next.scene.progress - previous.scene.progress;
|
|
843
|
+
const rawT = range <= 0 ? 0 : (progress - previous.scene.progress) / range;
|
|
844
|
+
const t = resolveCameraEasing(nextCamera.easing)(Math.max(0, Math.min(1, rawT)));
|
|
845
|
+
const viewBox = lerpViewBox(previousViewBox, nextViewBox, t);
|
|
846
|
+
const target = t < 1 ? previousCamera.target : nextCamera.target;
|
|
847
|
+
this._setCameraViewBox(svg, viewBox, target);
|
|
848
|
+
}
|
|
849
|
+
_effectiveCameraAt(index) {
|
|
850
|
+
for (let i = index; i >= 0; i--) {
|
|
851
|
+
const camera = this.scenes[i]?.camera;
|
|
852
|
+
if (camera) {
|
|
853
|
+
return {
|
|
854
|
+
target: camera.target,
|
|
855
|
+
padding: camera.padding,
|
|
856
|
+
duration: camera.duration,
|
|
857
|
+
easing: camera.easing
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return { target: { type: "reset" } };
|
|
862
|
+
}
|
|
863
|
+
_resolveCameraViewBox(camera, options = {}) {
|
|
864
|
+
if (!this._bundle) throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized bundle");
|
|
865
|
+
if (camera.target.type === "reset") return getResolvedViewBox(this._bundle);
|
|
866
|
+
if (camera.target.type === "area") {
|
|
867
|
+
return expandViewBox(
|
|
868
|
+
getGridAreaBounds(this._bundle, { at: camera.target.at, size: camera.target.size }),
|
|
869
|
+
camera.padding ?? 32
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const svg = this._getSceneSvg();
|
|
873
|
+
if (!svg) throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
|
|
874
|
+
const bounds = options.forInterpolation ? this._resolveElementBoundsForInterpolation(svg, camera.target.id) : getCurrentElementBounds(svg, camera.target.id);
|
|
875
|
+
if (!bounds) {
|
|
876
|
+
throw new ControllerError(
|
|
877
|
+
"CAMERA_TARGET_NOT_FOUND",
|
|
878
|
+
`Camera target element "${camera.target.id}" was not found`,
|
|
879
|
+
{
|
|
880
|
+
elementId: camera.target.id
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
return expandViewBox(bounds, camera.padding ?? 32);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Resolve element bounds for scroll/setProgress camera interpolation.
|
|
888
|
+
*
|
|
889
|
+
* Unlike `getCurrentElementBounds()`, this tolerates an element whose
|
|
890
|
+
* resolved presence is transiently "removed" because progress has not yet
|
|
891
|
+
* reached the stop that introduces it (the engine still carries valid
|
|
892
|
+
* authored geometry for it via nearest-geometry fallback). It falls back to
|
|
893
|
+
* that geometry instead of reporting the target as missing so a scene that
|
|
894
|
+
* both adds an element and focuses the camera on it in the same stop can be
|
|
895
|
+
* interpolated into smoothly. A genuinely unknown element id still resolves
|
|
896
|
+
* to `undefined` so callers can throw `CAMERA_TARGET_NOT_FOUND`.
|
|
897
|
+
*/
|
|
898
|
+
_resolveElementBoundsForInterpolation(svg, id) {
|
|
899
|
+
const direct = getCurrentElementBounds(svg, id);
|
|
900
|
+
if (direct) return direct;
|
|
901
|
+
const state = getElementState(svg, id);
|
|
902
|
+
if (!state || !this._bundle) return void 0;
|
|
903
|
+
const layout = getResolvedProjectionLayout(this._bundle);
|
|
904
|
+
const element = state.current;
|
|
905
|
+
const screen = projectToScreen(
|
|
906
|
+
element.pos[0] + element.size,
|
|
907
|
+
element.pos[1] + element.size,
|
|
908
|
+
layout.cellSize,
|
|
909
|
+
layout.selectedBounds.minX,
|
|
910
|
+
layout.selectedBounds.minY,
|
|
911
|
+
layout.padding.x,
|
|
912
|
+
layout.padding.y
|
|
913
|
+
);
|
|
914
|
+
const visualSize = calculateVisualSize(element.size, layout.cellSize);
|
|
915
|
+
const [anchorX, anchorY] = state.anchor;
|
|
916
|
+
return {
|
|
917
|
+
minX: roundCameraNumber(screen.screenX - visualSize * anchorX),
|
|
918
|
+
minY: roundCameraNumber(screen.screenY - visualSize * anchorY),
|
|
919
|
+
width: roundCameraNumber(Math.max(1, visualSize)),
|
|
920
|
+
height: roundCameraNumber(Math.max(1, visualSize))
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
_applyCameraDestination(viewBox, target, options) {
|
|
924
|
+
const svg = this._getSceneSvg();
|
|
925
|
+
if (!svg) throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
|
|
926
|
+
this._cancelCameraAnimation();
|
|
927
|
+
const duration = this._resolveCameraDuration(options);
|
|
928
|
+
if (duration === 0) {
|
|
929
|
+
this._setCameraViewBox(svg, viewBox, target);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const from = currentSvgViewBox(svg) ?? this._cameraState?.viewBox ?? viewBox;
|
|
933
|
+
const easing = resolveCameraEasing(options.easing ?? this._config.transitionEasing);
|
|
934
|
+
const start = performance.now();
|
|
935
|
+
const step = (now) => {
|
|
936
|
+
const t = Math.min((now - start) / duration, 1);
|
|
937
|
+
this._setCameraViewBox(svg, lerpViewBox(from, viewBox, easing(t)), target);
|
|
938
|
+
if (t < 1) {
|
|
939
|
+
this._cameraAnim = requestAnimationFrame(step);
|
|
940
|
+
} else {
|
|
941
|
+
this._cameraAnim = null;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
this._cameraAnim = requestAnimationFrame(step);
|
|
945
|
+
}
|
|
946
|
+
_setCameraViewBox(svg, viewBox, target) {
|
|
947
|
+
applySceneViewBox(svg, viewBox);
|
|
948
|
+
const full = this._bundle ? getResolvedViewBox(this._bundle) : viewBox;
|
|
949
|
+
this._cameraState = {
|
|
950
|
+
viewBox,
|
|
951
|
+
target,
|
|
952
|
+
isZoomed: !sameViewBox(viewBox, full)
|
|
953
|
+
};
|
|
954
|
+
this._emit("camera-change", this.getCameraState());
|
|
955
|
+
}
|
|
956
|
+
_getSceneSvg() {
|
|
957
|
+
return this._sceneElement ?? this._container?.querySelector("svg") ?? null;
|
|
958
|
+
}
|
|
959
|
+
_resolveCameraPadding(options) {
|
|
960
|
+
const padding = options.padding ?? 32;
|
|
961
|
+
if (!Number.isFinite(padding) || padding < 0 || padding > 2048) {
|
|
962
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera padding must be between 0 and 2048");
|
|
963
|
+
}
|
|
964
|
+
return padding;
|
|
965
|
+
}
|
|
966
|
+
_resolveCameraDuration(options) {
|
|
967
|
+
const duration = options.duration ?? this._config.transitionDuration;
|
|
968
|
+
if (!Number.isInteger(duration) || duration < 0 || duration > 1e4) {
|
|
969
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera duration must be an integer from 0 to 10000");
|
|
970
|
+
}
|
|
971
|
+
return duration;
|
|
972
|
+
}
|
|
973
|
+
_assertValidCameraArea(area) {
|
|
974
|
+
if (!Array.isArray(area.at) || area.at.length !== 2 || !area.at.every((value) => Number.isFinite(value) && value >= 0) || !Array.isArray(area.size) || area.size.length !== 2 || !area.size.every((value) => Number.isFinite(value) && value > 0)) {
|
|
975
|
+
throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera area must use non-negative at and positive size");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
_applyLifecycleChanges(elements) {
|
|
979
|
+
for (const elDef of elements) {
|
|
980
|
+
const transition = this._engine.getLifecycleTransition(elDef.id);
|
|
981
|
+
if (!transition) continue;
|
|
982
|
+
const svgForState = this._sceneElement ?? this._container?.querySelector("svg");
|
|
983
|
+
if (!svgForState) continue;
|
|
984
|
+
const state = getElementState(svgForState, elDef.id);
|
|
985
|
+
if (!state) continue;
|
|
986
|
+
if (transition.to === "entering" || transition.to === "present") {
|
|
987
|
+
state.isHidden = false;
|
|
988
|
+
unhideElementOnReadd(state.node);
|
|
989
|
+
}
|
|
990
|
+
if (isForwardEntryTransition(transition, this._backward)) {
|
|
991
|
+
this._applyEntryAnimation(elDef, state);
|
|
992
|
+
}
|
|
993
|
+
if (isReverseExitTransition(transition, this._backward)) {
|
|
994
|
+
this._applyExitAnimation({ ...elDef, exit: oppositeExitAnimation(elDef.enter ?? "fade-in") }, state);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (isForwardExitTransition(transition, this._backward)) {
|
|
998
|
+
this._applyExitAnimation(elDef, state);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (isReverseEntryTransition(transition, this._backward)) {
|
|
1002
|
+
state.isHidden = false;
|
|
1003
|
+
unhideElementOnReadd(state.node);
|
|
1004
|
+
this._applyEntryAnimation({ ...elDef, enter: oppositeEntryAnimation(elDef.exit ?? "fade-out") }, state);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (transition.to === "removed") {
|
|
1008
|
+
state.isHidden = true;
|
|
1009
|
+
hideElementAfterExit(state.node);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
_applyConnectorLifecycleChanges(connectors) {
|
|
1014
|
+
for (const connectorDef of connectors) {
|
|
1015
|
+
const transition = this._engine.getConnectorLifecycleTransition(connectorDef.id);
|
|
1016
|
+
if (!transition) continue;
|
|
1017
|
+
const svgForState = this._sceneElement ?? this._container?.querySelector("svg");
|
|
1018
|
+
if (!svgForState) continue;
|
|
1019
|
+
const state = getConnectorState(svgForState, connectorDef.id);
|
|
1020
|
+
if (!state) continue;
|
|
1021
|
+
if (transition.to === "entering" || transition.to === "present") {
|
|
1022
|
+
state.isHidden = false;
|
|
1023
|
+
unhideElementOnReadd(state.node);
|
|
1024
|
+
}
|
|
1025
|
+
if (isForwardEntryTransition(transition, this._backward)) {
|
|
1026
|
+
this._applyConnectorEntryAnimation(connectorDef, state);
|
|
1027
|
+
}
|
|
1028
|
+
if (isReverseExitTransition(transition, this._backward)) {
|
|
1029
|
+
this._applyConnectorExitAnimation(
|
|
1030
|
+
{
|
|
1031
|
+
...connectorDef,
|
|
1032
|
+
exit: oppositeExitAnimation(connectorDef.enter ?? "fade-in")
|
|
1033
|
+
},
|
|
1034
|
+
state
|
|
1035
|
+
);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (isForwardExitTransition(transition, this._backward)) {
|
|
1039
|
+
this._applyConnectorExitAnimation(connectorDef, state);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (isReverseEntryTransition(transition, this._backward)) {
|
|
1043
|
+
state.isHidden = false;
|
|
1044
|
+
unhideElementOnReadd(state.node);
|
|
1045
|
+
this._applyConnectorEntryAnimation(
|
|
1046
|
+
{
|
|
1047
|
+
...connectorDef,
|
|
1048
|
+
enter: oppositeEntryAnimation(connectorDef.exit ?? "fade-out")
|
|
1049
|
+
},
|
|
1050
|
+
state
|
|
1051
|
+
);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (transition.to === "removed") {
|
|
1055
|
+
state.isHidden = true;
|
|
1056
|
+
hideElementAfterExit(state.node);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
_applyEntryAnimation(elDef, state) {
|
|
1061
|
+
const entryAnim = elDef.enter ?? "fade-in";
|
|
1062
|
+
if (entryAnim === "none") return;
|
|
1063
|
+
animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
|
|
1064
|
+
const expectedAnimation = state.node.style.animation;
|
|
1065
|
+
state.node.addEventListener(
|
|
1066
|
+
"animationend",
|
|
1067
|
+
() => {
|
|
1068
|
+
if (state.node.style.animation === expectedAnimation) {
|
|
1069
|
+
state.node.style.animation = "";
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
{ once: true }
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
_applyExitAnimation(elDef, state) {
|
|
1076
|
+
const exitAnim = elDef.exit ?? "fade-out";
|
|
1077
|
+
if (exitAnim === "none") {
|
|
1078
|
+
hideElementAfterExit(state.node);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
|
|
1082
|
+
const expectedAnimation = state.node.style.animation;
|
|
1083
|
+
state.node.addEventListener(
|
|
1084
|
+
"animationend",
|
|
1085
|
+
() => {
|
|
1086
|
+
if (state.node.style.animation === expectedAnimation) {
|
|
1087
|
+
hideElementAfterExit(state.node);
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
{ once: true }
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
_applyConnectorEntryAnimation(connectorDef, state) {
|
|
1094
|
+
const entryAnim = connectorDef.enter ?? "fade-in";
|
|
1095
|
+
if (entryAnim === "none") return;
|
|
1096
|
+
animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
|
|
1097
|
+
const expectedAnimation = state.node.style.animation;
|
|
1098
|
+
state.node.addEventListener(
|
|
1099
|
+
"animationend",
|
|
1100
|
+
() => {
|
|
1101
|
+
if (state.node.style.animation === expectedAnimation) {
|
|
1102
|
+
state.node.style.animation = "";
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
{ once: true }
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
_applyConnectorExitAnimation(connectorDef, state) {
|
|
1109
|
+
const exitAnim = connectorDef.exit ?? "fade-out";
|
|
1110
|
+
if (exitAnim === "none") {
|
|
1111
|
+
hideElementAfterExit(state.node);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
|
|
1115
|
+
const expectedAnimation = state.node.style.animation;
|
|
1116
|
+
state.node.addEventListener(
|
|
1117
|
+
"animationend",
|
|
1118
|
+
() => {
|
|
1119
|
+
if (state.node.style.animation === expectedAnimation) {
|
|
1120
|
+
hideElementAfterExit(state.node);
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
{ once: true }
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
// ── Scene transitions ──────────────────────────────────────────────────
|
|
1127
|
+
_transitionToScene(index) {
|
|
1128
|
+
this._cancelTransition();
|
|
1129
|
+
const stop = this.scenes[index];
|
|
1130
|
+
const from = this._progress;
|
|
1131
|
+
const to = stop.progress;
|
|
1132
|
+
if (index === this._sceneIndex && from === to) return;
|
|
1133
|
+
this._sceneIndex = index;
|
|
1134
|
+
this._emit("scene-change", index);
|
|
1135
|
+
const duration = this._config.transitionDuration;
|
|
1136
|
+
if (duration > 0 && from !== to) {
|
|
1137
|
+
this._animateProgress(from, to, duration);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
this._progress = to;
|
|
1141
|
+
this._cancelCameraAnimation();
|
|
1142
|
+
this._scheduleProgressForward(to);
|
|
1143
|
+
}
|
|
1144
|
+
_animateProgress(from, to, duration) {
|
|
1145
|
+
const easing = resolveEasing(
|
|
1146
|
+
this._config.transitionEasing === "ease-in-out" ? "easeInOutCubic" : this._config.transitionEasing === "ease-out" ? "easeOutCubic" : "linear"
|
|
1147
|
+
);
|
|
1148
|
+
const start = performance.now();
|
|
1149
|
+
const step = (now) => {
|
|
1150
|
+
const elapsed = now - start;
|
|
1151
|
+
const t = Math.min(elapsed / duration, 1);
|
|
1152
|
+
const easedT = easing(t);
|
|
1153
|
+
const currentProgress = from + (to - from) * easedT;
|
|
1154
|
+
this._setProgress(currentProgress);
|
|
1155
|
+
if (t < 1) {
|
|
1156
|
+
this._transitionAnim = requestAnimationFrame(step);
|
|
1157
|
+
} else {
|
|
1158
|
+
this._transitionAnim = null;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
this._transitionAnim = requestAnimationFrame(step);
|
|
1162
|
+
}
|
|
1163
|
+
_cancelTransition() {
|
|
1164
|
+
if (this._transitionAnim !== null) {
|
|
1165
|
+
cancelAnimationFrame(this._transitionAnim);
|
|
1166
|
+
this._transitionAnim = null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
_cancelCameraAnimation() {
|
|
1170
|
+
if (this._cameraAnim !== null) {
|
|
1171
|
+
cancelAnimationFrame(this._cameraAnim);
|
|
1172
|
+
this._cameraAnim = null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// ── Scroll binding ─────────────────────────────────────────────────────
|
|
1176
|
+
_bindScroll() {
|
|
1177
|
+
if (this._config.keyboardControls) {
|
|
1178
|
+
document.addEventListener("keydown", this._onKeyDown);
|
|
1179
|
+
}
|
|
1180
|
+
const container = this._config.container;
|
|
1181
|
+
if (!container) return;
|
|
1182
|
+
this._container = container;
|
|
1183
|
+
this._calculateScrollBounds();
|
|
1184
|
+
container.addEventListener("scroll", this._onScroll, { passive: true });
|
|
1185
|
+
window.addEventListener("resize", this._onResize, { passive: true });
|
|
1186
|
+
if (this._config.touchControls) {
|
|
1187
|
+
container.addEventListener("touchstart", this._onTouchStart, {
|
|
1188
|
+
passive: true
|
|
1189
|
+
});
|
|
1190
|
+
container.addEventListener("touchmove", this._onTouchMove, {
|
|
1191
|
+
passive: false
|
|
1192
|
+
});
|
|
1193
|
+
container.addEventListener("touchend", this._onTouchEnd);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
_unbindScroll() {
|
|
1197
|
+
document.removeEventListener("keydown", this._onKeyDown);
|
|
1198
|
+
const container = this._config.container;
|
|
1199
|
+
if (!container) return;
|
|
1200
|
+
container.removeEventListener("scroll", this._onScroll);
|
|
1201
|
+
window.removeEventListener("resize", this._onResize);
|
|
1202
|
+
container.removeEventListener("touchstart", this._onTouchStart);
|
|
1203
|
+
container.removeEventListener("touchmove", this._onTouchMove);
|
|
1204
|
+
container.removeEventListener("touchend", this._onTouchEnd);
|
|
1205
|
+
}
|
|
1206
|
+
_calculateScrollBounds() {
|
|
1207
|
+
const container = this._config.container;
|
|
1208
|
+
if (!container) return;
|
|
1209
|
+
const offset = this._config.scrollOffset ?? {};
|
|
1210
|
+
if (this._config.scrollDirection === "horizontal") {
|
|
1211
|
+
this._minScroll = offset.left ?? 0;
|
|
1212
|
+
this._maxScroll = container.scrollWidth - container.clientWidth - (offset.right ?? 0);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
this._minScroll = offset.top ?? 0;
|
|
1216
|
+
this._maxScroll = container.scrollHeight - container.clientHeight - (offset.bottom ?? 0);
|
|
1217
|
+
}
|
|
1218
|
+
_onScroll = () => {
|
|
1219
|
+
if (this._paused || this._destroyed) return;
|
|
1220
|
+
const container = this._config.container;
|
|
1221
|
+
if (!container) return;
|
|
1222
|
+
const currentScroll = this._config.scrollDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
|
|
1223
|
+
const range = this._maxScroll - this._minScroll;
|
|
1224
|
+
if (range <= 0) return;
|
|
1225
|
+
const rawProgress = (currentScroll - this._minScroll) / range;
|
|
1226
|
+
const sensitivity = this._config.scrollSensitivity ?? 1;
|
|
1227
|
+
const scaledProgress = rawProgress * sensitivity;
|
|
1228
|
+
const clampedProgress = Math.max(
|
|
1229
|
+
this._config.minProgress ?? 0,
|
|
1230
|
+
Math.min(this._config.maxProgress ?? 1, scaledProgress)
|
|
1231
|
+
);
|
|
1232
|
+
if (clampedProgress !== this._progress) {
|
|
1233
|
+
this.setProgress(clampedProgress);
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
_onResize = () => {
|
|
1237
|
+
if (this._destroyed) return;
|
|
1238
|
+
this._calculateScrollBounds();
|
|
1239
|
+
};
|
|
1240
|
+
_onKeyDown = (e) => {
|
|
1241
|
+
if (this._destroyed) return;
|
|
1242
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown" || e.key === " ") {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
this.nextScene();
|
|
1245
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
1246
|
+
e.preventDefault();
|
|
1247
|
+
this.prevScene();
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
_onTouchStart = (e) => {
|
|
1251
|
+
if (this._destroyed) return;
|
|
1252
|
+
this._isDragging = true;
|
|
1253
|
+
const touch = e.touches[0];
|
|
1254
|
+
if (this._config.scrollDirection === "horizontal") {
|
|
1255
|
+
this._lastTouchX = touch.clientX;
|
|
1256
|
+
} else {
|
|
1257
|
+
this._lastTouchY = touch.clientY;
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
_onTouchMove = (e) => {
|
|
1261
|
+
if (this._destroyed) return;
|
|
1262
|
+
if (!this._isDragging) return;
|
|
1263
|
+
const touch = e.touches[0];
|
|
1264
|
+
const isHorizontal = this._config.scrollDirection === "horizontal";
|
|
1265
|
+
const delta = isHorizontal ? this._lastTouchX - touch.clientX : this._lastTouchY - touch.clientY;
|
|
1266
|
+
if (isHorizontal) {
|
|
1267
|
+
this._lastTouchX = touch.clientX;
|
|
1268
|
+
} else {
|
|
1269
|
+
this._lastTouchY = touch.clientY;
|
|
1270
|
+
}
|
|
1271
|
+
const sensitivity = this._config.scrollSensitivity ?? 1;
|
|
1272
|
+
const progressDelta = delta / 300 * sensitivity;
|
|
1273
|
+
const newProgress = Math.max(0, Math.min(1, this._progress + progressDelta));
|
|
1274
|
+
this.setProgress(newProgress);
|
|
1275
|
+
};
|
|
1276
|
+
_onTouchEnd = () => {
|
|
1277
|
+
if (this._destroyed) return;
|
|
1278
|
+
this._isDragging = false;
|
|
1279
|
+
};
|
|
1280
|
+
// ── Pause state ────────────────────────────────────────────────────────
|
|
1281
|
+
_applyPauseState(pause) {
|
|
1282
|
+
const svg = this._getSceneSvg();
|
|
1283
|
+
if (!svg) return;
|
|
1284
|
+
if (pause) svg.classList.add("iso-motion-paused");
|
|
1285
|
+
else svg.classList.remove("iso-motion-paused");
|
|
1286
|
+
const playState = pause ? "paused" : "running";
|
|
1287
|
+
const ambientElements = svg.querySelectorAll('[class*="iso-ambient-"]');
|
|
1288
|
+
for (let i = 0; i < ambientElements.length; i++) {
|
|
1289
|
+
const el = ambientElements[i];
|
|
1290
|
+
el.style.animationPlayState = playState;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
_assertNotDestroyed() {
|
|
1294
|
+
if (!this._destroyed) return;
|
|
1295
|
+
throw new ControllerError("CONTROLLER_DESTROYED", "AnimationController has been destroyed");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function isForwardEntryTransition(transition, backward) {
|
|
1299
|
+
return !backward && (transition.from === "removed" || transition.from === "exiting") && (transition.to === "entering" || transition.to === "present");
|
|
1300
|
+
}
|
|
1301
|
+
function isForwardExitTransition(transition, backward) {
|
|
1302
|
+
return !backward && (transition.to === "exiting" || transition.to === "removed" && transition.from !== "exiting");
|
|
1303
|
+
}
|
|
1304
|
+
function isReverseExitTransition(transition, backward) {
|
|
1305
|
+
return backward && (transition.to === "removed" || transition.to === "exiting") && (transition.from === "entering" || transition.from === "present");
|
|
1306
|
+
}
|
|
1307
|
+
function isReverseEntryTransition(transition, backward) {
|
|
1308
|
+
return backward && (transition.from === "exiting" || transition.from === "removed") && (transition.to === "present" || transition.to === "entering");
|
|
1309
|
+
}
|
|
1310
|
+
function oppositeExitAnimation(entry) {
|
|
1311
|
+
switch (entry) {
|
|
1312
|
+
case "fade-in":
|
|
1313
|
+
return "fade-out";
|
|
1314
|
+
case "fade-in-grow":
|
|
1315
|
+
return "fade-out-shrink";
|
|
1316
|
+
case "fall-in":
|
|
1317
|
+
return "rise-away";
|
|
1318
|
+
case "rise-from-ground":
|
|
1319
|
+
return "fall-through-ground";
|
|
1320
|
+
case "slide-in-left":
|
|
1321
|
+
return "slide-out-left";
|
|
1322
|
+
case "slide-in-right":
|
|
1323
|
+
return "slide-out-right";
|
|
1324
|
+
case "flip-in":
|
|
1325
|
+
return "flip-out";
|
|
1326
|
+
case "none":
|
|
1327
|
+
return "none";
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function oppositeEntryAnimation(exit) {
|
|
1331
|
+
switch (exit) {
|
|
1332
|
+
case "fade-out":
|
|
1333
|
+
return "fade-in";
|
|
1334
|
+
case "fade-out-shrink":
|
|
1335
|
+
return "fade-in-grow";
|
|
1336
|
+
case "fall-through-ground":
|
|
1337
|
+
return "rise-from-ground";
|
|
1338
|
+
case "rise-away":
|
|
1339
|
+
return "fall-in";
|
|
1340
|
+
case "slide-out-left":
|
|
1341
|
+
return "slide-in-left";
|
|
1342
|
+
case "slide-out-right":
|
|
1343
|
+
return "slide-in-right";
|
|
1344
|
+
case "flip-out":
|
|
1345
|
+
return "flip-in";
|
|
1346
|
+
case "none":
|
|
1347
|
+
return "none";
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function surroundingStops(scenes, progress) {
|
|
1351
|
+
let previousIndex = 0;
|
|
1352
|
+
let nextIndex = scenes.length - 1;
|
|
1353
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
1354
|
+
if (scenes[i].progress <= progress) previousIndex = i;
|
|
1355
|
+
if (scenes[i].progress >= progress) {
|
|
1356
|
+
nextIndex = i;
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
previous: { scene: scenes[previousIndex], index: previousIndex },
|
|
1362
|
+
next: { scene: scenes[nextIndex], index: nextIndex }
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function expandViewBox(viewBox, padding) {
|
|
1366
|
+
return {
|
|
1367
|
+
minX: roundCameraNumber(viewBox.minX - padding),
|
|
1368
|
+
minY: roundCameraNumber(viewBox.minY - padding),
|
|
1369
|
+
width: roundCameraNumber(Math.max(1, viewBox.width + padding * 2)),
|
|
1370
|
+
height: roundCameraNumber(Math.max(1, viewBox.height + padding * 2))
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function lerpViewBox(from, to, t) {
|
|
1374
|
+
return {
|
|
1375
|
+
minX: roundCameraNumber(from.minX + (to.minX - from.minX) * t),
|
|
1376
|
+
minY: roundCameraNumber(from.minY + (to.minY - from.minY) * t),
|
|
1377
|
+
width: roundCameraNumber(from.width + (to.width - from.width) * t),
|
|
1378
|
+
height: roundCameraNumber(from.height + (to.height - from.height) * t)
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function sameViewBox(a, b) {
|
|
1382
|
+
return a.minX === b.minX && a.minY === b.minY && a.width === b.width && a.height === b.height;
|
|
1383
|
+
}
|
|
1384
|
+
function currentSvgViewBox(svg) {
|
|
1385
|
+
const raw = svg.getAttribute("viewBox");
|
|
1386
|
+
if (!raw) return void 0;
|
|
1387
|
+
const parts = raw.trim().split(/\s+/).map(Number);
|
|
1388
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) return void 0;
|
|
1389
|
+
return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] };
|
|
1390
|
+
}
|
|
1391
|
+
function resolveCameraEasing(easing) {
|
|
1392
|
+
return resolveEasing(easing === "ease-in-out" ? "easeInOutCubic" : easing === "ease-out" ? "easeOutCubic" : "linear");
|
|
1393
|
+
}
|
|
1394
|
+
function copyCameraState(state) {
|
|
1395
|
+
return {
|
|
1396
|
+
viewBox: { ...state.viewBox },
|
|
1397
|
+
target: state.target?.type === "area" ? { type: "area", at: [...state.target.at], size: [...state.target.size] } : state.target ? { ...state.target } : void 0,
|
|
1398
|
+
isZoomed: state.isZoomed
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function roundCameraNumber(value) {
|
|
1402
|
+
return Math.round(value * 1e3) / 1e3;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const CSS_CLASS_INTERACTIVE_ROOT = "iso-interactive";
|
|
1406
|
+
const CSS_CLASS_HOVER = "iso-hover";
|
|
1407
|
+
class SceneInteractivity {
|
|
1408
|
+
svg;
|
|
1409
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1410
|
+
destroyed = false;
|
|
1411
|
+
attached = false;
|
|
1412
|
+
onClick = (event) => this.handleClick(event);
|
|
1413
|
+
onPointerOver = (event) => this.handlePointerOver(event);
|
|
1414
|
+
onPointerOut = (event) => this.handlePointerOut(event);
|
|
1415
|
+
constructor(svg, interactive) {
|
|
1416
|
+
this.svg = svg;
|
|
1417
|
+
if (interactive) {
|
|
1418
|
+
svg.classList.add(CSS_CLASS_INTERACTIVE_ROOT);
|
|
1419
|
+
svg.addEventListener("click", this.onClick);
|
|
1420
|
+
svg.addEventListener("pointerover", this.onPointerOver);
|
|
1421
|
+
svg.addEventListener("pointerout", this.onPointerOut);
|
|
1422
|
+
this.attached = true;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
/** Subscribe to interactivity events. Returns an unsubscribe function. */
|
|
1426
|
+
on(event, listener) {
|
|
1427
|
+
if (this.destroyed) {
|
|
1428
|
+
throw new RenderError("MOUNT_DESTROYED", "Cannot call on() after destroy()");
|
|
1429
|
+
}
|
|
1430
|
+
const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
1431
|
+
set.add(listener);
|
|
1432
|
+
this.listeners.set(event, set);
|
|
1433
|
+
return () => {
|
|
1434
|
+
set.delete(listener);
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/** Remove the three delegated listeners and all subscriptions. Safe to call more than once. */
|
|
1438
|
+
destroy() {
|
|
1439
|
+
if (this.destroyed) return;
|
|
1440
|
+
this.destroyed = true;
|
|
1441
|
+
if (this.attached) {
|
|
1442
|
+
this.svg.removeEventListener("click", this.onClick);
|
|
1443
|
+
this.svg.removeEventListener("pointerover", this.onPointerOver);
|
|
1444
|
+
this.svg.removeEventListener("pointerout", this.onPointerOut);
|
|
1445
|
+
this.attached = false;
|
|
1446
|
+
}
|
|
1447
|
+
this.listeners.clear();
|
|
1448
|
+
}
|
|
1449
|
+
handleClick(event) {
|
|
1450
|
+
const id = this.resolveVisibleElementId(event.target);
|
|
1451
|
+
if (!id) return;
|
|
1452
|
+
this.emit("element-click", { id, originalEvent: event });
|
|
1453
|
+
}
|
|
1454
|
+
handlePointerOver(event) {
|
|
1455
|
+
const enteredId = this.resolveVisibleElementId(event.target);
|
|
1456
|
+
if (!enteredId) return;
|
|
1457
|
+
const previousId = this.resolveVisibleElementId(event.relatedTarget);
|
|
1458
|
+
if (enteredId === previousId) return;
|
|
1459
|
+
this.toggleHoverClass(enteredId, true);
|
|
1460
|
+
this.emit("element-enter", { id: enteredId, originalEvent: event });
|
|
1461
|
+
}
|
|
1462
|
+
handlePointerOut(event) {
|
|
1463
|
+
const leftId = this.resolveVisibleElementId(event.target);
|
|
1464
|
+
if (!leftId) return;
|
|
1465
|
+
const nextId = this.resolveVisibleElementId(event.relatedTarget);
|
|
1466
|
+
if (leftId === nextId) return;
|
|
1467
|
+
this.toggleHoverClass(leftId, false);
|
|
1468
|
+
this.emit("element-leave", { id: leftId, originalEvent: event });
|
|
1469
|
+
}
|
|
1470
|
+
toggleHoverClass(id, hovered) {
|
|
1471
|
+
const state = getElementState(this.svg, id);
|
|
1472
|
+
if (!state) return;
|
|
1473
|
+
state.node.classList.toggle(CSS_CLASS_HOVER, hovered);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Resolve the scene element id for a pointer event target: walk up
|
|
1477
|
+
* `parentNode` from the event target until the root SVG, looking for the
|
|
1478
|
+
* first ancestor `<g>` that is a member of the engine's element state map
|
|
1479
|
+
* (connector groups also carry `data-id` but are never members of this
|
|
1480
|
+
* map, and floor/defs/diagnostics nodes carry no `data-id` at all). An
|
|
1481
|
+
* element whose current presence is `removed` never resolves.
|
|
1482
|
+
*/
|
|
1483
|
+
resolveVisibleElementId(target) {
|
|
1484
|
+
let node = target;
|
|
1485
|
+
while (node && node !== this.svg) {
|
|
1486
|
+
const id = node.getAttribute?.("data-id");
|
|
1487
|
+
if (id) {
|
|
1488
|
+
const state = getElementState(this.svg, id);
|
|
1489
|
+
if (state && state.current.presence !== "removed") return id;
|
|
1490
|
+
return void 0;
|
|
1491
|
+
}
|
|
1492
|
+
node = node.parentNode;
|
|
1493
|
+
}
|
|
1494
|
+
return void 0;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Invoke every listener for `event`. Exceptions are not caught: consistent
|
|
1498
|
+
* with `AnimationController`'s event system, a throwing listener is
|
|
1499
|
+
* rethrown asynchronously (via `queueMicrotask`) rather than swallowed, so
|
|
1500
|
+
* it surfaces as an unhandled error without breaking delivery to sibling
|
|
1501
|
+
* listeners in the same `Set`.
|
|
1502
|
+
*/
|
|
1503
|
+
emit(event, payload) {
|
|
1504
|
+
const set = this.listeners.get(event);
|
|
1505
|
+
if (!set) return;
|
|
1506
|
+
for (const listener of set) {
|
|
1507
|
+
try {
|
|
1508
|
+
listener(payload);
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
queueMicrotask(() => {
|
|
1511
|
+
throw error;
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
|
|
1519
|
+
const RUNTIME_VERSION = "0.6.0";
|
|
1520
|
+
const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
1521
|
+
function mountScene(target, bundle, options = {}) {
|
|
1522
|
+
assertMountTarget(target);
|
|
1523
|
+
validateRuntimeBundle(bundle);
|
|
1524
|
+
const engine = new AnimationEngine();
|
|
1525
|
+
engine.init(bundle);
|
|
1526
|
+
const svg = buildSceneDOM(target, bundle, {
|
|
1527
|
+
label: options.label,
|
|
1528
|
+
themeVars: options.themeVars
|
|
1529
|
+
});
|
|
1530
|
+
let controller;
|
|
1531
|
+
if (options.controller !== void 0 && options.controller !== false) {
|
|
1532
|
+
controller = new AnimationController();
|
|
1533
|
+
controller.init(
|
|
1534
|
+
bundle,
|
|
1535
|
+
{
|
|
1536
|
+
...options.controller,
|
|
1537
|
+
container: options.controller.container ?? target,
|
|
1538
|
+
sceneElement: svg
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
engine,
|
|
1542
|
+
sceneElement: svg
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
const interactivity = new SceneInteractivity(svg, options.interactive === true);
|
|
1547
|
+
let destroyed = false;
|
|
1548
|
+
return {
|
|
1549
|
+
svg,
|
|
1550
|
+
engine,
|
|
1551
|
+
controller,
|
|
1552
|
+
getResolvedConfig: () => getResolvedConfig(bundle, options, controller),
|
|
1553
|
+
on: (event, listener) => interactivity.on(event, listener),
|
|
1554
|
+
destroy: () => {
|
|
1555
|
+
if (destroyed) return;
|
|
1556
|
+
destroyed = true;
|
|
1557
|
+
interactivity.destroy();
|
|
1558
|
+
destroyControllerSafely(controller);
|
|
1559
|
+
engine.destroy();
|
|
1560
|
+
if (svg.parentNode === target) {
|
|
1561
|
+
target.removeChild(svg);
|
|
1562
|
+
} else {
|
|
1563
|
+
svg.parentNode?.removeChild(svg);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function destroyControllerSafely(controller) {
|
|
1569
|
+
if (!controller) return;
|
|
1570
|
+
try {
|
|
1571
|
+
controller.destroy();
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
if (error instanceof ControllerError && error.code === "CONTROLLER_DESTROYED") return;
|
|
1574
|
+
throw error;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function assertMountTarget(target) {
|
|
1578
|
+
if (!target || typeof target.appendChild !== "function" || typeof target.removeChild !== "function") {
|
|
1579
|
+
throw new RenderError("INVALID_MOUNT_TARGET", "mountScene() requires a DOM HTMLElement target");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function validateRuntimeBundle(bundle) {
|
|
1583
|
+
if (!bundle || typeof bundle !== "object") {
|
|
1584
|
+
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle must be a plain object");
|
|
1585
|
+
}
|
|
1586
|
+
if (bundle._format !== RUNTIME_BUNDLE_FORMAT) {
|
|
1587
|
+
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle format is missing or unsupported", {
|
|
1588
|
+
expected: RUNTIME_BUNDLE_FORMAT
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
if (!Array.isArray(bundle.scenes) || bundle.scenes.length === 0) {
|
|
1592
|
+
throw new RenderError("BUNDLE_FORMAT_MISSING", "Runtime bundle must include compiled scenes[]");
|
|
1593
|
+
}
|
|
1594
|
+
if (majorVersion(bundle._version) !== majorVersion(RUNTIME_VERSION)) {
|
|
1595
|
+
throw new RenderError(
|
|
1596
|
+
"BUNDLE_VERSION_MISMATCH",
|
|
1597
|
+
`Runtime bundle version ${bundle._version} is not compatible with runtime ${RUNTIME_VERSION}`,
|
|
1598
|
+
{ bundleVersion: bundle._version, runtimeVersion: RUNTIME_VERSION }
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
if (!bundle._digest) {
|
|
1602
|
+
throw new RenderError("BUNDLE_DIGEST_MISSING", "Runtime bundle digest is missing");
|
|
1603
|
+
}
|
|
1604
|
+
if (typeof bundle._digest !== "string" || !HEX_DIGEST_PATTERN.test(bundle._digest)) {
|
|
1605
|
+
throw new RenderError("BUNDLE_DIGEST_MISMATCH", "Runtime bundle digest is malformed");
|
|
1606
|
+
}
|
|
1607
|
+
const { _digest, ...bundleWithoutDigest } = bundle;
|
|
1608
|
+
const actualDigest = sha256(canonicalStringify(bundleWithoutDigest));
|
|
1609
|
+
if (actualDigest !== _digest) {
|
|
1610
|
+
throw new RenderError("BUNDLE_DIGEST_MISMATCH", "Runtime bundle digest does not match bundle content", {
|
|
1611
|
+
expected: _digest,
|
|
1612
|
+
actual: actualDigest
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function getResolvedConfig(bundle, options = {}, controller) {
|
|
1617
|
+
return {
|
|
1618
|
+
grid: { cellSize: bundle.grid.cellSize },
|
|
1619
|
+
floor: { ...bundle.floor, size: [...bundle.floor.size], origin: [...bundle.floor.origin] },
|
|
1620
|
+
layout: {
|
|
1621
|
+
...bundle.layout,
|
|
1622
|
+
padding: { ...bundle.layout.padding },
|
|
1623
|
+
align: [...bundle.layout.align]
|
|
1624
|
+
},
|
|
1625
|
+
viewBox: getResolvedViewBox(bundle),
|
|
1626
|
+
camera: resolveCameraState(bundle, controller),
|
|
1627
|
+
theme: bundle.theme,
|
|
1628
|
+
themeVars: getResolvedThemeVars(bundle, options.themeVars),
|
|
1629
|
+
scenes: bundle.scenes.map((scene) => ({
|
|
1630
|
+
id: scene.id,
|
|
1631
|
+
progress: scene.progress
|
|
1632
|
+
})),
|
|
1633
|
+
layerOrder: bundle.layers.map((layer) => ({ name: layer.name, order: layer.order })).sort((a, b) => a.order - b.order || compareCodePointOrder(a.name, b.name))
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
function resolveCameraState(bundle, controller) {
|
|
1637
|
+
if (controller) {
|
|
1638
|
+
try {
|
|
1639
|
+
return controller.getCameraState();
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
if (!(error instanceof ControllerError)) throw error;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
viewBox: getResolvedViewBox(bundle),
|
|
1646
|
+
isZoomed: false
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function compareCodePointOrder(a, b) {
|
|
1650
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1651
|
+
}
|
|
1652
|
+
function getResolvedThemeVars(bundle, overrides = {}) {
|
|
1653
|
+
return {
|
|
1654
|
+
...resolveTheme(bundle.theme) ?? {},
|
|
1655
|
+
...bundle.themeVars ?? {},
|
|
1656
|
+
...overrides
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function majorVersion(version) {
|
|
1660
|
+
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
1661
|
+
return Number.isFinite(major) ? major : Number.NaN;
|
|
1662
|
+
}
|
|
1663
|
+
function canonicalStringify(value) {
|
|
1664
|
+
return JSON.stringify(normalizeValue(value));
|
|
1665
|
+
}
|
|
1666
|
+
function normalizeValue(value) {
|
|
1667
|
+
if (Array.isArray(value)) {
|
|
1668
|
+
return value.map((item) => item === void 0 ? null : normalizeValue(item));
|
|
1669
|
+
}
|
|
1670
|
+
if (!isPlainObject(value)) return value;
|
|
1671
|
+
const normalized = {};
|
|
1672
|
+
for (const key of Object.keys(value).sort()) {
|
|
1673
|
+
const child = value[key];
|
|
1674
|
+
if (child !== void 0) {
|
|
1675
|
+
normalized[key] = normalizeValue(child);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return normalized;
|
|
1679
|
+
}
|
|
1680
|
+
function isPlainObject(value) {
|
|
1681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
export { AnimationController as A, AnimationEngine as a, easeInOutCubic as b, easeOutCubic as c, easeInCubic as e, linear as l, mountScene as m, resolveEasing as r };
|
|
1685
|
+
//# sourceMappingURL=index-WmsNtuaZ.js.map
|