@grundyjs/algiviz 0.4.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,27 @@
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
+
3
25
  ## 0.4.0 — 2026-10-09
4
26
 
5
27
  - Verify the packed npm archive in an isolated TypeScript consumer before publishing.
package/README.md CHANGED
@@ -183,7 +183,49 @@ visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
183
183
  See [examples/tree-scene.mjs](examples/tree-scene.mjs) and open
184
184
  `examples/tree.html` after building and serving the repository. The application
185
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.
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.
187
229
 
188
230
  ## Migrating from 0.3.x
189
231
 
@@ -229,6 +271,10 @@ Try the [interactive insertion sort demo](https://grundyjs.ru/algorithms/inserti
229
271
 
230
272
  Requires Node.js 22 or later and npm.
231
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
+
232
278
  ```sh
233
279
  npm install
234
280
  npm test
@@ -448,3 +494,42 @@ that commit. Check the Actions run and the npm package version afterward. Do not
448
494
  reuse an already published npm version; if setup failed before publication,
449
495
  correct the settings and rerun the failed workflow.
450
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.
@@ -1,4 +1,4 @@
1
- import { createSceneRenderer } from "../scene/index.js";
1
+ import { createSceneRenderer, createSceneProjector } from "../scene/index.js";
2
2
  /** Adapts array identities to the same public scene contract used by custom objects. */
3
3
  export function arrayScene(state) {
4
4
  return Object.freeze({
@@ -37,19 +37,20 @@ function positions(state) {
37
37
  /** Stateless full-frame renderer. Bitmap dimensions determine layout and video size. */
38
38
  export function createSortRenderer(options) {
39
39
  const colors = palettes[options.theme];
40
+ const projectFrame = createSceneProjector(arrayScene);
40
41
  return Object.freeze({
41
42
  render(ctx, frame) {
42
43
  const { width, height } = ctx.canvas;
43
44
  if (width <= 0 || height <= 0)
44
45
  return;
45
- const after = positions(frame.current);
46
- const items = [...after.values()];
46
+ const projected = projectFrame(frame);
47
+ const items = projected.current.objects;
47
48
  const count = frame.current.slots.length;
48
49
  const margin = Math.min(32, width * 0.05);
49
50
  const cell = (width - 2 * margin) / Math.max(count, 1);
50
51
  const barWidth = cell * 0.7;
51
52
  const available = height * 0.48;
52
- const maxMagnitude = Math.max(1, ...items.map(({ item }) => Math.abs(item.value)));
53
+ const maxMagnitude = Math.max(1, ...items.map(({ data }) => Math.abs(data.item.value)));
53
54
  const baseline = height * 0.69;
54
55
  const fontSize = Math.max(9, Math.min(20, cell * 0.45, width * 0.035));
55
56
  const progress = frame.progress;
@@ -99,11 +100,7 @@ export function createSortRenderer(options) {
99
100
  ctx.fillText(String(next.index), margin + (next.index + 0.5) * cell, baseline + height * 0.06);
100
101
  }
101
102
  });
102
- sceneRenderer.render(ctx, {
103
- ...frame,
104
- previous: arrayScene(frame.previous),
105
- current: arrayScene(frame.current)
106
- });
103
+ sceneRenderer.render(ctx, projected);
107
104
  ctx.textAlign = "left";
108
105
  ctx.font = `${Math.max(11, Math.min(18, width * 0.03))}px sans-serif`;
109
106
  ctx.fillStyle = colors.text;
@@ -40,3 +40,5 @@ export declare function createSceneFrame<Types extends object, Event>(frame: Fra
40
40
  export declare function createSceneRenderer<Types extends object, Context, Event = unknown>(visualizations: SceneVisualizations<Types, Context, Event>): Readonly<{
41
41
  render(context: Context, frame: Frame<Scene<Types>, Event>): void;
42
42
  }>;
43
+ /** Projects immutable state endpoints, retaining at most two scenes. */
44
+ export declare function createSceneProjector<State, Types extends object>(project: (state: State) => Scene<Types>): <Event>(frame: Frame<State, Event>) => Frame<Scene<Types>, Event>;
@@ -63,3 +63,22 @@ export function createSceneRenderer(visualizations) {
63
63
  }
64
64
  });
65
65
  }
66
+ /** Projects immutable state endpoints, retaining at most two scenes. */
67
+ export function createSceneProjector(project) {
68
+ let cache = [];
69
+ return (frame) => {
70
+ const next = [];
71
+ function endpoint(state) {
72
+ const found = [...next, ...cache].find(entry => Object.is(entry.state, state));
73
+ const entry = found ?? { state, scene: project(state) };
74
+ if (!next.includes(entry))
75
+ next.push(entry);
76
+ return entry.scene;
77
+ }
78
+ const previous = endpoint(frame.previous);
79
+ const current = endpoint(frame.current);
80
+ // Failed projections do not replace the previously successful cache.
81
+ cache = next;
82
+ return Object.freeze({ previous, current, progress: frame.progress, stepIndex: frame.stepIndex, event: frame.event });
83
+ };
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Deterministic algorithm steps for educational visualizations",
5
5
  "type": "module",
6
6
  "files": [
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsc -p tsconfig.json",
41
+ "benchmark": "npm run build && node --expose-gc scripts/benchmark.mjs",
41
42
  "test": "npm run build && tsc -p test/tsconfig.json && node --test test/*.test.mjs",
42
43
  "test:package": "node scripts/test-package.mjs",
43
44
  "prepack": "npm run build",