@grundyjs/algiviz 0.3.0 → 0.5.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,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-10-14
4
+
5
+ - Cache immutable array and quick-sort scene projections and avoid repeated scale/layout
6
+ calculations and hidden text styling on dense charts; add a reproducible CPU/heap benchmark.
7
+ - Add application-owned Lomuto and three-way quick-sort demos with shared playback
8
+ controls and limits (64 history values or 2,000 generator values).
9
+ - Move the tree cursor along edges through shared ancestors in Canvas and SVG.
10
+
11
+ - Add a retained SVG tree renderer alongside Canvas, with layer ordering and seek-safe element cleanup.
12
+
13
+ - Add createSceneProjector to adapt immutable state frames with a two-endpoint cache; use it in the merge example.
14
+
15
+ - Run tests and isolated package-consumer checks on pushes and pull requests,
16
+ using Node.js 22/24 on Linux and Windows.
17
+
18
+ - Share array generation controls and validation across insertion, bubble and merge demos.
19
+
20
+ - Support forward-only generator playback in the merge sort example without retaining history.
21
+
22
+ - Add an application-owned merge sort example using scene/playback, with a main
23
+ array and buffer, stable identity labels, seeking and accessible playback controls.
24
+
25
+ ## 0.4.0 — 2026-10-09
26
+
27
+ - Verify the packed npm archive in an isolated TypeScript consumer before publishing.
28
+
29
+ - Add `/scene` with typed user-defined object data and visualization handlers,
30
+ identity matching, enter/update/exit transitions, layers and reference lookup.
31
+ - Render array bars through the scene dispatcher and expose `arrayScene`.
32
+ - Add an external tree traversal demo defining its own nodes, edges and pointer.
33
+
34
+ - Add algorithm-free `/array` and `/playback` entry points: user-defined generators,
35
+ validated array operations, generic states/events and optional rendering callbacks.
36
+ - Add `highlight` events and `createArrayRenderer`; existing rendering names remain available.
37
+ - Move demo algorithms into application-owned examples using only the public API.
38
+ Old `/core` algorithm exports remain deprecated compatibility adapters.
39
+ - Preserve all original insertion/bubble traces; premature exhaustion now reports
40
+ a missing terminal step rather than a missing done event.
41
+
3
42
  ## 0.3.0 — 2026-10-09
4
43
 
5
44
  - Extend `SortEvent` with `swap` and `pass`, and `SortSnapshot` with optional
package/README.md CHANGED
@@ -1,6 +1,263 @@
1
1
  # AlgiViz
2
2
 
