@grundyjs/algiviz 0.8.0 → 0.8.2

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2 — 2026-10-27
4
+
5
+ - Add a self-contained minimal usage example to both English and Russian READMEs.
6
+ - Link an interactive walkthrough of generator states, playback and custom drawing.
7
+ - Add repository examples with sequential code highlighting and explicit startup.
8
+ - No runtime API or behavior changes; examples remain outside the npm package.
9
+
10
+ ## 0.8.1 — 2026-10-26
11
+
12
+ - Use Array-prefixed internal types in public declarations and make
13
+ createArrayRenderer the canonical function name, retaining createSortRenderer
14
+ as the identical compatibility alias.
15
+ - Allow explicit undefined palette roles with exactOptionalPropertyTypes,
16
+ matching the existing runtime fallback to theme defaults.
17
+ - Verify the full runtime export inventory, private-path rejection, all public
18
+ types and headless compilation against the installed package.
19
+ - Add a strictly typed custom-object starter and optimize merge example drawing.
20
+ - Expand application-owned BFS examples and add DFS with a call stack and
21
+ backtracking. Algorithms remain outside the npm package.
22
+
3
23
  ## 0.8.0 — 2026-10-19
4
24
 
5
25
  - Add partial custom colors to the Canvas array renderer and its compatible alias,
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # AlgiViz
2
2
 
3
+ [Русский](ru/README.md)
4
+
3
5
  **Your algorithms and objects. Step-by-step playback and custom rendering.**
4
6
 
