@grundyjs/algiviz 0.10.0 → 0.11.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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 — 2026-11-02
4
+
5
+ - Add createStatePlayer() for lazy playback of plain immutable states without
6
+ step wrappers, event fields or an isTerminal predicate.
7
+ - Consume one state per request without lookahead. A request after the final
8
+ state detects exhaustion, preserves the last frame and index, and completes
9
+ playback or starts an explicitly configured final hold.
10
+ - Support manual and timed playback, infinite sources, disposal and source errors.
11
+ - Simplify the interactive minimal example and English/Russian READMEs; update
12
+ the highlighted GIF to show the extra completion request.
13
+
3
14
  ## 0.10.0 — 2026-10-27
4
15
 
5
16
  - Default finalHoldMs to zero for generic and array players.
package/README.md CHANGED
@@ -55,14 +55,14 @@ module through a bundler that resolves imports from the installed npm package.
55
55
  ```
56
56
 
57
57
  ```js
58
- import { createPlayer } from '@grundyjs/algiviz/playback';
58
+ import { createStatePlayer } from '@grundyjs/algiviz/playback';
59
59
  import { createSceneRenderer } from '@grundyjs/algiviz/scene';
60
60
 
61
61
  const ctx = document.querySelector('canvas').getContext('2d');
62
- function* steps() {
63
- for (const [index, x] of [40, 200, 360, 560].entries()) {
62
+ function* states() {
63
+ for (const x of [40, 200, 360, 560]) {
64
64
  const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
65
- yield { index, state, event: index === 3 ? 'done' : 'position' };
65
+ yield state;
66
66
  }
67
67
  }
68
68
  const renderer = createSceneRenderer({
@@ -73,8 +73,7 @@ const renderer = createSceneRenderer({
73
73
  ctx.fill();
74
74
  },
75
75
  });
76
- const player = createPlayer(steps(), {
77
- isTerminal: step => step.event === 'done',
76
+ const player = createStatePlayer(states(), {
78
77
  render(frame) {
79
78
  ctx.clearRect(0, 0, 600, 160);
80
79
  renderer.render(ctx, frame);
@@ -83,7 +82,9 @@ const player = createPlayer(steps(), {
83
82
  document.querySelector('#next').onclick = () => player.nextStep();
84
83
  ```
85
84
 
86
- The first frame (`x = 40`) is drawn immediately inside `createPlayer(...)`. Each
85
+ After the last state, one more click detects generator exhaustion and keeps the final position. No lookahead is performed.
86
+
87
+ The first frame (`x = 40`) is drawn immediately inside `createStatePlayer(...)`. Each
87
88
  click requests the next state: the player resumes the loop until the next
88
89
  `yield`, then renders. There is no interpolation here: the handler draws only
89
90
  `current`. Each snapshot is created afresh and never mutated. Call
@@ -107,7 +108,7 @@ scheduling belong to the application.
107
108
 
108
109
  ## Playback modes
109
110
 
110
- | | Generator: `createPlayer` | History: `createTimeline` |
111
+ | | Generator: `createPlayer` / `createStatePlayer` | History: `createTimeline` |
111
112
  | --- | --- | --- |
112
113
  | Steps | Consumed on demand | Collected up front |
113
114
  | Retention | Adjacent snapshots | Complete history |
@@ -141,6 +142,7 @@ Generators reduce retained history, not the cost of producing each snapshot.
141
142
  | [Binary search](https://github.com/urffin/algiviz/blob/master/examples/binary-search.html) | First occurrence in sorted input, closed search intervals and lazy playback |
142
143
  | [Graph BFS](https://github.com/urffin/algiviz/blob/master/examples/graph.html) | Queue, layers, shortest paths, unreachable targets and history/generator playback |
143
144
  | [Graph DFS](https://github.com/urffin/algiviz/blob/master/examples/dfs.html) | Recursive call frames, backtracking, tree depth and paths; history/generator playback |
145
+ | [Maze](https://github.com/urffin/algiviz/blob/master/examples/maze.html) | Randomized DFS generation, BFS wave, DFS backtracking, cycles and unreachable exits; history/generator playback |
144
146
  | [Tree traversal](https://github.com/urffin/algiviz/blob/master/examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
145
147
 
146
148
  [Full catalog, local setup and algorithm details](https://github.com/urffin/algiviz/blob/master/docs/examples.md).
@@ -1,4 +1,5 @@
1
1
  export { createPlayer } from "./player.js";
2
+ export { createStatePlayer } from "./state-player.js";
2
3
  export type { Player } from "./player.js";
3
4
  export { createTimeline } from "./timeline.js";
4
5
  export type { Step, Frame, Timeline } from "./types.js";
@@ -1,3 +1,4 @@
1
1
  export { createPlayer } from "./player.js";
2
+ export { createStatePlayer } from "./state-player.js";
2
3
  export { createTimeline } from "./timeline.js";
3
4
  export { createStepEmitter } from "./step-emitter.js";
@@ -21,3 +21,5 @@ export declare function createPlayer<S, E>(steps: Iterable<Step<S, E>>, options:
21
21
  /** Called initially and on each advance or nextStep, including after completion. */
22
22
  render?: (frame: Frame<S, E>) => void;
23
23
  }): Player<S, E>;
24
+ /** Internal engine shared by explicit-step and exhaustion-based sources. */
25
+ export declare function createPlayback<S, E>(steps: Iterable<Step<S, E>>, options: Parameters<typeof createPlayer<S, E>>[1], finishOnExhaustion?: boolean): Player<S, E>;
@@ -1,5 +1,9 @@
1
1
  /** Forward-only playback. The source must reach a step accepted by isTerminal. */
2
2
  export function createPlayer(steps, options) {
3
+ return createPlayback(steps, options);
4
+ }
5
+ /** Internal engine shared by explicit-step and exhaustion-based sources. */
6
+ export function createPlayback(steps, options, finishOnExhaustion = false) {
3
7
  const manual = options.stepDurationMs === undefined;
4
8
  // Unit endpoint for manual steps; positive timed advancement is disallowed.
5
9
  const { stepDurationMs = 1, finalHoldMs = 0 } = options;
@@ -38,8 +42,10 @@ export function createPlayer(steps, options) {
38
42
  let hold = 0;
39
43
  let disposed = false;
40
44
  let finished = false;
45
+ let exhausted = false;
46
+ const terminal = () => exhausted || options.isTerminal(current);
41
47
  const complete = () => {
42
- if (options.isTerminal(current) && elapsed === stepDurationMs) {
48
+ if (terminal() && elapsed === stepDurationMs) {
43
49
  previous = current;
44
50
  close();
45
51
  finished = hold >= finalHoldMs;
@@ -67,15 +73,20 @@ export function createPlayer(steps, options) {
67
73
  try {
68
74
  if (!finished) {
69
75
  if (elapsed === stepDurationMs) {
70
- if (options.isTerminal(current)) {
76
+ if (terminal()) {
71
77
  hold = finalHoldMs;
72
78
  }
73
79
  else {
74
80
  const next = iterator.next();
75
- if (next.done)
76
- throw new RangeError("Step source ended without a terminal step");
77
- previous = current;
78
- current = next.value;
81
+ if (next.done) {
82
+ if (!finishOnExhaustion)
83
+ throw new RangeError("Step source ended without a terminal step");
84
+ exhausted = true;
85
+ }
86
+ else {
87
+ previous = current;
88
+ current = next.value;
89
+ }
79
90
  }
80
91
  }
81
92
  // Assign the endpoint directly: time arithmetic can undershoot or
@@ -106,14 +117,19 @@ export function createPlayer(steps, options) {
106
117
  let remaining = deltaMs;
107
118
  while (remaining > 0 && !finished) {
108
119
  if (elapsed === stepDurationMs) {
109
- if (options.isTerminal(current)) {
120
+ if (terminal()) {
110
121
  hold += Math.min(remaining, finalHoldMs - hold);
111
122
  complete();
112
123
  break;
113
124
  }
114
125
  const next = iterator.next();
115
- if (next.done)
116
- throw new RangeError("Step source ended without a terminal step");
126
+ if (next.done) {
127
+ if (!finishOnExhaustion)
128
+ throw new RangeError("Step source ended without a terminal step");
129
+ exhausted = true;
130
+ complete();
131
+ continue;
132
+ }
117
133
  previous = current;
118
134
  current = next.value;
119
135
  elapsed = 0;
@@ -0,0 +1,9 @@
1
+ import { type Player } from './player.js';
2
+ import type { Frame } from './types.js';
3
+ /** Lazy state playback. Exhaustion is detected on the request after the last state.
4
+ * States must be immutable snapshots. Generator return values are ignored. */
5
+ export declare function createStatePlayer<S>(states: Iterable<S>, options?: {
6
+ stepDurationMs?: number | undefined;
7
+ finalHoldMs?: number | undefined;
8
+ render?: (frame: Frame<S, undefined>) => void;
9
+ }): Player<S, undefined>;
@@ -0,0 +1,36 @@
1
+ import { createPlayback } from './player.js';
2
+ /** Lazy state playback. Exhaustion is detected on the request after the last state.
3
+ * States must be immutable snapshots. Generator return values are ignored. */
4
+ export function createStatePlayer(states, options = {}) {
5
+ function* steps() {
6
+ const iterator = states[Symbol.iterator]();
7
+ let index = 0, exhausted = false, failed = false;
8
+ try {
9
+ while (true) {
10
+ const item = iterator.next();
11
+ if (item.done) {
12
+ exhausted = true;
13
+ return;
14
+ }
15
+ yield { index: index++, state: item.value, event: undefined };
16
+ }
17
+ }
18
+ catch (error) {
19
+ failed = true;
20
+ throw error;
21
+ }
22
+ finally {
23
+ if (!exhausted) {
24
+ if (failed) {
25
+ try {
26
+ iterator.return?.();
27
+ }
28
+ catch { /* Preserve source failure. */ }
29
+ }
30
+ else
31
+ iterator.return?.();
32
+ }
33
+ }
34
+ }
35
+ return createPlayback(steps(), { ...options, isTerminal: () => false }, true);
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "TypeScript toolkit for custom algorithm visualizations with step-by-step playback and extensible rendering",
5
5
  "type": "module",
6
6
  "files": [
package/ru/README.md CHANGED
@@ -54,14 +54,14 @@ npm install @grundyjs/algiviz
54
54
  ```
55
55
 
56
56
  ```js
57
- import { createPlayer } from '@grundyjs/algiviz/playback';
57
+ import { createStatePlayer } from '@grundyjs/algiviz/playback';
58
58
  import { createSceneRenderer } from '@grundyjs/algiviz/scene';
59
59
 
60
60
  const ctx = document.querySelector('canvas').getContext('2d');
61
- function* steps() {
62
- for (const [index, x] of [40, 200, 360, 560].entries()) {
61
+ function* states() {
62
+ for (const x of [40, 200, 360, 560]) {
63
63
  const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
64
- yield { index, state, event: index === 3 ? 'done' : 'position' };
64
+ yield state;
65
65
  }
66
66
  }
67
67
  const renderer = createSceneRenderer({
@@ -72,8 +72,7 @@ const renderer = createSceneRenderer({
72
72
  ctx.fill();
73
73
  },
74
74
  });
75
- const player = createPlayer(steps(), {
76
- isTerminal: step => step.event === 'done',
75
+ const player = createStatePlayer(states(), {
77
76
  render(frame) {
78
77
  ctx.clearRect(0, 0, 600, 160);
79
78
  renderer.render(ctx, frame);
@@ -82,7 +81,9 @@ const player = createPlayer(steps(), {
82
81
  document.querySelector('#next').onclick = () => player.nextStep();
83
82
  ```
84
83
 
85
- Первый кадр (`x = 40`) рисуется сразу внутри `createPlayer(...)`. Каждый клик
84
+ После последнего состояния ещё один клик обнаруживает конец генератора и сохраняет позицию круга. Чтения вперёд нет.
85
+
86
+ Первый кадр (`x = 40`) рисуется сразу внутри `createStatePlayer(...)`. Каждый клик
86
87
  запрашивает следующее состояние: плеер возобновляет цикл до следующего
87
88
  `yield`, затем вызывает отрисовку. Здесь нет плавного перехода: обработчик рисует
88
89
  только `current`. Каждый снимок создаётся заново и больше не изменяется. При
@@ -105,7 +106,7 @@ document.querySelector('#next').onclick = () => player.nextStep();
105
106
 
106
107
  ## Режимы воспроизведения
107
108
 
108
- | | Генератор: `createPlayer` | История: `createTimeline` |
109
+ | | Генератор: `createPlayer` / `createStatePlayer` | История: `createTimeline` |
109
110
  | --- | --- | --- |
110
111
  | Получение шагов | По мере продвижения | Все шаги заранее |
111
112
  | Хранение | Соседние снимки | Вся история |
@@ -139,6 +140,7 @@ document.querySelector('#next').onclick = () => player.nextStep();
139
140
  | [Бинарный поиск](https://github.com/urffin/algiviz/blob/master/examples/binary-search.html) | Первое вхождение в отсортированном массиве, замкнутый интервал поиска и ленивое воспроизведение |
140
141
  | [BFS графа](https://github.com/urffin/algiviz/blob/master/examples/graph.html) | Очередь, слои, кратчайшие пути, недостижимая цель и история/генератор |
141
142
  | [DFS графа](https://github.com/urffin/algiviz/blob/master/examples/dfs.html) | Стек вызовов, возвраты, глубина и путь дерева; история/генератор |
143
+ | [Лабиринт](https://github.com/urffin/algiviz/blob/master/examples/maze.html) | Генерация рандомизированным DFS, волна BFS, поиск DFS, циклы и недостижимый выход; история/генератор |
142
144
  | [Обход дерева](https://github.com/urffin/algiviz/blob/master/examples/tree.html) | Собственные узлы, рёбра и указатель с Canvas и SVG с сохранением элементов |
143
145
 
144
146
  [Полный каталог, локальный запуск и особенности алгоритмов](https://github.com/urffin/algiviz/blob/master/docs/ru/examples.md).