3
- A TypeScript library for educational algorithm visualization: stable insertion and bubble sort traces, a seekable timeline and a canvas renderer. No runtime dependencies. The core works without DOM, React or Next.js. ESM only, with TypeScript declarations.
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.
4
+
5
+ ## User-defined algorithms
6
+
7
+ The following API is introduced in 0.4.0.
8
+ New applications use these entry points, which do not import bundled algorithms:
9
+
10
+ - `@grundyjs/algiviz/array`: array operations and generator definitions.
11
+ - `@grundyjs/algiviz/playback`: generic playback of any immutable states and events.
12
+ - `@grundyjs/algiviz/scene`: typed objects and user-provided visualizations.
13
+ - `@grundyjs/algiviz/canvas`: the ready-made array renderer.
14
+
15
+ You own the loops, conditions and operation order. No library registration or
16
+ changes to AlgiViz are required. For example, reversing an array:
17
+
18
+ ```js
19
+ import { defineArrayAlgorithm, createArrayPlayer, createArrayTimeline } from "@grundyjs/algiviz/array";
20
+ import { createArrayRenderer } from "@grundyjs/algiviz/canvas";
21
+
22
+ const reverse = defineArrayAlgorithm(function* (array) {
23
+ yield array.start();
24
+ for (let i = 0; i < Math.floor(array.length / 2); i++) {
25
+ yield array.highlight([i, array.length - 1 - i]);
26
+ yield array.swap(i, array.length - 1 - i);
27
+ }
28
+ yield array.done();
29
+ });
30
+
31
+ const options = { stepDurationMs: 350, finalHoldMs: 1000 };
32
+ const ctx = canvas.getContext("2d");
33
+ const renderer = createArrayRenderer({ theme: "dark" });
34
+ const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), {
35
+ ...options,
36
+ render: frame => renderer.render(ctx, frame)
37
+ });
38
+ // Your animation loop calls player.advance(deltaMs * speed).
39
+ // No calls while paused; advance(0) redraws after resizing.
40
+ // On teardown: cancel your animation loop and call player.dispose().
41
+
42
+ // Alternatively, retain history and seek:
43
+ const timeline = createArrayTimeline(reverse.steps([1, 2, 3, 4]), options);
44
+ renderer.render(ctx, timeline.sample(500));
45
+ ```
46
+
47
+ `defineArrayAlgorithm` returns `iterate(values)` and `steps(values)`. Each
48
+ iterator owns a separate workspace and copies/validates input on the first
49
+ `next()`. `steps` collects the iterator into a frozen array. Yield every
50
+ operation immediately, begin with `start`, and end with `done`. Each operation
51
+ updates the workspace and returns an immutable snapshot; it does not draw.
52
+ The player invokes the supplied `render` initially and on every `advance`.
53
+ Rendering errors close the iterator and propagate to the caller.
54
+
55
+ ### Array operations
56
+
57
+ | Operation | Effect |
58
+ | --- | --- |
59
+ | `at(index)`, `held`, `length` | Read a frozen item, the held item or array length |
60
+ | `start(regions?)` | Emit initial state; call once |
61
+ | `compare(left, right)` | Highlight two positions and count one comparison; either position can be `"held"` |
62
+ | `highlight(positions)` | Highlight items for this step without changing counters |
63
+ | `swap(left, right, regions?)` | Exchange occupied positions; count two writes (non-adjacent swaps are supported) |
64
+ | `select(from, regions?)` | Hold an item and leave a hole; no counted writes |
65
+ | `shift(from, to, regions?)` | Move an item into the hole; count one write |
66
+ | `insert(to, regions?)` | Fill the hole with the held item; count one write |
67
+ | `pass(end, regions?)` | Describe a completed pass without changing items or counters |
68
+ | `done(regions?)` | Finish; requires no held item; does not sort or infer sorted regions |
69
+
70
+ `regions` can contain `sortedPrefixLength` and `sortedSuffixLength`.
71
+ Omitted markers retain their previous values. The author decides when regions
72
+ are sorted; AlgiViz validates their bounds but does not prove that claim. Positions
73
+ are zero-based; invalid positions, occupied destinations and invalid operation
74
+ order throw before mutating the workspace. Read snapshots rather than modifying
75
+ items yourself. `createArrayTrace(values)` exposes the same operations directly,
76
+ with immediate input validation, for authors writing their own generator wrapper.
77
+
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.
84
+
85
+ ### Custom structures and renderers
86
+
87
+ `/playback` does not know arrays, sorting, Canvas, or event names. A source yields
88
+ `Step<State, Event>` objects with contiguous zero-based `index`, immutable
89
+ `state` and `event`. Supply `isTerminal` to identify the final step and
90
+ `render` to draw each interpolated frame using any rendering technology:
91
+
92
+ ```js
93
+ import { createPlayer, createTimeline } from "@grundyjs/algiviz/playback";
94
+
95
+ const player = createPlayer(myTraversal(), {
96
+ stepDurationMs: 300,
97
+ finalHoldMs: 1000,
98
+ isTerminal: step => step.event.kind === "finished",
99
+ render: frame => drawGraph(frame.previous, frame.current, frame.progress, frame.event)
100
+ });
101
+ ```
102
+
103
+ The render callback is optional. Without it, read `player.frame` and render
104
+ manually. `createTimeline(steps, timing)` supports seeking with the same generic
105
+ frame shape; call your renderer with its `sample(timeMs)` result. The generic
106
+ engine does not clone or freeze user states; that is the source's responsibility.
107
+ It retains adjacent snapshots only; the timeline retains the supplied history.
108
+ Custom array events or tree/graph operations can use this contract and a custom
109
+ renderer. The scene API below dispatches individual objects; there is no built-in
110
+ graph/tree algorithm or operation set.
111
+
112
+ ### User-defined objects and visualizations
113
+
114
+ The `/scene` API is introduced in 0.4.0. Define a type map with your own data, then
115
+ provide a visualization for every type. AlgiViz matches identities and dispatches
116
+ objects; your handlers decide how data becomes geometry and how it interpolates.
117
+
118
+ ```ts
119
+ import { createSceneRenderer, type Scene } from "@grundyjs/algiviz/scene";
120
+
121
+ interface MyObjects {
122
+ badge: { x: number; y: number; label: string };
123
+ }
124
+
125
+ const renderer = createSceneRenderer<MyObjects, CanvasRenderingContext2D>({
126
+ badge(ctx, object) {
127
+ const from = (object.previous ?? object.current)!.data;
128
+ const to = (object.current ?? object.previous)!.data;
129
+ const x = from.x + (to.x - from.x) * object.progress;
130
+ const y = from.y + (to.y - from.y) * object.progress;
131
+ ctx.save();
132
+ try {
133
+ ctx.globalAlpha = object.presence;
134
+ ctx.fillText(to.label, x, y);
135
+ } finally { ctx.restore(); }
136
+ }
137
+ });
138
+
139
+ const before: Scene<MyObjects> = {
140
+ objects: [{ id: "first", type: "badge", data: { x: 20, y: 30, label: "A" } }]
141
+ };
142
+ const after: Scene<MyObjects> = {
143
+ objects: [{ id: "first", type: "badge", data: { x: 120, y: 30, label: "A" } }]
144
+ };
145
+ // Keep these states immutable. A generic timeline/player can provide this frame.
146
+ renderer.render(ctx, {
147
+ previous: before, current: after, progress: 0.5, stepIndex: 1, event: "move"
148
+ });
149
+ ```
150
+
151
+ - An object has a non-empty stable `id`, a `type`, user-defined `data` and an
152
+ optional finite `zIndex` (default zero). IDs are unique within a snapshot. A
153
+ retained ID cannot change type; use a new ID to replace an object of another type.
154
+ - Handlers receive `previous` and `current` objects, with `null` for a missing
155
+ endpoint. `phase` is `enter`, `update` or `exit`. `progress` is the raw 0–1 frame
156
+ progress. `presence` is progress on entry, 1 on update and 1−progress on exit;
157
+ handlers may use it for opacity or scale. It is not applied automatically.
158
+ - Lower `zIndex` draws first. Ties follow the current snapshot's object order;
159
+ removed objects follow in their previous order. The current layer wins when it
160
+ changes. There is no implicit layout or coordinate system.
161
+ - The third handler argument exposes `scene.get(id)` for links, pointers and other
162
+ references. It returns the referenced object's full transition, including both
163
+ endpoints; missing references return `undefined`. The application decides how to
164
+ handle them and how to interpolate positions or changing targets.
165
+ - `createSceneFrame(frame)` exposes the same resolved transitions without drawing.
166
+ The registry is captured at renderer construction. Missing handlers, duplicate
167
+ IDs, invalid progress/layers and changed types fail before any handler is called.
168
+ - Scene processing is stateless, so arbitrary seeking is supported. Handlers may
169
+ see zero-presence objects. They are frame draws, **not guaranteed lifecycle
170
+ callbacks**: skipping steps can skip appearances/exits entirely. Clear Canvas
171
+ each frame; a retained SVG/DOM backend must reconcile its elements against the
172
+ current frame, removing stale IDs itself. Save/restore Canvas state inside each
173
+ handler. Exceptions propagate; there is no rollback of drawing already performed.
174
+ - Snapshots and user data are not cloned or deep-frozen. The author owns their
175
+ immutability, as with generic playback. Scene indexing uses O(n) additional
176
+ memory and layer ordering O(n log n) time per frame.
177
+
178
+ `arrayScene(snapshot)` from `/canvas` adapts an array to a `Scene<ArrayObjects>`
179
+ with `bar` objects whose data contains `item`, `index` and `held`. The built-in
180
+ array renderer uses this same dispatcher. You can provide a different `bar`
181
+ visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
182
+
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
207
+ playback frames from your state model to scenes without changing the algorithm:
208
+
209
+ ```js
210
+ import { createSceneProjector } from '@grundyjs/algiviz/scene';
211
+ const projectFrame = createSceneProjector(state => ({
212
+ objects: state.items.map(item => ({ id: item.id, type: 'node', data: item }))
213
+ }));
214
+ // Reuse the projector across renders, with either history or generator playback.
215
+ renderer.render(ctx, projectFrame(frame));
216
+ ```
217
+
218
+ The projection receives only a state, not an event or progress. Use a pure
219
+ function and immutable states/scenes. Endpoints are compared with `Object.is`;
220
+ mutating a retained state or changing captured projection settings will not
221
+ invalidate the cache. Create a new projector when those settings change.
222
+ At most two endpoint projections are retained; seeking to older states may
223
+ recompute them. Events, step indices and progress are preserved. Viewport layout
224
+ and animation belong in render handlers, not in this cached projection.
225
+ The returned frame is frozen; user states and projected scenes are not cloned
226
+ or deep-frozen. Scene validation still happens in the scene renderer.
227
+ The merge example uses this adapter to avoid rebuilding two cell scenes on every
228
+ animation frame. The algorithm, object types and visual behavior remain external.
229
+
230
+ ## Migrating from 0.3.x
231
+
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).
236
+
237
+ | Previous API | Application-owned API |
238
+ | --- | --- |
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.
4
261
 