5
7
  ![BFS from A: custom graph vertices, edges and queue animated with AlgiViz](https://raw.githubusercontent.com/urffin/algiviz/master/docs/assets/bfs.gif)
@@ -28,30 +30,69 @@ renderer help you get started.
28
30
 
29
31
  Use it for educational demos, interactive articles and algorithm walkthroughs.
30
32
  Algorithms and layouts belong to your application; AlgiViz provides the playback
31
- and rendering API. This README describes version 0.8.0.
33
+ and rendering API. This README describes version 0.8.2.
32
34
 
33
35
  ## Start with one custom object
34
36
 
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:
37
+ Four states of one circle: `40 → 200 → 360 → 560`. The generator supplies
38
+ coordinates, the `dot` handler draws the circle, and the player requests the next
39
+ state. Add a canvas and button to your HTML, then load the JavaScript below as a
40
+ module through a bundler that resolves imports from the installed npm package.
40
41
 
41
- ```js
42
- import { createTokenPlayer } from './minimal.mjs';
42
+ ```html
43
+ <canvas width="600" height="160"></canvas>
44
+ <button id="next">Next state</button>
45
+ ```
43
46
 
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.
47
+ ```js
48
+ import { createPlayer } from '@grundyjs/algiviz/playback';
49
+ import { createSceneRenderer } from '@grundyjs/algiviz/scene';
50
+
51
+ const ctx = document.querySelector('canvas').getContext('2d');
52
+ function* steps() {
53
+ for (const [index, x] of [40, 200, 360, 560].entries()) {
54
+ const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
55
+ yield { index, state, event: index === 3 ? 'done' : 'position' };
56
+ }
57
+ }
58
+ const renderer = createSceneRenderer({
59
+ dot(ctx, { current }) {
60
+ ctx.beginPath();
61
+ ctx.arc(current.data.x, 80, 18, 0, Math.PI * 2);
62
+ ctx.fillStyle = '#b39bff';
63
+ ctx.fill();
64
+ },
65
+ });
66
+ const player = createPlayer(steps(), {
67
+ stepDurationMs: 1000, finalHoldMs: 0,
68
+ isTerminal: step => step.event === 'done',
69
+ render(frame) {
70
+ ctx.clearRect(0, 0, 600, 160);
71
+ renderer.render(ctx, frame);
72
+ },
73
+ });
74
+ document.querySelector('#next').onclick = () => player.advance(1000);
49
75
  ```
50
76
 
77
+ The first frame (`x = 40`) is drawn immediately inside `createPlayer(...)`. Each
78
+ click advances one 1000 ms step: the player resumes the loop until the next
79
+ `yield`, then renders. There is no interpolation here: the handler draws only
80
+ `current`. Each snapshot is created afresh and never mutated. Call
81
+ `player.dispose()` when removing the visualization.
82
+
83
+ [Interactive walkthrough with sequential code highlighting](https://github.com/urffin/algiviz/blob/master/examples/visualization-pipeline.html)
84
+ shows initial rendering behind a Start button and the subsequent loop iterations
85
+ (Russian UI). This is an example page in the repository; run it locally using
86
+ the setup instructions below.
87
+
51
88
  [Follow the walkthrough](https://github.com/urffin/algiviz/blob/master/docs/minimal-example.md)
52
89
  for local setup, snapshot ownership and lifecycle. For a larger custom-object
53
90
  example, see the [graph BFS walkthrough](https://github.com/urffin/algiviz/blob/master/docs/custom-graph-example.md).
54
91
 
92
+ For a complete TypeScript integration with two object types and Play/Pause/Restart,
93
+ see [custom-objects.ts](examples/custom-objects.ts) and the
94
+ [setup instructions](docs/minimal-example.md#a-complete-typescript-integration).
95
+
55
96
  ## Install
56
97
 
57
98
  ```sh
@@ -144,7 +185,7 @@ The algorithms belong to the application; they require no changes to the library
144
185
  ### Custom structures and renderers
145
186
 
146
187
  `/playback` does not know arrays, sorting, Canvas, or event names. A source yields
147
- `Step<State, Event>` objects with contiguous zero-based `index`, immutable
188
+ `Step<State, Event>` objects with a numeric `index`, immutable
148
189
  `state` and `event`. Supply `isTerminal` to identify the final step and
149
190
  `render` to draw each interpolated frame using any rendering technology:
150
191
 
@@ -159,6 +200,10 @@ const player = createPlayer(myTraversal(), {
159
200
  });
160
201
  ```
161
202
 
203
+ Both engines preserve the supplied index; it need not start at zero or be
204
+ contiguous. Timing follows source order. `createStepEmitter` provides consecutive
205
+ zero-based numbering for convenience.
206
+
162
207
  The render callback is optional. Without it, read `player.frame` and render
163
208
  manually. `createTimeline(steps, timing)` supports seeking with the same generic
164
209
  frame shape; call your renderer with its `sample(timeMs)` result. The generic
@@ -415,11 +460,14 @@ included in the npm archive.
415
460
 
416
461
  | Example | What it demonstrates |
417
462
  | --- | --- |
463
+ | [How a frame is made](https://github.com/urffin/algiviz/blob/master/examples/visualization-pipeline.html) | AlgiViz visualizes its own generator → player → scene → renderer pipeline (Russian walkthrough) |
418
464
  | [Sorting lab](examples/index.html) | Insertion and bubble sort using the array operations and renderer |
419
465
  | [Merge sort](examples/merge.html) | Custom state and a two-row scene for the main array and buffer |
420
466
  | [Quick sort partitions](examples/quick.html) | Lomuto and three-way partitioning with custom state and scene |
421
467
  | [Heap sort](examples/heap.html) | Max-heap construction and extraction with synchronized tree and array identities |
422
- | [Graph BFS](examples/graph.html) | Cycles, disconnected components, shortest distances and an animated queue |
468
+ | [Binary search](examples/binary-search.html) | First occurrence in sorted input, closed search intervals and lazy playback |
469
+ | [Graph BFS](examples/graph.html) | Queue, layers, shortest paths, unreachable targets and history/generator playback |
470
+ | [Graph DFS](examples/dfs.html) | Recursive call frames, backtracking, tree depth and paths; history/generator playback |
423
471
  | [Tree traversal](examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
424
472
 
425
473
  All examples reuse [demo-player.mjs](examples/demo-player.mjs) and
@@ -465,6 +513,12 @@ in-place algorithm uses O(1) auxiliary space. This demo copies input and retains
465
513
  O(n) adjacent snapshot data in generator mode; full history can use O(n² log n).
466
514
  It is not stable: [2a, 2b, 1] becomes [1, 2b, 2a].
467
515
 
516
+ ### Binary search
517
+
518
+ The [algorithm](examples/binary-search-algorithm.mjs) finds the first occurrence, including duplicates. Equal values save a candidate and continue searching left. Input must be sorted; invalid loads preserve the current session. Generate sorted arrays by count with the shared history/generator limits. The counter counts inspected middle elements.
519
+
520
+ The [scene](examples/binary-search-scene.mjs) uses stable index-based IDs and viewport-independent projection. Up to 32 cells wrap; larger arrays use a dense strip and suppress unreadable labels. Search takes O(log n) time and O(1) extra algorithm space. The demo validates and copies input in O(n), shares a frozen array across snapshots and stores O(n + log n) history data. The example is not part of the npm package and needs no new public export.
521
+
468
522
  ### Tree traversal
469
523
 
470
524
  The example defines its own `node`, `edge` and `pointer` types. The pointer follows
@@ -476,16 +530,47 @@ No tree-specific algorithm or renderer is added to the package.
476
530
 
477
531
  ### Graph breadth-first search
478
532
 
479
- Open [examples/graph.html](examples/graph.html) and choose a start vertex.
480
- The example explores only its connected component, marks vertices when enqueued
481
- and shows shortest unweighted distances. Its custom vertex, connection and queue
482
- entry types are projected from immutable algorithm state. Both playback modes use
483
- the same generator; changing the start creates a new traversal.
533
+ Open [examples/graph.html](examples/graph.html), choose a graph, start and optional
534
+ target, then Apply. BFS marks on enqueue, completes the reachable component and
535
+ restores one shortest path by following parents. Try the isolated H, a zero-edge
536
+ path to the start, a chain or a diamond with equal-length paths. The original
537
+ disconnected graph shown in the README GIF remains a separate preset.
538
+
539
+ The scene shows a fixed graph, an animated queue, distances, parent/path edges and
540
+ explicit discovery/processing/neighbor-inspection counters. On narrow screens the
541
+ queue wraps below the graph. Canvas rendering uses logical coordinates and DPR.
542
+ Self-loops and parallel edges remain supported; neighbor order can change which
543
+ shortest path is chosen, but not its length.
544
+
545
+ Both playback modes use the same immutable generator. Restart and mode changes
546
+ replay the applied graph/start/target, not draft controls. Generator mode consumes
547
+ lazily without collecting history. Full history enables backward seeks and has an
548
+ O(V(V + E)) snapshot-space upper bound, separately from graph and algorithm data.
484
549
 
485
550
  See [Building a custom graph example](docs/custom-graph-example.md) for the
486
551
  algorithm, projection and renderer boundaries. This example uses the existing
487
552
  public APIs and adds no graph-specific package exports.
488
553
 
554
+ ### Graph depth-first search
555
+
556
+ Open [examples/dfs.html](examples/dfs.html) to compare DFS with BFS on the same
557
+ graph layouts. Choose a graph, start and optional target. Each explicit call frame
558
+ stores its next neighbor cursor; marking happens before entry. Returning pops the
559
+ finished call and resumes its parent. The stack contains the active branch only.
560
+
561
+ The default discovery order is A, B, D, G, E, C, F; return order is F, C, E, G, D, B, A.
562
+ The DFS tree path to F has 6 edges; BFS finds A → C → F with 2. DFS tree depth is not
563
+ a shortest distance. H is unreachable. Traversal finishes before reconstructing the
564
+ path. The example accepts simple undirected graphs; self-loops and parallel edges
565
+ are rejected. Neighbor order follows edge input order.
566
+
567
+ The fixed-capacity stack moves below the graph on narrow screens. Canvas uses
568
+ logical coordinates and DPR. Both modes share one immutable generator and the
569
+ [shared demo player](examples/demo-player.mjs); restart reuses applied inputs.
570
+ Generator playback is lazy, while history enables backward seeks. The traversal
571
+ uses O(V + E) time and O(V) auxiliary space; snapshot history has a separate
572
+ O(V(V + E)) upper bound. No graph API or built-in DFS is added to the package.
573
+
489
574
  ## Development
490
575
 
491
576
  Requires Node.js 22 or later and npm.
@@ -1,6 +1,6 @@
1
1
  export { createArrayTrace, defineArrayAlgorithm } from "./trace.js";
2
2
  export type { ArrayTrace, ArrayAlgorithm, SortedRegions } from "./trace.js";
3
- export type { Item, SortEvent as ArrayEvent, SortSnapshot as ArraySnapshot, SortStep as ArrayStep } from "./types.js";
3
+ export type { Item, ArrayEvent, ArraySnapshot, ArrayStep } from "./types.js";
4
4
  export { createArrayPlayer } from "./player.js";
5
5
  export { createArrayTimeline } from "./timeline.js";
6
6
  export type { ArrayPlayer } from "./player.js";
@@ -1,9 +1,9 @@
1
1
  import { type Player } from "../playback/index.js";
2
- import type { SortStep, SortSnapshot, SortEvent } from "./types.js";
2
+ import type { ArrayStep, ArraySnapshot, ArrayEvent } from "./types.js";
3
3
  import type { ArrayFrame } from "./timeline.js";
4
- export type ArrayPlayer = Player<SortSnapshot, SortEvent>;
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<SortStep>, options: {
6
+ export declare function createArrayPlayer(steps: Iterable<ArrayStep>, options: {
7
7
  stepDurationMs: number;
8
8
  finalHoldMs: number;
9
9
  render?: (frame: ArrayFrame) => void;
@@ -1,4 +1,4 @@
1
- import type { Item, SortEvent, SortStep } from "./types.js";
1
+ import type { Item, ArrayEvent, ArrayStep } from "./types.js";
2
2
  /** Internal mutable workspace; only emit() exposes immutable snapshots. */
3
3
  export declare function createSortTrace(values: readonly number[]): {
4
4
  state: {
@@ -7,5 +7,5 @@ export declare function createSortTrace(values: readonly number[]): {
7
7
  sortedPrefixLength: number;
8
8
  sortedSuffixLength?: number;
9
9
  };
10
- emit: (event: SortEvent) => SortStep;
10
+ emit: (event: ArrayEvent) => ArrayStep;
11
11
  };
@@ -1,8 +1,8 @@
1
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: {
2
+ import type { ArrayEvent, ArraySnapshot, ArrayStep } from "./types.js";
3
+ export type ArrayFrame = Frame<ArraySnapshot, ArrayEvent>;
4
+ export type ArrayTimeline = Timeline<ArraySnapshot, ArrayEvent>;
5
+ export declare function createArrayTimeline(steps: readonly ArrayStep[], options: {
6
6
  stepDurationMs: number;
7
7
  finalHoldMs: number;
8
8
  }): ArrayTimeline;
@@ -1,4 +1,4 @@
1
- import type { Item, SortStep } from "./types.js";
1
+ import type { Item, ArrayStep } from "./types.js";
2
2
  export interface SortedRegions {
3
3
  readonly sortedPrefixLength?: number;
4
4
  readonly sortedSuffixLength?: number;
@@ -7,21 +7,21 @@ export interface ArrayTrace {
7
7
  readonly length: number;
8
8
  readonly held: Item | null;
9
9
  at(index: number): Item | null;
10
- start(regions?: SortedRegions): SortStep;
11
- compare(left: number | "held", right: number | "held"): SortStep;
12
- highlight(positions: readonly (number | "held")[]): SortStep;
13
- swap(left: number, right: number, regions?: SortedRegions): SortStep;
14
- select(from: number, regions?: SortedRegions): SortStep;
15
- shift(from: number, to: number, regions?: SortedRegions): SortStep;
16
- insert(to: number, regions?: SortedRegions): SortStep;
17
- pass(end: number, regions?: SortedRegions): SortStep;
18
- done(regions?: SortedRegions): SortStep;
10
+ start(regions?: SortedRegions): ArrayStep;
11
+ compare(left: number | "held", right: number | "held"): ArrayStep;
12
+ highlight(positions: readonly (number | "held")[]): ArrayStep;
13
+ swap(left: number, right: number, regions?: SortedRegions): ArrayStep;
14
+ select(from: number, regions?: SortedRegions): ArrayStep;
15
+ shift(from: number, to: number, regions?: SortedRegions): ArrayStep;
16
+ insert(to: number, regions?: SortedRegions): ArrayStep;
17
+ pass(end: number, regions?: SortedRegions): ArrayStep;
18
+ done(regions?: SortedRegions): ArrayStep;
19
19
  }
20
20
  /** Operations own mutations, counters and immutable snapshots, but no algorithm. */
21
21
  export declare function createArrayTrace(values: readonly number[]): ArrayTrace;
22
22
  export interface ArrayAlgorithm {
23
- iterate(values: readonly number[]): Generator<SortStep, void, unknown>;
24
- steps(values: readonly number[]): readonly SortStep[];
23
+ iterate(values: readonly number[]): Generator<ArrayStep, void, unknown>;
24
+ steps(values: readonly number[]): readonly ArrayStep[];
25
25
  }
26
26
  /** Each iterator owns its workspace; input is read on its first next(). */
27
- export declare function defineArrayAlgorithm(algorithm: (array: ArrayTrace) => Generator<SortStep, void, unknown>): ArrayAlgorithm;
27
+ export declare function defineArrayAlgorithm(algorithm: (array: ArrayTrace) => Generator<ArrayStep, void, unknown>): ArrayAlgorithm;
@@ -2,7 +2,7 @@ export type Item = Readonly<{
2
2
  id: string;
3
3
  value: number;
4
4
  }>;
5
- export type SortEvent = Readonly<{
5
+ export type ArrayEvent = Readonly<{
6
6
  type: "start";
7
7
  } | {
8
8
  type: "select";
@@ -36,7 +36,7 @@ export type SortEvent = Readonly<{
36
36
  } | {
37
37
  type: "done";
38
38
  }>;
39
- export interface SortSnapshot {
39
+ export interface ArraySnapshot {
40
40
  readonly slots: readonly (Item | null)[];
41
41
  readonly held: Item | null;
42
42
  /** Sorted leading occupied slots; during insertion it ends at the hole. */
@@ -47,9 +47,9 @@ export interface SortSnapshot {
47
47
  /** Writes into array slots; selecting a key does not count as a write. */
48
48
  readonly writes: number;
49
49
  }
50
- export interface SortStep {
50
+ export interface ArrayStep {
51
51
  readonly index: number;
52
- readonly event: SortEvent;
52
+ readonly event: ArrayEvent;
53
53
  /** Complete immutable state immediately after the event. */
54
- readonly state: SortSnapshot;
54
+ readonly state: ArraySnapshot;
55
55
  }
@@ -1,5 +1,5 @@
1
- import type { Item, SortSnapshot } from "../array/types.js";
2
- import type { ArrayFrame as SortFrame } from "../array/timeline.js";
1
+ import type { Item, ArraySnapshot } from "../array/types.js";
2
+ import type { ArrayFrame } from "../array/timeline.js";
3
3
  import { type Scene } from "../scene/index.js";
4
4
  export type ArrayObjects = {
5
5
  bar: {
@@ -9,7 +9,7 @@ export type ArrayObjects = {
9
9
  };
10
10
  };
11
11
  /** Adapts array identities to the same public scene contract used by custom objects. */
12
- export declare function arrayScene(state: SortSnapshot): Scene<ArrayObjects>;
12
+ export declare function arrayScene(state: ArraySnapshot): Scene<ArrayObjects>;
13
13
  /** Canvas color strings for the semantic roles in the array renderer. */
14
14
  export interface ArrayPalette {
15
15
  readonly background: string;
@@ -20,13 +20,15 @@ export interface ArrayPalette {
20
20
  readonly held: string;
21
21
  }
22
22
  /** Overrides are copied at renderer creation; omitted roles retain the theme color. */
23
- export type ArrayPaletteOverrides = Partial<ArrayPalette>;
23
+ export type ArrayPaletteOverrides = {
24
+ readonly [Role in keyof ArrayPalette]?: ArrayPalette[Role] | undefined;
25
+ };
24
26
  export interface ArrayRendererOptions {
25
27
  readonly theme: "light" | "dark";
26
28
  readonly colors?: ArrayPaletteOverrides;
27
29
  }
28
- export { createSortRenderer as createArrayRenderer };
30
+ export { createArrayRenderer as createSortRenderer };
29
31
  /** Stateless full-frame renderer. Bitmap dimensions determine layout and video size. */
30
- export declare function createSortRenderer(options: ArrayRendererOptions): Readonly<{
31
- render(ctx: CanvasRenderingContext2D, frame: SortFrame): void;
32
+ export declare function createArrayRenderer(options: ArrayRendererOptions): Readonly<{
33
+ render(ctx: CanvasRenderingContext2D, frame: ArrayFrame): void;
32
34
  }>;
@@ -23,7 +23,7 @@ const palettes = {
23
23
  held: "#7c3aed"
24
24
  }
25
25
  };
26
- export { createSortRenderer as createArrayRenderer };
26
+ export { createArrayRenderer as createSortRenderer };
27
27
  function positions(state) {
28
28
  const result = new Map();
29
29
  state.slots.forEach((item, index) => {
@@ -35,7 +35,7 @@ function positions(state) {
35
35
  return result;
36
36
  }
37
37
  /** Stateless full-frame renderer. Bitmap dimensions determine layout and video size. */
38
- export function createSortRenderer(options) {
38
+ export function createArrayRenderer(options) {
39
39
  const colors = { ...palettes[options.theme] };
40
40
  for (const key of Object.keys(colors)) {
41
41
  const color = options.colors?.[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "TypeScript toolkit for custom algorithm visualizations with step-by-step playback and extensible rendering",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,6 +9,7 @@
9
9
  "dist/scene",
10
10
  "dist/canvas",
11
11
  "README.md",
12
+ "ru/README.md",
12
13
  "LICENSE",
13
14
  "CHANGELOG.md"
14
15
  ],
package/ru/README.md ADDED
@@ -0,0 +1,652 @@
1
+ # AlgiViz
2
+
3
+ [English](../README.md)
4
+
5
+ **Ваши алгоритмы и объекты. Пошаговое воспроизведение и собственная отрисовка.**
6
+
7
+ ![BFS из вершины A: собственные вершины графа, рёбра и очередь, анимированные с AlgiViz](https://raw.githubusercontent.com/urffin/algiviz/master/docs/assets/bfs.gif)
8
+
9
+ *Алгоритм обхода графа в ширину с собственными объектами, реализованный в приложении.
10
+ Экспортировано с [CanvasTake](https://github.com/urffin/canvas-take) —
11
+ [попробуйте интерактивную сортировку](https://grundyjs.ru/algorithms/insertion-sort/)
12
+ или изучите [исходный код BFS](https://github.com/urffin/algiviz/blob/master/examples/graph-demo.mjs).*
13
+
14
+ TypeScript-библиотека для интерактивного объяснения алгоритмов. Напишите алгоритм
15
+ как генератор, выдавайте неизменяемые состояния и определяйте внешний вид объектов.
16
+ AlgiViz управляет воспроизведением и сопоставляет объекты сцен; операции над массивами
17
+ и готовый Canvas-рендерер помогают начать работу.
18
+
19
+ [Интерактивная сортировка](https://grundyjs.ru/algorithms/insertion-sort/) ·
20
+ [Обзор библиотеки](https://grundyjs.ru/algiviz/) ·
21
+ [Пакет npm](https://www.npmjs.com/package/@grundyjs/algiviz) ·
22
+ [Исходный код минимального примера](https://github.com/urffin/algiviz/blob/master/examples/minimal.mjs)
23
+
24
+ - **Ваш алгоритм:** вы управляете циклами, условиями и порядком операций.
25
+ - **Ваши объекты:** определяйте собственные типы и обработчики для Canvas, SVG или другой среды отрисовки.
26
+ - **Два режима воспроизведения:** сохраняйте историю для перемотки или читайте генератор,
27
+ оставляя в плеере только соседние снимки состояния.
28
+ - **Без зависимостей во время выполнения:** ESM и декларации TypeScript;
29
+ воспроизведение работает без DOM, React и Next.js.
30
+
31
+ Используйте библиотеку для учебных примеров, интерактивных статей и пошаговых разборов.
32
+ Алгоритмы и расположение объектов остаются в приложении; AlgiViz предоставляет API
33
+ воспроизведения и отрисовки. Этот README описывает версию 0.8.2.
34
+
35
+ ## Начните с одного собственного объекта
36
+
37
+ Четыре состояния одного круга: `40 → 200 → 360 → 560`. Генератор задаёт
38
+ координаты, обработчик `dot` рисует круг, а плеер запрашивает следующее состояние.
39
+ Добавьте Canvas и кнопку в HTML, а JavaScript ниже подключите как модуль через
40
+ сборщик, разрешающий импорты установленного npm-пакета.
41
+
42
+ ```html
43
+ <canvas width="600" height="160"></canvas>
44
+ <button id="next">Следующее состояние</button>
45
+ ```
46
+
47
+ ```js
48
+ import { createPlayer } from '@grundyjs/algiviz/playback';
49
+ import { createSceneRenderer } from '@grundyjs/algiviz/scene';
50
+
51
+ const ctx = document.querySelector('canvas').getContext('2d');
52
+ function* steps() {
53
+ for (const [index, x] of [40, 200, 360, 560].entries()) {
54
+ const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
55
+ yield { index, state, event: index === 3 ? 'done' : 'position' };
56
+ }
57
+ }
58
+ const renderer = createSceneRenderer({
59
+ dot(ctx, { current }) {
60
+ ctx.beginPath();
61
+ ctx.arc(current.data.x, 80, 18, 0, Math.PI * 2);
62
+ ctx.fillStyle = '#b39bff';
63
+ ctx.fill();
64
+ },
65
+ });
66
+ const player = createPlayer(steps(), {
67
+ stepDurationMs: 1000, finalHoldMs: 0,
68
+ isTerminal: step => step.event === 'done',
69
+ render(frame) {
70
+ ctx.clearRect(0, 0, 600, 160);
71
+ renderer.render(ctx, frame);
72
+ },
73
+ });
74
+ document.querySelector('#next').onclick = () => player.advance(1000);
75
+ ```
76
+
77
+ Первый кадр (`x = 40`) рисуется сразу внутри `createPlayer(...)`. Каждый клик
78
+ проходит один шаг длительностью 1000 мс: плеер возобновляет цикл до следующего
79
+ `yield`, затем вызывает отрисовку. Здесь нет плавного перехода: обработчик рисует
80
+ только `current`. Каждый снимок создаётся заново и больше не изменяется. При
81
+ удалении визуализации вызовите `player.dispose()`.
82
+
83
+ [Интерактивный разбор с последовательной подсветкой кода](https://github.com/urffin/algiviz/blob/master/examples/visualization-pipeline.html)
84
+ показывает создание первого кадра по кнопке «Начать» и последующие итерации цикла.
85
+ Это страница примера в репозитории; запускайте её локально по инструкции ниже.
86
+
87
+ [Пошаговое руководство](https://github.com/urffin/algiviz/blob/master/docs/minimal-example.md)
88
+ объясняет локальный запуск, владение снимками состояния и жизненный цикл.
89
+ Более крупный пример с собственными объектами описан в
90
+ [разборе BFS](https://github.com/urffin/algiviz/blob/master/docs/custom-graph-example.md).
91
+
92
+ Цельный пример на TypeScript с двумя типами объектов и кнопками запуска, паузы
93
+ и перезапуска: [custom-objects.ts](../examples/custom-objects.ts).
94
+ [Инструкция запуска](../docs/minimal-example.md#a-complete-typescript-integration)
95
+ объясняет типизацию, подключение и очистку при удалении визуализации.
96
+
97
+ ## Установка
98
+
99
+ ```sh
100
+ npm install @grundyjs/algiviz
101
+ ```
102
+
103
+ У пакета нет корневой точки входа и сборки CommonJS. Импортируйте нужные модули:
104
+ `/array`, `/playback`, `/scene` и `/canvas`.
105
+
106
+ Алгоритмы находятся в коде приложения и примерах. Для новых интеграций используйте
107
+ API ниже. В версии 0.x минорные релизы могут нарушать обратную совместимость;
108
+ при обновлении сверяйтесь с [CHANGELOG.md](../CHANGELOG.md).
109
+
110
+ ## Собственные алгоритмы
111
+
112
+ Новые приложения используют следующие точки входа, не импортирующие готовые алгоритмы:
113
+
114
+ - `@grundyjs/algiviz/array`: операции над массивами и определение генераторов.
115
+ - `@grundyjs/algiviz/playback`: универсальное воспроизведение неизменяемых состояний и событий.
116
+ - `@grundyjs/algiviz/scene`: типизированные объекты и пользовательские визуализации.
117
+ - `@grundyjs/algiviz/canvas`: готовый рендерер массива.
118
+
119
+ Циклы, условия и порядок операций определяете вы. Регистрация в библиотеке
120
+ или изменение AlgiViz не требуются. Например, разворот массива:
121
+
122
+ ```js
123
+ import { defineArrayAlgorithm, createArrayPlayer, createArrayTimeline } from "@grundyjs/algiviz/array";
124
+ import { createArrayRenderer } from "@grundyjs/algiviz/canvas";
125
+
126
+ const reverse = defineArrayAlgorithm(function* (array) {
127
+ yield array.start();
128
+ for (let i = 0; i < Math.floor(array.length / 2); i++) {
129
+ yield array.highlight([i, array.length - 1 - i]);
130
+ yield array.swap(i, array.length - 1 - i);
131
+ }
132
+ yield array.done();
133
+ });
134
+
135
+ const options = { stepDurationMs: 350, finalHoldMs: 1000 };
136
+ const ctx = canvas.getContext("2d");
137
+ const renderer = createArrayRenderer({ theme: "dark" });
138
+ const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), {
139
+ ...options,
140
+ render: frame => renderer.render(ctx, frame)
141
+ });
142
+ // Your animation loop calls player.advance(deltaMs * speed).
143
+ // No calls while paused; advance(0) redraws after resizing.
144
+ // On teardown: cancel your animation loop and call player.dispose().
145
+
146
+ // Alternatively, retain history and seek:
147
+ const timeline = createArrayTimeline(reverse.steps([1, 2, 3, 4]), options);
148
+ renderer.render(ctx, timeline.sample(500));
149
+ ```
150
+
151
+ `defineArrayAlgorithm` возвращает `iterate(values)` и `steps(values)`. Каждый итератор
152
+ владеет отдельным рабочим состоянием, копирует и проверяет входные данные при первом
153
+ `next()`. `steps` собирает итератор в замороженный массив. Выдавайте результат каждой
154
+ операции сразу через `yield`, начинайте с `start` и заканчивайте `done`. Операция
155
+ обновляет рабочее состояние и возвращает неизменяемый снимок, но ничего не рисует.
156
+ Плеер вызывает переданный `render` при создании и каждом `advance`.
157
+ Ошибки отрисовки закрывают итератор и передаются вызывающему коду.
158
+
159
+ ### Операции над массивом
160
+
161
+ | Операция | Действие |
162
+ | --- | --- |
163
+ | `at(index)`, `held`, `length` | Прочитать замороженный элемент, удерживаемый элемент или длину массива |
164
+ | `start(regions?)` | Выдать начальное состояние; вызывается один раз |
165
+ | `compare(left, right)` | Подсветить две позиции и учесть одно сравнение; любая позиция может быть `"held"` |
166
+ | `highlight(positions)` | Подсветить элементы на этом шаге без изменения счётчиков |
167
+ | `swap(left, right, regions?)` | Обменять занятые позиции и учесть две записи; поддерживаются несоседние позиции |
168
+ | `select(from, regions?)` | Удержать элемент, оставив пустую позицию; записи не учитываются |
169
+ | `shift(from, to, regions?)` | Переместить элемент в пустую позицию и учесть одну запись |
170
+ | `insert(to, regions?)` | Заполнить пустую позицию удерживаемым элементом и учесть одну запись |
171
+ | `pass(end, regions?)` | Описать завершённый проход без изменения элементов и счётчиков |
172
+ | `done(regions?)` | Завершить работу; удерживаемого элемента быть не должно; не сортирует и не определяет отсортированные области |
173
+
174
+ `regions` может содержать `sortedPrefixLength` и `sortedSuffixLength`.
175
+ Пропущенные маркеры сохраняют предыдущие значения. Автор алгоритма решает, когда область
176
+ отсортирована; AlgiViz проверяет границы, но не доказывает упорядоченность. Индексы
177
+ начинаются с нуля. Неверные позиции, занятые места назначения и неправильный порядок
178
+ операций вызывают исключение до изменения рабочего состояния. Читайте снимки, а не
179
+ изменяйте элементы самостоятельно. `createArrayTrace(values)` предоставляет те же
180
+ операции напрямую и сразу проверяет входные данные — для собственной оболочки генератора.
181
+
182
+ Сортировки вставками и пузырьком реализованы в [examples/algorithms.mjs](../examples/algorithms.mjs).
183
+ Алгоритмы принадлежат приложению и не требуют изменений библиотеки.
184
+
185
+ ### Собственные структуры и рендереры
186
+
187
+ `/playback` ничего не знает о массивах, сортировках, Canvas и названиях событий.
188
+ Источник выдаёт объекты `Step<State, Event>` с числовым `index`,
189
+ неизменяемыми `state` и `event`. Передайте `isTerminal`, чтобы определить последний
190
+ шаг, и `render`, чтобы рисовать интерполированные кадры любым способом:
191
+
192
+ Оба движка сохраняют исходный индекс: он не обязан начинаться с нуля или идти
193
+ без пропусков. Время определяется порядком шагов. `createStepEmitter` для удобства
194
+ создаёт последовательные индексы от нуля.
195
+
196
+ ```js
197
+ import { createPlayer, createTimeline } from "@grundyjs/algiviz/playback";
198
+
199
+ const player = createPlayer(myTraversal(), {
200
+ stepDurationMs: 300,
201
+ finalHoldMs: 1000,
202
+ isTerminal: step => step.event.kind === "finished",
203
+ render: frame => drawGraph(frame.previous, frame.current, frame.progress, frame.event)
204
+ });
205
+ ```
206
+
207
+ Функция `render` необязательна: без неё читайте `player.frame` и рисуйте самостоятельно.
208
+ `createTimeline(steps, timing)` поддерживает перемотку с тем же универсальным форматом
209
+ кадра; передавайте рендереру результат `sample(timeMs)`. Универсальный движок не
210
+ копирует и не замораживает пользовательские состояния — это обязанность источника.
211
+ Плеер хранит только соседние снимки, а временная шкала — переданную историю.
212
+ Собственные события массива и операции с деревьями или графами могут использовать
213
+ этот контракт и свой рендерер. API сцен ниже направляет отдельные объекты обработчикам;
214
+ встроенного алгоритма или набора операций для графов и деревьев нет.
215
+
216
+ ### Контракты воспроизведения
217
+
218
+ При воспроизведении снимки приложения сохраняются без копирования: шаги, состояния
219
+ и события должны оставаться неизменяемыми, пока используются. Временная шкала копирует
220
+ только входной массив. Время определяется позицией в массиве, а конечные значения
221
+ времени ограничиваются диапазоном шкалы. Генератор должен явно выдать завершающий
222
+ шаг; раннее окончание вызывает исключение. `dispose()` закрывает источник не более
223
+ одного раза, сохраняет доступ к последнему кадру и запрещает дальнейшее продвижение.
224
+ Для перезапуска нужен новый источник.
225
+
226
+ Начиная с 0.7.0 `stepIndex` временной шкалы сохраняет исходный `step.index`, как и плеер.
227
+ Если чтение источника, проверка завершения или отрисовка выбрасывают исключение,
228
+ выполняется попытка очистки без подмены исходной ошибки. Самостоятельная ошибка
229
+ очистки при `dispose()` или штатном завершении по-прежнему передаётся вызывающему коду.
230
+
231
+ ### Вспомогательная функция выдачи шагов (с 0.6.0)
232
+
233
+ В версии 0.6.0 в `/playback` появилась `createStepEmitter`.
234
+ Она автоматически нумерует шаги и замораживает объект-обёртку шага:
235
+
236
+ ```js
237
+ import { createStepEmitter } from '@grundyjs/algiviz/playback';
238
+
239
+ function* counter() {
240
+ let count = 0;
241
+ const emit = createStepEmitter(() => Object.freeze({ count }));
242
+ yield emit('start');
243
+ count++;
244
+ yield emit('done');
245
+ }
246
+ ```
247
+
248
+ Создавайте emitter внутри каждого итератора. Его функция вызывается один раз на выдачу;
249
+ индексы начинаются с нуля, а неудачный захват состояния не расходует индекс.
250
+ Функция не копирует и не замораживает данные состояния и события, не определяет
251
+ завершающие события и не хранит историю. Захват должен возвращать обособленные
252
+ неизменяемые снимки; общие неизменяемые структуры можно переиспользовать.
253
+ Возврат изменяемого рабочего объекта сам по себе не превращает его в снимок.
254
+
255
+ ### Собственные объекты и визуализации
256
+
257
+ Определите карту типов со своими данными и визуализацию для каждого типа.
258
+ AlgiViz сопоставляет объекты по идентификаторам и вызывает обработчики;
259
+ обработчики определяют геометрию и способ интерполяции данных.
260
+
261
+ ```ts
262
+ import { createSceneRenderer, type Scene } from "@grundyjs/algiviz/scene";
263
+
264
+ interface MyObjects {
265
+ badge: { x: number; y: number; label: string };
266
+ }
267
+
268
+ const renderer = createSceneRenderer<MyObjects, CanvasRenderingContext2D>({
269
+ badge(ctx, object) {
270
+ const from = (object.previous ?? object.current)!.data;
271
+ const to = (object.current ?? object.previous)!.data;
272
+ const x = from.x + (to.x - from.x) * object.progress;
273
+ const y = from.y + (to.y - from.y) * object.progress;
274
+ ctx.save();
275
+ try {
276
+ ctx.globalAlpha = object.presence;
277
+ ctx.fillText(to.label, x, y);
278
+ } finally { ctx.restore(); }
279
+ }
280
+ });
281
+
282
+ const before: Scene<MyObjects> = {
283
+ objects: [{ id: "first", type: "badge", data: { x: 20, y: 30, label: "A" } }]
284
+ };
285
+ const after: Scene<MyObjects> = {
286
+ objects: [{ id: "first", type: "badge", data: { x: 120, y: 30, label: "A" } }]
287
+ };
288
+ // Keep these states immutable. A generic timeline/player can provide this frame.
289
+ renderer.render(ctx, {
290
+ previous: before, current: after, progress: 0.5, stepIndex: 1, event: "move"
291
+ });
292
+ ```
293
+
294
+ - У объекта есть непустой стабильный `id`, `type`, пользовательские `data` и необязательный
295
+ конечный `zIndex` (по умолчанию ноль). ID уникальны внутри снимка. Сохранившийся ID
296
+ не может менять тип: для замены объектом другого типа нужен новый ID.
297
+ - Обработчики получают объекты `previous` и `current`; отсутствующая сторона представлена
298
+ `null`. `phase` принимает `enter`, `update` или `exit`. `progress` — исходный прогресс
299
+ кадра от 0 до 1. `presence` равен progress при появлении, 1 при обновлении и 1−progress
300
+ при исчезновении; его можно использовать для прозрачности или масштаба. Автоматически
301
+ он не применяется.
302
+ - Сначала рисуется меньший `zIndex`. При равенстве используется порядок объектов текущего
303
+ снимка; удалённые объекты следуют в своём прежнем порядке. При изменении слоя действует
304
+ текущее значение. Неявной раскладки и системы координат нет.
305
+ - Третий аргумент обработчика предоставляет `scene.get(id)` для связей, указателей и других
306
+ ссылок. Он возвращает полный переход объекта с обеими сторонами; для отсутствующего
307
+ объекта — `undefined`. Приложение само обрабатывает такие случаи и интерполирует
308
+ координаты или смену цели.
309
+ - `createSceneFrame(frame)` возвращает те же вычисленные переходы без отрисовки. Реестр
310
+ фиксируется при создании рендерера. Отсутствующие обработчики, повторные ID, неверный
311
+ progress, некорректные слои и смена типа вызывают ошибку до вызова любого обработчика.
312
+ - Обработка сцены не хранит состояния и поддерживает произвольную перемотку. Обработчик
313
+ может получить объект с нулевым presence. Это отрисовка кадра, **а не гарантированные
314
+ вызовы жизненного цикла**: при пропуске шагов появление или исчезновение может быть
315
+ полностью пропущено. Очищайте Canvas в каждом кадре; SVG/DOM-рендерер с сохранением
316
+ элементов должен сам сверять их с текущим кадром и удалять устаревшие ID. Сохраняйте
317
+ и восстанавливайте состояние Canvas внутри каждого обработчика. Исключения передаются
318
+ вызывающему коду; уже выполненная отрисовка не откатывается.
319
+ - Снимки и пользовательские данные не копируются и не замораживаются рекурсивно.
320
+ Их неизменяемость обеспечивает автор, как и при универсальном воспроизведении.
321
+ Индексация сцены требует O(n) дополнительной памяти, сортировка по слоям — O(n log n)
322
+ времени на кадр.
323
+
324
+ `arrayScene(snapshot)` из `/canvas` преобразует массив в `Scene<ArrayObjects>`
325
+ с объектами `bar`, в данных которых находятся `item`, `index` и `held`.
326
+ Встроенный рендерер массива использует тот же механизм вызова обработчиков.
327
+ Можно задать другую визуализацию `bar` через `createSceneRenderer<ArrayObjects, YourContext>`.
328
+
329
+ ### Проекция состояния в сцену
330
+
331
+ `createSceneProjector` преобразует кадры воспроизведения из вашей модели состояния
332
+ в сцены без изменения алгоритма:
333
+
334
+ ```js
335
+ import { createSceneProjector } from '@grundyjs/algiviz/scene';
336
+ const projectFrame = createSceneProjector(state => ({
337
+ objects: state.items.map(item => ({ id: item.id, type: 'node', data: item }))
338
+ }));
339
+ // Reuse the projector across renders, with either history or generator playback.
340
+ renderer.render(ctx, projectFrame(frame));
341
+ ```
342
+
343
+ Проекция получает только состояние, без события и прогресса. Используйте чистую функцию
344
+ и неизменяемые состояния и сцены. Стороны перехода сравниваются через `Object.is`;
345
+ изменение сохранённого состояния или захваченных настроек проекции не сбрасывает кэш.
346
+ При смене настроек создавайте новый projector. Сохраняется не более двух проекций
347
+ состояний; перемотка к старым состояниям может вычислять их повторно. События, индексы
348
+ шагов и прогресс сохраняются. Раскладка под размер экрана и анимация относятся к
349
+ обработчикам отрисовки, а не к этой кэшируемой проекции. Возвращаемый кадр заморожен;
350
+ пользовательские состояния и сцены не копируются и не замораживаются рекурсивно.
351
+ Проверка сцены по-прежнему выполняется в рендерере. Пример сортировки слиянием использует
352
+ этот адаптер, чтобы не строить заново две сцены ячеек на каждом кадре анимации.
353
+ Алгоритм, типы объектов и визуальное поведение остаются за пределами библиотеки.
354
+
355
+ ## Воспроизведение и отрисовка
356
+
357
+ Плееры и временные шкалы массивов используют тот же формат кадра, что и универсальное
358
+ воспроизведение: `previous`, `current`, `progress`, `stepIndex` и `event`.
359
+ Кадр описывает переход к текущему шагу; состояние шага соответствует состоянию
360
+ после выполнения его операции.
361
+
362
+ | API | Поведение |
363
+ | --- | --- |
364
+ | `createArrayPlayer(iterable, options)` | Воспроизведение массива; распознаёт событие `done` |
365
+ | `createPlayer(iterable, options)` | Универсальное воспроизведение; требует `isTerminal(step)` |
366
+ | `createArrayTimeline(steps, timing)` / `createTimeline(steps, timing)` | Сохранённая история с `durationMs` и `sample(timeMs)` |
367
+ | `createArrayRenderer({ theme: "dark" \| "light" })` | Отрисовка полного кадра массива на Canvas |
368
+ | `createSceneRenderer(handlers)` | Передаёт собственные объекты вашим обработчикам визуализации |
369
+ | `createSceneProjector(project)` | Преобразует неизменяемые состояния перехода в сцены с ограниченным кэшем |
370
+
371
+ Плеер читает первый шаг при создании и предоставляет `frame`, `advance(deltaMs)`,
372
+ `finished` и `dispose()`. У начального кадра progress равен 1.
373
+ `advance` принимает конечное неотрицательное прошедшее время воспроизведения;
374
+ ноль не читает новые шаги. Для паузы не вызывайте продвижение. Для изменения скорости
375
+ умножайте прошедшее время. Необязательный `render` вызывается при создании и каждом advance.
376
+
377
+ Источник должен дойти до завершающего шага. Плеер закрывает итератор после последнего
378
+ перехода и переходит в finished после `finalHoldMs`. Преждевременное окончание,
379
+ ошибки источника и отрисовки передаются вызывающему коду и закрывают итератор.
380
+ `dispose()` закрывает его досрочно и допускает повторные вызовы; кадр остаётся доступен,
381
+ но продвижение освобождённого плеера вызывает исключение. Для перезапуска создайте
382
+ новые итератор и плеер.
383
+
384
+ Временной шкале нужен массив шагов. Шаг i завершается в `i * stepDurationMs`;
385
+ `finalHoldMs` продлевает последний кадр. Выборка ограничивает конечное время диапазоном
386
+ шкалы и отвергает неконечные значения. При воспроизведении генератора нет перемотки
387
+ и заранее известной общей длительности.
388
+
389
+ Планированием анимации управляет приложение. Например:
390
+
391
+ ```js
392
+ const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), options);
393
+ let last;
394
+ let requestId;
395
+ let paused = false;
396
+ let speed = 1;
397
+
398
+ function tick(now) {
399
+ const delta = last === undefined ? 0 : Math.min(100, now - last);
400
+ last = now;
401
+ if (!paused) player.advance(delta * speed);
402
+ renderer.render(ctx, player.frame);
403
+ if (!player.finished) requestId = requestAnimationFrame(tick);
404
+ }
405
+ requestId = requestAnimationFrame(tick);
406
+ // On teardown: cancelAnimationFrame(requestId); player.dispose();
407
+ // To replay after finishing, create a new player and schedule a new loop.
408
+ ```
409
+
410
+ Это альтернатива настройке плеера из предыдущего примера с массивом; здесь используются
411
+ его `reverse`, `options`, `ctx` и `renderer`. Большие интервалы времени синхронно читают
412
+ промежуточные шаги, поэтому ограничивайте delta или приостанавливайте скрытые вкладки,
413
+ чтобы избежать долгого наверстывания.
414
+
415
+ Рендерер массива ожидает неизменяемые снимки. Для раскладки он использует размеры
416
+ битовой карты Canvas, рисует полный кадр и восстанавливает состояние контекста.
417
+ Высота столбца отрицательного значения определяется его модулем, а подпись сохраняет
418
+ знак; это не график со знаковой осью. В плотных диаграммах подписи скрываются.
419
+ Для записи используйте canvas с фиксированными размерами экспорта.
420
+ Отрисовка не запускает цикл анимации и не записывает видео.
421
+
422
+ #### Пользовательские цвета массива (с 0.8.0)
423
+
424
+ Версия 0.8.0 принимает частичное переопределение палитры в `createArrayRenderer`
425
+ и совместимом псевдониме `createSortRenderer`:
426
+
427
+ ```ts
428
+ import { createArrayRenderer, type ArrayPaletteOverrides } from '@grundyjs/algiviz/canvas';
429
+ const colors: ArrayPaletteOverrides = { background: '#ffffff', text: '#182230', active: '#e87900' };
430
+ const renderer = createArrayRenderer({ theme: 'light', colors });
431
+ renderer.render(ctx, currentFrame);
432
+ ```
433
+
434
+ `ArrayPalette` задаёт шесть строковых ролей: `background` (весь canvas), `text`
435
+ (все подписи), `bar` (обычные столбцы), `sorted` (отсортированный префикс/суффикс),
436
+ `active` (подсвеченные, сравниваемые или перемещаемые элементы) и `held` (удерживаемый
437
+ элемент; приоритет выше active и sorted). `ArrayPaletteOverrides` — частичная форма
438
+ палитры, `ArrayRendererOptions` описывает объект настроек. Значения — строки цветов
439
+ Canvas, передаваемые без CSS-проверки. Для пропущенных ролей и `undefined` используется
440
+ выбранная тема. Вызовы только с темой и стандартные цвета не изменились.
441
+
442
+ Переопределения копируются при создании рендерера. Последующее изменение входного
443
+ объекта не влияет на результат. Для смены темы или палитры создайте новый рендерер
444
+ и нарисуйте тот же текущий кадр. Меняются только цвета, но не воспроизведение,
445
+ снимки, раскладка или обработка bitmap/DPR. Попробуйте темы, шесть полей выбора цвета
446
+ и сброс в [лаборатории сортировок](../examples/index.html).
447
+
448
+ ## Память и стоимость генерации
449
+
450
+ Операции массива копируют входные данные, проверяют конечность чисел и отвергают
451
+ разреженные массивы. `defineArrayAlgorithm` делает это при первом `next()` итератора,
452
+ а `createArrayTrace` — сразу. Каждый итератор владеет своим рабочим состоянием.
453
+ Шаги массива, события, снимки и элементы заморожены; неизменяемые элементы могут
454
+ быть общими у нескольких снимков.
455
+
456
+ Воспроизведение генератора сохраняет соседние снимки, а не всю историю. Поэтому для
457
+ массива требуется O(n) памяти снимков, если вызывающий код не хранит старые кадры.
458
+ Произвольные состояния и внутренние структуры алгоритма могут иметь другую стоимость.
459
+ Каждый полный снимок массива всё равно копирует n позиций: ленивость уменьшает
460
+ хранимую историю, а не стоимость каждого шага. Сбор всех шагов возвращает затраты на
461
+ историю. Например, трасса квадратичной сортировки со снимками размера O(n) может
462
+ потребовать O(n³) времени и памяти истории. Итерация синхронна и может блокировать UI.
463
+
464
+ ## Примеры в браузере
465
+
466
+ Соберите проект, раздайте корень репозитория статическим HTTP-сервером и откройте
467
+ одну из страниц ниже. Примеры — код приложения в репозитории;
468
+ в npm-архив они не входят.
469
+
470
+ | Пример | Что демонстрирует |
471
+ | --- | --- |
472
+ | [Как рождается кадр](https://github.com/urffin/algiviz/blob/master/examples/visualization-pipeline.html) | Путь от генератора через плеер и сцену к отрисовке, показанный средствами AlgiViz |
473
+ | [Лаборатория сортировок](../examples/index.html) | Сортировки вставками и пузырьком с операциями массива и готовым рендерером |
474
+ | [Сортировка слиянием](../examples/merge.html) | Собственное состояние и сцена из двух строк для основного массива и буфера |
475
+ | [Разбиения быстрой сортировки](../examples/quick.html) | Разбиения Ломуто и трёхстороннее с собственными состоянием и сценой |
476
+ | [Пирамидальная сортировка](../examples/heap.html) | Построение max-heap и извлечение с согласованными идентификаторами элементов дерева и массива |
477
+ | [Бинарный поиск](../examples/binary-search.html) | Первое вхождение в отсортированном массиве, замкнутый интервал поиска и ленивое воспроизведение |
478
+ | [BFS графа](../examples/graph.html) | Очередь, слои, кратчайшие пути, недостижимая цель и история/генератор |
479
+ | [DFS графа](../examples/dfs.html) | Стек вызовов, возвраты, глубина и путь дерева; история/генератор |
480
+ | [Обход дерева](../examples/tree.html) | Собственные узлы, рёбра и указатель с Canvas и SVG с сохранением элементов |
481
+
482
+ Все примеры используют общие [demo-player.mjs](../examples/demo-player.mjs) и
483
+ [player-settings.mjs](../examples/player-settings.mjs): 650 мс на шаг, удержание последнего
484
+ кадра 1 000 мс, скорости от 0,25× до 32×, воспроизведение/пауза, перезапуск и переходы
485
+ по шагам. История позволяет идти назад и перематывать; режим генератора отключает
486
+ обе возможности. При reduced motion интерполяция отключается, скрытые вкладки
487
+ приостанавливают воспроизведение.
488
+
489
+ Все примеры сортировок принимают до **64 конечных значений с историей** или **2 000
490
+ в режиме генератора**. Введите значения или создайте массив заданной длины в порядке
491
+ Random, Sorted или Reverse. Это ограничения демонстраций, а не библиотеки. Для
492
+ переключения большого массива на историю сначала загрузите массив допустимой длины.
493
+ Плотные диаграммы скрывают подписи; примеры с собственными сценами сортировок
494
+ показывают в текстовом статусе первые 24 ячейки.
495
+
496
+ ### Сортировка слиянием
497
+
498
+ [Алгоритм](../examples/merge-algorithm.mjs) и [рендерер](../examples/merge-scene.mjs)
499
+ используют `/playback` и `/scene`. Равные значения получают буквенные суффиксы,
500
+ чтобы показать устойчивость. Ячейки используют ID строки и позиции, поскольку
501
+ копирование из буфера временно дублирует идентичности элементов. При равенстве
502
+ выбирается левый элемент. Алгоритм использует O(n) дополнительной памяти;
503
+ верхняя граница памяти полной истории снимков — O(n² log n).
504
+
505
+ ### Быстрая сортировка
506
+
507
+ Выбирайте разбиение Ломуто или трёхстороннее, сохраняя загруженные значения.
508
+ Оба алгоритма и их сцена находятся в примере. Трёхсторонняя рекурсия пропускает
509
+ область равных: для n ≥ 2 одинаковых элементов нужны 2n скалярных проверок и ноль
510
+ обменов, а для этого варианта Ломуто — n(n−1)/2 проверок. Ни один вариант не
511
+ гарантирует устойчивость. В худшем случае время квадратичное, рекурсия может
512
+ потребовать O(n) памяти стека. Отдельная верхняя граница памяти полной истории — O(n³).
513
+
514
+ ### Пирамидальная сортировка
515
+
516
+ [Алгоритм](../examples/heap-algorithm.mjs) определяет неизменяемые состояния и события.
517
+ [Сцена](../examples/heap-scene.mjs) задаёт каждому элементу отдельные визуальные ID в
518
+ дереве и массиве и использует публичную проекцию сцен с состояниями, независимыми
519
+ от размера экрана. Позиции дерева фиксированы при уменьшении кучи. До 15 ячеек
520
+ массива переносятся на узких экранах; более крупные массивы используют плотную строку
521
+ без нечитаемых подписей. Построение снизу вверх занимает O(n), сортировка в худшем
522
+ случае — O(n log n), итеративный алгоритм на месте использует O(1) дополнительной
523
+ памяти. Эта демонстрация копирует вход и хранит O(n) данных соседних снимков в режиме
524
+ генератора; полная история может потребовать O(n² log n).
525
+ Сортировка неустойчива: [2a, 2b, 1] превращается в [1, 2b, 2a].
526
+
527
+ ### Бинарный поиск
528
+
529
+ [Алгоритм](../examples/binary-search-algorithm.mjs) находит первое вхождение, в том числе
530
+ при дубликатах. При равенстве сохраняется кандидат и поиск продолжается слева.
531
+ Входной массив должен быть отсортирован; неверный ввод сохраняет текущий сеанс.
532
+ Можно генерировать отсортированные массивы заданной длины с общими ограничениями
533
+ режимов истории и генератора. Счётчик учитывает проверенные средние элементы.
534
+
535
+ [Сцена](../examples/binary-search-scene.mjs) использует стабильные ID по индексам и
536
+ проекцию, независимую от размера экрана. До 32 ячеек переносятся; более крупные
537
+ массивы отображаются плотной полосой без нечитаемых подписей. Поиск занимает
538
+ O(log n) времени и O(1) дополнительной памяти алгоритма. Демонстрация проверяет
539
+ и копирует вход за O(n), разделяет замороженный массив между снимками и хранит
540
+ O(n + log n) данных истории. Пример не входит в npm-пакет и не требует нового экспорта.
541
+
542
+ ### Обход дерева
543
+
544
+ Пример определяет собственные типы `node`, `edge` и `pointer`. Указатель проходит
545
+ по рёбрам через общих предков между посещаемыми узлами. Выбор Renderer переключает
546
+ Canvas и SVG без перезапуска воспроизведения. [tree-svg.mjs](../examples/tree-svg.mjs)
547
+ сохраняет элементы по ID, восстанавливает порядок слоёв и удаляет устаревшие элементы,
548
+ даже если перемотка пропустила переход исчезновения. Специальные алгоритм и рендерер
549
+ дерева в пакет не добавляются.
550
+
551
+ ### Обход графа в ширину
552
+
553
+ Откройте [examples/graph.html](../examples/graph.html), выберите граф, старт и
554
+ необязательную цель, затем нажмите Apply. BFS отмечает вершины при добавлении в
555
+ очередь, полностью обходит достижимую компоненту и восстанавливает один кратчайший
556
+ путь по предкам. Попробуйте изолированную H, путь до старта длиной 0, цепочку или
557
+ ромб с равными путями. Исходный несвязный граф из GIF в README сохранён отдельным
558
+ вариантом.
559
+
560
+ Сцена показывает неподвижный граф, анимированную очередь, расстояния, рёбра предков
561
+ и пути, отдельные счётчики обнаружения, обработки и просмотров соседей. На узком
562
+ экране очередь переносится под граф. Canvas использует логические координаты и DPR.
563
+ Поддержка петель и параллельных рёбер сохранена; порядок соседей может изменить
564
+ выбранный кратчайший путь, но не его длину.
565
+
566
+ Оба режима воспроизведения используют один генератор с неизменяемыми состояниями.
567
+ Перезапуск и смена режима используют применённые граф/старт/цель, а не черновые поля.
568
+ Генератор вычисляет шаги лениво, без предварительного сбора истории. Полная история
569
+ позволяет перематывать назад и имеет отдельную верхнюю границу памяти снимков
570
+ O(V(V + E)), помимо хранения графа и данных алгоритма.
571
+
572
+ В [разборе собственного примера графа](../docs/custom-graph-example.md) описаны границы
573
+ алгоритма, проекции и рендерера. Пример использует существующий публичный API
574
+ и не добавляет специальные экспорты для графов.
575
+
576
+ ### Обход графа в глубину
577
+
578
+ Откройте [examples/dfs.html](../examples/dfs.html), чтобы сравнить DFS и BFS
579
+ на одинаковых графах. Выберите граф, старт и необязательную цель. Каждый явный
580
+ кадр вызова хранит курсор следующего соседа; вершина отмечается до входа.
581
+ Возврат снимает завершённый вызов и возобновляет родителя. Стек содержит только
582
+ активную ветку обхода.
583
+
584
+ Порядок обнаружения по умолчанию — A, B, D, G, E, C, F; возвратов — F, C, E, G, D, B, A.
585
+ Путь дерева DFS до F содержит 6 рёбер; BFS находит A → C → F длиной 2 ребра.
586
+ Глубина дерева DFS не является кратчайшим расстоянием. H недостижима.
587
+ Путь восстанавливается после полного обхода. Допустимы простые неориентированные
588
+ графы; петли и параллельные рёбра отклоняются. Порядок соседей соответствует входу.
589
+
590
+ На узком экране стек фиксированной вместимости переносится под граф.
591
+ Canvas использует логические координаты и DPR. Оба режима используют один
592
+ неизменяемый генератор и [общий плеер](../examples/demo-player.mjs); перезапуск
593
+ повторяет применённые входные данные. Генератор вычисляет шаги лениво,
594
+ история позволяет перематывать назад. Обход занимает O(V + E) времени и O(V)
595
+ дополнительной памяти; верхняя граница памяти истории снимков отдельно составляет
596
+ O(V(V + E)). Новые API графа или встроенный DFS в пакет не добавляются.
597
+
598
+ ## Разработка
599
+
600
+ Требуются Node.js 22 или новее и npm.
601
+
602
+ ```sh
603
+ npm ci
604
+ npm test
605
+ npm run test:package
606
+ ```
607
+
608
+ `npm test` собирает библиотеку, проверяет TypeScript-примеры и запускает тесты.
609
+ `npm run test:package` собирает архив, устанавливает его без сети в изолированное
610
+ временное приложение, компилирует код с установленными декларациями и проверяет
611
+ публичный API, включая отказ для удалённого экспорта `/core`.
612
+ Затем временное приложение удаляется.
613
+
614
+ GitHub Actions запускает обе проверки при push в `master` и в pull request,
615
+ используя Node.js 22 и 24 на Linux и Windows. Проверки CI не публикуют пакеты.
616
+
617
+ `npm pack` автоматически пересобирает пакет. Архив включает dist, README, LICENSE,
618
+ CHANGELOG и метаданные пакета. Исходный код и задачи:
619
+ [urffin/algiviz](https://github.com/urffin/algiviz). Лицензия MIT.
620
+
621
+ ### Проверки производительности
622
+
623
+ Запустите `npm run benchmark` для массивов из 2 000 элементов: сортировки вставками,
624
+ пузырьком, слиянием и оба варианта быстрой сортировки. Отчёт показывает генерацию
625
+ 1 000 шагов, медиану CPU-времени отрисовки по пяти выборкам из 100 кадров и занятую
626
+ кучу Node после каждых 5 000 шагов до 25 000. Canvas заменён заглушкой: это сравнение
627
+ CPU, а не измерение FPS браузера или GPU. Замеры кучи используют явную сборку мусора
628
+ и включают процесс Node; это диагностика, а не универсальные пороги прохождения теста.
629
+
630
+ ## Выпуск версии
631
+
632
+ Публикация выполняется сопровождающим проекта. Обновите версию пакета, lockfile
633
+ и changelog; запустите `npm test` и `npm run test:package`, сделайте коммит,
634
+ отправьте его и проверьте CI для этого коммита.
635
+
636
+ [Workflow публикации](../.github/workflows/npm-publish.yml) запускается при публикации
637
+ стабильного GitHub Release. Черновики, предварительные релизы и отправка одного
638
+ лишь тега пакет не публикуют. Используйте `v` и точную версию пакета, например
639
+ `v0.8.0`, на проверенном коммите.
640
+
641
+ Workflow использует Node.js 24 и npm 11, проверяет версию и приложение с упакованным
642
+ пакетом, затем публикует с provenance через npm trusted publishing (OIDC).
643
+ Жизненный цикл публикации также запускает тесты и пересборку.
644
+ Секрет `NPM_TOKEN` не требуется.
645
+
646
+ Trusted publisher в npm должен соответствовать владельцу GitHub `urffin`,
647
+ репозиторию `algiviz` и workflow `npm-publish.yml`. GitHub environment не используется.
648
+ См. [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
649
+
650
+ После публикации проверьте запуск Actions и точную версию в npm.
651
+ Никогда не используйте повторно уже опубликованную версию npm. Если workflow завершился
652
+ ошибкой, проверьте, успел ли пакет опубликоваться, прежде чем запускать его повторно.