@grundyjs/algiviz 0.8.3 → 0.10.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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 — 2026-10-27
4
+
5
+ - Default finalHoldMs to zero for generic and array players.
6
+ - Make stepDurationMs optional for manual nextStep playback; advance(0) redraws,
7
+ while positive timed advancement requires an explicit duration.
8
+ - Allow array players to omit the options object entirely.
9
+ - Simplify the minimal example and both READMEs; refresh the highlighted GIF.
10
+ - Preserve explicit timing behavior and timeline requirements.
11
+
12
+ ## 0.9.0 — 2026-10-27
13
+
14
+ - Add player.nextStep() for exact manual stepping in generic and array players.
15
+ - Finish an in-progress transition without reading another snapshot; at a boundary,
16
+ consume one next snapshot. Complete terminal holds without extra source reads.
17
+ - Preserve rendering, disposal and error handling contracts; avoid rounding errors
18
+ when mixing timed advancement with manual steps.
19
+ - Update the minimal example, English/Russian READMEs and highlighted GIF to use nextStep().
20
+
3
21
  ## 0.8.3 — 2026-10-27
4
22
 
5
23
  - Streamline English and Russian READMEs around installation and a minimal example.
package/README.md CHANGED
@@ -74,18 +74,17 @@ const renderer = createSceneRenderer({
74
74
  },
75
75
  });
76
76
  const player = createPlayer(steps(), {
77
- stepDurationMs: 1000, finalHoldMs: 0,
78
77
  isTerminal: step => step.event === 'done',
79
78
  render(frame) {
80
79
  ctx.clearRect(0, 0, 600, 160);
81
80
  renderer.render(ctx, frame);
82
81
  },
83
82
  });