5
262
  ## Install
6
263
 
@@ -14,6 +271,10 @@ Try the [interactive insertion sort demo](https://grundyjs.ru/algorithms/inserti
14
271
 
15
272
  Requires Node.js 22 or later and npm.
16
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
+
17
278
  ```sh
18
279
  npm install
19
280
  npm test
@@ -189,53 +450,29 @@ catch-up work to keep controls responsive.
189
450
 
190
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.
191
452
 
192
- ## Adding an algorithm to this repository
453
+ ## Adding an algorithm
193
454
 
194
- Implement a generator in `src/core/` using the internal `createSortTrace` helper
195
- from `./sort-trace.js`. Call it **inside** the generator body to preserve lazy
196
- validation. It provides a mutable `state` workspace and `emit(event)`; emitting
197
- copies and freezes the snapshot, assigns its step index, and updates counters.
198
- It retains no history. The helper is internal, not a public package export.
199
-
200
- The algorithm owns its loop, array changes and sorted-region markers. Always
201
- change the workspace before yielding the event that describes that change:
202
-
203
- ```ts
204
- // Inside a generator, after checking that adjacent items need exchanging:
205
- const a = state.slots[left]!;
206
- const b = state.slots[right]!;
207
- state.slots[left] = b;
208
- state.slots[right] = a;
209
- yield emit({ type: "swap", leftId: a.id, rightId: b.id, left, right });
210
- ```
211
-
212
- Yield `start` before sorting and `done` after updating the final sorted region.
213
- Do not increment counters manually: `compare` adds one comparison, `shift` and
214
- `insert` add one write, and `swap` adds two writes. Reuse the frozen items created
215
- by the helper and preserve each item's identity across moves.
216
-
217
- Add a history wrapper with `Object.freeze([...iterateYourSortSteps(values)])`,
218
- export both functions from `src/core/index.ts`, and register the algorithm in
219
- `examples/demo.mjs` plus its select option in `examples/index.html`. The timeline
220
- and sequential player need no algorithm-specific changes. A new operation also
221
- requires updating `SortEvent`, its counter semantics and canvas highlighting or
222
- movement; existing operations can reuse the renderer.
223
-
224
- Use the bubble-sort tests as a guide: independently replay events, verify stable
225
- ordering and item conservation, check frozen snapshots and lazy validation, and
226
- compare sequential frames with the timeline. This helper reduces snapshot
227
- boilerplate; it does not validate that an algorithm's events match its mutations.
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.
228
459
 
229
460
  ## Release
230
461
 
231
- Run `npm test`, `npm pack --dry-run` and test the resulting tarball in a separate consumer project. Source and issues: [urffin/algiviz](https://github.com/urffin/algiviz). Publishing is a separate maintainer action.
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.
232
469
 
233
470
  ### Publishing from GitHub Releases
234
471
 
235
472
  The `.github/workflows/npm-publish.yml` workflow publishes to npm when a stable
236
473
  GitHub Release is published (`release: published`). Drafts, prereleases and tag
237
474
  pushes alone do not publish a package. The release tag must be `v` followed by
238
- the exact version in `package.json` and `package-lock.json` (for example `v0.3.0`).
475
+ the exact version in `package.json` and `package-lock.json` (for example `v0.4.0`).
239
476
  The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
240
477
  build before publishing with provenance, using npm trusted publishing (OIDC).
241
478
 
@@ -257,3 +494,42 @@ that commit. Check the Actions run and the npm package version afterward. Do not
257
494
  reuse an already published npm version; if setup failed before publication,
258
495
  correct the settings and rerun the failed workflow.
259
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.
525
+
526
+ ### Performance checks
527
+
528
+ Run `npm run benchmark` for 2,000-element insertion, bubble, merge and both quick-sort
529
+ 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.
@@ -0,0 +1,7 @@
1
+ export { createArrayTrace, defineArrayAlgorithm } from "./trace.js";
2
+ export type { ArrayTrace, ArrayAlgorithm, SortedRegions } from "./trace.js";
3
+ export type { Item, SortEvent as ArrayEvent, SortSnapshot as ArraySnapshot, SortStep as ArrayStep } from "./types.js";
4
+ export { createSortPlayer as createArrayPlayer } from "../core/player.js";
5
+ export { createSortTimeline as createArrayTimeline } from "../core/timeline.js";
6
+ export type { SortPlayer as ArrayPlayer } from "../core/player.js";
7
+ export type { SortFrame as ArrayFrame, SortTimeline as ArrayTimeline } from "../core/timeline.js";
@@ -0,0 +1,3 @@
1
+ export { createArrayTrace, defineArrayAlgorithm } from "./trace.js";
2
+ export { createSortPlayer as createArrayPlayer } from "../core/player.js";
3
+ export { createSortTimeline as createArrayTimeline } from "../core/timeline.js";
@@ -0,0 +1,27 @@
1
+ import type { Item, SortStep } from "./types.js";
2
+ export interface SortedRegions {
3
+ readonly sortedPrefixLength?: number;
4
+ readonly sortedSuffixLength?: number;
5
+ }
6
+ export interface ArrayTrace {
7
+ readonly length: number;
8
+ readonly held: Item | null;
9
+ at(index: number): Item | null;
10
+ start(regions?: SortedRegions): SortStep;
11
+ compare(left: number | "held", right: number | "held"): SortStep;
12
+ highlight(positions: readonly (number | "held")[]): SortStep;
13
+ swap(left: number, right: number, regions?: SortedRegions): SortStep;
14
+ select(from: number, regions?: SortedRegions): SortStep;
15
+ shift(from: number, to: number, regions?: SortedRegions): SortStep;
16
+ insert(to: number, regions?: SortedRegions): SortStep;
17
+ pass(end: number, regions?: SortedRegions): SortStep;
18
+ done(regions?: SortedRegions): SortStep;
19
+ }
20
+ /** Operations own mutations, counters and immutable snapshots, but no algorithm. */
21
+ export declare function createArrayTrace(values: readonly number[]): ArrayTrace;
22
+ export interface ArrayAlgorithm {
23
+ iterate(values: readonly number[]): Generator<SortStep, void, unknown>;
24
+ steps(values: readonly number[]): readonly SortStep[];
25
+ }
26
+ /** Each iterator owns its workspace; input is read on its first next(). */
27
+ export declare function defineArrayAlgorithm(algorithm: (array: ArrayTrace) => Generator<SortStep, void, unknown>): ArrayAlgorithm;
@@ -0,0 +1,119 @@
1
+ import { createSortTrace } from "./snapshot-builder.js";
2
+ /** Operations own mutations, counters and immutable snapshots, but no algorithm. */
3
+ export function createArrayTrace(values) {
4
+ const { state, emit } = createSortTrace(values);
5
+ const { slots } = state;
6
+ let phase = "new";
7
+ function index(value) {
8
+ if (!Number.isInteger(value) || value < 0 || value >= slots.length)
9
+ throw new RangeError(`Invalid array index: ${value}`);
10
+ }
11
+ function active() {
12
+ if (phase !== "running")
13
+ throw new Error("Call start before operations; no operations are allowed after done");
14
+ }
15
+ function item(position) {
16
+ if (position !== "held")
17
+ index(position);
18
+ const value = position === "held" ? state.held : slots[position];
19
+ if (!value)
20
+ throw new Error(`No item at ${position}`);
21
+ return value;
22
+ }
23
+ function regions(update = {}) {
24
+ for (const value of [update.sortedPrefixLength, update.sortedSuffixLength]) {
25
+ if (value !== undefined && (!Number.isInteger(value) || value < 0 || value > slots.length)) {
26
+ throw new RangeError("Sorted region length must be between zero and array length");
27
+ }
28
+ }
29
+ if (update.sortedPrefixLength !== undefined)
30
+ state.sortedPrefixLength = update.sortedPrefixLength;
31
+ if (update.sortedSuffixLength !== undefined)
32
+ state.sortedSuffixLength = update.sortedSuffixLength;
33
+ }
34
+ return Object.freeze({
35
+ get length() { return slots.length; },
36
+ get held() { return state.held; },
37
+ at(position) { index(position); return slots[position]; },
38
+ start(update) {
39
+ if (phase !== "new")
40
+ throw new Error("Trace has already started");
41
+ regions(update);
42
+ phase = "running";
43
+ return emit({ type: "start" });
44
+ },
45
+ compare(left, right) {
46
+ active();
47
+ const a = item(left), b = item(right);
48
+ return emit({ type: "compare", leftId: a.id, rightId: b.id });
49
+ },
50
+ highlight(positions) {
51
+ active();
52
+ const itemIds = Object.freeze(positions.map(position => item(position).id));
53
+ return emit({ type: "highlight", itemIds });
54
+ },
55
+ swap(left, right, update) {
56
+ active();
57
+ const a = item(left), b = item(right);
58
+ if (left === right)
59
+ throw new RangeError("Swap requires distinct positions");
60
+ regions(update);
61
+ slots[left] = b;
62
+ slots[right] = a;
63
+ return emit({ type: "swap", leftId: a.id, rightId: b.id, left, right });
64
+ },
65
+ select(from, update) {
66
+ active();
67
+ if (state.held)
68
+ throw new Error("An item is already held");
69
+ const selected = item(from);
70
+ regions(update);
71
+ state.held = selected;
72
+ slots[from] = null;
73
+ return emit({ type: "select", itemId: selected.id, from });
74
+ },
75
+ shift(from, to, update) {
76
+ active();
77
+ const moved = item(from);
78
+ index(to);
79
+ if (slots[to] !== null)
80
+ throw new Error("Shift destination must be empty");
81
+ regions(update);
82
+ slots[to] = moved;
83
+ slots[from] = null;
84
+ return emit({ type: "shift", itemId: moved.id, from, to });
85
+ },
86
+ insert(to, update) {
87
+ active();
88
+ const inserted = item("held");
89
+ index(to);
90
+ if (slots[to] !== null)
91
+ throw new Error("Insert destination must be empty");
92
+ regions(update);
93
+ slots[to] = inserted;
94
+ state.held = null;
95
+ return emit({ type: "insert", itemId: inserted.id, to });
96
+ },
97
+ pass(end, update) {
98
+ active();
99
+ index(end);
100
+ regions(update);
101
+ return emit({ type: "pass", end });
102
+ },
103
+ done(update) {
104
+ active();
105
+ if (state.held)
106
+ throw new Error("Insert the held item before completing the trace");
107
+ regions(update);
108
+ phase = "done";
109
+ return emit({ type: "done" });
110
+ }
111
+ });
112
+ }
113
+ /** Each iterator owns its workspace; input is read on its first next(). */
114
+ export function defineArrayAlgorithm(algorithm) {
115
+ function* iterate(values) {
116
+ yield* algorithm(createArrayTrace(values));
117
+ }
118
+ return Object.freeze({ iterate, steps: (values) => Object.freeze([...iterate(values)]) });
119
+ }
@@ -0,0 +1,55 @@
1
+ export type Item = Readonly<{
2
+ id: string;
3
+ value: number;
4
+ }>;
5
+ export type SortEvent = Readonly<{
6
+ type: "start";
7
+ } | {
8
+ type: "select";
9
+ itemId: string;
10
+ from: number;
11
+ } | {
12
+ type: "compare";
13
+ leftId: string;
14
+ rightId: string;
15
+ } | {
16
+ type: "shift";
17
+ itemId: string;
18
+ from: number;
19
+ to: number;
20
+ } | {
21
+ type: "insert";
22
+ itemId: string;
23
+ to: number;
24
+ } | {
25
+ type: "swap";
26
+ leftId: string;
27
+ rightId: string;
28
+ left: number;
29
+ right: number;
30
+ } | {
31
+ type: "pass";
32
+ end: number;
33
+ } | {
34
+ type: "highlight";
35
+ itemIds: readonly string[];
36
+ } | {
37
+ type: "done";
38
+ }>;
39
+ export interface SortSnapshot {
40
+ readonly slots: readonly (Item | null)[];
41
+ readonly held: Item | null;
42
+ /** Sorted leading occupied slots; during insertion it ends at the hole. */
43
+ readonly sortedPrefixLength: number;
44
+ /** Final sorted trailing slots. Omitted by older traces and insertion sort. */
45
+ readonly sortedSuffixLength?: number;
46
+ readonly comparisons: number;
47
+ /** Writes into array slots; selecting a key does not count as a write. */
48
+ readonly writes: number;
49
+ }
50
+ export interface SortStep {
51
+ readonly index: number;
52
+ readonly event: SortEvent;
53
+ /** Complete immutable state immediately after the event. */
54
+ readonly state: SortSnapshot;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,16 @@
1
+ import type { Item, SortSnapshot } from "../core/types.js";
1
2
  import type { SortFrame } from "../core/timeline.js";
3
+ import { type Scene } from "../scene/index.js";
4
+ export type ArrayObjects = {
5
+ bar: {
6
+ item: Item;
7
+ index: number;
8
+ held: boolean;
9
+ };
10
+ };
11
+ /** Adapts array identities to the same public scene contract used by custom objects. */
12
+ export declare function arrayScene(state: SortSnapshot): Scene<ArrayObjects>;
13
+ export { createSortRenderer as createArrayRenderer };
2
14
  /** Stateless full-frame renderer. Bitmap dimensions determine layout and video size. */
3
15
  export declare function createSortRenderer(options: {
4
16
  theme: "light" | "dark";