@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 +22 -0
- package/README.md +86 -1
- package/dist/canvas/index.js +6 -9
- package/dist/scene/index.d.ts +2 -0
- package/dist/scene/index.js +19 -0
- package/package.json +2 -1
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.
|
package/dist/canvas/index.js
CHANGED
|
@@ -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
|
|
46
|
-
const items =
|
|
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(({
|
|
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;
|
package/dist/scene/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/scene/index.js
CHANGED
|
@@ -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.
|
|
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",
|