84
- document.querySelector('#next').onclick = () => player.advance(1000);
83
+ document.querySelector('#next').onclick = () => player.nextStep();
85
84
  ```
86
85
 
87
86
  The first frame (`x = 40`) is drawn immediately inside `createPlayer(...)`. Each
88
- click advances one 1000 ms step: the player resumes the loop until the next
87
+ click requests the next state: the player resumes the loop until the next
89
88
  `yield`, then renders. There is no interpolation here: the handler draws only
90
89
  `current`. Each snapshot is created afresh and never mutated. Call
91
90
  `player.dispose()` when removing the visualization.
@@ -3,8 +3,8 @@ import type { ArrayStep, ArraySnapshot, ArrayEvent } from "./types.js";
3
3
  import type { ArrayFrame } from "./timeline.js";
4
4
  export type ArrayPlayer = Player<ArraySnapshot, ArrayEvent>;
5
5
  /** Array playback adapter for array steps ending in done. */
6
- export declare function createArrayPlayer(steps: Iterable<ArrayStep>, options: {
7
- stepDurationMs: number;
8
- finalHoldMs: number;
6
+ export declare function createArrayPlayer(steps: Iterable<ArrayStep>, options?: {
7
+ stepDurationMs?: number | undefined;
8
+ finalHoldMs?: number | undefined;
9
9
  render?: (frame: ArrayFrame) => void;
10
10
  }): ArrayPlayer;
@@ -1,5 +1,5 @@
1
1
  import { createPlayer } from "../playback/index.js";
2
2
  /** Array playback adapter for array steps ending in done. */
3
- export function createArrayPlayer(steps, options) {
3
+ export function createArrayPlayer(steps, options = {}) {
4
4
  return createPlayer(steps, { ...options, isTerminal: step => step.event.type === "done" });
5
5
  }
@@ -5,14 +5,19 @@ export interface Player<S, E> {
5
5
  readonly finished: boolean;
6
6
  /** Advance by playback time; pass zero to redraw without consuming steps. */
7
7
  advance(deltaMs: number): Frame<S, E>;
8
+ /** Finish the current transition, or consume one next step at a boundary.
9
+ * At the terminal boundary, finish the hold. Renders once, even if finished. */
10
+ nextStep(): Frame<S, E>;
8
11
  /** Close the source early. The last frame remains readable. */
9
12
  dispose(): void;
10
13
  }
11
14
  /** Forward-only playback. The source must reach a step accepted by isTerminal. */
12
15
  export declare function createPlayer<S, E>(steps: Iterable<Step<S, E>>, options: {
13
- stepDurationMs: number;
14
- finalHoldMs: number;
16
+ /** Omit for manual playback; positive advance requires a duration. */
17
+ stepDurationMs?: number | undefined;
18
+ /** Final hold duration, default 0. */
19
+ finalHoldMs?: number | undefined;
15
20
  isTerminal: (step: Step<S, E>) => boolean;
16
- /** Called initially and on each advance, including advance(0). */
21
+ /** Called initially and on each advance or nextStep, including after completion. */
17
22
  render?: (frame: Frame<S, E>) => void;
18
23
  }): Player<S, E>;
@@ -1,6 +1,8 @@
1
1
  /** Forward-only playback. The source must reach a step accepted by isTerminal. */
2
2
  export function createPlayer(steps, options) {
3
- const { stepDurationMs, finalHoldMs } = options;
3
+ const manual = options.stepDurationMs === undefined;
4
+ // Unit endpoint for manual steps; positive timed advancement is disallowed.
5
+ const { stepDurationMs = 1, finalHoldMs = 0 } = options;
4
6
  if (!Number.isFinite(stepDurationMs) || stepDurationMs <= 0 ||
5
7
  !Number.isFinite(finalHoldMs) || finalHoldMs < 0) {
6
8
  throw new RangeError("Expected positive step duration and non-negative final hold");
@@ -59,12 +61,47 @@ export function createPlayer(steps, options) {
59
61
  return Object.freeze({
60
62
  get frame() { return frame(); },
61
63
  get finished() { return finished; },
64
+ nextStep() {
65
+ if (disposed)
66
+ throw new Error("Player has been disposed");
67
+ try {
68
+ if (!finished) {
69
+ if (elapsed === stepDurationMs) {
70
+ if (options.isTerminal(current)) {
71
+ hold = finalHoldMs;
72
+ }
73
+ else {
74
+ 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;
79
+ }
80
+ }
81
+ // Assign the endpoint directly: time arithmetic can undershoot or
82
+ // spill a rounding remainder into an unwanted additional step.
83
+ elapsed = stepDurationMs;
84
+ complete();
85
+ }
86
+ const result = frame();
87
+ options.render?.(result);
88
+ return result;
89
+ }
90
+ catch (error) {
91
+ disposed = true;
92
+ closeAfterFailure();
93
+ throw error;
94
+ }
95
+ },
62
96
  advance(deltaMs) {
63
97
  if (disposed)
64
98
  throw new Error("Player has been disposed");
65
99
  if (!Number.isFinite(deltaMs) || deltaMs < 0) {
66
100
  throw new RangeError("Expected finite non-negative playback time");
67
101
  }
102
+ if (manual && deltaMs > 0) {
103
+ throw new Error("Set stepDurationMs to use advance with positive time; use nextStep for manual playback");
104
+ }
68
105
  try {
69
106
  let remaining = deltaMs;
70
107
  while (remaining > 0 && !finished) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.8.3",
3
+ "version": "0.10.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
@@ -73,18 +73,17 @@ const renderer = createSceneRenderer({
73
73
  },
74
74
  });
75
75
  const player = createPlayer(steps(), {
76
- stepDurationMs: 1000, finalHoldMs: 0,
77
76
  isTerminal: step => step.event === 'done',
78
77
  render(frame) {
79
78
  ctx.clearRect(0, 0, 600, 160);
80
79
  renderer.render(ctx, frame);
81
80
  },
82
81
  });
83
- document.querySelector('#next').onclick = () => player.advance(1000);
82
+ document.querySelector('#next').onclick = () => player.nextStep();
84
83
  ```
85
84
 
86
85
  Первый кадр (`x = 40`) рисуется сразу внутри `createPlayer(...)`. Каждый клик
87
- проходит один шаг длительностью 1000 мс: плеер возобновляет цикл до следующего
86
+ запрашивает следующее состояние: плеер возобновляет цикл до следующего
88
87
  `yield`, затем вызывает отрисовку. Здесь нет плавного перехода: обработчик рисует
89
88
  только `current`. Каждый снимок создаётся заново и больше не изменяется. При
90
89
  удалении визуализации вызовите `player.dispose()`.