@vizij/runtime-react 0.4.2 → 1.0.1

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 CHANGED
@@ -1,470 +1,110 @@
1
1
  # @vizij/runtime-react
2
2
 
3
- `@vizij/runtime-react` is the bundle-first React runtime for Vizij faces. It loads a GLB or prebuilt world, extracts embedded Vizij metadata, composes the rig/pose/program graphs into the behavior of an Arora device ([`@vizij/runtime`](https://www.npmjs.com/package/@vizij/runtime) — an Arora runtime compiled to WebAssembly), mirrors resolved values into the renderer store, and exposes a React-friendly control surface for apps.
3
+ One Vizij face's lifecycle in React, over
4
+ [`@vizij/runtime`](https://www.npmjs.com/package/@vizij/runtime) — the Bevy
5
+ view and each face's Arora device in one wasm module. The provider loads the
6
+ face and hands its children the face's `Runtime`, unwrapped: every device
7
+ operation is called on it. The package adds only what React needs: the
8
+ provider owning the face's one change drain, hooks that subscribe a component
9
+ to keys, and a JS tween.
4
10
 
5
- The package is intentionally aimed at app authors. If your app wants to render a Vizij face and drive it through authored rig inputs, this is the layer to build on.
6
-
7
- > Status: experimental. Public API is still moving with the runtime/export pipeline.
8
-
9
- ## What It Handles
10
-
11
- - load a face from a GLB URL, a `Blob`, or an already loaded world
12
- - extract the embedded `VIZIJ_bundle` payload when present
13
- - merge explicit `rig`, `pose`, `animations`, and `programs` with discovered bundle content
14
- - compose the registered graphs into the device's behavior and drive its step loop
15
- - expose runtime status, controls, diagnostics, and update hooks through React context
16
- - render the resolved face with `VizijRuntimeFace`
11
+ The design, with the justification of each export and the mapping of every
12
+ 0.4 call, is [`DESIGN-1.0.md`](./DESIGN-1.0.md).
17
13
 
18
14
  ## Installation
19
15
 
20
16
  ```bash
21
- pnpm add @vizij/runtime-react react react-dom
22
- ```
23
-
24
- If your app also imports lower-level renderer or engine APIs directly, install those packages too:
25
-
26
- ```bash
27
- pnpm add @vizij/render @vizij/runtime
28
- ```
29
-
30
- `@vizij/runtime-react`, `@vizij/render`, and `@vizij/runtime` should stay on the same workspace/release line.
31
-
32
- ### Bundler Notes
33
-
34
- The runtime depends on Vizij wasm packages transitively. Your bundler needs to emit `.wasm` assets and allow async wasm loading.
35
-
36
- Example `next.config.js`:
37
-
38
- ```js
39
- module.exports = {
40
- webpack: (config) => {
41
- config.experiments = {
42
- ...(config.experiments ?? {}),
43
- asyncWebAssembly: true,
44
- };
45
- config.module.rules.push({
46
- test: /\.wasm$/,
47
- type: "asset/resource",
48
- });
49
- return config;
50
- },
51
- };
17
+ pnpm add @vizij/runtime-react @vizij/render @vizij/runtime react
52
18
  ```
53
19
 
54
- If you override wasm URLs manually, pass plain string URLs to the underlying init helpers. Avoid wrappers that turn them into `RelativeURL` objects.
20
+ `@vizij/runtime` and `@vizij/render` are peer dependencies, so that a page
21
+ resolves one copy of each: one wasm module, one page canvas.
55
22
 
56
- ## Quick Start
23
+ ## Usage
57
24
 
58
25
  ```tsx
26
+ import { VizijCanvas } from "@vizij/render";
59
27
  import {
60
- VizijRuntimeFace,
61
28
  VizijRuntimeProvider,
29
+ VizijRuntimeFace,
30
+ useAnimateValue,
62
31
  useVizijRuntime,
63
- type VizijAssetBundle,
64
32
  } from "@vizij/runtime-react";
65
33
 
66
- const faceAssetUrl = new URL("./face.glb", import.meta.url).href;
67
-
68
- const assetBundle: VizijAssetBundle = {
69
- namespace: "demo-face",
70
- glb: {
71
- kind: "url",
72
- src: faceAssetUrl,
73
- aggressiveImport: true,
74
- },
75
- pose: {
76
- stageNeutralFilter: (_id, path) => !path.includes("/color/"),
77
- },
78
- };
79
-
80
- export function App() {
34
+ export function Page() {
81
35
  return (
82
- <VizijRuntimeProvider assetBundle={assetBundle} autostart>
83
- <RuntimeStage />
84
- </VizijRuntimeProvider>
36
+ <>
37
+ <VizijCanvas />
38
+ <VizijRuntimeProvider glb="/faces/Quori_Current_Extended.glb">
39
+ <VizijRuntimeFace style={{ width: 480, height: 360 }} />
40
+ <Controls />
41
+ </VizijRuntimeProvider>
42
+ </>
85
43
  );
86
44
  }
87
45
 
88
- function RuntimeStage() {
89
- const { loading, ready, error, stagePoseNeutral } = useVizijRuntime();
90
-
91
- if (loading) return <p>Loading face…</p>;
92
- if (error) return <p>Runtime failed: {error.message}</p>;
93
- if (!ready) return <p>Preparing runtime…</p>;
94
-
46
+ function Controls() {
47
+ const { runtime, status } = useVizijRuntime();
48
+ const { animateValue } = useAnimateValue();
49
+ if (!runtime || status !== "ready") return null;
95
50
  return (
96
- <div>
97
- <button onClick={() => stagePoseNeutral(true)}>Reset pose</button>
98
- <VizijRuntimeFace className="face-canvas" showSafeArea={false} />
99
- </div>
51
+ <>
52
+ <button
53
+ onClick={() =>
54
+ animateValue(runtime.path("standard/vizij/expression/happy"), 1)
55
+ }
56
+ >
57
+ happy
58
+ </button>
59
+ <button onClick={() => runtime.invoke("say", { text: "Hello" })}>
60
+ say hello
61
+ </button>
62
+ </>
100
63
  );
101
64
  }
102
65
  ```
103
66
 
104
- The provider resolves the face bundle, boots its Arora device with the composed graphs as the device's behavior, and publishes the merged runtime state through `useVizijRuntime()`.
105
-
106
- ## Core Concepts
107
-
108
- ### Bundle-first runtime
109
-
110
- The main contract is `VizijAssetBundle`. In the default workflow you hand the runtime one GLB and let it discover as much as possible from the embedded `VIZIJ_bundle`.
111
-
112
- Explicit overrides still work. If you provide `rig`, `pose`, `animations`, or `programs`, the runtime merges them with embedded bundle data and deduplicates animations/programs by id.
113
-
114
- ### One device per provider
115
-
116
- Each `VizijRuntimeProvider` owns one Arora device. The device runs the
117
- composed graph as its behavior on a shared key/value store: graph `input`
118
- nodes read store paths each tick, graph outputs write back, and the provider
119
- mirrors changed values into the renderer store after every step. Providers
120
- are fully isolated from each other — multiple faces mean multiple devices,
121
- and namespacing keeps their store keys apart.
122
-
123
- `driveRuntime={false}` mounts a runtime that does not step its device
124
- from its own loop; use it for surfaces that are stepped manually (see
125
- [Manual stepping](#manual-stepping)) or at a background cadence.
126
-
127
- ### Where the composed graph comes from
128
-
129
- The device runs **one** graph, composed from several sources
130
- (`composeGraphSpecs`; the live list is `graphSourcesRef` in
131
- `VizijRuntimeProvider`). Each source has a distinct provenance:
132
-
133
- - **Rig graph** — shipped in the loaded GLB/asset bundle (`VIZIJ_bundle`'s
134
- rig, or the explicit `rig` override). It maps rig input paths to the
135
- face's morph/bone/material writes: this is the face itself.
136
- - **Pose-driver graph** — the bundle's `pose` graph (or the
137
- `pose-driver`/`pose` graph discovered in the bundle). It turns high-level
138
- pose controls into rig-input writes, which the rig source reads back
139
- through the shared store paths on the next tick.
140
- - **Program graphs** — one source per _playing_ program: procedural graphs
141
- from the bundle's `programs` started via the transport, and (in
142
- `vizij-authoring`) the motiongraph editor's graph, published as a program
143
- so it evaluates on the device like everything else.
144
- - **Animations** — a single source, composed whenever any clip is playing.
145
- It is an `ExternalFunction` node that steps the **animation module**
146
- ([`@vizij/animation-module`](https://www.npmjs.com/package/@vizij/animation-module))
147
- every device tick off the golden `arora/dt`. Clips register into the module
148
- as data (through its call surface), the module samples them **inside the
149
- device**, and its per-track outputs are routed on to the rig-input store
150
- paths (VIZ-61 Stage B — the JS clip pipeline no longer samples clips).
151
-
152
- Sources are namespaced by id (`source::node`) so nodes can't collide;
153
- **store paths are deliberately shared** — that is the cross-source contract.
154
-
155
- **How a change reaches the device.** When the composition changes — a program
156
- starts or stops, the rig or a source is swapped — the runtime diffs the old and
157
- new composed specs and **patches the running graph in place** through the
158
- behavior interpreter's `apply(GraphDiff)`: only the nodes and edges the change
159
- touched are re-installed (VIZ-79). A structurally empty diff is a no-op; if a
160
- patch ever fails the runtime falls back to a whole-spec `loadGraph`. The Vizij
161
- node graph is a full behavior interpreter — it accepts both an incremental
162
- `apply(GraphDiff)` and a whole `load` (see
163
- [`vizij-arora-behavior/docs/node-graph.md`](https://github.com/vizij-ai/vizij-rs/blob/main/crates/interop/vizij-arora-behavior/docs/node-graph.md)).
164
- The device, its store, and its loaded module set are untouched across the
165
- change; the device is rebuilt only when the **module set** itself changes.
166
- Stateful nodes keep their state too — springs, dampers, and the graph clock
167
- carry over, and a node the edit didn't touch keeps its integration state, so a
168
- program starting or stopping (or an unrelated edit) does not reset it.
169
-
170
- ### Animations and the device
171
-
172
- Playback lives inside the device, end to end. Clips load into the animation
173
- module with their **final store keys resolved at load** (the rig routing the
174
- JS pipeline used to apply per tick), the composed animations source steps
175
- the module off the golden `arora/dt`, and a path-less `output` node applies
176
- the sampled batch onto those keys — no JS touches the per-tick path.
177
-
178
- Transport rides the module (0.2.0): `play`/`pause` resume and hold the
179
- player, `stop` resets its playhead (the next tick emits the clip's t=0
180
- pose; `stopAnimation({ clearOutputs: false })` holds the pose instead),
181
- `seekAnimation`, `setAnimationLoop(false)` (one-shot), and
182
- `playAnimation({ speed, weight })` are real player commands.
183
- `getAnimationState()` reads the module's `player_states` feedback — a live
184
- playhead, duration, playing, speed — and `playAnimation()`'s promise
185
- resolves when a non-looping clip reaches its end.
186
-
187
- Remaining fidelity gap: authored `cubic` keyframes carry no explicit
188
- handles in the stored form, so they sample the engine's default ease
189
- (`linear`/`step` timing rides through as explicit bezier handles).
190
-
191
- ### Asset reloads vs graph re-registration
192
-
193
- When you swap the `assetBundle` prop, the runtime decides whether it needs to reload assets or only re-register controllers. That behavior is controlled by `updateTier` and is also exposed as `resolveRuntimeUpdatePlan()`.
194
-
195
- This matters for tooling apps like `vizij-authoring`, where graphs/animations can change without replacing the face asset itself.
196
-
197
- ## `VizijAssetBundle`
198
-
199
- ```ts
200
- type VizijAssetBundle = {
201
- namespace?: string;
202
- faceId?: string;
203
- glb: VizijGlbAsset;
204
- rig?: VizijGraphAsset;
205
- pose?: {
206
- graph?: VizijGraphAsset;
207
- config?: PoseRigConfig;
208
- stageNeutralFilter?: (id: string, path: string) => boolean;
209
- };
210
- animations?: VizijAnimationAsset[];
211
- programs?: VizijProgramAsset[];
212
- initialInputs?: Record<string, ValueJSON>;
213
- metadata?: Record<string, unknown>;
214
- bundle?: VizijBundleExtension | null;
215
- };
216
- ```
217
-
218
- ### `glb`
219
-
220
- Required. One of:
221
-
222
- - `{ kind: "url", src, aggressiveImport?, rootBounds? }`
223
- - `{ kind: "blob", blob, aggressiveImport?, rootBounds? }`
224
- - `{ kind: "world", world, animatables, bundle? }`
225
-
226
- Use `kind: "world"` when your app already loaded the scene and wants runtime-react to wire only the engine/runtime layer.
227
-
228
- ### `rig`
229
-
230
- Optional `VizijGraphAsset` for the main rig graph. When omitted, the runtime looks for a compatible graph in the embedded bundle.
231
-
232
- ### `pose`
233
-
234
- Optional pose graph/config surface:
235
-
236
- - `graph`: pose-driver graph override
237
- - `config`: `PoseRigConfig` used by pose-aware UIs and pose-blending configuration
238
- - `stageNeutralFilter`: lets you skip specific neutral writes, for example baked color channels
239
-
240
- `PoseRigConfig.poseGroups` defines how subsets of poses are grouped for local blend behavior and wider composition. Group labels such as `viseme` or `emotion` are not runtime path segments. Runtime-facing pose writes still use canonical per-pose paths.
241
-
242
- ### `animations`
243
-
244
- Optional authored clips. These merge with embedded bundle animations and extracted GLTF animation clips.
245
-
246
- ### `programs`
247
-
248
- Optional procedural programs. These merge with embedded bundle `motiongraph` entries.
249
-
250
- ### `initialInputs`
251
-
252
- Optional `ValueJSON` map staged before autostart/manual stepping.
253
-
254
- ### `metadata`
255
-
256
- Arbitrary app metadata. The runtime keeps it attached to the resolved `assetBundle`.
257
-
258
- ### `bundle`
259
-
260
- Optional pre-parsed `VizijBundleExtension`. Useful when you already decoded bundle metadata yourself.
261
-
262
- ## Provider Props
263
-
264
- `VizijRuntimeProviderProps`:
265
-
266
- - `assetBundle`: required runtime bundle
267
- - `namespace`, `faceId`: override resolved ids without mutating the incoming bundle
268
- - `updateTier`: `"auto"` (default), `"assets"`, or `"graphs"`
269
- - `autoCreate`: load the engine wasm and boot the device automatically on mount
270
- - `autostart`: start the runtime loop automatically after registration
271
- - `driveRuntime`: whether this runtime instance should call `step()` during its loop
272
- - `mergeStrategy`: forwarded to graph registration
273
- - `transformOutputWrite(write)`: intercept or drop output writes before they hit the renderer store
274
- - `onRegisterControllers(ids)`: receive registered graph/animation ids
275
- - `onStatusChange(status)`: subscribe to runtime status changes
276
-
277
- ### Important runtime flags
278
-
279
- - `autostart` controls whether the device begins stepping automatically once ready.
280
- - `driveRuntime={false}` is useful for faces stepped manually or at a background cadence.
281
- - `transformOutputWrite` is the hook to remap or suppress specific runtime outputs before they update renderer state.
282
-
283
- ## Runtime Context API
284
-
285
- Use `useVizijRuntime()` inside the provider tree.
286
-
287
- ### Status and identity
288
-
289
- - `loading`, `ready`
290
- - `error`, `errors`
291
- - `namespace`, `faceId`, `rootId`
292
- - `controllers.graphs`, `controllers.anims`
293
- - `outputPaths`
294
- - `stepHz`
295
- - `assetBundle`
296
- - `inputConstraints`
297
-
298
- `inputConstraints` is built from graph metadata and is the right source for slider defaults/ranges in tooling UIs.
299
-
300
- ### Input and renderer writes
301
-
302
- - `setInput(path, value, shape?)`
303
- - `getValueSnapshot(path)` — the current engine-store value of a path (reads your own writes)
304
- - `setValue(id, namespace, value)`
305
- - `stagePoseNeutral(force?)`
306
-
307
- ### Runtime graph updates
308
-
309
- - `setGraphBundle(bundle, options?)`
310
-
311
- `setGraphBundle()` lets you swap `rig`, `pose`, `animations`, and `programs` at runtime. This is the API that tooling apps use when the face asset stays the same but the authored runtime bundle changes.
312
-
313
- ### Value animation helpers
314
-
315
- - `animateValue(path, target, options?)`
316
- - `cancelAnimation(path)`
317
- - `setAnimationActive(active)`
318
- - `isAnimationActive()`
319
-
320
- `animateValue()` is the simple way to tween a single rig input path with built-in easing.
321
-
322
- ### Clip transport
323
-
324
- - `playAnimation(id, options?)`
325
- - `pauseAnimation(id)`
326
- - `seekAnimation(id, timeSeconds)`
327
- - `setAnimationLoop(id, enabled)`
328
- - `getAnimationState(id)`
329
- - `stopAnimation(id, options?)`
330
-
331
- ### Program transport
332
-
333
- - `playProgram(id)`
334
- - `pauseProgram(id)`
335
- - `stopProgram(id, options?)`
336
- - `getProgramState(id)`
337
-
338
- ### Manual stepping
339
-
340
- - `step(dt, opts?)`
341
- - `advanceAnimations(dt)`
342
-
343
- Use manual stepping when you do not want the provider to own the runtime loop or when hidden/shared faces need low-frequency background stepping.
344
-
345
- ### Driver registration
346
-
347
- - `registerInputDriver(id, factory)`
348
-
349
- Custom input drivers receive:
350
-
351
- - `setInput(path, value, shape?)`
352
- - `setRendererValue(id, namespace, valueOrUpdater)`
353
- - `namespace`
354
- - `faceId`
355
-
356
- Return `{ start, stop, dispose }`.
357
-
358
- ## Hooks
359
-
360
- ### `useVizijRuntime()`
361
-
362
- Throws when used outside the provider.
363
-
364
- ### `useOptionalVizijRuntime()`
365
-
366
- Returns `null` when no provider is present. This is useful for shared components that can operate with or without a runtime.
367
-
368
- ### `useRigInput(path)`
369
-
370
- Returns `[value, setValue]` for a single runtime input path. The setter writes into the device's store, while the value mirrors the renderer store.
371
-
372
- ### `useVizijOutputs(paths)`
373
-
374
- Subscribes to renderer output values for the current namespace and returns a path-to-value map.
375
-
376
- ## Components
377
-
378
- ### `VizijRuntimeFace`
379
-
380
- `VizijRuntimeFace` renders the resolved face using the current runtime namespace and root id.
381
-
382
- It accepts normal `Vizij` renderer props except `rootId` and `namespace`, which are owned by the runtime. It also supports `namespaceOverride` when you need to inspect another namespace while keeping the current runtime context.
383
-
384
- ## Exported Utilities
385
-
386
- ### Pose path helpers
387
-
388
- - `buildRigInputPath(faceId, path)`
389
- - `buildPoseWeightInputPathSegment(poseId)`
390
- - `buildPoseWeightRelativePath(poseId)`
391
- - `buildPoseWeightPathMap(poses, faceId)`
392
-
393
- These are the canonical helpers for pose-weight paths. The current runtime/export contract is:
394
-
395
- ```text
396
- rig/{faceId}/poses/{poseId}.weight
397
- ```
398
-
399
- That contract does not change when the pose belongs to a group labeled `viseme` or `emotion`. Group membership exists to control blend behavior, not to build a different input path family.
400
-
401
- ### Pose semantics helpers
402
-
403
- - `normalizePoseSemanticKey()`
404
- - `getPoseSemanticKey()`
405
- - `resolvePoseMembership()`
406
- - `resolvePoseSemantics()`
407
- - `filterPosesBySemanticKind()`
408
- - `buildSemanticPoseWeightPathMap()`
409
- - constants such as `VISEME_POSE_KEYS`, `EMOTION_POSE_KEYS`, and `EXPRESSIVE_EMOTION_POSE_KEYS`
410
-
411
- These helpers are convenience utilities for example apps that need to order or match current poses without hard-coding face-specific names. Pose groups themselves still exist to control blending/composition, and these helpers do not introduce paths such as `rig/{faceId}/visemes/...`.
412
-
413
- ### Automatic input-path detection and pose-control bridging
414
-
415
- Runtime-react also auto-detects the actual rig input paths it needs from the registered rig graph:
416
-
417
- 1. `collectInputPathMap()` scans `input` nodes and records aliases for authored channel ids.
418
- 2. When both direct rig inputs and `pose/control` inputs exist, the direct rig input path is preferred for bare channel ids.
419
- 3. Compiled pose graphs still emit internal pose-control outputs on:
420
-
421
- ```text
422
- rig/{faceId}/pose/control/{inputId}
423
- ```
424
-
425
- 4. The provider bridges those internal outputs back onto the detected rig input path when possible and falls back to the native `pose/control` path only when necessary.
426
-
427
- This is why dependent apps should prefer runtime-react helpers and resolved metadata over hard-coded face-specific input paths.
428
-
429
- ### Face control helpers
430
-
431
- - `resolveFaceControls(assetBundle, runtimeFaceId?, inputConstraints?)`
432
- - `mapNormalizedControlValue(control, value)`
433
- - `mapUnitControlValue(control, value)`
434
-
435
- Use these when you want to build gaze/blink/eyelid controls from runtime metadata rather than hard-coded paths.
436
-
437
- ### Update policy helpers
438
-
439
- - `resolveRuntimeUpdatePlan(previous, next, tier)`
440
-
441
- This is the same policy used internally by the provider to decide between:
442
-
443
- - reloading assets
444
- - only re-registering graphs/animations/programs
445
- - doing nothing
446
-
447
- ## Common Patterns
448
-
449
- ### Multiple faces in one app
450
-
451
- Mount one `VizijRuntimeProvider` per face; each owns its device. Give hidden or non-driver faces `driveRuntime={false}` and step them at a background cadence (see `vizij-showcase`'s `HiddenStepController`).
452
-
453
- ### Bundle-first player
454
-
455
- See [`apps/demo-vizij-player`](../../../apps/demo-vizij-player/README.md) for the reference “one bundled GLB in, runtime UI out” flow.
456
-
457
- ### Fullscreen face tutorials
458
-
459
- See:
460
-
461
- - [`apps/tutorial-fullscreen-face/tutorial.md`](../../../apps/tutorial-fullscreen-face/tutorial.md)
462
- - [`apps/tutorial-agent-face/tutorial.md`](../../../apps/tutorial-agent-face/tutorial.md)
463
- - [`apps/vizij-standalone`](../../../apps/vizij-standalone/README.md) — the face page with a voice agent, on `@vizij/runtime` directly
464
-
465
- ### Runtime-truthful authoring
466
-
467
- See [`apps/vizij-authoring/README.md`](../../../apps/vizij-authoring/README.md) for the `setGraphBundle()` and `transformOutputWrite()` tooling workflow.
67
+ A face is driven through its Arora API — the methods it declares (`invoke`)
68
+ and its store (`setValue`, `writeValues`) — on the keys of the Vizij face
69
+ standard, `runtime.path("standard/vizij/…")`. See
70
+ [`DESIGN-1.0.md`](./DESIGN-1.0.md) section 4 for every intent and its key or
71
+ method, and for the keys a composed mapping or a running program writes over.
72
+
73
+ ## API
74
+
75
+ - **`<VizijRuntimeProvider glb vizijId? options? run?>`** — fetches the GLB
76
+ (a URL, a `Blob` or bytes), reads it with `describe`, waits for the page's
77
+ `<VizijCanvas>`, loads the face with `loadVizij(vizijId, bytes, options)` and
78
+ steps its device (`run`, default `true`): once per animation frame while the
79
+ page is visible, on a timer while it is hidden; `false` leaves the page to
80
+ call `runtime.step(dtMs)`. A new `glb` reloads the face in place; unmounting
81
+ unloads it. `options` are `loadVizij`'s, read at each load. It reports the
82
+ face's stages to `@vizij/render`'s `setVizijProgress` — the GLB's bytes as
83
+ they arrive, `loadVizij`, the first frame — which the face's slot shows in
84
+ its loading view.
85
+ - **`useVizijRuntime()`** — the face: `vizijId`, `status` (`loading`,
86
+ `ready`, `error`), `error`, `behaviorError` (the device's standing behavior
87
+ error), `runtime` (the `Runtime`) and `description` (`describe(glb)`: poses,
88
+ rig inputs, programs, animations, metadata, bounds).
89
+ `useOptionalVizijRuntime()` returns `null` outside a provider.
90
+ - **`useVizijValues(paths)`** — the current values of `paths`, re-rendering on
91
+ change.
92
+ - **`useVizijChanges(listener)`** — each batch of store changes, at most once
93
+ per animation frame, the first batch after a (re)load being the whole store;
94
+ for consumers that must not re-render per change.
95
+ - **`useAnimateValue()`** — `animateValue(path, target, options)` and
96
+ `cancelAnimation(path)`: a JS tween, one `setValue` per animation frame, in
97
+ the numeric kind the key holds or rests at; `options` are `duration`
98
+ (seconds) and `easing`. A newer tween on a path replaces the running one;
99
+ unmounting cancels a component's tweens.
100
+ - **`<VizijRuntimeFace …>`** — `@vizij/render`'s `<Vizij>` slot bound to the
101
+ provider's face.
102
+
103
+ The provider is the only caller of `runtime.drainChanges()`, whose cursor is
104
+ one per `Runtime`; read changes through the two hooks. It drains once per
105
+ animation frame while one of them listens, and not at all otherwise: the
106
+ runtime holds what is not drained as the latest value per key, so the next
107
+ batch is the net change since the previous drain.
468
108
 
469
109
  ## Development
470
110
 
@@ -473,10 +113,11 @@ pnpm --filter "@vizij/runtime-react" build
473
113
  pnpm --filter "@vizij/runtime-react" test
474
114
  pnpm --filter "@vizij/runtime-react" typecheck
475
115
  pnpm --filter "@vizij/runtime-react" lint
476
- pnpm --filter "@vizij/runtime-react" dev
477
116
  ```
478
117
 
479
- When you change runtime behavior, validate at least one bundle-first app and one shared-runtime app. In this repo the fastest pair is usually:
118
+ The tests (Vitest, jsdom) run the provider and the hooks against stand-ins of
119
+ `@vizij/runtime` and `@vizij/render`.
480
120
 
481
- - `demo-vizij-player`
482
- - `vizij-showcase` or `vizij-authoring`
121
+ A new export that wraps or duplicates what Arora or `@vizij/runtime` provides
122
+ needs its justification written in [`DESIGN-1.0.md`](./DESIGN-1.0.md) section
123
+ 5.1; a capability the apps miss is added to `@vizij/runtime` instead.