vizcraft 0.1.5 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # vizcraft
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c3984f2`](https://github.com/ChipiKaf/vizcraft/commit/c3984f200af3a3388b3a52f38fb068c8bc955ba1) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Implemented the animation builder api
8
+
3
9
  ## 0.1.5
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -8,7 +8,7 @@ VizCraft is designed to make creating beautiful, animated node-link diagrams and
8
8
 
9
9
  - **Fluent Builder API**: Define your visualization scene using a readable, chainable API.
10
10
  - **Grid System**: Built-in 2D grid system for easy, structured layout of nodes.
11
- - **Declarative Animations**: Animate layout changes, edge flow, and node states with a simple declarative config.
11
+ - **Two Animation Systems**: Lightweight registry/CSS animations (e.g. edge `flow`) and data-only timeline animations (`AnimationSpec`).
12
12
  - **Framework Agnostic**: The core logic is pure TypeScript and can be used with any framework or Vanilla JS.
13
13
  - **Custom Overlays**: Create complex, custom UI elements that float on top of your visualization.
14
14
 
@@ -48,6 +48,25 @@ const container = document.getElementById('viz-basic');
48
48
  if (container) builder.mount(container);
49
49
  ```
50
50
 
51
+ ## 📚 Documentation (Topics)
52
+
53
+ For a guided walkthrough, the repo docs are organized like this:
54
+
55
+ - [Introduction](../../packages/docs/docs/intro.md)
56
+ - [Examples](../../packages/docs/docs/examples.mdx)
57
+ - [Essentials](../../packages/docs/docs/essentials.mdx)
58
+ - [Animations](../../packages/docs/docs/animations/index.mdx)
59
+ - [Animation Builder API](../../packages/docs/docs/animations/animation-builder-api.mdx)
60
+ - [Advanced](../../packages/docs/docs/advanced.mdx)
61
+ - [Types](../../packages/docs/docs/types.mdx)
62
+
63
+ Run the docs locally (monorepo):
64
+
65
+ ```bash
66
+ pnpm install
67
+ pnpm -C packages/docs start
68
+ ```
69
+
51
70
  ## 📖 Core Concepts
52
71
 
53
72
  ### The Builder (`VizBuilder`)
@@ -90,11 +109,128 @@ b.edge('n1', 'n2')
90
109
 
91
110
  ### Animations
92
111
 
