@grundyjs/algiviz 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +200 -243
- 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,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 — 2026-10-14
|
|
4
|
+
|
|
5
|
+
- Update the README around the current public API and examples; remove migration
|
|
6
|
+
instructions and detailed legacy API documentation.
|
|
7
|
+
- Correct demo input limits and clarify playback, snapshot memory and rendering.
|
|
8
|
+
- No runtime API or behavior changes.
|
|
9
|
+
|
|
10
|
+
## 0.5.0 — 2026-10-14
|
|
11
|
+
|
|
12
|
+
- Cache immutable array and quick-sort scene projections and avoid repeated scale/layout
|
|
13
|
+
calculations and hidden text styling on dense charts; add a reproducible CPU/heap benchmark.
|
|
14
|
+
- Add application-owned Lomuto and three-way quick-sort demos with shared playback
|
|
15
|
+
controls and limits (64 history values or 2,000 generator values).
|
|
16
|
+
- Move the tree cursor along edges through shared ancestors in Canvas and SVG.
|
|
17
|
+
|
|
18
|
+
- Add a retained SVG tree renderer alongside Canvas, with layer ordering and seek-safe element cleanup.
|
|
19
|
+
|
|
20
|
+
- Add createSceneProjector to adapt immutable state frames with a two-endpoint cache; use it in the merge example.
|
|
21
|
+
|
|
22
|
+
- Run tests and isolated package-consumer checks on pushes and pull requests,
|
|
23
|
+
using Node.js 22/24 on Linux and Windows.
|
|
24
|
+
|
|
25
|
+
- Share array generation controls and validation across insertion, bubble and merge demos.
|
|
26
|
+
|
|
27
|
+
- Support forward-only generator playback in the merge sort example without retaining history.
|
|
28
|
+
|
|
29
|
+
- Add an application-owned merge sort example using scene/playback, with a main
|
|
30
|
+
array and buffer, stable identity labels, seeking and accessible playback controls.
|
|
31
|
+
|
|
3
32
|
## 0.4.0 — 2026-10-09
|
|
4
33
|
|
|
5
34
|
- Verify the packed npm archive in an isolated TypeScript consumer before publishing.
|
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
|
|
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.5.1.
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
The
|
|
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
|
|
|
@@ -111,7 +125,7 @@ graph/tree algorithm or operation set.
|
|
|
111
125
|
|
|
112
126
|
### User-defined objects and visualizations
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
Define a type map with your own data, then
|
|
115
129
|
provide a visualization for every type. AlgiViz matches identities and dispatches
|
|
116
130
|
objects; your handlers decide how data becomes geometry and how it interpolates.
|
|
117
131
|
|
|
@@ -180,271 +194,214 @@ with `bar` objects whose data contains `item`, `index` and `held`. The built-in
|
|
|
180
194
|
array renderer uses this same dispatcher. You can provide a different `bar`
|
|
181
195
|
visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
|
|
182
196
|
|
|
183
|
-
|
|
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.
|
|
187
|
-
|
|
188
|
-
## Migrating from 0.3.x
|
|
189
|
-
|
|
190
|
-
Existing `/core` imports continue to work in 0.4.0. Built-in insertion and bubble
|
|
191
|
-
sort exports are deprecated, but their traces remain unchanged. Migrate each
|
|
192
|
-
algorithm into your application using `defineArrayAlgorithm`; complete examples
|
|
193
|
-
are in [examples/algorithms.mjs](https://github.com/urffin/algiviz/blob/v0.4.0/examples/algorithms.mjs).
|
|
194
|
-
|
|
195
|
-
| Previous API | Application-owned API |
|
|
196
|
-
| --- | --- |
|
|
197
|
-
| `insertionSortSteps(values)` | `insertion.steps(values)` on your algorithm definition |
|
|
198
|
-
| `iterateInsertionSortSteps(values)` | `insertion.iterate(values)` |
|
|
199
|
-
| `bubbleSortSteps(values)` / `iterateBubbleSortSteps(values)` | `bubble.steps(values)` / `bubble.iterate(values)` |
|
|
200
|
-
| `/core` `createSortTimeline` / `createSortPlayer` | `/array` `createArrayTimeline` / `createArrayPlayer` |
|
|
201
|
-
| `/canvas` `createSortRenderer` | `/canvas` `createArrayRenderer` |
|
|
202
|
-
| `/core` `SortEvent`, `SortSnapshot`, `SortStep`, `SortFrame` | `/array` `ArrayEvent`, `ArraySnapshot`, `ArrayStep`, `ArrayFrame` |
|
|
203
|
-
|
|
204
|
-
Timing options and array frames remain compatible. Exhaustive event switches
|
|
205
|
-
must handle the new `highlight` event, even though the legacy algorithms do not
|
|
206
|
-
emit it. An incomplete player source now reports a missing terminal step; avoid
|
|
207
|
-
depending on the old error text. Generic `/playback` requires an explicit
|
|
208
|
-
`isTerminal` predicate; `/array` players still recognize the `done` event.
|
|
209
|
-
|
|
210
|
-
Use `/scene` when you need your own object types and visualization handlers.
|
|
211
|
-
This is optional for array users: `createArrayRenderer` already uses scenes.
|
|
212
|
-
Generic states and custom scene data must remain immutable; unlike array
|
|
213
|
-
operations, the generic engine does not clone or deep-freeze them.
|
|
214
|
-
|
|
215
|
-
## Legacy sorting API
|
|
216
|
-
|
|
217
|
-
The following `/core` examples remain supported for compatibility. Prefer the
|
|
218
|
-
user-defined algorithm API above for new integrations.
|
|
219
|
-
|
|
220
|
-
## Install
|
|
221
|
-
|
|
222
|
-
```sh
|
|
223
|
-
npm install @grundyjs/algiviz
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
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.
|
|
227
|
-
|
|
228
|
-
## Development
|
|
229
|
-
|
|
230
|
-
Requires Node.js 22 or later and npm.
|
|
231
|
-
|
|
232
|
-
```sh
|
|
233
|
-
npm install
|
|
234
|
-
npm test
|
|
235
|
-
npm pack
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
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.
|
|
239
|
-
|
|
240
|
-
```js
|
|
241
|
-
import { insertionSortSteps } from "@grundyjs/algiviz/core";
|
|
242
|
-
|
|
243
|
-
const steps = insertionSortSteps([5, 2, 4, 2, 1]);
|
|
244
|
-
console.log(steps.at(-1).state.slots.map(item => item.value));
|
|
245
|
-
// [1, 2, 2, 4, 5]
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
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.
|
|
249
|
-
|
|
250
|
-
`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.
|
|
251
|
-
|
|
252
|
-
`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.
|
|
253
|
-
|
|
254
|
-
The result, steps, events, snapshots, slot arrays and items are frozen at runtime. Items may be shared between snapshots because they are immutable.
|
|
255
|
-
|
|
256
|
-
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.
|
|
197
|
+
### State-to-scene projection
|
|
257
198
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
## Bubble sort
|
|
261
|
-
|
|
262
|
-
`bubbleSortSteps(values)` collects a frozen history; `iterateBubbleSortSteps(values)`
|
|
263
|
-
yields the same snapshots on demand. Both work with the existing timeline, player
|
|
264
|
-
and canvas renderer. Available since version 0.3.0.
|
|
265
|
-
|
|
266
|
-
```js
|
|
267
|
-
import { bubbleSortSteps, iterateBubbleSortSteps, createSortPlayer } from "@grundyjs/algiviz/core";
|
|
268
|
-
|
|
269
|
-
const steps = bubbleSortSteps([5, 2, 4, 2, 1]);
|
|
270
|
-
const player = createSortPlayer(iterateBubbleSortSteps([5, 2, 4, 2, 1]), {
|
|
271
|
-
stepDurationMs: 350, finalHoldMs: 1000
|
|
272
|
-
});
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
Each left-to-right pass compares adjacent items. `compare` identifies both items;
|
|
276
|
-
`swap` contains their pre-swap `leftId`, `rightId`, `left` and `right` positions,
|
|
277
|
-
and its snapshot is after the exchange. Each swap counts as two array writes.
|
|
278
|
-
Equal items are never swapped, so the sort is stable. `held` remains null and
|
|
279
|
-
`sortedPrefixLength` is zero.
|
|
280
|
-
|
|
281
|
-
`pass` marks a completed pass, with `end` identifying its last compared position.
|
|
282
|
-
The optional `sortedSuffixLength` snapshot field counts trailing items in their
|
|
283
|
-
final positions; renderers treat an omitted value as zero for older traces.
|
|
284
|
-
A pass with no swaps ends the sort early and marks the entire array sorted.
|
|
285
|
-
`done` always has `sortedSuffixLength` equal to the array length.
|
|
286
|
-
|
|
287
|
-
Validation, lazy input copying, freezing and memory costs match insertion sort.
|
|
288
|
-
The underlying algorithm performs O(n²) comparisons in the worst case and O(n)
|
|
289
|
-
on sorted input; full snapshots still make worst-case trace generation O(n³).
|
|
290
|
-
Consumers switching on `SortEvent.type` should handle the new `swap` and `pass` events.
|
|
291
|
-
|
|
292
|
-
## Lazy steps
|
|
293
|
-
|
|
294
|
-
Use `iterateInsertionSortSteps` to consume steps on demand without retaining the
|
|
295
|
-
entire history. It yields the same immutable `SortStep` snapshots as
|
|
296
|
-
`insertionSortSteps`, which collects this iterator into a frozen array.
|
|
199
|
+
`createSceneProjector` adapts
|
|
200
|
+
playback frames from your state model to scenes without changing the algorithm:
|
|
297
201
|
|
|
298
202
|
```js
|
|
299
|
-
import {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
203
|
+
import { createSceneProjector } from '@grundyjs/algiviz/scene';
|
|
204
|
+
const projectFrame = createSceneProjector(state => ({
|
|
205
|
+
objects: state.items.map(item => ({ id: item.id, type: 'node', data: item }))
|
|
206
|
+
}));
|
|
207
|
+
// Reuse the projector across renders, with either history or generator playback.
|
|
208
|
+
renderer.render(ctx, projectFrame(frame));
|
|
305
209
|
```
|
|
306
210
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
211
|
+
The projection receives only a state, not an event or progress. Use a pure
|
|
212
|
+
function and immutable states/scenes. Endpoints are compared with `Object.is`;
|
|
213
|
+
mutating a retained state or changing captured projection settings will not
|
|
214
|
+
invalidate the cache. Create a new projector when those settings change.
|
|
215
|
+
At most two endpoint projections are retained; seeking to older states may
|
|
216
|
+
recompute them. Events, step indices and progress are preserved. Viewport layout
|
|
217
|
+
and animation belong in render handlers, not in this cached projection.
|
|
218
|
+
The returned frame is frozen; user states and projected scenes are not cloned
|
|
219
|
+
or deep-frozen. Scene validation still happens in the scene renderer.
|
|
220
|
+
The merge example uses this adapter to avoid rebuilding two cell scenes on every
|
|
221
|
+
animation frame. The algorithm, object types and visual behavior remain external.
|
|
222
|
+
|
|
223
|
+
## Playback and rendering
|
|
224
|
+
|
|
225
|
+
Array players and timelines use the same frame shape as generic playback:
|
|
226
|
+
`previous`, `current`, `progress`, `stepIndex` and `event`.
|
|
227
|
+
A frame describes the transition into the current step; that step's state is the
|
|
228
|
+
state after its operation.
|
|
229
|
+
|
|
230
|
+
| API | Behavior |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| `createArrayPlayer(iterable, options)` | Array playback; recognizes the `done` event |
|
|
233
|
+
| `createPlayer(iterable, options)` | Generic playback; requires `isTerminal(step)` |
|
|
234
|
+
| `createArrayTimeline(steps, timing)` / `createTimeline(steps, timing)` | Retained history with `durationMs` and `sample(timeMs)` |
|
|
235
|
+
| `createArrayRenderer({ theme: "dark" \| "light" })` | Full-frame Canvas array rendering |
|
|
236
|
+
| `createSceneRenderer(handlers)` | Dispatches custom objects to your visualization handlers |
|
|
237
|
+
| `createSceneProjector(project)` | Adapts immutable state endpoints to scenes with a bounded cache |
|
|
238
|
+
|
|
239
|
+
Players read the first step at construction and expose `frame`, `advance(deltaMs)`,
|
|
240
|
+
`finished` and `dispose()`. The initial frame has progress 1.
|
|
241
|
+
`advance` accepts finite, non-negative elapsed playback time; zero consumes no steps.
|
|
242
|
+
Pause by not advancing. Apply speed by multiplying elapsed time.
|
|
243
|
+
The optional render callback runs initially and on every advance.
|
|
244
|
+
|
|
245
|
+
The source must reach a terminal step. The player closes the iterator after its
|
|
246
|
+
final transition and becomes finished after `finalHoldMs`. Premature exhaustion,
|
|
247
|
+
source errors and render errors propagate and close the iterator.
|
|
248
|
+
`dispose()` closes early and can be repeated; the frame remains readable, but
|
|
249
|
+
advancing a disposed player throws. Restart with a new iterator and player.
|
|
250
|
+
|
|
251
|
+
Timelines require an array of steps. Step i completes at `i * stepDurationMs`;
|
|
252
|
+
`finalHoldMs` extends the last frame. Sampling clamps finite times to the timeline
|
|
253
|
+
range and rejects non-finite times. There is no seeking or known total duration
|
|
254
|
+
in generator playback.
|
|
255
|
+
|
|
256
|
+
Animation scheduling belongs to your application. For example:
|
|
328
257
|
|
|
329
258
|
```js
|
|
330
|
-
|
|
331
|
-
import { createSortRenderer } from "@grundyjs/algiviz/canvas";
|
|
332
|
-
|
|
333
|
-
const player = createSortPlayer(iterateInsertionSortSteps([5, 2, 4, 1]), {
|
|
334
|
-
stepDurationMs: 650, finalHoldMs: 2000
|
|
335
|
-
});
|
|
336
|
-
const renderer = createSortRenderer({ theme: "dark" });
|
|
337
|
-
const ctx = canvas.getContext("2d");
|
|
338
|
-
let paused = false;
|
|
339
|
-
let speed = 1;
|
|
259
|
+
const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), options);
|
|
340
260
|
let last;
|
|
341
261
|
let requestId;
|
|
262
|
+
let paused = false;
|
|
263
|
+
let speed = 1;
|
|
264
|
+
|
|
342
265
|
function tick(now) {
|
|
343
|
-
const delta = last === undefined ? 0 : now - last;
|
|
266
|
+
const delta = last === undefined ? 0 : Math.min(100, now - last);
|
|
344
267
|
last = now;
|
|
345
268
|
if (!paused) player.advance(delta * speed);
|
|
346
269
|
renderer.render(ctx, player.frame);
|
|
347
270
|
if (!player.finished) requestId = requestAnimationFrame(tick);
|
|
348
271
|
}
|
|
349
272
|
requestId = requestAnimationFrame(tick);
|
|
350
|
-
// Set paused = true/false or speed = 2 from your controls.
|
|
351
273
|
// On teardown: cancelAnimationFrame(requestId); player.dispose();
|
|
274
|
+
// To replay after finishing, create a new player and schedule a new loop.
|
|
352
275
|
```
|
|
353
276
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
returns a `SortFrame` compatible with the existing canvas renderer. Zero does
|
|
358
|
-
not consume steps. Pause by not advancing; multiply delta by a non-negative
|
|
359
|
-
speed factor to change speed.
|
|
360
|
-
- `finished` becomes true after the final transition and `finalHoldMs`. Further
|
|
361
|
-
advances return the final frame. There is no known total duration or seeking.
|
|
362
|
-
- The source must end with a `done` event. The player closes the iterator when
|
|
363
|
-
that transition completes; unexpected exhaustion or source errors throw and
|
|
364
|
-
close playback. `dispose()` closes early and is safe to repeat; afterward the
|
|
365
|
-
frame remains readable but `advance()` throws.
|
|
366
|
-
- Restart by disposing the old player and creating a new player and generator.
|
|
367
|
-
Large deltas synchronously consume all intervening steps, so limit the elapsed
|
|
368
|
-
delta in the UI if resuming from a background tab should not cause a long catch-up.
|
|
369
|
-
|
|
370
|
-
## Timeline and canvas
|
|
371
|
-
|
|
372
|
-
Import `createSortTimeline` from `@grundyjs/algiviz/core` and `createSortRenderer` from `@grundyjs/algiviz/canvas`.
|
|
373
|
-
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.
|
|
374
|
-
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.
|
|
277
|
+
This is an alternative to the player setup in the earlier array example, reusing
|
|
278
|
+
its `reverse`, `options`, `ctx` and `renderer`. Large deltas consume intervening steps
|
|
279
|
+
synchronously, so cap elapsed time or pause on hidden tabs to avoid a long catch-up.
|
|
375
280
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
281
|
+
The array renderer expects immutable snapshots. It uses Canvas bitmap dimensions
|
|
282
|
+
for layout, draws a full frame and restores context state. Negative values use
|
|
283
|
+
magnitude for bar height and retain their sign in labels; this is not a signed-axis
|
|
284
|
+
plot. Dense charts hide labels. For recording, use a canvas with fixed export
|
|
285
|
+
dimensions. Rendering does not start an animation loop or record video.
|
|
379
286
|
|
|
380
|
-
|
|
381
|
-
const renderer = createSortRenderer({ theme: "dark" });
|
|
382
|
-
renderer.render(canvas.getContext("2d"), timeline.sample(3250));
|
|
383
|
-
```
|
|
287
|
+
### Memory and generation cost
|
|
384
288
|
|
|
385
|
-
|
|
289
|
+
Array operations copy the input, validate finite numbers and reject sparse inputs.
|
|
290
|
+
`defineArrayAlgorithm` performs this work on the first iterator `next()`;
|
|
291
|
+
`createArrayTrace` validates immediately. Each iterator owns its workspace.
|
|
292
|
+
Array steps, events, snapshots and items are frozen; immutable items can be shared
|
|
293
|
+
between snapshots.
|
|
386
294
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
295
|
+
Generator playback retains adjacent snapshots, not the entire history. Array
|
|
296
|
+
playback therefore uses O(n) snapshot memory if the caller does not retain old
|
|
297
|
+
frames. Generic states and algorithm-specific storage may have different costs.
|
|
298
|
+
Each full array snapshot still copies n slots: laziness reduces retained history,
|
|
299
|
+
not the cost of producing every step. Collecting steps restores the history cost.
|
|
300
|
+
For example, a quadratic sorting trace with O(n)-sized snapshots can take O(n³)
|
|
301
|
+
time and history space. Iteration is synchronous and can block the UI.
|
|
394
302
|
|
|
303
|
+
## Browser examples
|
|
395
304
|
|
|
396
|
-
|
|
305
|
+
Build the project, serve the repository root with a static HTTP server and open
|
|
306
|
+
one of these pages. Examples are application code in the repository and are not
|
|
307
|
+
included in the npm archive.
|
|
397
308
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
309
|
+
| Example | What it demonstrates |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| [Sorting lab](examples/index.html) | Insertion and bubble sort using the array operations and renderer |
|
|
312
|
+
| [Merge sort](examples/merge.html) | Custom state and a two-row scene for the main array and buffer |
|
|
313
|
+
| [Quick sort partitions](examples/quick.html) | Lomuto and three-way partitioning with custom state and scene |
|
|
314
|
+
| [Tree traversal](examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
|
|
315
|
+
|
|
316
|
+
All examples reuse [demo-player.mjs](examples/demo-player.mjs) and
|
|
317
|
+
[player-settings.mjs](examples/player-settings.mjs): 650 ms per step, 1,000 ms final
|
|
318
|
+
hold, speeds 0.25× through 32×, play/pause, restart and step controls.
|
|
319
|
+
History enables backward stepping and seeking; generator mode disables both.
|
|
320
|
+
Reduced motion disables interpolation, and hidden tabs pause playback.
|
|
321
|
+
|
|
322
|
+
All sorting examples accept up to **64 finite values with history** or **2,000 in
|
|
323
|
+
generator mode**. Enter values or generate an array by count with Random, Sorted or
|
|
324
|
+
Reverse order. These are demo limits, not library restrictions. Switching a large
|
|
325
|
+
array to history requires first loading an array within the history limit.
|
|
326
|
+
Dense charts hide labels; custom sorting examples preview the first 24 cells in
|
|
327
|
+
their text status.
|
|
328
|
+
|
|
329
|
+
### Merge sort
|
|
330
|
+
|
|
331
|
+
The [algorithm](examples/merge-algorithm.mjs) and
|
|
332
|
+
[renderer](examples/merge-scene.mjs) use `/playback` and `/scene`.
|
|
333
|
+
Equal values have letter suffixes to demonstrate stability. Cells use row/position
|
|
334
|
+
IDs because copy-back temporarily duplicates item identities. Left wins ties.
|
|
335
|
+
The algorithm uses O(n) auxiliary memory; full snapshot history has an
|
|
336
|
+
O(n² log n) space upper bound.
|
|
337
|
+
|
|
338
|
+
### Quick sort
|
|
339
|
+
|
|
340
|
+
Choose Lomuto or three-way partitioning while preserving loaded values.
|
|
341
|
+
Both algorithms and their scene belong to the example. Three-way recursion skips
|
|
342
|
+
the equal band: n ≥ 2 equal items take 2n scalar checks and zero swaps, versus
|
|
343
|
+
n(n−1)/2 checks with this Lomuto variant. Neither variant guarantees stability.
|
|
344
|
+
Worst-case algorithm time is quadratic; recursion can use O(n) stack space.
|
|
345
|
+
Full snapshot history has a separate O(n³) space upper bound.
|
|
346
|
+
|
|
347
|
+
### Tree traversal
|
|
348
|
+
|
|
349
|
+
The example defines its own `node`, `edge` and `pointer` types. The pointer follows
|
|
350
|
+
tree edges through shared ancestors between visited nodes. The Renderer selector
|
|
351
|
+
switches between Canvas and SVG without restarting playback.
|
|
352
|
+
[tree-svg.mjs](examples/tree-svg.mjs) retains elements by ID, restores layer order
|
|
353
|
+
and removes stale elements even after seeking skips exit transitions.
|
|
354
|
+
No tree-specific algorithm or renderer is added to the package.
|
|
404
355
|
|
|
405
|
-
|
|
356
|
+
## Development
|
|
406
357
|
|
|
407
|
-
|
|
358
|
+
Requires Node.js 22 or later and npm.
|
|
408
359
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
360
|
+
```sh
|
|
361
|
+
npm ci
|
|
362
|
+
npm test
|
|
363
|
+
npm run test:package
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`npm test` builds the library, checks TypeScript fixtures and runs the tests.
|
|
367
|
+
`npm run test:package` builds a tarball, installs it offline in an isolated temporary
|
|
368
|
+
consumer, compiles against the installed declarations and exercises public APIs,
|
|
369
|
+
including compatibility exports. The temporary consumer is removed afterward.
|
|
370
|
+
|
|
371
|
+
GitHub Actions runs both checks on pushes to `master` and pull requests, using
|
|
372
|
+
Node.js 22 and 24 on Linux and Windows. CI checks do not publish packages.
|
|
373
|
+
|
|
374
|
+
`npm pack` rebuilds automatically. The archive includes dist, README, LICENSE,
|
|
375
|
+
CHANGELOG and package metadata. Source and issues:
|
|
376
|
+
[urffin/algiviz](https://github.com/urffin/algiviz). Licensed under MIT.
|
|
377
|
+
|
|
378
|
+
### Performance checks
|
|
379
|
+
|
|
380
|
+
Run `npm run benchmark` for 2,000-element insertion, bubble, merge and both quick-sort
|
|
381
|
+
variants. It reports generation of 1,000 steps, median rendering CPU time over five
|
|
382
|
+
100-frame samples and retained Node heap after each 5,000 playback steps, up to
|
|
383
|
+
25,000. Drawing uses a mock Canvas: these are CPU comparisons, not browser FPS or
|
|
384
|
+
GPU measurements. Heap samples use explicit GC and include the Node process;
|
|
385
|
+
they are diagnostics, not portable pass/fail thresholds.
|
|
413
386
|
|
|
414
387
|
## Release
|
|
415
388
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
The
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
### Publishing from GitHub Releases
|
|
425
|
-
|
|
426
|
-
The `.github/workflows/npm-publish.yml` workflow publishes to npm when a stable
|
|
427
|
-
GitHub Release is published (`release: published`). Drafts, prereleases and tag
|
|
428
|
-
pushes alone do not publish a package. The release tag must be `v` followed by
|
|
429
|
-
the exact version in `package.json` and `package-lock.json` (for example `v0.4.0`).
|
|
430
|
-
The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
|
|
431
|
-
build before publishing with provenance, using npm trusted publishing (OIDC).
|
|
432
|
-
|
|
433
|
-
One-time setup in the npm package settings, under **Trusted Publisher**:
|
|
434
|
-
|
|
435
|
-
- Provider: GitHub Actions.
|
|
436
|
-
- Organization or user: `urffin`.
|
|
437
|
-
- Repository: `algiviz`.
|
|
438
|
-
- Workflow filename: `npm-publish.yml` (without the directory).
|
|
439
|
-
- Environment: leave empty; this workflow does not use a GitHub environment.
|
|
440
|
-
- Allow direct publishing with `npm publish` if the settings show this option.
|
|
441
|
-
|
|
442
|
-
No `NPM_TOKEN` secret is required. Keep account 2FA enabled.
|
|
443
|
-
See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
389
|
+
Publishing is a maintainer action. Update the package version, lockfile and changelog;
|
|
390
|
+
run `npm test` and `npm run test:package`, commit and push, and check CI on the
|
|
391
|
+
candidate commit.
|
|
392
|
+
|
|
393
|
+
The [publish workflow](.github/workflows/npm-publish.yml) runs when a stable GitHub
|
|
394
|
+
Release is published. Drafts, prereleases and tag pushes alone do not publish.
|
|
395
|
+
Use `v` followed by the exact package version, such as `v0.5.1`, at the tested commit.
|
|
444
396
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
397
|
+
The workflow uses Node.js 24 and npm 11, checks the version and packed consumer,
|
|
398
|
+
then publishes with provenance through npm trusted publishing (OIDC).
|
|
399
|
+
The publish lifecycle also runs tests and rebuilds. No `NPM_TOKEN` secret is required.
|
|
400
|
+
|
|
401
|
+
The npm trusted publisher must match GitHub owner `urffin`, repository `algiviz`
|
|
402
|
+
and workflow `npm-publish.yml`. The workflow does not use a GitHub environment.
|
|
403
|
+
See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
450
404
|
|
|
405
|
+
After publication, verify the Actions run and the exact version in npm.
|
|
406
|
+
Never reuse an already published npm version. If a workflow fails, check whether
|
|
407
|
+
publication occurred before deciding whether to rerun it.
|
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.1",
|
|
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",
|