@grundyjs/algiviz 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — 2026-10-14
4
+
5
+ - Add an application-owned graph BFS example with custom vertices, edges and queue
6
+ entries, shortest distances, disconnected components and both playback modes.
7
+ - Add createStepEmitter to /playback for per-iterator step numbering and frozen
8
+ wrappers; snapshot copying and state/event immutability remain author-owned.
9
+ - Use the helper in graph and merge examples, and document custom graph integration.
10
+
11
+ ## 0.5.1 — 2026-10-14
12
+
13
+ - Update the README around the current public API and examples; remove migration
14
+ instructions and detailed legacy API documentation.
15
+ - Correct demo input limits and clarify playback, snapshot memory and rendering.
16
+ - No runtime API or behavior changes.
17
+
3
18
  ## 0.5.0 — 2026-10-14
4
19
 
5
20
  - Cache immutable array and quick-sort scene projections and avoid repeated scale/layout
package/README.md CHANGED
@@ -1,10 +1,28 @@
1
1
  # AlgiViz
2
2
 
3
- A TypeScript toolkit for user-defined algorithm visualizations. You write the algorithm; AlgiViz provides array operations, immutable steps, playback and canvas rendering. No runtime dependencies. Playback works without DOM, React or Next.js. ESM only, with TypeScript declarations.
3
+ A TypeScript toolkit for user-defined algorithm visualizations. You write the
4
+ algorithm and define your objects; AlgiViz provides array operations, playback,
5
+ typed scene dispatch and a ready-made Canvas array renderer. No runtime dependencies.
6
+ Playback works without DOM, React or Next.js. ESM only, with TypeScript declarations.
7
+
8
+ This README describes version 0.6.0.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @grundyjs/algiviz
14
+ ```
15
+
16
+ There is no root entry point or CommonJS build. Import the modules you need:
17
+ `/array`, `/playback`, `/scene` and `/canvas`.
18
+
19
+ The deprecated `/core` sorting API remains available for compatibility.
20
+ For new integrations, use the APIs below. Version 0.x may introduce breaking changes
21
+ in minor releases; consult [CHANGELOG.md](CHANGELOG.md) when updating.
4
22
 
5
23
  ## User-defined algorithms
6
24
 
7
- The following API is introduced in 0.4.0.
25
+
8
26
  New applications use these entry points, which do not import bundled algorithms:
9
27
 
10
28
  - `@grundyjs/algiviz/array`: array operations and generator definitions.
@@ -75,12 +93,8 @@ order throw before mutating the workspace. Read snapshots rather than modifying
75
93
  items yourself. `createArrayTrace(values)` exposes the same operations directly,
76
94
  with immediate input validation, for authors writing their own generator wrapper.
77
95
 
78
- Full insertion/bubble implementations live in [examples/algorithms.mjs](examples/algorithms.mjs).
79
- The browser demo imports those external algorithms. Deprecated `/core` exports
80
- remain available through isolated compatibility implementations using the same
81
- operations; they are retained in the package until a breaking release removes them.
82
- Existing insertion/bubble traces remain unchanged. `ArrayEvent` also includes the
83
- new `highlight` event, so exhaustive event switches must handle it.
96
+ Insertion and bubble sort implementations live in [examples/algorithms.mjs](examples/algorithms.mjs).
97
+ The algorithms belong to the application; they require no changes to the library.
84
98
 
85
99
  ### Custom structures and renderers
86
100
 
@@ -109,9 +123,31 @@ Custom array events or tree/graph operations can use this contract and a custom
109
123
  renderer. The scene API below dispatches individual objects; there is no built-in
110
124
  graph/tree algorithm or operation set.
111
125
 
126
+ ### Step emission helper (since 0.6.0)
127
+
128
+ Version 0.6.0 adds `createStepEmitter` from `/playback`. It removes manual step numbering and freezes the step wrapper:
129
+
130
+ ```js
131
+ import { createStepEmitter } from '@grundyjs/algiviz/playback';
132
+
133
+ function* counter() {
134
+ let count = 0;
135
+ const emit = createStepEmitter(() => Object.freeze({ count }));
136
+ yield emit('start');
137
+ count++;
138
+ yield emit('done');
139
+ }
140
+ ```
141
+
142
+ Create the emitter inside each iterator. Its callback runs once per emission;
143
+ indices start at zero and failed captures do not consume an index. The helper
144
+ does not clone or freeze state/event data, infer terminal events or store history.
145
+ The callback must produce isolated immutable snapshots; immutable shared structures
146
+ can be reused. Returning a mutable workspace directly does not make it a snapshot.
147
+
112
148
  ### User-defined objects and visualizations
113
149
 
114
- The `/scene` API is introduced in 0.4.0. Define a type map with your own data, then
150
+ Define a type map with your own data, then
115
151
  provide a visualization for every type. AlgiViz matches identities and dispatches
116
152
  objects; your handlers decide how data becomes geometry and how it interpolates.
117
153
 
@@ -180,30 +216,9 @@ with `bar` objects whose data contains `item`, `index` and `held`. The built-in
180
216
  array renderer uses this same dispatcher. You can provide a different `bar`
181
217
  visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
182
218
 
183
- See [examples/tree-scene.mjs](examples/tree-scene.mjs) and open
184
- `examples/tree.html` after building and serving the repository. The application
185
- defines its own tree traversal, `node`, `edge` and `pointer` types, appearance,
186
- pointer movement and removal. No tree-specific code is added to AlgiViz. The Renderer selector switches between Canvas and SVG without restarting playback. The SVG example retains elements by ID, restores layer order and removes stale elements on every frame, including after a seek that skips exit transitions. See [examples/tree-svg.mjs](examples/tree-svg.mjs).
187
-
188
- For a second custom scene, open `examples/merge.html`. Its application-owned
189
- [merge algorithm](examples/merge-algorithm.mjs) and
190
- [renderer](examples/merge-scene.mjs) use `/playback` and `/scene` to show the main
191
- array and a reusable buffer. Equal values have letter suffixes to demonstrate
192
- stability. Cells use row/position IDs because copy-back temporarily duplicates
193
- item identities. Play/pause, step controls, speed and seeking support up to 64
194
- input values; text status, reduced motion and hidden-tab pausing are included.
195
- Select Generator for forward-only playback with O(n) retained snapshot memory;
196
- steps are produced on demand, and restarting creates a fresh iterator. History
197
- mode enables backward stepping and seeking with up to 64 values. Generator mode
198
- accepts up to 2,000 values; dense charts hide labels and text status previews the
199
- first 24 cells. Switching to history requires loading an array within its limit.
200
- The example stores full history, costing O(n² log n) space in the worst case;
201
- the underlying algorithm uses O(n) auxiliary memory. No merge-sort package
202
- export or new library API is needed.
203
-
204
- ### State-to-scene projection (since 0.5.0)
205
-
206
- `createSceneProjector` was added in 0.5.0. It adapts
219
+ ### State-to-scene projection
220
+
221
+ `createSceneProjector` adapts
207
222
  playback frames from your state model to scenes without changing the algorithm:
208
223
 
209
224
  ```js
