@grundyjs/algiviz 0.6.0 → 0.7.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1 — 2026-10-15
4
+
5
+ - Update package description and keywords for custom algorithm visualizations.
6
+ - Improve the README introduction, demo links and standalone integration guide.
7
+ - Add a BFS GIF preview exported with CanvasTake, hosted on GitHub.
8
+ - No runtime API or behavior changes.
9
+
10
+ ## 0.7.0 — 2026-10-15
11
+
12
+ - Breaking: remove the deprecated /core entry point and bundled insertion/bubble
13
+ algorithms. Application-owned examples use /array; playback adapters now live there.
14
+
15
+ - Preserve source step indices in generic timeline frames, matching generator playback.
16
+ - Preserve primary playback errors when iterator cleanup also fails; standalone
17
+ cleanup failures still propagate.
18
+
3
19
  ## 0.6.0 — 2026-10-14
4
20
 
5
21
  - Add an application-owned graph BFS example with custom vertices, edges and queue
package/README.md CHANGED
@@ -1,11 +1,56 @@
1
1
  # AlgiViz
2
2
 
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.
3
+ **Your algorithms and objects. Step-by-step playback and custom rendering.**
7
4
 
8
- This README describes version 0.6.0.
5
+ ![BFS from A: custom graph vertices, edges and queue animated with AlgiViz](https://raw.githubusercontent.com/urffin/algiviz/master/docs/assets/bfs.gif)
6
+
7
+ *An application-owned BFS algorithm with custom objects. Exported with
8
+ [CanvasTake](https://github.com/urffin/canvas-take) —
9
+ [try an interactive sorting demo](https://grundyjs.ru/algorithms/insertion-sort/)
10
+ or explore the [BFS source](https://github.com/urffin/algiviz/blob/master/examples/graph-demo.mjs).*
11
+
12
+ A TypeScript toolkit for building interactive algorithm explanations. Write your
13
+ algorithm as a generator, emit immutable states and choose how your objects look.
14
+ AlgiViz handles playback and scene matching; optional array operations and a Canvas
15
+ renderer help you get started.
16
+
17
+ [Live sorting demo](https://grundyjs.ru/algorithms/insertion-sort/) ·
18
+ [Library overview](https://grundyjs.ru/algiviz/) ·
19
+ [npm package](https://www.npmjs.com/package/@grundyjs/algiviz) ·
20
+ [Minimal example source](https://github.com/urffin/algiviz/blob/master/examples/minimal.mjs)
21
+
22
+ - **Your algorithm:** control the loops, conditions and order of operations.
23
+ - **Your objects:** define custom types and handlers for Canvas, SVG or another target.
24
+ - **Two playback modes:** retain history for seeking, or consume a generator with
25
+ only adjacent snapshots retained by the player.
26
+ - **No runtime dependencies:** ESM and TypeScript declarations; playback works
27
+ without DOM, React or Next.js.
28
+
29
+ Use it for educational demos, interactive articles and algorithm walkthroughs.
30
+ Algorithms and layouts belong to your application; AlgiViz provides the playback
31
+ and rendering API. This README describes version 0.7.1.
32
+
33
+ ## Start with one custom object
34
+
35
+ The [standalone page](https://github.com/urffin/algiviz/blob/master/examples/minimal.html)
36
+ moves one token with a generator and a custom Canvas handler. It uses only public
37
+ package imports, without the shared demo controller. Copy its
38
+ [complete module](https://github.com/urffin/algiviz/blob/master/examples/minimal.mjs)
39
+ into your application and connect a canvas:
40
+
41
+ ```js
42
+ import { createTokenPlayer } from './minimal.mjs';
43
+
44
+ const canvas = document.querySelector('canvas');
45
+ const player = createTokenPlayer(canvas.getContext('2d'));
46
+ player.advance(100); // Advance playback by 100 ms.
47
+ // Call advance(deltaMs) from your controls or animation loop.
48
+ // Call player.dispose() when removing the visualization.
49
+ ```
50
+
51
+ [Follow the walkthrough](https://github.com/urffin/algiviz/blob/master/docs/minimal-example.md)
52
+ for local setup, snapshot ownership and lifecycle. For a larger custom-object
53
+ example, see the [graph BFS walkthrough](https://github.com/urffin/algiviz/blob/master/docs/custom-graph-example.md).
9
54
 
10
55
  ## Install
11
56
 
@@ -16,7 +61,7 @@ npm install @grundyjs/algiviz
16
61
  There is no root entry point or CommonJS build. Import the modules you need:
17
62
  `/array`, `/playback`, `/scene` and `/canvas`.
18
63
 
19
- The deprecated `/core` sorting API remains available for compatibility.
64
+ Algorithms live in application code and examples.
20
65
  For new integrations, use the APIs below. Version 0.x may introduce breaking changes
21
66
  in minor releases; consult [CHANGELOG.md](CHANGELOG.md) when updating.
22
67
 
@@ -123,6 +168,20 @@ Custom array events or tree/graph operations can use this contract and a custom
123
168
  renderer. The scene API below dispatches individual objects; there is no built-in
124
169
  graph/tree algorithm or operation set.
125
170
 
171
+ ### Playback contracts
172
+
173
+ Playback retains application snapshots without cloning them; keep steps, state and
174
+ events immutable while they are in use. A timeline copies the input array only.
175
+ Timeline timing follows array position, and finite sample times are clamped to its
176
+ range. Generator playback requires an explicit terminal step; an early end throws.
177
+ `dispose()` closes the source at most once, keeps the last frame readable and
178
+ prevents further advancement. Restart uses a fresh source.
179
+
180
+ Version 0.7.0 makes timeline `stepIndex` preserve the source `step.index`,
181
+ matching the player. If source consumption, a terminal predicate or rendering
182
+ throws, cleanup is attempted without replacing that primary error. A standalone
183
+ cleanup error from disposal or normal completion still propagates.
184
+
126
185
  ### Step emission helper (since 0.6.0)
127
186
 
128
187
  Version 0.6.0 adds `createStepEmitter` from `/playback`. It removes manual step numbering and freezes the step wrapper:
@@ -333,6 +392,7 @@ included in the npm archive.
333
392
  | [Sorting lab](examples/index.html) | Insertion and bubble sort using the array operations and renderer |
334
393
  | [Merge sort](examples/merge.html) | Custom state and a two-row scene for the main array and buffer |
335
394
  | [Quick sort partitions](examples/quick.html) | Lomuto and three-way partitioning with custom state and scene |
395
+ | [Heap sort](examples/heap.html) | Max-heap construction and extraction with synchronized tree and array identities |
336
396
  | [Graph BFS](examples/graph.html) | Cycles, disconnected components, shortest distances and an animated queue |
337
397
  | [Tree traversal](examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
338
398
 
@@ -367,6 +427,18 @@ n(n−1)/2 checks with this Lomuto variant. Neither variant guarantees stability
367
427
  Worst-case algorithm time is quadratic; recursion can use O(n) stack space.
368
428
  Full snapshot history has a separate O(n³) space upper bound.
369
429
 
430
+ ### Heap sort
431
+
432
+ The [algorithm](examples/heap-algorithm.mjs) owns immutable states and events.
433
+ The [scene](examples/heap-scene.mjs) assigns separate tree/array visual IDs to each
434
+ element and uses the public scene projector with viewport-independent endpoints.
435
+ Tree positions stay fixed as the heap shrinks. At most 15 array cells wrap on narrow
436
+ screens; larger arrays use a dense row and hide unreadable labels.
437
+ Bottom-up construction is O(n), worst-case sorting O(n log n), and the iterative
438
+ in-place algorithm uses O(1) auxiliary space. This demo copies input and retains
439
+ O(n) adjacent snapshot data in generator mode; full history can use O(n² log n).
440
+ It is not stable: [2a, 2b, 1] becomes [1, 2b, 2a].
441
+
370
442
  ### Tree traversal
371
443
 
372
444
  The example defines its own `node`, `edge` and `pointer` types. The pointer follows
@@ -401,7 +473,7 @@ npm run test:package
401
473
  `npm test` builds the library, checks TypeScript fixtures and runs the tests.
402
474
  `npm run test:package` builds a tarball, installs it offline in an isolated temporary
403
475
  consumer, compiles against the installed declarations and exercises public APIs,
404
- including compatibility exports. The temporary consumer is removed afterward.
476
+ including rejection of the removed `/core` export. The temporary consumer is removed afterward.
405
477
 
406
478
  GitHub Actions runs both checks on pushes to `master` and pull requests, using
407
479
  Node.js 22 and 24 on Linux and Windows. CI checks do not publish packages.
@@ -427,7 +499,7 @@ candidate commit.
427
499
 
428
500
  The [publish workflow](.github/workflows/npm-publish.yml) runs when a stable GitHub
429
501
  Release is published. Drafts, prereleases and tag pushes alone do not publish.
430
- Use `v` followed by the exact package version, such as `v0.6.0`, at the tested commit.
502
+ Use `v` followed by the exact package version, such as `v0.7.1`, at the tested commit.
431
503
 
432
504
  The workflow uses Node.js 24 and npm 11, checks the version and packed consumer,
433
505
  then publishes with provenance through npm trusted publishing (OIDC).
@@ -1,7 +1,7 @@
1
1
  export { createArrayTrace, defineArrayAlgorithm } from "./trace.js";
2
2
  export type { ArrayTrace, ArrayAlgorithm, SortedRegions } from "./trace.js";
3
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";
4
+ export { createArrayPlayer } from "./player.js";
5
+ export { createArrayTimeline } from "./timeline.js";
6
+ export type { ArrayPlayer } from "./player.js";
7
+ export type { ArrayFrame, ArrayTimeline } from "./timeline.js";
@@ -1,3 +1,3 @@
1
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";
2
+ export { createArrayPlayer } from "./player.js";
3
+ export { createArrayTimeline } from "./timeline.js";
@@ -0,0 +1,10 @@
1
+ import { type Player } from "../playback/index.js";
2
+ import type { SortStep, SortSnapshot, SortEvent } from "./types.js";
3
+ import type { ArrayFrame } from "./timeline.js";
4
+ export type ArrayPlayer = Player<SortSnapshot, SortEvent>;
5
+ /** Array playback adapter for array steps ending in done. */
6
+ export declare function createArrayPlayer(steps: Iterable<SortStep>, options: {
7
+ stepDurationMs: number;
8
+ finalHoldMs: number;
9
+ render?: (frame: ArrayFrame) => void;
10
+ }): ArrayPlayer;
@@ -1,5 +1,5 @@
1
1
  import { createPlayer } from "../playback/index.js";
2
- /** Compatibility adapter for array steps ending in done. */
3
- export function createSortPlayer(steps, options) {
2
+ /** Array playback adapter for array steps ending in done. */
3
+ export function createArrayPlayer(steps, options) {
4
4
  return createPlayer(steps, { ...options, isTerminal: step => step.event.type === "done" });
5
5
  }
@@ -0,0 +1,8 @@
1
+ import { type Frame, type Timeline } from "../playback/index.js";
2
+ import type { SortEvent, SortSnapshot, SortStep } from "./types.js";
3
+ export type ArrayFrame = Frame<SortSnapshot, SortEvent>;
4
+ export type ArrayTimeline = Timeline<SortSnapshot, SortEvent>;
5
+ export declare function createArrayTimeline(steps: readonly SortStep[], options: {
6
+ stepDurationMs: number;
7
+ finalHoldMs: number;
8
+ }): ArrayTimeline;
@@ -1,4 +1,4 @@
1
1
  import { createTimeline } from "../playback/index.js";
2
- export function createSortTimeline(steps, options) {
2
+ export function createArrayTimeline(steps, options) {
3
3
  return createTimeline(steps, options);
4
4
  }
@@ -1,5 +1,5 @@
1
- import type { Item, SortSnapshot } from "../core/types.js";
2
- import type { SortFrame } from "../core/timeline.js";
1
+ import type { Item, SortSnapshot } from "../array/types.js";
2
+ import type { ArrayFrame as SortFrame } from "../array/timeline.js";
3
3
  import { type Scene } from "../scene/index.js";
4
4
  export type ArrayObjects = {
5
5
  bar: {
@@ -13,6 +13,13 @@ export function createPlayer(steps, options) {
13
13
  iterator.return?.();
14
14
  }
15
15
  };
16
+ // A cleanup failure must not replace the source, predicate or render error.
17
+ const closeAfterFailure = () => {
18
+ try {
19
+ close();
20
+ }
21
+ catch { /* Preserve the error already being handled. */ }
22
+ };
16
23
  let current;
17
24
  try {
18
25
  const first = iterator.next();
@@ -21,7 +28,7 @@ export function createPlayer(steps, options) {
21
28
  current = first.value;
22
29
  }
23
30
  catch (error) {
24
- close();
31
+ closeAfterFailure();
25
32
  throw error;
26
33
  }
27
34
  let previous = current;
@@ -46,7 +53,7 @@ export function createPlayer(steps, options) {
46
53
  options.render?.(frame());
47
54
  }
48
55
  catch (error) {
49
- close();
56
+ closeAfterFailure();
50
57
  throw error;
51
58
  }
52
59
  return Object.freeze({
@@ -85,7 +92,7 @@ export function createPlayer(steps, options) {
85
92
  }
86
93
  catch (error) {
87
94
  disposed = true;
88
- close();
95
+ closeAfterFailure();
89
96
  throw error;
90
97
  }
91
98
  },
@@ -1,5 +1,5 @@
1
1
  import type { Step, Timeline } from "./types.js";
2
- /** Step i is complete at i * stepDurationMs; the final state is held afterwards. */
2
+ /** The step at array position i is complete at i * stepDurationMs; the final state is held afterwards. */
3
3
  export declare function createTimeline<S, E>(steps: readonly Step<S, E>[], options: {
4
4
  stepDurationMs: number;
5
5
  finalHoldMs: number;
@@ -1,4 +1,4 @@
1
- /** Step i is complete at i * stepDurationMs; the final state is held afterwards. */
1
+ /** The step at array position i is complete at i * stepDurationMs; the final state is held afterwards. */
2
2
  export function createTimeline(steps, options) {
3
3
  if (!steps.length)
4
4
  throw new RangeError("Timeline requires at least one step");
@@ -24,7 +24,7 @@ export function createTimeline(steps, options) {
24
24
  const progress = previous === step ? 1 :
25
25
  Math.min(1, (time - (index - 1) * stepDurationMs) / stepDurationMs);
26
26
  return Object.freeze({ previous: previous.state, current: step.state,
27
- progress, stepIndex: index, event: step.event });
27
+ progress, stepIndex: step.index, event: step.event });
28
28
  }
29
29
  });
30
30
  }
@@ -8,6 +8,7 @@ export interface Frame<S, E> {
8
8
  readonly previous: S;
9
9
  readonly current: S;
10
10
  readonly progress: number;
11
+ /** Source step.index; timing is based on sequence position, not this value. */
11
12
  readonly stepIndex: number;
12
13
  readonly event: E;
13
14
  }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.6.0",
4
- "description": "Deterministic algorithm steps for educational visualizations",
3
+ "version": "0.7.1",
4
+ "description": "TypeScript toolkit for custom algorithm visualizations with step-by-step playback and extensible rendering",
5
5
  "type": "module",
6
6
  "files": [
7
- "dist",
7
+ "dist/array",
8
+ "dist/playback",
9
+ "dist/scene",
10
+ "dist/canvas",
8
11
  "README.md",
9
12
  "LICENSE",
10
13
  "CHANGELOG.md"
@@ -25,11 +28,6 @@
25
28
  "import": "./dist/playback/index.js",
26
29
  "default": "./dist/playback/index.js"
27
30
  },
28
- "./core": {
29
- "types": "./dist/core/index.d.ts",
30
- "import": "./dist/core/index.js",
31
- "default": "./dist/core/index.js"
32
- },
33
31
  "./canvas": {
34
32
  "types": "./dist/canvas/index.d.ts",
35
33
  "import": "./dist/canvas/index.js",
@@ -54,10 +52,14 @@
54
52
  },
55
53
  "homepage": "https://grundyjs.ru/algiviz/",
56
54
  "keywords": [
55
+ "algorithm-visualization",
57
56
  "algorithms",
58
57
  "visualization",
59
- "insertion-sort",
60
- "bubble-sort",
58
+ "animation",
59
+ "step-by-step",
60
+ "playback",
61
+ "data-structures",
62
+ "educational",
61
63
  "canvas",
62
64
  "typescript"
63
65
  ],
@@ -1,2 +0,0 @@
1
- export declare const insertion: import("../array/trace.js").ArrayAlgorithm;
2
- export declare const bubble: import("../array/trace.js").ArrayAlgorithm;
@@ -1,36 +0,0 @@
1
- import { defineArrayAlgorithm } from "../array/trace.js";
2
- // Legacy core exports only. New consumers provide algorithms like examples/algorithms.mjs.
3
- export const insertion = defineArrayAlgorithm(function* (array) {
4
- yield array.start({ sortedPrefixLength: Math.min(1, array.length) });
5
- for (let i = 1; i < array.length; i++) {
6
- const key = array.at(i);
7
- yield array.select(i, { sortedPrefixLength: i });
8
- let hole = i;
9
- while (hole > 0) {
10
- yield array.compare(hole - 1, 'held');
11
- if (array.at(hole - 1).value <= key.value)
12
- break;
13
- yield array.shift(hole - 1, hole, { sortedPrefixLength: hole - 1 });
14
- hole--;
15
- }
16
- yield array.insert(hole, { sortedPrefixLength: i + 1 });
17
- }
18
- yield array.done({ sortedPrefixLength: array.length });
19
- });
20
- export const bubble = defineArrayAlgorithm(function* (array) {
21
- yield array.start({ sortedSuffixLength: array.length < 2 ? array.length : 0 });
22
- for (let end = array.length - 1; end > 0; end--) {
23
- let swapped = false;
24
- for (let left = 0; left < end; left++) {
25
- yield array.compare(left, left + 1);
26
- if (array.at(left).value <= array.at(left + 1).value)
27
- continue;
28
- yield array.swap(left, left + 1);
29
- swapped = true;
30
- }
31
- yield array.pass(end, { sortedSuffixLength: swapped ? array.length - end : array.length });
32
- if (!swapped)
33
- break;
34
- }
35
- yield array.done({ sortedSuffixLength: array.length });
36
- });
@@ -1,4 +0,0 @@
1
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
2
- export declare const bubbleSortSteps: (values: readonly number[]) => readonly import("./types.js").SortStep[];
3
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
4
- export declare const iterateBubbleSortSteps: (values: readonly number[]) => Generator<import("./types.js").SortStep, void, unknown>;
@@ -1,5 +0,0 @@
1
- import { bubble } from "../compat/algorithms.js";
2
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
3
- export const bubbleSortSteps = bubble.steps;
4
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
5
- export const iterateBubbleSortSteps = bubble.iterate;
@@ -1,7 +0,0 @@
1
- export { insertionSortSteps, iterateInsertionSortSteps } from "./insertion-sort.js";
2
- export { bubbleSortSteps, iterateBubbleSortSteps } from "./bubble-sort.js";
3
- export type { Item, SortEvent, SortSnapshot, SortStep } from "./types.js";
4
- export { createSortTimeline } from "./timeline.js";
5
- export type { SortFrame, SortTimeline } from "./timeline.js";
6
- export { createSortPlayer } from "./player.js";
7
- export type { SortPlayer } from "./player.js";
@@ -1,4 +0,0 @@
1
- export { insertionSortSteps, iterateInsertionSortSteps } from "./insertion-sort.js";
2
- export { bubbleSortSteps, iterateBubbleSortSteps } from "./bubble-sort.js";
3
- export { createSortTimeline } from "./timeline.js";
4
- export { createSortPlayer } from "./player.js";
@@ -1,4 +0,0 @@
1
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
2
- export declare const insertionSortSteps: (values: readonly number[]) => readonly import("./types.js").SortStep[];
3
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
4
- export declare const iterateInsertionSortSteps: (values: readonly number[]) => Generator<import("./types.js").SortStep, void, unknown>;
@@ -1,5 +0,0 @@
1
- import { insertion } from "../compat/algorithms.js";
2
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
3
- export const insertionSortSteps = insertion.steps;
4
- /** @deprecated Define your algorithm with @grundyjs/algiviz/array. */
5
- export const iterateInsertionSortSteps = insertion.iterate;
@@ -1,10 +0,0 @@
1
- import { type Player } from "../playback/index.js";
2
- import type { SortStep, SortSnapshot, SortEvent } from "./types.js";
3
- import type { SortFrame } from "./timeline.js";
4
- export type SortPlayer = Player<SortSnapshot, SortEvent>;
5
- /** Compatibility adapter for array steps ending in done. */
6
- export declare function createSortPlayer(steps: Iterable<SortStep>, options: {
7
- stepDurationMs: number;
8
- finalHoldMs: number;
9
- render?: (frame: SortFrame) => void;
10
- }): SortPlayer;
@@ -1,8 +0,0 @@
1
- import { type Frame, type Timeline } from "../playback/index.js";
2
- import type { SortEvent, SortSnapshot, SortStep } from "./types.js";
3
- export type SortFrame = Frame<SortSnapshot, SortEvent>;
4
- export type SortTimeline = Timeline<SortSnapshot, SortEvent>;
5
- export declare function createSortTimeline(steps: readonly SortStep[], options: {
6
- stepDurationMs: number;
7
- finalHoldMs: number;
8
- }): SortTimeline;
@@ -1 +0,0 @@
1
- export type { Item, SortEvent, SortSnapshot, SortStep } from "../array/types.js";
@@ -1 +0,0 @@
1
- export {};