@grundyjs/algiviz 0.10.0 → 0.11.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 +23 -0
- package/README.md +12 -10
- package/dist/array/trace.d.ts +3 -2
- package/dist/array/trace.js +4 -1
- package/dist/playback/index.d.ts +1 -0
- package/dist/playback/index.js +1 -0
- package/dist/playback/player.d.ts +2 -0
- package/dist/playback/player.js +25 -9
- package/dist/playback/state-player.d.ts +9 -0
- package/dist/playback/state-player.js +36 -0
- package/package.json +1 -1
- package/ru/README.md +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- Validate both numeric positions in array swap/shift before reading or mutating
|
|
6
|
+
the trace, rejecting invalid JavaScript arguments without losing item identities
|
|
7
|
+
or changing snapshots, counters, markers or the next step index.
|
|
8
|
+
- Allow explicitly undefined sorted-region inputs with exactOptionalPropertyTypes,
|
|
9
|
+
matching the existing runtime behavior of preserving the current marker.
|
|
10
|
+
- Document array guarantees in English and Russian: fixed length, one held item,
|
|
11
|
+
trace-local IDs, immutable snapshots, counters and author-supplied sorted markers.
|
|
12
|
+
- Add an application-owned object lifecycle example and expand playback completion,
|
|
13
|
+
strict type and installed-package consumer checks. No new public exports.
|
|
14
|
+
|
|
15
|
+
## 0.11.0 — 2026-11-02
|
|
16
|
+
|
|
17
|
+
- Add createStatePlayer() for lazy playback of plain immutable states without
|
|
18
|
+
step wrappers, event fields or an isTerminal predicate.
|
|
19
|
+
- Consume one state per request without lookahead. A request after the final
|
|
20
|
+
state detects exhaustion, preserves the last frame and index, and completes
|
|
21
|
+
playback or starts an explicitly configured final hold.
|
|
22
|
+
- Support manual and timed playback, infinite sources, disposal and source errors.
|
|
23
|
+
- Simplify the interactive minimal example and English/Russian READMEs; update
|
|
24
|
+
the highlighted GIF to show the extra completion request.
|
|
25
|
+
|
|
3
26
|
## 0.10.0 — 2026-10-27
|
|
4
27
|
|
|
5
28
|
- 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 {
|
|
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*
|
|
63
|
-
for (const
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
@@ -101,13 +102,13 @@ the [local setup instructions](https://github.com/urffin/algiviz/blob/master/doc
|
|
|
101
102
|
- **Scene:** describes objects with stable IDs, types and application-owned data.
|
|
102
103
|
- **Handler:** chooses geometry and drawing behavior, including interpolation.
|
|
103
104
|
|
|
104
|
-
Do not mutate emitted states.
|
|
105
|
-
|
|
105
|
+
Do not mutate emitted states. `createStatePlayer` completes on source exhaustion;
|
|
106
|
+
`createPlayer` requires an explicit terminal step. Call `dispose()` when removing a visualization. Algorithms, layouts and animation
|
|
106
107
|
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).
|
package/dist/array/trace.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Item, ArrayStep } from "./types.js";
|
|
2
|
+
/** Omitted or undefined markers preserve their current values. */
|
|
2
3
|
export interface SortedRegions {
|
|
3
|
-
readonly sortedPrefixLength?: number;
|
|
4
|
-
readonly sortedSuffixLength?: number;
|
|
4
|
+
readonly sortedPrefixLength?: number | undefined;
|
|
5
|
+
readonly sortedSuffixLength?: number | undefined;
|
|
5
6
|
}
|
|
6
7
|
export interface ArrayTrace {
|
|
7
8
|
readonly length: number;
|
package/dist/array/trace.js
CHANGED
|
@@ -54,6 +54,8 @@ export function createArrayTrace(values) {
|
|
|
54
54
|
},
|
|
55
55
|
swap(left, right, update) {
|
|
56
56
|
active();
|
|
57
|
+
index(left);
|
|
58
|
+
index(right);
|
|
57
59
|
const a = item(left), b = item(right);
|
|
58
60
|
if (left === right)
|
|
59
61
|
throw new RangeError("Swap requires distinct positions");
|
|
@@ -74,8 +76,9 @@ export function createArrayTrace(values) {
|
|
|
74
76
|
},
|
|
75
77
|
shift(from, to, update) {
|
|
76
78
|
active();
|
|
77
|
-
|
|
79
|
+
index(from);
|
|
78
80
|
index(to);
|
|
81
|
+
const moved = item(from);
|
|
79
82
|
if (slots[to] !== null)
|
|
80
83
|
throw new Error("Shift destination must be empty");
|
|
81
84
|
regions(update);
|
package/dist/playback/index.d.ts
CHANGED
package/dist/playback/index.js
CHANGED
|
@@ -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>;
|
package/dist/playback/player.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
76
|
+
if (terminal()) {
|
|
71
77
|
hold = finalHoldMs;
|
|
72
78
|
}
|
|
73
79
|
else {
|
|
74
80
|
const next = iterator.next();
|
|
75
|
-
if (next.done)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 (
|
|
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
|
-
|
|
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
package/ru/README.md
CHANGED
|
@@ -54,14 +54,14 @@ npm install @grundyjs/algiviz
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```js
|
|
57
|
-
import {
|
|
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*
|
|
62
|
-
for (const
|
|
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
|
|
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 =
|
|
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
|
-
|
|
84
|
+
После последнего состояния ещё один клик обнаруживает конец генератора и сохраняет позицию круга. Чтения вперёд нет.
|
|
85
|
+
|
|
86
|
+
Первый кадр (`x = 40`) рисуется сразу внутри `createStatePlayer(...)`. Каждый клик
|
|
86
87
|
запрашивает следующее состояние: плеер возобновляет цикл до следующего
|
|
87
88
|
`yield`, затем вызывает отрисовку. Здесь нет плавного перехода: обработчик рисует
|
|
88
89
|
только `current`. Каждый снимок создаётся заново и больше не изменяется. При
|
|
@@ -99,13 +100,14 @@ document.querySelector('#next').onclick = () => player.nextStep();
|
|
|
99
100
|
- **Сцена** описывает объекты: стабильный `id`, тип и собственные данные.
|
|
100
101
|
- **Обработчик** выбирает геометрию и способ рисования. Плавность — его решение.
|
|
101
102
|
|
|
102
|
-
Состояния после выдачи нельзя изменять.
|
|
103
|
+
Состояния после выдачи нельзя изменять. `createStatePlayer` завершается при исчерпании
|
|
104
|
+
источника; `createPlayer` требует явного завершающего шага;
|
|
103
105
|
при удалении визуализации вызовите `dispose()`. Алгоритмы, раскладка и цикл
|
|
104
106
|
анимации принадлежат приложению.
|
|
105
107
|
|
|
106
108
|
## Режимы воспроизведения
|
|
107
109
|
|
|
108
|
-
| | Генератор: `createPlayer` | История: `createTimeline` |
|
|
110
|
+
| | Генератор: `createPlayer` / `createStatePlayer` | История: `createTimeline` |
|
|
109
111
|
| --- | --- | --- |
|
|
110
112
|
| Получение шагов | По мере продвижения | Все шаги заранее |
|
|
111
113
|
| Хранение | Соседние снимки | Вся история |
|
|
@@ -139,6 +141,7 @@ document.querySelector('#next').onclick = () => player.nextStep();
|
|
|
139
141
|
| [Бинарный поиск](https://github.com/urffin/algiviz/blob/master/examples/binary-search.html) | Первое вхождение в отсортированном массиве, замкнутый интервал поиска и ленивое воспроизведение |
|
|
140
142
|
| [BFS графа](https://github.com/urffin/algiviz/blob/master/examples/graph.html) | Очередь, слои, кратчайшие пути, недостижимая цель и история/генератор |
|
|
141
143
|
| [DFS графа](https://github.com/urffin/algiviz/blob/master/examples/dfs.html) | Стек вызовов, возвраты, глубина и путь дерева; история/генератор |
|
|
144
|
+
| [Лабиринт](https://github.com/urffin/algiviz/blob/master/examples/maze.html) | Генерация рандомизированным DFS, волна BFS, поиск DFS, циклы и недостижимый выход; история/генератор |
|
|
142
145
|
| [Обход дерева](https://github.com/urffin/algiviz/blob/master/examples/tree.html) | Собственные узлы, рёбра и указатель с Canvas и SVG с сохранением элементов |
|
|
143
146
|
|
|
144
147
|
[Полный каталог, локальный запуск и особенности алгоритмов](https://github.com/urffin/algiviz/blob/master/docs/ru/examples.md).
|