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 +6 -0
- package/README.md +142 -6
- package/dist/anim/adapter.d.ts +15 -0
- package/dist/anim/adapter.js +1 -0
- package/dist/anim/animationBuilder.d.ts +67 -0
- package/dist/anim/animationBuilder.js +101 -0
- package/dist/anim/extendAdapter.d.ts +2 -0
- package/dist/anim/extendAdapter.js +1 -0
- package/dist/anim/playback.d.ts +47 -0
- package/dist/anim/playback.js +46 -0
- package/dist/anim/player.d.ts +14 -0
- package/dist/anim/player.js +204 -0
- package/dist/anim/player.test.d.ts +1 -0
- package/dist/anim/player.test.js +49 -0
- package/dist/anim/registryAdapter.d.ts +14 -0
- package/dist/anim/registryAdapter.js +94 -0
- package/dist/anim/spec.d.ts +18 -0
- package/dist/anim/spec.js +1 -0
- package/dist/anim/specExtensions.d.ts +7 -0
- package/dist/anim/specExtensions.js +5 -0
- package/dist/anim/vizcraftAdapter.d.ts +9 -0
- package/dist/anim/vizcraftAdapter.js +79 -0
- package/dist/builder.d.ts +36 -0
- package/dist/builder.js +304 -114
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.test.js +51 -0
- package/dist/runtimePatcher.d.ts +13 -0
- package/dist/runtimePatcher.js +162 -0
- package/dist/shapes.d.ts +20 -0
- package/dist/shapes.js +113 -0
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +7 -0
- package/dist/types.d.ts +19 -0
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
|
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
|
-
-
|
|
96
|
-
-
|
|
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
|
|
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 @@
|
|
|
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 {};
|