@@ -227,309 +242,201 @@ or deep-frozen. Scene validation still happens in the scene renderer.
227
242
  The merge example uses this adapter to avoid rebuilding two cell scenes on every
228
243
  animation frame. The algorithm, object types and visual behavior remain external.
229
244
 
230
- ## Migrating from 0.3.x
245
+ ## Playback and rendering
231
246
 
232
- Existing `/core` imports continue to work in 0.4.0. Built-in insertion and bubble
233
- sort exports are deprecated, but their traces remain unchanged. Migrate each
234
- algorithm into your application using `defineArrayAlgorithm`; complete examples
235
- are in [examples/algorithms.mjs](https://github.com/urffin/algiviz/blob/v0.4.0/examples/algorithms.mjs).
247
+ Array players and timelines use the same frame shape as generic playback:
248
+ `previous`, `current`, `progress`, `stepIndex` and `event`.
249
+ A frame describes the transition into the current step; that step's state is the
250
+ state after its operation.
236
251
 
237
- | Previous API | Application-owned API |
252
+ | API | Behavior |
238
253
  | --- | --- |
239
- | `insertionSortSteps(values)` | `insertion.steps(values)` on your algorithm definition |
240
- | `iterateInsertionSortSteps(values)` | `insertion.iterate(values)` |
241
- | `bubbleSortSteps(values)` / `iterateBubbleSortSteps(values)` | `bubble.steps(values)` / `bubble.iterate(values)` |
242
- | `/core` `createSortTimeline` / `createSortPlayer` | `/array` `createArrayTimeline` / `createArrayPlayer` |
243
- | `/canvas` `createSortRenderer` | `/canvas` `createArrayRenderer` |
244
- | `/core` `SortEvent`, `SortSnapshot`, `SortStep`, `SortFrame` | `/array` `ArrayEvent`, `ArraySnapshot`, `ArrayStep`, `ArrayFrame` |
245
-
246
- Timing options and array frames remain compatible. Exhaustive event switches
247
- must handle the new `highlight` event, even though the legacy algorithms do not
248
- emit it. An incomplete player source now reports a missing terminal step; avoid
249
- depending on the old error text. Generic `/playback` requires an explicit
250
- `isTerminal` predicate; `/array` players still recognize the `done` event.
251
-
252
- Use `/scene` when you need your own object types and visualization handlers.
253
- This is optional for array users: `createArrayRenderer` already uses scenes.
254
- Generic states and custom scene data must remain immutable; unlike array
255
- operations, the generic engine does not clone or deep-freeze them.
256
-
257
- ## Legacy sorting API
258
-
259
- The following `/core` examples remain supported for compatibility. Prefer the
260
- user-defined algorithm API above for new integrations.
261
-
262
- ## Install
263
-
264
- ```sh
265
- npm install @grundyjs/algiviz
266
- ```
267
-
268
- Try the [interactive insertion sort demo](https://grundyjs.ru/algorithms/insertion-sort/). Import the core from `@grundyjs/algiviz/core` and the browser renderer from `@grundyjs/algiviz/canvas`; there is no root entry point or CommonJS build. Version 0.x is an initial API and may change in later minor releases.
269
-
270
- ## Development
271
-
272
- Requires Node.js 22 or later and npm.
273
-
274
- GitHub Actions runs `npm test` and `npm run test:package` on pushes to `master`
275
- and pull requests, using Node.js 22 and 24 on Linux and Windows. CI checks do not
276
- publish packages; publishing remains tied to a GitHub Release.
277
-
278
- ```sh
279
- npm install
280
- npm test
281
- npm pack
282
- ```
283
-
284
- Build output includes ESM and TypeScript declarations. `npm pack` rebuilds automatically; `npm publish` runs the tests before packing. The archive includes only dist, README, LICENSE, CHANGELOG and package metadata. Licensed under MIT.
285
-
286
- ```js
287
- import { insertionSortSteps } from "@grundyjs/algiviz/core";
288
-
289
- const steps = insertionSortSteps([5, 2, 4, 2, 1]);
290
- console.log(steps.at(-1).state.slots.map(item => item.value));
291
- // [1, 2, 2, 4, 5]
292
- ```
293
-
294
- Each step contains a typed event and the complete state **after** it. `start` preserves the input; `done` has no held item and a fully sorted array. Original-index ids (`item-0`, etc.) distinguish equal values and are deterministic within one trace. The input is never modified. Invalid values, including NaN, Infinity and sparse holes, throw TypeError.
295
-
296
- `select` moves the key into `held` and leaves one null slot. `compare` compares the neighbor with the key. `shift` moves the neighbor into the hole. `insert` puts the key back. All original items occur exactly once across `slots` and `held` in every snapshot. Equal values are never shifted past each other.
297
-
298
- `sortedPrefixLength` counts the leading occupied slots known to be sorted: initially min(1, n), during insertion it ends at the hole, after insertion it extends through the processed region. `comparisons` counts compare events. `writes` counts shift and insert events, including reinserting an unchanged key; extracting a key and clearing the hole are visualization bookkeeping, not counted writes.
299
-
300
- The result, steps, events, snapshots, slot arrays and items are frozen at runtime. Items may be shared between snapshots because they are immutable.
301
-
302
- The underlying sort uses O(n²) comparisons in the worst case and O(n) on already sorted input. This API stores full history, so its worst-case time and memory are O(n³), unlike ordinary in-place insertion sort. Intended for small educational inputs; the demo site limits inputs to 24 items. This core imposes no UI limit.
303
-
304
- Tests replay every event independently, check item conservation, stability, prefix ordering, counters, frozen snapshots and 1,093 exhaustive inputs with values -1, 0 and 1 of lengths 0–6.
305
-
306
- ## Bubble sort
307
-
308
- `bubbleSortSteps(values)` collects a frozen history; `iterateBubbleSortSteps(values)`
309
- yields the same snapshots on demand. Both work with the existing timeline, player
310
- and canvas renderer. Available since version 0.3.0.
311
-
312
- ```js
313
- import { bubbleSortSteps, iterateBubbleSortSteps, createSortPlayer } from "@grundyjs/algiviz/core";
314
-
315
- const steps = bubbleSortSteps([5, 2, 4, 2, 1]);
316
- const player = createSortPlayer(iterateBubbleSortSteps([5, 2, 4, 2, 1]), {
317
- stepDurationMs: 350, finalHoldMs: 1000
318
- });
319
- ```
320
-
321
- Each left-to-right pass compares adjacent items. `compare` identifies both items;
322
- `swap` contains their pre-swap `leftId`, `rightId`, `left` and `right` positions,
323
- and its snapshot is after the exchange. Each swap counts as two array writes.
324
- Equal items are never swapped, so the sort is stable. `held` remains null and
325
- `sortedPrefixLength` is zero.
326
-
327
- `pass` marks a completed pass, with `end` identifying its last compared position.
328
- The optional `sortedSuffixLength` snapshot field counts trailing items in their
329
- final positions; renderers treat an omitted value as zero for older traces.
330
- A pass with no swaps ends the sort early and marks the entire array sorted.
331
- `done` always has `sortedSuffixLength` equal to the array length.
332
-
333
- Validation, lazy input copying, freezing and memory costs match insertion sort.
334
- The underlying algorithm performs O(n²) comparisons in the worst case and O(n)
335
- on sorted input; full snapshots still make worst-case trace generation O(n³).
336
- Consumers switching on `SortEvent.type` should handle the new `swap` and `pass` events.
337
-
338
- ## Lazy steps
339
-
340
- Use `iterateInsertionSortSteps` to consume steps on demand without retaining the
341
- entire history. It yields the same immutable `SortStep` snapshots as
342
- `insertionSortSteps`, which collects this iterator into a frozen array.
343
-
344
- ```js
345
- import { iterateInsertionSortSteps } from "@grundyjs/algiviz/core";
346
-
347
- const iterator = iterateInsertionSortSteps([5, 2, 4, 2, 1]);
348
- const start = iterator.next().value;
349
- const selected = iterator.next().value;
350
- iterator.return(); // Stop early when no more steps are needed.
351
- ```
352
-
353
- Creating the generator does not read the input or run the algorithm. The first
354
- `next()` copies and validates the input; invalid values throw at that point.
355
- Changes to the input before the first `next()` are observed; later changes do not
356
- affect the iterator. Each subsequent `next()` runs only to the next step. An
357
- iterator is single-use; create a new one to restart. A `for...of` loop can also
358
- consume it, and `break` closes it early.
359
-
360
- When the consumer retains only a fixed number of snapshots, memory is O(n).
361
- Every snapshot still copies n slots, so consuming the complete worst-case trace
362
- still takes O(n³) time. Collecting the iterator into an array restores the full
363
- history memory cost. Iteration is synchronous; a long loop can block the UI.
364
-
365
- The existing `createSortTimeline` still requires an array of steps for seeking.
366
- It does not accept this iterator directly. Use `createSortPlayer` for forward-only
367
- playback; the generator itself provides no timing or backward seeking.
368
-
369
- ## Sequential playback
370
-
371
- `createSortPlayer` consumes an iterable of steps and retains only the two adjacent
372
- snapshots needed for rendering. With the lazy generator, playback uses O(n)
373
- memory as long as the caller does not retain old frames.
254
+ | `createArrayPlayer(iterable, options)` | Array playback; recognizes the `done` event |
255
+ | `createPlayer(iterable, options)` | Generic playback; requires `isTerminal(step)` |
256
+ | `createArrayTimeline(steps, timing)` / `createTimeline(steps, timing)` | Retained history with `durationMs` and `sample(timeMs)` |
257
+ | `createArrayRenderer({ theme: "dark" \| "light" })` | Full-frame Canvas array rendering |
258
+ | `createSceneRenderer(handlers)` | Dispatches custom objects to your visualization handlers |
259
+ | `createSceneProjector(project)` | Adapts immutable state endpoints to scenes with a bounded cache |
260
+
261
+ Players read the first step at construction and expose `frame`, `advance(deltaMs)`,
262
+ `finished` and `dispose()`. The initial frame has progress 1.
263
+ `advance` accepts finite, non-negative elapsed playback time; zero consumes no steps.
264
+ Pause by not advancing. Apply speed by multiplying elapsed time.
265
+ The optional render callback runs initially and on every advance.
266
+
267
+ The source must reach a terminal step. The player closes the iterator after its
268
+ final transition and becomes finished after `finalHoldMs`. Premature exhaustion,
269
+ source errors and render errors propagate and close the iterator.
270
+ `dispose()` closes early and can be repeated; the frame remains readable, but
271
+ advancing a disposed player throws. Restart with a new iterator and player.
272
+
273
+ Timelines require an array of steps. Step i completes at `i * stepDurationMs`;
274
+ `finalHoldMs` extends the last frame. Sampling clamps finite times to the timeline
275
+ range and rejects non-finite times. There is no seeking or known total duration
276
+ in generator playback.
277
+
278
+ Animation scheduling belongs to your application. For example:
374
279
 
375
280
  ```js
376
- import { createSortPlayer, iterateInsertionSortSteps } from "@grundyjs/algiviz/core";
377
- import { createSortRenderer } from "@grundyjs/algiviz/canvas";
378
-
379
- const player = createSortPlayer(iterateInsertionSortSteps([5, 2, 4, 1]), {
380
- stepDurationMs: 650, finalHoldMs: 2000
381
- });
382
- const renderer = createSortRenderer({ theme: "dark" });
383
- const ctx = canvas.getContext("2d");
384
- let paused = false;
385
- let speed = 1;
281
+ const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), options);
386
282
  let last;
387
283
  let requestId;
284
+ let paused = false;
285
+ let speed = 1;
286
+
388
287
  function tick(now) {
389
- const delta = last === undefined ? 0 : now - last;
288
+ const delta = last === undefined ? 0 : Math.min(100, now - last);
390
289
  last = now;
391
290
  if (!paused) player.advance(delta * speed);
392
291
  renderer.render(ctx, player.frame);
393
292
  if (!player.finished) requestId = requestAnimationFrame(tick);
394
293
  }
395
294
  requestId = requestAnimationFrame(tick);
396
- // Set paused = true/false or speed = 2 from your controls.
397
295
  // On teardown: cancelAnimationFrame(requestId); player.dispose();
296
+ // To replay after finishing, create a new player and schedule a new loop.
398
297
  ```
399
298
 
400
- - Construction reads the first step immediately, including input validation by
401
- the generator. The initial `frame` is fully rendered at progress 1.
402
- - `advance(deltaMs)` accepts finite, non-negative elapsed playback time and
403
- returns a `SortFrame` compatible with the existing canvas renderer. Zero does
404
- not consume steps. Pause by not advancing; multiply delta by a non-negative
405
- speed factor to change speed.
406
- - `finished` becomes true after the final transition and `finalHoldMs`. Further
407
- advances return the final frame. There is no known total duration or seeking.
408
- - The source must end with a `done` event. The player closes the iterator when
409
- that transition completes; unexpected exhaustion or source errors throw and
410
- close playback. `dispose()` closes early and is safe to repeat; afterward the
411
- frame remains readable but `advance()` throws.
412
- - Restart by disposing the old player and creating a new player and generator.
413
- Large deltas synchronously consume all intervening steps, so limit the elapsed
414
- delta in the UI if resuming from a background tab should not cause a long catch-up.
415
-
416
- ## Timeline and canvas
417
-
418
- Import `createSortTimeline` from `@grundyjs/algiviz/core` and `createSortRenderer` from `@grundyjs/algiviz/canvas`.
419
- Step i completes at i * stepDurationMs; finalHoldMs extends the final frame. sample clamps finite times to the timeline range and rejects non-finite times. Frames include the current event for explanations and highlights.
420
- The renderer draws a full frame using the canvas bitmap dimensions and restores context state. Signed values use magnitude for bar height and retain their sign in labels. Use small inputs for readable labels.
299
+ This is an alternative to the player setup in the earlier array example, reusing
300
+ its `reverse`, `options`, `ctx` and `renderer`. Large deltas consume intervening steps
301
+ synchronously, so cap elapsed time or pause on hidden tabs to avoid a long catch-up.
421
302
 
422
- ```js
423
- import { createSortTimeline } from "@grundyjs/algiviz/core";
424
- import { createSortRenderer } from "@grundyjs/algiviz/canvas";
303
+ The array renderer expects immutable snapshots. It uses Canvas bitmap dimensions
304
+ for layout, draws a full frame and restores context state. Negative values use
305
+ magnitude for bar height and retain their sign in labels; this is not a signed-axis
306
+ plot. Dense charts hide labels. For recording, use a canvas with fixed export
307
+ dimensions. Rendering does not start an animation loop or record video.
425
308
 
426
- const timeline = createSortTimeline(steps, { stepDurationMs: 650, finalHoldMs: 2000 });
427
- const renderer = createSortRenderer({ theme: "dark" });
428
- renderer.render(canvas.getContext("2d"), timeline.sample(3250));
429
- ```
309
+ ### Memory and generation cost
430
310
 
431
- Build, serve this project root using any static HTTP server, and open examples/index.html for play, pause, restart and seeking. The example changes bitmap size to fit its container; for video use a separate canvas with fixed export dimensions. Canvas rendering does not start any animation loop. There are no video recording dependencies.
311
+ Array operations copy the input, validate finite numbers and reject sparse inputs.
312
+ `defineArrayAlgorithm` performs this work on the first iterator `next()`;
313
+ `createArrayTrace` validates immediately. Each iterator owns its workspace.
314
+ Array steps, events, snapshots and items are frozen; immutable items can be shared
315
+ between snapshots.
432
316
 
433
- The demo supports history (up to 64 items) and generator playback (up to 2,000).
434
- Enter values or generate random, sorted or reversed arrays. Both modes support
435
- pause, restart and speed changes; seeking is available only with full history.
436
- Algorithm and mode changes restart the loaded array. Choose insertion or bubble
437
- sort; both support both playback modes. These limits apply only to the demo.
438
- Dense charts hide bar labels. Hidden tabs pause playback, and delayed frames cap
439
- catch-up work to keep controls responsive.
317
+ Generator playback retains adjacent snapshots, not the entire history. Array
318
+ playback therefore uses O(n) snapshot memory if the caller does not retain old
319
+ frames. Generic states and algorithm-specific storage may have different costs.
320
+ Each full array snapshot still copies n slots: laziness reduces retained history,
321
+ not the cost of producing every step. Collecting steps restores the history cost.
322
+ For example, a quadratic sorting trace with O(n)-sized snapshots can take O(n³)
323
+ time and history space. Iteration is synchronous and can block the UI.
440
324
 
325
+ ## Browser examples
441
326
 
442
- ## API at a glance
327
+ Build the project, serve the repository root with a static HTTP server and open
328
+ one of these pages. Examples are application code in the repository and are not
329
+ included in the npm archive.
443
330
 
444
- - `insertionSortSteps(values)` returns immutable `SortStep[]` snapshots. Empty arrays are valid.
445
- - `bubbleSortSteps(values)` returns the same snapshot format for stable bubble sort.
446
- - `iterateInsertionSortSteps(values)` and `iterateBubbleSortSteps(values)` yield snapshots lazily.
447
- - `createSortPlayer(iterable, { stepDurationMs, finalHoldMs })` provides forward-only playback with `frame`, `advance(deltaMs)`, `finished` and `dispose()`.
448
- - `createSortTimeline(steps, { stepDurationMs, finalHoldMs })` returns `durationMs` and `sample(timeMs)`. Frames expose `previous`, `current`, `progress`, `stepIndex` and `event`.
449
- - `createSortRenderer({ theme: "dark" | "light" }).render(ctx, frame)` draws into a browser 2D canvas context. Drawing and animation scheduling remain separate.
331
+ | Example | What it demonstrates |
332
+ | --- | --- |
333
+ | [Sorting lab](examples/index.html) | Insertion and bubble sort using the array operations and renderer |
334
+ | [Merge sort](examples/merge.html) | Custom state and a two-row scene for the main array and buffer |
335
+ | [Quick sort partitions](examples/quick.html) | Lomuto and three-way partitioning with custom state and scene |
336
+ | [Graph BFS](examples/graph.html) | Cycles, disconnected components, shortest distances and an animated queue |
337
+ | [Tree traversal](examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
338
+
339
+ All examples reuse [demo-player.mjs](examples/demo-player.mjs) and
340
+ [player-settings.mjs](examples/player-settings.mjs): 650 ms per step, 1,000 ms final
341
+ hold, speeds 0.25× through 32×, play/pause, restart and step controls.
342
+ History enables backward stepping and seeking; generator mode disables both.
343
+ Reduced motion disables interpolation, and hidden tabs pause playback.
344
+
345
+ All sorting examples accept up to **64 finite values with history** or **2,000 in
346
+ generator mode**. Enter values or generate an array by count with Random, Sorted or
347
+ Reverse order. These are demo limits, not library restrictions. Switching a large
348
+ array to history requires first loading an array within the history limit.
349
+ Dense charts hide labels; custom sorting examples preview the first 24 cells in
350
+ their text status.
351
+
352
+ ### Merge sort
353
+
354
+ The [algorithm](examples/merge-algorithm.mjs) and
355
+ [renderer](examples/merge-scene.mjs) use `/playback` and `/scene`.
356
+ Equal values have letter suffixes to demonstrate stability. Cells use row/position
357
+ IDs because copy-back temporarily duplicates item identities. Left wins ties.
358
+ The algorithm uses O(n) auxiliary memory; full snapshot history has an
359
+ O(n² log n) space upper bound.
360
+
361
+ ### Quick sort
362
+
363
+ Choose Lomuto or three-way partitioning while preserving loaded values.
364
+ Both algorithms and their scene belong to the example. Three-way recursion skips
365
+ the equal band: n ≥ 2 equal items take 2n scalar checks and zero swaps, versus
366
+ n(n−1)/2 checks with this Lomuto variant. Neither variant guarantees stability.
367
+ Worst-case algorithm time is quadratic; recursion can use O(n) stack space.
368
+ Full snapshot history has a separate O(n³) space upper bound.
369
+
370
+ ### Tree traversal
371
+
372
+ The example defines its own `node`, `edge` and `pointer` types. The pointer follows
373
+ tree edges through shared ancestors between visited nodes. The Renderer selector
374
+ switches between Canvas and SVG without restarting playback.
375
+ [tree-svg.mjs](examples/tree-svg.mjs) retains elements by ID, restores layer order
376
+ and removes stale elements even after seeking skips exit transitions.
377
+ No tree-specific algorithm or renderer is added to the package.
378
+
379
+ ### Graph breadth-first search
380
+
381
+ Open [examples/graph.html](examples/graph.html) and choose a start vertex.
382
+ The example explores only its connected component, marks vertices when enqueued
383
+ and shows shortest unweighted distances. Its custom vertex, connection and queue
384
+ entry types are projected from immutable algorithm state. Both playback modes use
385
+ the same generator; changing the start creates a new traversal.
386
+
387
+ See [Building a custom graph example](docs/custom-graph-example.md) for the
388
+ algorithm, projection and renderer boundaries. This example uses the existing
389
+ public APIs and adds no graph-specific package exports.
450
390
 
451
- For playback, call `timeline.sample(elapsedMs)` in your own requestAnimationFrame loop. For recording, use a fixed-size export canvas. Negative values are shown by magnitude with signed labels; the chart is not a signed-axis plot.
391
+ ## Development
452
392
 
453
- ## Adding an algorithm
393
+ Requires Node.js 22 or later and npm.
454
394
 
455
- Create an application-owned generator with `defineArrayAlgorithm`, as shown
456
- above. Add it to your application's controls or the demo registry. AlgiViz needs
457
- no change when the existing operations are sufficient. For a new kind of state
458
- or event, implement the generic playback contract and its renderer.
395
+ ```sh
396
+ npm ci
397
+ npm test
398
+ npm run test:package
399
+ ```
459
400
 
460
- ## Release
401
+ `npm test` builds the library, checks TypeScript fixtures and runs the tests.
402
+ `npm run test:package` builds a tarball, installs it offline in an isolated temporary
403
+ consumer, compiles against the installed declarations and exercises public APIs,
404
+ including compatibility exports. The temporary consumer is removed afterward.
461
405
 
462
- Run `npm test` and `npm run test:package`. The package check builds a real tarball,
463
- installs it offline in a separate temporary consumer, compiles TypeScript against
464
- the installed declarations and runs an external algorithm and custom scene with
465
- both playback modes. It also checks the array adapter and legacy compatibility.
466
- The temporary consumer is removed afterward. The release workflow runs this check
467
- before publishing. Source and issues: [urffin/algiviz](https://github.com/urffin/algiviz).
468
- Publishing is a separate maintainer action.
469
-
470
- ### Publishing from GitHub Releases
471
-
472
- The `.github/workflows/npm-publish.yml` workflow publishes to npm when a stable
473
- GitHub Release is published (`release: published`). Drafts, prereleases and tag
474
- pushes alone do not publish a package. The release tag must be `v` followed by
475
- the exact version in `package.json` and `package-lock.json` (for example `v0.4.0`).
476
- The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
477
- build before publishing with provenance, using npm trusted publishing (OIDC).
478
-
479
- One-time setup in the npm package settings, under **Trusted Publisher**:
480
-
481
- - Provider: GitHub Actions.
482
- - Organization or user: `urffin`.
483
- - Repository: `algiviz`.
484
- - Workflow filename: `npm-publish.yml` (without the directory).
485
- - Environment: leave empty; this workflow does not use a GitHub environment.
486
- - Allow direct publishing with `npm publish` if the settings show this option.
487
-
488
- No `NPM_TOKEN` secret is required. Keep account 2FA enabled.
489
- See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
406
+ GitHub Actions runs both checks on pushes to `master` and pull requests, using
407
+ Node.js 22 and 24 on Linux and Windows. CI checks do not publish packages.
490
408
 
491
- For each release, update the version and changelog, commit and push the changes
492
- (including the workflow), then publish a GitHub Release for the matching tag at
493
- that commit. Check the Actions run and the npm package version afterward. Do not
494
- reuse an already published npm version; if setup failed before publication,
495
- correct the settings and rerun the failed workflow.
496
-
497
-
498
- All browser examples share playback defaults from examples/player-settings.mjs:
499
- The browser controller in `examples/demo-player.mjs` also owns animation scheduling,
500
- mode changes, stepping, seeking, restart, reduced motion and generator cleanup.
501
- 650 ms per step, 1,000 ms final hold, speeds 0.25× through 32×, history/generator
502
- modes and forward/backward step controls. Backward stepping and seeking are
503
- disabled in generator mode. Array input limits are 64 for history and 2,000
504
- for generators. Reduced motion disables interpolation; hidden tabs pause.
505
- All sorting demos also share array generation: choose the element count and
506
- Random, Sorted or Reverse order, then Generate & load. The generated values
507
- appear in the input field and obey the selected playback mode's limit.
508
-
509
- ## Quick sort partitions example
510
-
511
- Build and serve the repository root, then open [examples/quick.html](examples/quick.html).
512
- Choose Lomuto or three-way partitioning, preserving loaded values when switching.
513
- Both use the shared demo player: history supports seeking; generator mode retains
514
- adjacent snapshots and supports forward stepping. Speed, restart, reduced motion
515
- and hidden-tab pausing follow the other examples. Both modes are capped at 64
516
- finite values here because direct recursion and worst-case traces are expensive.
517
- This is a demo limit, not a library restriction.
518
-
519
- The example owns its algorithm and scene and uses the public /scene and /playback
520
- APIs. Three-way recursion skips the equal band: n >= 2 equal items take 2n scalar
521
- checks and zero swaps, versus n(n-1)/2 checks with this Lomuto variant. Both may
522
- be unstable and take quadratic worst-case time. The recursion stack can reach
523
- O(n); stored history has a separate O(n³) memory upper bound. This adds no new
524
- package exports and does not require a package release.
409
+ `npm pack` rebuilds automatically. The archive includes dist, README, LICENSE,
410
+ CHANGELOG and package metadata. Source and issues:
411
+ [urffin/algiviz](https://github.com/urffin/algiviz). Licensed under MIT.
525
412
 
526
413
  ### Performance checks
527
414
 
528
415
  Run `npm run benchmark` for 2,000-element insertion, bubble, merge and both quick-sort
529
416
  variants. It reports generation of 1,000 steps, median rendering CPU time over five
530
- 100-frame samples, and retained Node heap after each 5,000 playback steps (up to
531
- 25,000). Drawing uses a mock Canvas: these are CPU comparisons, not browser FPS or
532
- GPU measurements. Heap samples use explicit GC and include the Node process; they
533
- are diagnostics, not portable pass/fail thresholds. Generator playback and scene
534
- projection retain adjacent endpoints, but snapshot size still grows with input size.
535
- Array rendering expects immutable snapshots, as produced by the array workspace.
417
+ 100-frame samples and retained Node heap after each 5,000 playback steps, up to
418
+ 25,000. Drawing uses a mock Canvas: these are CPU comparisons, not browser FPS or
419
+ GPU measurements. Heap samples use explicit GC and include the Node process;
420
+ they are diagnostics, not portable pass/fail thresholds.
421
+
422
+ ## Release
423
+
424
+ Publishing is a maintainer action. Update the package version, lockfile and changelog;
425
+ run `npm test` and `npm run test:package`, commit and push, and check CI on the
426
+ candidate commit.
427
+
428
+ The [publish workflow](.github/workflows/npm-publish.yml) runs when a stable GitHub
429
+ Release is published. Drafts, prereleases and tag pushes alone do not publish.
430
+ Use `v` followed by the exact package version, such as `v0.6.0`, at the tested commit.
431
+
432
+ The workflow uses Node.js 24 and npm 11, checks the version and packed consumer,
433
+ then publishes with provenance through npm trusted publishing (OIDC).
434
+ The publish lifecycle also runs tests and rebuilds. No `NPM_TOKEN` secret is required.
435
+
436
+ The npm trusted publisher must match GitHub owner `urffin`, repository `algiviz`
437
+ and workflow `npm-publish.yml`. The workflow does not use a GitHub environment.
438
+ See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
439
+
440
+ After publication, verify the Actions run and the exact version in npm.
441
+ Never reuse an already published npm version. If a workflow fails, check whether
442
+ publication occurred before deciding whether to rerun it.
@@ -2,3 +2,4 @@ export { createPlayer } from "./player.js";
2
2
  export type { Player } from "./player.js";
3
3
  export { createTimeline } from "./timeline.js";
4
4
  export type { Step, Frame, Timeline } from "./types.js";
5
+ export { createStepEmitter } from "./step-emitter.js";
@@ -1,2 +1,3 @@
1
1
  export { createPlayer } from "./player.js";
2
2
  export { createTimeline } from "./timeline.js";
3
+ export { createStepEmitter } from "./step-emitter.js";
@@ -0,0 +1,6 @@
1
+ import type { Step } from "./types.js";
2
+ /**
3
+ * Capture state in a frozen, consecutively indexed step. Use one emitter per iterator.
4
+ * The callback owns snapshot isolation; state and event are not cloned or frozen.
5
+ */
6
+ export declare function createStepEmitter<State>(snapshot: () => State): <Event>(event: Event) => Step<State, Event>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Capture state in a frozen, consecutively indexed step. Use one emitter per iterator.
3
+ * The callback owns snapshot isolation; state and event are not cloned or frozen.
4
+ */
5
+ export function createStepEmitter(snapshot) {
6
+ let index = 0;
7
+ return (event) => {
8
+ const state = snapshot();
9
+ const step = Object.freeze({ index, state, event });
10
+ index++;
11
+ return step;
12
+ };
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Deterministic algorithm steps for educational visualizations",
5
5
  "type": "module",
6
6
  "files": [