93
- VizCraft supports declarative animations. You define _what_ happens, and the renderer handles the interpolation.
112
+ VizCraft supports **two complementary animation approaches**:
113
+
114
+ 1. **Registry/CSS animations** (simple, reusable effects)
115
+
116
+ Attach an animation by name to a node/edge. The default core registry includes:
117
+
118
+ - `flow` (edge)
119
+
120
+ ```ts
121
+ import { viz } from 'vizcraft';
122
+
123
+ const b = viz().view(520, 160);
124
+
125
+ b.node('a')
126
+ .at(70, 80)
127
+ .circle(18)
128
+ .label('A')
129
+ .node('b')
130
+ .at(450, 80)
131
+ .rect(70, 44, 10)
132
+ .label('B')
133
+ .edge('a', 'b')
134
+ .arrow()
135
+ .animate('flow', { duration: '1s' })
136
+ .done();
137
+ ```
138
+
139
+ 2. **Data-only timeline animations (`AnimationSpec`)** (sequenced tweens)
140
+
141
+ - Author with `builder.animate((anim) => ...)`.
142
+ - VizCraft stores compiled specs on the scene as `scene.animationSpecs`.
143
+ - Play them with `builder.play()`.
144
+
145
+ ```ts
146
+ import { viz } from 'vizcraft';
147
+
148
+ const b = viz().view(520, 240);
149
+
150
+ b.node('a')
151
+ .at(120, 120)
152
+ .circle(20)
153
+ .label('A')
154
+ .node('b')
155
+ .at(400, 120)
156
+ .rect(70, 44, 10)
157
+ .label('B')
158
+ .edge('a', 'b')
159
+ .arrow()
160
+ .done();
161
+
162
+ b.animate((anim) =>
163
+ anim
164
+ .node('a')
165
+ .to({ x: 200, opacity: 0.35 }, { duration: 600 })
166
+ .node('b')
167
+ .to({ x: 440, y: 170 }, { duration: 700 })
168
+ .edge('a->b')
169
+ .to({ strokeDashoffset: -120 }, { duration: 900 })
170
+ );
171
+
172
+ const container = document.getElementById('viz-basic');
173
+ if (container) {
174
+ b.mount(container);
175
+ b.play();
176
+ }
177
+ ```
178
+
179
+ #### Animating edges with custom ids
180
+
181
+ If you create an edge with a custom id (third arg), target it explicitly in animations:
182
+
183
+ ```ts
184
+ const b = viz().view(520, 240);
185
+ b.node('a')
186
+ .at(120, 120)
187
+ .circle(20)
188
+ .node('b')
189
+ .at(400, 120)
190
+ .rect(70, 44, 10)
191
+ .edge('a', 'b', 'e1')
192
+ .done();
193
+
194
+ b.animate((anim) =>
195
+ anim.edge('a', 'b', 'e1').to({ strokeDashoffset: -120 }, { duration: 900 })
196
+ );
197
+ ```
198
+
199
+ #### Custom animatable properties (advanced)
200
+
201
+ Specs can carry adapter extensions so you can animate your own numeric properties:
202
+
203
+ ```ts
204
+ b.animate((anim) =>
205
+ anim
206
+ .extendAdapter((adapter) => {
207
+ adapter.register?.('node', 'r', {
208
+ get: (target) => adapter.get(target, 'r'),
209
+ set: (target, v) => adapter.set(target, 'r', v),
210
+ });
211
+ })
212
+ .node('a')
213
+ .to({ r: 42 }, { duration: 500 })
214
+ );
215
+ ```
216
+
217
+ ### Playback controls
218
+
219
+ `builder.play()` returns a controller with `pause()`, `play()` (resume), and `stop()`.
220
+
221
+ ```ts
222
+ const controller = b.play();
223
+ controller?.pause();
224
+ controller?.play();
225
+ controller?.stop();
226
+ ```
227
+
228
+ ### Supported properties (core adapter)
229
+
230
+ Out of the box, timeline playback supports these numeric properties:
94
231
 
95
- - **`stream`**: Particles flowing along an edge.
96
- - **`pulse`**: Rhythmic scaling or opacity changes.
97
- - **Transition**: Moving a node from one position to another.
232
+ - Node: `x`, `y`, `opacity`, `scale`, `rotation`
233
+ - Edge: `opacity`, `strokeDashoffset`
98
234
 
99
235
  ## 🎨 Styling
100
236
 
@@ -130,4 +266,4 @@ Contributions are welcome! This is a monorepo managed with Turbo.
130
266
 
131
267
  ## 📄 License
132
268
 
