@nitesh-tyagi/vectorflow 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, JSON-driven SVG pose animation library.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/vectorflow)
|
|
6
|
-
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/@nitesh-tyagi/vectorflow)
|
|
6
|
+
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -12,6 +12,7 @@ A minimal, JSON-driven SVG pose animation library.
|
|
|
12
12
|
VectorFlow loads an SVG containing multiple **pose states** and a JSON config defining **routes** and **actions**, then animates smoothly between poses using attribute interpolation. No path morphing, no crossfades — just clean, transform-and-attribute-based transitions.
|
|
13
13
|
|
|
14
14
|
**Features:**
|
|
15
|
+
|
|
15
16
|
- Declarative JSON config — define states, routes, and actions
|
|
16
17
|
- Smooth SVG attribute interpolation (position, size, color, transform)
|
|
17
18
|
- Sequence and loop action types with cancellation support
|
|
@@ -21,20 +22,34 @@ VectorFlow loads an SVG containing multiple **pose states** and a JSON config de
|
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [Quick Start](#quick-start)
|
|
28
|
+
- [Preparing Your SVG](#preparing-your-svg)
|
|
29
|
+
- [Writing the JSON Config](#writing-the-json-config)
|
|
30
|
+
- [API Reference](#api-reference)
|
|
31
|
+
- [Full Example](#full-example)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
24
36
|
## Quick Start
|
|
25
37
|
|
|
26
38
|
### Install
|
|
27
39
|
|
|
28
40
|
```bash
|
|
29
|
-
npm install vectorflow
|
|
41
|
+
npm install @nitesh-tyagi/vectorflow
|
|
30
42
|
```
|
|
31
43
|
|
|
32
44
|
### Browser (UMD)
|
|
33
45
|
|
|
34
46
|
```html
|
|
35
|
-
<script src="https://unpkg.com/vectorflow/dist/vectorflow.umd.js"></script>
|
|
47
|
+
<script src="https://unpkg.com/@nitesh-tyagi/vectorflow/dist/vectorflow.umd.js"></script>
|
|
36
48
|
<script>
|
|
37
|
-
const vf = new VectorFlow({
|
|
49
|
+
const vf = new VectorFlow({
|
|
50
|
+
svgElement: document.querySelector('svg'),
|
|
51
|
+
json: config,
|
|
52
|
+
});
|
|
38
53
|
vf.play('shuffle');
|
|
39
54
|
</script>
|
|
40
55
|
```
|
|
@@ -42,11 +57,11 @@ npm install vectorflow
|
|
|
42
57
|
### ES Module
|
|
43
58
|
|
|
44
59
|
```js
|
|
45
|
-
import VectorFlow from 'vectorflow';
|
|
60
|
+
import VectorFlow from '@nitesh-tyagi/vectorflow';
|
|
46
61
|
|
|
47
62
|
const vf = new VectorFlow({
|
|
48
63
|
svgElement: document.querySelector('#my-svg'),
|
|
49
|
-
json: configObject, // or JSON string
|
|
64
|
+
json: configObject, // or a JSON string
|
|
50
65
|
});
|
|
51
66
|
|
|
52
67
|
vf.play('left'); // play an action
|
|
@@ -56,7 +71,219 @@ vf.stop(); // cancel current playback
|
|
|
56
71
|
|
|
57
72
|
---
|
|
58
73
|
|
|
59
|
-
##
|
|
74
|
+
## Preparing Your SVG
|
|
75
|
+
|
|
76
|
+
VectorFlow discovers animation states and parts directly from your SVG markup. Follow these rules when creating your SVG:
|
|
77
|
+
|
|
78
|
+
### 1. Define State Groups
|
|
79
|
+
|
|
80
|
+
Wrap each pose/state in a `<g>` (group) element with an `id` of the form `state_{name}`:
|
|
81
|
+
|
|
82
|
+
```xml
|
|
83
|
+
<g id="state_left"> <!-- state name: "left" -->
|
|
84
|
+
...
|
|
85
|
+
</g>
|
|
86
|
+
<g id="state_center"> <!-- state name: "center" -->
|
|
87
|
+
...
|
|
88
|
+
</g>
|
|
89
|
+
<g id="state_right"> <!-- state name: "right" -->
|
|
90
|
+
...
|
|
91
|
+
</g>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- The `state_` prefix is **required**. Everything after it becomes the state name used in your JSON config.
|
|
95
|
+
- You can have as many states as you need.
|
|
96
|
+
|
|
97
|
+
### 2. Tag Parts with `data-part`
|
|
98
|
+
|
|
99
|
+
Inside each state group, mark the elements that should be animated with a `data-part` attribute:
|
|
100
|
+
|
|
101
|
+
```xml
|
|
102
|
+
<g id="state_left">
|
|
103
|
+
<rect data-part="body" x="20" y="80" width="40" height="40" fill="red" />
|
|
104
|
+
<circle data-part="head" cx="40" cy="60" r="15" fill="orange" />
|
|
105
|
+
</g>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- VectorFlow matches parts **by name** across states. A part called `"body"` in `state_left` will animate to the `"body"` in `state_right`.
|
|
109
|
+
- Part names must be **identical** across all states.
|
|
110
|
+
|
|
111
|
+
### 3. Keep Part Sets Consistent
|
|
112
|
+
|
|
113
|
+
**Every state group must contain the exact same set of `data-part` names.** If `state_left` has parts `body` and `head`, then `state_center` and `state_right` must also have `body` and `head`.
|
|
114
|
+
|
|
115
|
+
VectorFlow will throw an error if part sets don't match:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
VectorFlow: Part mismatch between states. State "left" has parts [body,head]
|
|
119
|
+
but state "right" has parts [body].
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Animatable Attributes
|
|
123
|
+
|
|
124
|
+
VectorFlow interpolates the following SVG attributes and properties between states:
|
|
125
|
+
|
|
126
|
+
| Type | Attributes / Properties |
|
|
127
|
+
|------|------------------------|
|
|
128
|
+
| **Numeric** | `x`, `y`, `width`, `height`, `rx`, `ry`, `cx`, `cy`, `r`, `opacity` |
|
|
129
|
+
| **Colors** | `fill`, `stroke` (hex `#RGB`/`#RRGGBB` or `rgb()`) |
|
|
130
|
+
| **Transforms** | `translate(x,y)`, `scale(x,y)`, `rotate(angle)` |
|
|
131
|
+
|
|
132
|
+
Design your states so that the visual differences are expressed through these attributes. For example, to move a shape left, change its `x`; to recolor it, change its `fill`.
|
|
133
|
+
|
|
134
|
+
### 5. Minimal SVG Template
|
|
135
|
+
|
|
136
|
+
```xml
|
|
137
|
+
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
138
|
+
<!-- State: left -->
|
|
139
|
+
<g id="state_left">
|
|
140
|
+
<rect data-part="shape" x="20" y="80" width="40" height="40" fill="#e74c3c" />
|
|
141
|
+
</g>
|
|
142
|
+
|
|
143
|
+
<!-- State: center -->
|
|
144
|
+
<g id="state_center">
|
|
145
|
+
<rect data-part="shape" x="80" y="80" width="40" height="40" fill="#3498db" />
|
|
146
|
+
</g>
|
|
147
|
+
|
|
148
|
+
<!-- State: right -->
|
|
149
|
+
<g id="state_right">
|
|
150
|
+
<rect data-part="shape" x="140" y="80" width="40" height="40" fill="#2ecc71" />
|
|
151
|
+
</g>
|
|
152
|
+
</svg>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **Tip:** At runtime, VectorFlow hides all state groups and creates a single `<g id="live_character">` clone to animate. Your original state groups serve as snapshots defining each pose.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Writing the JSON Config
|
|
160
|
+
|
|
161
|
+
The JSON config tells VectorFlow which state to start in, how to transition between states, and what actions are available.
|
|
162
|
+
|
|
163
|
+
### Top-Level Structure
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"name": "my-animation",
|
|
168
|
+
"initial_state": "center",
|
|
169
|
+
"ms": 120,
|
|
170
|
+
"routes": { ... },
|
|
171
|
+
"actions": { ... }
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Field | Type | Required | Description |
|
|
176
|
+
|-------|------|----------|-------------|
|
|
177
|
+
| `name` | `string` | No | A label for this config (for your own reference). |
|
|
178
|
+
| `initial_state` | `string` | **Yes** | The state name to start in (must match an `id="state_..."` in your SVG). |
|
|
179
|
+
| `ms` | `number` | No | Default animation duration in milliseconds (default: `120`). |
|
|
180
|
+
| `routes` | `object` | **Yes** | Defines how to travel between states. |
|
|
181
|
+
| `actions` | `object` | **Yes** | Named animation sequences/loops that users can trigger. |
|
|
182
|
+
|
|
183
|
+
### Routes
|
|
184
|
+
|
|
185
|
+
Routes define the **path** the animation takes when transitioning to a target state. Each route key is a target and its value maps source states (or `"*"` for any source) to an array of intermediate + destination states.
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
"routes": {
|
|
189
|
+
"left": {
|
|
190
|
+
"*": ["left"],
|
|
191
|
+
"right": ["center", "left"]
|
|
192
|
+
},
|
|
193
|
+
"right": {
|
|
194
|
+
"*": ["right"],
|
|
195
|
+
"left": ["center", "right"]
|
|
196
|
+
},
|
|
197
|
+
"center": {
|
|
198
|
+
"*": ["center"],
|
|
199
|
+
"ms": 60
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**How route resolution works:**
|
|
205
|
+
|
|
206
|
+
1. When the engine needs to go to `"left"` from the current state, it looks up `routes["left"]`.
|
|
207
|
+
2. If there's a specific entry for the current state (e.g. `"right": ["center", "left"]`), it follows that path — first animate to `center`, then to `left`.
|
|
208
|
+
3. If no specific entry exists, it uses the wildcard `"*"` path.
|
|
209
|
+
4. If no route exists at all, VectorFlow jumps directly to the target state.
|
|
210
|
+
|
|
211
|
+
**Route-level `ms`:** You can set a per-route `ms` to override the global duration for transitions to that target state.
|
|
212
|
+
|
|
213
|
+
**Route keys vs state names:** Route keys don't have to match a state name. You can create alias routes that resolve to real states:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
"routes": {
|
|
217
|
+
"direct-left": { "*": ["left"] },
|
|
218
|
+
"direct-right": { "*": ["right"] }
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Here `"direct-left"` is a route key (not a state) that resolves to state `"left"`. This is useful for defining multiple paths to the same state.
|
|
223
|
+
|
|
224
|
+
### Actions
|
|
225
|
+
|
|
226
|
+
Actions are the named animations you trigger via `vf.play('actionName')`. Each action has a type — either `"sequence"` (play once) or `"loop"` (repeat).
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
"actions": {
|
|
230
|
+
"left": { "type": "sequence", "order": ["left"] },
|
|
231
|
+
"right": { "type": "sequence", "order": ["right"] },
|
|
232
|
+
"center": { "type": "sequence", "order": ["center"] },
|
|
233
|
+
"shuffle": {
|
|
234
|
+
"type": "loop",
|
|
235
|
+
"mode": "direct",
|
|
236
|
+
"order": ["left", "center", "right", "center"],
|
|
237
|
+
"ms": 80,
|
|
238
|
+
"count": 99999
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Field | Type | Default | Description |
|
|
244
|
+
|-------|------|---------|-------------|
|
|
245
|
+
| `type` | `"sequence" \| "loop"` | `"sequence"` | `sequence` plays once; `loop` repeats `count` times. |
|
|
246
|
+
| `order` | `string[]` | — | Array of route keys or state names to visit in order. |
|
|
247
|
+
| `ms` | `number` | — | Override animation duration for this action (takes highest priority). |
|
|
248
|
+
| `mode` | `"direct"` | — | If `"direct"`, skips intermediate route steps — jumps straight to the final resolved state. |
|
|
249
|
+
| `count` | `number` | `99999` | For loops: how many times to repeat the order cycle. |
|
|
250
|
+
|
|
251
|
+
**`order` items can be route keys or state names.** The engine resolves each entry through the routes table. If an entry matches a route key, VectorFlow follows that route's path. If it matches a state name directly, it goes there.
|
|
252
|
+
|
|
253
|
+
### Duration Precedence
|
|
254
|
+
|
|
255
|
+
When determining how long a step takes, VectorFlow checks in this order:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
action.ms > routes[target].ms > json.ms > 120ms (fallback)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Full JSON Example
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"name": "character",
|
|
266
|
+
"initial_state": "idle",
|
|
267
|
+
"ms": 150,
|
|
268
|
+
"routes": {
|
|
269
|
+
"idle": { "*": ["idle"] },
|
|
270
|
+
"walk-left": { "*": ["lean-left", "walk-left"], "walk-right": ["idle", "lean-left", "walk-left"] },
|
|
271
|
+
"walk-right": { "*": ["lean-right", "walk-right"], "walk-left": ["idle", "lean-right", "walk-right"] },
|
|
272
|
+
"lean-left": { "*": ["lean-left"], "ms": 80 },
|
|
273
|
+
"lean-right": { "*": ["lean-right"], "ms": 80 }
|
|
274
|
+
},
|
|
275
|
+
"actions": {
|
|
276
|
+
"go-left": { "type": "sequence", "order": ["walk-left"] },
|
|
277
|
+
"go-right": { "type": "sequence", "order": ["walk-right"] },
|
|
278
|
+
"reset": { "type": "sequence", "order": ["idle"], "ms": 200 },
|
|
279
|
+
"patrol": { "type": "loop", "order": ["walk-left", "idle", "walk-right", "idle"], "count": 10 }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## API Reference
|
|
60
287
|
|
|
61
288
|
### Constructor
|
|
62
289
|
|
|
@@ -66,19 +293,19 @@ const vf = new VectorFlow({ svgElement, json });
|
|
|
66
293
|
|
|
67
294
|
| Param | Type | Description |
|
|
68
295
|
|-------|------|-------------|
|
|
69
|
-
| `svgElement` | `SVGElement \| string` | An SVG DOM element or raw SVG string |
|
|
70
|
-
| `json` | `object \| string` | VectorFlow JSON config (object or string) |
|
|
296
|
+
| `svgElement` | `SVGElement \| string` | An SVG DOM element or raw SVG string. |
|
|
297
|
+
| `json` | `object \| string` | VectorFlow JSON config (object or JSON string). |
|
|
71
298
|
|
|
72
299
|
### Properties
|
|
73
300
|
|
|
74
301
|
| Property | Type | Description |
|
|
75
302
|
|----------|------|-------------|
|
|
76
|
-
| `vf.states` | `string[]` | All discovered state names |
|
|
77
|
-
| `vf.currentState` | `string` |
|
|
78
|
-
| `vf.actions` | `object` | Actions config from JSON |
|
|
79
|
-
| `vf.isPlaying` | `boolean` | Whether an action is currently playing |
|
|
80
|
-
| `vf.currentAction` | `string \| null` | Name of the playing action |
|
|
81
|
-
| `vf.svgElement` | `SVGElement` | The SVG element being animated |
|
|
303
|
+
| `vf.states` | `string[]` | All discovered state names from the SVG. |
|
|
304
|
+
| `vf.currentState` | `string` | The current active state. |
|
|
305
|
+
| `vf.actions` | `object` | Actions config from the JSON. |
|
|
306
|
+
| `vf.isPlaying` | `boolean` | Whether an action is currently playing. |
|
|
307
|
+
| `vf.currentAction` | `string \| null` | Name of the playing action, or `null`. |
|
|
308
|
+
| `vf.svgElement` | `SVGElement` | The SVG element being animated. |
|
|
82
309
|
|
|
83
310
|
### Methods
|
|
84
311
|
|
|
@@ -86,62 +313,103 @@ const vf = new VectorFlow({ svgElement, json });
|
|
|
86
313
|
|--------|---------|-------------|
|
|
87
314
|
| `vf.play(actionName)` | `Promise<void>` | Play a named action. Auto-cancels any running action. |
|
|
88
315
|
| `vf.stop()` | `void` | Cancel current playback immediately. |
|
|
89
|
-
| `vf.on(event, callback)` | `VectorFlow` | Register an event listener. |
|
|
90
|
-
| `vf.off(event, callback)` | `VectorFlow` | Remove an event listener. |
|
|
91
|
-
| `vf.destroy()` | `void` |
|
|
316
|
+
| `vf.on(event, callback)` | `VectorFlow` | Register an event listener (chainable). |
|
|
317
|
+
| `vf.off(event, callback)` | `VectorFlow` | Remove an event listener (chainable). |
|
|
318
|
+
| `vf.destroy()` | `void` | Stop playback, restore original SVG state groups, remove listeners. |
|
|
92
319
|
|
|
93
320
|
### Events
|
|
94
321
|
|
|
95
322
|
| Event | Callback Arg | Description |
|
|
96
323
|
|-------|-------------|-------------|
|
|
97
|
-
| `stateChange` | `string` | Fired after each step completes |
|
|
98
|
-
| `actionStart` | `string` | Fired when an action begins |
|
|
99
|
-
| `actionEnd` | `string` | Fired when an action finishes or is cancelled |
|
|
100
|
-
| `error` | `Error` | Fired on runtime errors |
|
|
324
|
+
| `stateChange` | `string` | Fired after each intermediate step completes (receives the new state name). |
|
|
325
|
+
| `actionStart` | `string` | Fired when an action begins playing. |
|
|
326
|
+
| `actionEnd` | `string` | Fired when an action finishes naturally or is cancelled. |
|
|
327
|
+
| `error` | `Error` | Fired on runtime errors during playback. |
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
vf.on('stateChange', (state) => {
|
|
331
|
+
console.log('Now in state:', state);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
vf.on('actionEnd', (actionName) => {
|
|
335
|
+
console.log(`Action "${actionName}" finished`);
|
|
336
|
+
});
|
|
337
|
+
```
|
|
101
338
|
|
|
102
339
|
---
|
|
103
340
|
|
|
104
|
-
##
|
|
341
|
+
## Full Example
|
|
342
|
+
|
|
343
|
+
### SVG (`character.svg`)
|
|
344
|
+
|
|
345
|
+
```xml
|
|
346
|
+
<svg viewBox="0 0 300 200" xmlns="http://www.w3.org/2000/svg">
|
|
347
|
+
<g id="state_left">
|
|
348
|
+
<rect data-part="body" x="30" y="60" width="50" height="80" rx="8" fill="#e74c3c" />
|
|
349
|
+
<circle data-part="head" cx="55" cy="45" r="18" fill="#f5b041" />
|
|
350
|
+
</g>
|
|
351
|
+
<g id="state_center">
|
|
352
|
+
<rect data-part="body" x="125" y="60" width="50" height="80" rx="8" fill="#3498db" />
|
|
353
|
+
<circle data-part="head" cx="150" cy="45" r="18" fill="#f5b041" />
|
|
354
|
+
</g>
|
|
355
|
+
<g id="state_right">
|
|
356
|
+
<rect data-part="body" x="220" y="60" width="50" height="80" rx="8" fill="#2ecc71" />
|
|
357
|
+
<circle data-part="head" cx="245" cy="45" r="18" fill="#f5b041" />
|
|
358
|
+
</g>
|
|
359
|
+
</svg>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### JSON (`character.json`)
|
|
105
363
|
|
|
106
364
|
```json
|
|
107
365
|
{
|
|
108
|
-
"name": "
|
|
366
|
+
"name": "character",
|
|
109
367
|
"initial_state": "center",
|
|
110
|
-
"ms":
|
|
368
|
+
"ms": 150,
|
|
111
369
|
"routes": {
|
|
112
370
|
"left": { "*": ["left"], "right": ["center", "left"] },
|
|
113
371
|
"right": { "*": ["right"], "left": ["center", "right"] },
|
|
114
|
-
"center": { "*": ["center"], "ms":
|
|
372
|
+
"center": { "*": ["center"], "ms": 80 }
|
|
115
373
|
},
|
|
116
374
|
"actions": {
|
|
117
|
-
"left":
|
|
118
|
-
"right":
|
|
119
|
-
"
|
|
120
|
-
"
|
|
375
|
+
"go-left": { "type": "sequence", "order": ["left"] },
|
|
376
|
+
"go-right": { "type": "sequence", "order": ["right"] },
|
|
377
|
+
"reset": { "type": "sequence", "order": ["center"] },
|
|
378
|
+
"patrol": {
|
|
379
|
+
"type": "loop",
|
|
380
|
+
"order": ["left", "center", "right", "center"],
|
|
381
|
+
"ms": 100,
|
|
382
|
+
"count": 5
|
|
383
|
+
}
|
|
121
384
|
}
|
|
122
385
|
}
|
|
123
386
|
```
|
|
124
387
|
|
|
125
|
-
|
|
388
|
+
### JavaScript
|
|
126
389
|
|
|
127
|
-
|
|
390
|
+
```js
|
|
391
|
+
import VectorFlow from '@nitesh-tyagi/vectorflow';
|
|
128
392
|
|
|
129
|
-
|
|
393
|
+
// Load SVG (already in the DOM) and JSON config
|
|
394
|
+
const vf = new VectorFlow({
|
|
395
|
+
svgElement: document.querySelector('#character-svg'),
|
|
396
|
+
json: characterConfig,
|
|
397
|
+
});
|
|
130
398
|
|
|
131
|
-
|
|
399
|
+
// Wire up buttons
|
|
400
|
+
document.querySelector('#btn-left').onclick = () => vf.play('go-left');
|
|
401
|
+
document.querySelector('#btn-right').onclick = () => vf.play('go-right');
|
|
402
|
+
document.querySelector('#btn-reset').onclick = () => vf.play('reset');
|
|
403
|
+
document.querySelector('#btn-patrol').onclick = () => vf.play('patrol');
|
|
404
|
+
document.querySelector('#btn-stop').onclick = () => vf.stop();
|
|
132
405
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</g>
|
|
141
|
-
<g id="state_right">
|
|
142
|
-
<rect data-part="shape" x="140" y="80" width="40" height="40" fill="green" />
|
|
143
|
-
</g>
|
|
144
|
-
</svg>
|
|
406
|
+
// Listen for state changes
|
|
407
|
+
vf.on('stateChange', (state) => {
|
|
408
|
+
document.querySelector('#current-state').textContent = state;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Clean up when done
|
|
412
|
+
window.addEventListener('beforeunload', () => vf.destroy());
|
|
145
413
|
```
|
|
146
414
|
|
|
147
415
|
---
|