@grundyjs/algiviz 0.7.1 → 0.8.1

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