133
- MIT License © Chipili Kafwilo
269
+ MIT License
@@ -0,0 +1,15 @@
1
+ import type { AnimationTarget, AnimProperty } from './spec';
2
+ export type PropReader = (el: unknown) => number | undefined;
3
+ export type PropWriter = (el: unknown, value: number) => void;
4
+ export interface PropHandlers {
5
+ get?: PropReader;
6
+ set?: PropWriter;
7
+ }
8
+ export interface AnimationHostAdapter {
9
+ get(target: AnimationTarget, prop: AnimProperty): number | undefined;
10
+ set(target: AnimationTarget, prop: AnimProperty, value: number): void;
11
+ flush?: () => void;
12
+ }
13
+ export interface RegistrableAdapter {
14
+ register(kind: string, prop: string, handlers: PropHandlers): void;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import type { AnimationSpec, AnimProperty, CoreAnimProperty, Ease } from './spec';
2
+ import type { ExtendAdapter } from './extendAdapter';
3
+ export type TweenOptions = {
4
+ duration: number;
5
+ easing?: Ease;
6
+ /** Optional per-property starting values. If omitted, the player captures from runtime/base. */
7
+ from?: Partial<Record<AnimProperty, number>>;
8
+ };
9
+ /**
10
+ * Properties that VizCraft core knows how to animate by default.
11
+ *
12
+ * Note: `AnimationSpec` supports arbitrary string properties, but core adapters
13
+ * only register these by default.
14
+ */
15
+ export type CoreAnimatableProps = Partial<Record<CoreAnimProperty, number>>;
16
+ /**
17
+ * A loose prop bag that still hints core properties in TS.
18
+ *
19
+ * Example: `{ x: 10, opacity: 0.5 }`.
20
+ */
21
+ export type AnimatableProps = CoreAnimatableProps & Partial<Record<string, number>>;
22
+ /**
23
+ * Fluent, authoring API that compiles to a portable `AnimationSpec`.
24
+ *
25
+ * - Data only: no callbacks stored as animation state.
26
+ * - Sequential by default via an internal cursor time.
27
+ */
28
+ export declare class AnimationBuilder {
29
+ private cursorMs;
30
+ private currentTarget;
31
+ private readonly tweens;
32
+ private readonly adapterExtensions;
33
+ /**
34
+ * Register custom animated properties for this spec.
35
+ *
36
+ * This avoids needing to thread an `extendAdapter` callback into playback helpers.
37
+ */
38
+ extendAdapter(cb: ExtendAdapter): this;
39
+ /** Select a node by id (compiles to target `node:<id>`). */
40
+ node(id: string): this;
41
+ /**
42
+ * Select an edge.
43
+ *
44
+ * - `edge('a->b')` (id form)
45
+ * - `edge('a', 'b')` (convenience; compiles to `edge:a->b`)
46
+ * - `edge('a', 'b', 'custom-id')` (explicit id; compiles to `edge:custom-id`)
47
+ */
48
+ edge(id: string): this;
49
+ edge(from: string, to: string, id?: string): this;
50
+ /**
51
+ * Set the internal cursor time (ms). Next `.to(...)` uses this as its delay.
52
+ */
53
+ at(ms: number): this;
54
+ /**
55
+ * Advance the internal cursor time (ms) without adding tweens.
56
+ */
57
+ wait(ms: number): this;
58
+ /**
59
+ * Tween properties on the current target.
60
+ *
61
+ * Emits one `TweenSpec` per property.
62
+ */
63
+ to(props: AnimatableProps, opts: TweenOptions): this;
64
+ build(): AnimationSpec;
65
+ }
66
+ /** Convenience helper for one-off compilation. */
67
+ export declare function buildAnimationSpec(cb: (anim: AnimationBuilder) => unknown): AnimationSpec;
@@ -0,0 +1,101 @@
1
+ import { ADAPTER_EXTENSIONS, } from './specExtensions';
2
+ function isNumber(v) {
3
+ return typeof v === 'number' && Number.isFinite(v);
4
+ }
5
+ function toTarget(kind, id) {
6
+ return `${kind}:${id}`;
7
+ }
8
+ /**
9
+ * Fluent, authoring API that compiles to a portable `AnimationSpec`.
10
+ *
11
+ * - Data only: no callbacks stored as animation state.
12
+ * - Sequential by default via an internal cursor time.
13
+ */
14
+ export class AnimationBuilder {
15
+ cursorMs = 0;
16
+ currentTarget = null;
17
+ tweens = [];
18
+ adapterExtensions = [];
19
+ /**
20
+ * Register custom animated properties for this spec.
21
+ *
22
+ * This avoids needing to thread an `extendAdapter` callback into playback helpers.
23
+ */
24
+ extendAdapter(cb) {
25
+ this.adapterExtensions.push(cb);
26
+ return this;
27
+ }
28
+ /** Select a node by id (compiles to target `node:<id>`). */
29
+ node(id) {
30
+ this.currentTarget = toTarget('node', id);
31
+ return this;
32
+ }
33
+ edge(a, b, c) {
34
+ const id = b === undefined ? a : (c ?? `${a}->${b}`);
35
+ this.currentTarget = toTarget('edge', id);
36
+ return this;
37
+ }
38
+ /**
39
+ * Set the internal cursor time (ms). Next `.to(...)` uses this as its delay.
40
+ */
41
+ at(ms) {
42
+ this.cursorMs = Math.max(0, ms);
43
+ return this;
44
+ }
45
+ /**
46
+ * Advance the internal cursor time (ms) without adding tweens.
47
+ */
48
+ wait(ms) {
49
+ this.cursorMs = Math.max(0, this.cursorMs + Math.max(0, ms));
50
+ return this;
51
+ }
52
+ /**
53
+ * Tween properties on the current target.
54
+ *
55
+ * Emits one `TweenSpec` per property.
56
+ */
57
+ to(props, opts) {
58
+ if (!this.currentTarget) {
59
+ throw new Error('AnimationBuilder.to(): no target selected (call node(...) or edge(...))');
60
+ }
61
+ const duration = Math.max(0, opts.duration);
62
+ const easing = opts.easing;
63
+ const froms = opts.from;
64
+ for (const [property, value] of Object.entries(props)) {
65
+ if (!isNumber(value))
66
+ continue;
67
+ const tween = {
68
+ kind: 'tween',
69
+ target: this.currentTarget,
70
+ property: property,
71
+ to: value,
72
+ duration,
73
+ delay: this.cursorMs,
74
+ easing,
75
+ };
76
+ const from = froms?.[property];
77
+ if (isNumber(from))
78
+ tween.from = from;
79
+ this.tweens.push(tween);
80
+ }
81
+ // Sequential by default.
82
+ this.cursorMs += duration;
83
+ return this;
84
+ }
85
+ build() {
86
+ const spec = {
87
+ version: 'viz-anim/1',
88
+ tweens: [...this.tweens],
89
+ };
90
+ if (this.adapterExtensions.length > 0) {
91
+ spec[ADAPTER_EXTENSIONS] = [...this.adapterExtensions];
92
+ }
93
+ return spec;
94
+ }
95
+ }
96
+ /** Convenience helper for one-off compilation. */
97
+ export function buildAnimationSpec(cb) {
98
+ const anim = new AnimationBuilder();
99
+ cb(anim);
100
+ return anim.build();
101
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnimationHostAdapter, RegistrableAdapter } from './adapter';
2
+ export type ExtendAdapter = (adapter: AnimationHostAdapter & Partial<RegistrableAdapter>) => void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import type { VizScene } from '../types';
2
+ import { type AnimationController } from './player';
3
+ import type { AnimationSpec } from './spec';
4
+ import type { ExtendAdapter } from './extendAdapter';
5
+ /**
6
+ * A player that can be (re)loaded with specs.
7
+ *
8
+ * Note: `createPlayer()` returns a controller that also has `.load(...)`, but the
9
+ * exported `AnimationController` interface does not include it.
10
+ */
11
+ export type PlaybackController = AnimationController & {
12
+ load(spec: AnimationSpec): AnimationController;
13
+ };
14
+ export type { ExtendAdapter };
15
+ export declare function createScenePlayback(opts: {
16
+ scene: VizScene;
17
+ requestRender: () => void;
18
+ extendAdapter?: ExtendAdapter;
19
+ }): PlaybackController;
20
+ /**
21
+ * Convenience helper for the common "builder + mounted container" case.
22
+ *
23
+ * - Uses `builder.build()` once to get stable node/edge references for runtime updates.
24
+ * - Uses `builder.patchRuntime(container)` as the render flush, so animations patch
25
+ * the existing SVG in-place (fast path).
26
+ */
27
+ export declare function createBuilderPlayback(opts: {
28
+ builder: {
29
+ build(): VizScene;
30
+ patchRuntime(container: HTMLElement): void;
31
+ };
32
+ container: HTMLElement;
33
+ extendAdapter?: ExtendAdapter;
34
+ }): PlaybackController;
35
+ /**
36
+ * Loads (and optionally auto-plays) a spec against a mounted builder.
37
+ */
38
+ export declare function playAnimationSpec(opts: {
39
+ builder: {
40
+ build(): VizScene;
41
+ patchRuntime(container: HTMLElement): void;
42
+ };
43
+ container: HTMLElement;
44
+ spec: AnimationSpec;
45
+ autoPlay?: boolean;
46
+ extendAdapter?: ExtendAdapter;
47
+ }): PlaybackController;
@@ -0,0 +1,46 @@
1
+ import { createPlayer } from './player';
2
+ import { createVizCraftAdapter } from './vizcraftAdapter';
3
+ import { getAdapterExtensions } from './specExtensions';
4
+ export function createScenePlayback(opts) {
5
+ const adapter = createVizCraftAdapter(opts.scene, opts.requestRender);
6
+ opts.extendAdapter?.(adapter);
7
+ return createPlayer(adapter);
8
+ }
9
+ /**
10
+ * Convenience helper for the common "builder + mounted container" case.
11
+ *
12
+ * - Uses `builder.build()` once to get stable node/edge references for runtime updates.
13
+ * - Uses `builder.patchRuntime(container)` as the render flush, so animations patch
14
+ * the existing SVG in-place (fast path).
15
+ */
16
+ export function createBuilderPlayback(opts) {
17
+ const scene = opts.builder.build();
18
+ const requestRender = () => opts.builder.patchRuntime(opts.container);
19
+ return createScenePlayback({
20
+ scene,
21
+ requestRender,
22
+ extendAdapter: opts.extendAdapter,
23
+ });
24
+ }
25
+ /**
26
+ * Loads (and optionally auto-plays) a spec against a mounted builder.
27
+ */
28
+ export function playAnimationSpec(opts) {
29
+ const adapterExtensions = getAdapterExtensions(opts.spec);
30
+ const extendAdapter = adapterExtensions.length > 0 || opts.extendAdapter
31
+ ? (adapter) => {
32
+ for (const ext of adapterExtensions)
33
+ ext(adapter);
34
+ opts.extendAdapter?.(adapter);
35
+ }
36
+ : undefined;
37
+ const controller = createBuilderPlayback({
38
+ builder: opts.builder,
39
+ container: opts.container,
40
+ extendAdapter,
41
+ });
42
+ controller.load(opts.spec);
43
+ if (opts.autoPlay !== false)
44
+ controller.play();
45
+ return controller;
46
+ }
@@ -0,0 +1,14 @@
1
+ import type { AnimationHostAdapter } from './adapter';
2
+ import type { AnimationSpec } from './spec';
3
+ export interface AnimationController {
4
+ play(): void;
5
+ pause(): void;
6
+ seek(ms: number): void;
7
+ stop(): void;
8
+ isPlaying(): boolean;
9
+ time(): number;
10
+ duration(): number;
11
+ }
12
+ export declare function createPlayer(adapter: AnimationHostAdapter): AnimationController & {
13
+ load(spec: AnimationSpec): AnimationController;
14
+ };
@@ -0,0 +1,204 @@
1
+ function clamp(v, a = 0, b = 1) {
2
+ return Math.max(a, Math.min(b, v));
3
+ }
4
+ const easingFns = {
5
+ linear: (t) => t,
6
+ easeIn: (t) => t * t,
7
+ easeOut: (t) => 1 - (1 - t) * (1 - t),
8
+ easeInOut: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
9
+ };
10
+ export function createPlayer(adapter) {
11
+ let spec = null;
12
+ let tracks = [];
13
+ let total = 0;
14
+ let timeMs = 0;
15
+ let playing = false;
16
+ let rafId = null;
17
+ let lastFrameTime = 0;
18
+ let captured = false;
19
+ function buildInternal(s) {
20
+ const list = s.tweens.map((t, i) => {
21
+ const start = t.delay ?? 0;
22
+ const end = start + t.duration;
23
+ return Object.assign({}, t, { start, end, _from: t.from ?? 0, _i: i });
24
+ });
25
+ const byKey = new Map();
26
+ for (const t of list) {
27
+ const key = `${String(t.target)}|${String(t.property)}`;
28
+ let track = byKey.get(key);
29
+ if (!track) {
30
+ track = {
31
+ key,
32
+ target: t.target,
33
+ property: t.property,
34
+ base: 0,
35
+ tweens: [],
36
+ };
37
+ byKey.set(key, track);
38
+ }
39
+ track.tweens.push(t);
40
+ }
41
+ const trackList = Array.from(byKey.values());
42
+ for (const tr of trackList) {
43
+ tr.tweens.sort((a, b) => a.start !== b.start ? a.start - b.start : a._i - b._i);
44
+ }
45
+ const dur = list.reduce((mx, t) => Math.max(mx, t.end), 0);
46
+ return {
47
+ tracks: trackList,
48
+ dur,
49
+ };
50
+ }
51
+ function captureFromsIfNeeded() {
52
+ if (captured || !spec)
53
+ return;
54
+ // Capture base values once per track and chain sequential tweens.
55
+ for (const tr of tracks) {
56
+ const got = adapter.get(tr.target, tr.property);
57
+ tr.base = typeof got === 'number' ? got : 0;
58
+ let prior = null;
59
+ for (const t of tr.tweens) {
60
+ if (t.from !== undefined) {
61
+ t._from = t.from;
62
+ }
63
+ else if (prior && prior.end <= t.start) {
64
+ // Common case: sequential tweens on the same prop should chain.
65
+ t._from = prior.to;
66
+ }
67
+ else {
68
+ // Fallback: capture from the base value.
69
+ t._from = tr.base;
70
+ }
71
+ prior = t;
72
+ }
73
+ }
74
+ captured = true;
75
+ }
76
+ function applyAt(ms) {
77
+ // Evaluate one active tween per target+property.
78
+ // This avoids "future" tweens overwriting current motion.
79
+ for (const tr of tracks) {
80
+ const list = tr.tweens;
81
+ // Find the last tween that has started.
82
+ let lo = 0;
83
+ let hi = list.length - 1;
84
+ let idx = -1;
85
+ while (lo <= hi) {
86
+ const mid = (lo + hi) >> 1;
87
+ if (list[mid].start <= ms) {
88
+ idx = mid;
89
+ lo = mid + 1;
90
+ }
91
+ else {
92
+ hi = mid - 1;
93
+ }
94
+ }
95
+ let value;
96
+ if (idx === -1) {
97
+ value = tr.base;
98
+ }
99
+ else {
100
+ const t = list[idx];
101
+ const local = ms - t.start;
102
+ if (local <= 0) {
103
+ value = t._from;
104
+ }
105
+ else if (t.duration <= 0) {
106
+ value = t.to;
107
+ }
108
+ else if (local >= t.duration) {
109
+ value = t.to;
110
+ }
111
+ else {
112
+ const p = clamp(local / t.duration, 0, 1);
113
+ const fn = easingFns[t.easing ?? 'linear'];
114
+ const eased = fn(p);
115
+ value = t._from + (t.to - t._from) * eased;
116
+ }
117
+ }
118
+ adapter.set(tr.target, tr.property, value);
119
+ }
120
+ adapter.flush?.();
121
+ }
122
+ function tick(now) {
123
+ if (!playing)
124
+ return;
125
+ const delta = now - lastFrameTime;
126
+ lastFrameTime = now;
127
+ timeMs = Math.min(total, timeMs + delta);
128
+ applyAt(timeMs);
129
+ if (timeMs >= total) {
130
+ playing = false;
131
+ rafId = null;
132
+ return;
133
+ }
134
+ rafId = globalThis.requestAnimationFrame(tick);
135
+ }
136
+ const controller = {
137
+ load(s) {
138
+ spec = s;
139
+ const built = buildInternal(s);
140
+ tracks = built.tracks;
141
+ total = built.dur;
142
+ timeMs = 0;
143
+ captured = false;
144
+ if (rafId != null) {
145
+ globalThis.cancelAnimationFrame(rafId);
146
+ rafId = null;
147
+ }
148
+ playing = false;
149
+ // apply initial state
150
+ captureFromsIfNeeded();
151
+ applyAt(0);
152
+ return controller;
153
+ },
154
+ play() {
155
+ if (!spec)
156
+ return;
157
+ captureFromsIfNeeded();
158
+ if (playing)
159
+ return;
160
+ playing = true;
161
+ lastFrameTime = performance.now();
162
+ rafId = globalThis.requestAnimationFrame(tick);
163
+ },
164
+ pause() {
165
+ if (!playing)
166
+ return;
167
+ playing = false;
168
+ if (rafId != null) {
169
+ globalThis.cancelAnimationFrame(rafId);
170
+ rafId = null;
171
+ }
172
+ },
173
+ seek(ms) {
174
+ if (!spec)
175
+ return;
176
+ captureFromsIfNeeded();
177
+ timeMs = clamp(ms, 0, total);
178
+ applyAt(timeMs);
179
+ },
180
+ stop() {
181
+ if (!spec)
182
+ return;
183
+ if (rafId != null) {
184
+ globalThis.cancelAnimationFrame(rafId);
185
+ rafId = null;
186
+ }
187
+ playing = false;
188
+ timeMs = 0;
189
+ // restore to from state (froms are captured or defined)
190
+ captureFromsIfNeeded();
191
+ applyAt(0);
192
+ },
193
+ isPlaying() {
194
+ return playing;
195
+ },
196
+ time() {
197
+ return timeMs;
198
+ },
199
+ duration() {
200
+ return total;
201
+ },
202
+ };
203
+ return controller;
204
+ }
@@ -0,0 +1 @@
1
+ export {};