@grundyjs/algiviz 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +268 -4
- package/dist/array/index.d.ts +7 -0
- package/dist/array/index.js +3 -0
- package/dist/array/snapshot-builder.d.ts +11 -0
- package/dist/array/snapshot-builder.js +34 -0
- package/dist/array/trace.d.ts +27 -0
- package/dist/array/trace.js +119 -0
- package/dist/array/types.d.ts +55 -0
- package/dist/array/types.js +1 -0
- package/dist/canvas/index.d.ts +12 -0
- package/dist/canvas/index.js +65 -24
- package/dist/compat/algorithms.d.ts +2 -0
- package/dist/compat/algorithms.js +36 -0
- package/dist/core/bubble-sort.d.ts +4 -0
- package/dist/core/bubble-sort.js +5 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/insertion-sort.d.ts +4 -8
- package/dist/core/insertion-sort.js +5 -65
- package/dist/core/player.d.ts +5 -10
- package/dist/core/player.js +3 -86
- package/dist/core/timeline.d.ts +3 -12
- package/dist/core/timeline.js +2 -28
- package/dist/core/types.d.ts +1 -41
- package/dist/playback/index.d.ts +4 -0
- package/dist/playback/index.js +2 -0
- package/dist/playback/player.d.ts +18 -0
- package/dist/playback/player.js +97 -0
- package/dist/playback/timeline.d.ts +6 -0
- package/dist/playback/timeline.js +30 -0
- package/dist/playback/types.d.ts +17 -0
- package/dist/playback/types.js +1 -0
- package/dist/scene/index.d.ts +42 -0
- package/dist/scene/index.js +65 -0
- package/package.json +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
- Verify the packed npm archive in an isolated TypeScript consumer before publishing.
|
|
6
|
+
|
|
7
|
+
- Add `/scene` with typed user-defined object data and visualization handlers,
|
|
8
|
+
identity matching, enter/update/exit transitions, layers and reference lookup.
|
|
9
|
+
- Render array bars through the scene dispatcher and expose `arrayScene`.
|
|
10
|
+
- Add an external tree traversal demo defining its own nodes, edges and pointer.
|
|
11
|
+
|
|
12
|
+
- Add algorithm-free `/array` and `/playback` entry points: user-defined generators,
|
|
13
|
+
validated array operations, generic states/events and optional rendering callbacks.
|
|
14
|
+
- Add `highlight` events and `createArrayRenderer`; existing rendering names remain available.
|
|
15
|
+
- Move demo algorithms into application-owned examples using only the public API.
|
|
16
|
+
Old `/core` algorithm exports remain deprecated compatibility adapters.
|
|
17
|
+
- Preserve all original insertion/bubble traces; premature exhaustion now reports
|
|
18
|
+
a missing terminal step rather than a missing done event.
|
|
19
|
+
|
|
20
|
+
## 0.3.0 — 2026-10-09
|
|
21
|
+
|
|
22
|
+
- Extend `SortEvent` with `swap` and `pass`, and `SortSnapshot` with optional
|
|
23
|
+
`sortedSuffixLength`. Consumers with exhaustive event switches must handle
|
|
24
|
+
the new event types. Existing insertion-sort traces remain unchanged.
|
|
25
|
+
|
|
26
|
+
- Share input validation, item identities, immutable snapshots and operation
|
|
27
|
+
counters between sorting algorithms; document adding an algorithm internally.
|
|
28
|
+
|
|
29
|
+
- Add stable bubble sort with `bubbleSortSteps` and `iterateBubbleSortSteps`,
|
|
30
|
+
adjacent swaps, sorted suffix tracking and early exit after a pass without swaps.
|
|
31
|
+
- Render both swapped items and the sorted suffix; select either algorithm in
|
|
32
|
+
the demo with history or generator playback.
|
|
33
|
+
|
|
3
34
|
## 0.2.0 — 2026-10-09
|
|
4
35
|
|
|
5
36
|
- Move the held key horizontally along the baseline without lifting it above the array;
|
package/README.md
CHANGED
|
@@ -1,6 +1,221 @@
|
|
|
1
1
|
# AlgiViz
|
|
2
2
|
|
|
3
|
-
A TypeScript
|
|
3
|
+
A TypeScript toolkit for user-defined algorithm visualizations. You write the algorithm; AlgiViz provides array operations, immutable steps, playback and canvas rendering. No runtime dependencies. Playback works without DOM, React or Next.js. ESM only, with TypeScript declarations.
|
|
4
|
+
|
|
5
|
+
## User-defined algorithms
|
|
6
|
+
|
|
7
|
+
The following API is introduced in 0.4.0.
|
|
8
|
+
New applications use these entry points, which do not import bundled algorithms:
|
|
9
|
+
|
|
10
|
+
- `@grundyjs/algiviz/array`: array operations and generator definitions.
|
|
11
|
+
- `@grundyjs/algiviz/playback`: generic playback of any immutable states and events.
|
|
12
|
+
- `@grundyjs/algiviz/scene`: typed objects and user-provided visualizations.
|
|
13
|
+
- `@grundyjs/algiviz/canvas`: the ready-made array renderer.
|
|
14
|
+
|
|
15
|
+
You own the loops, conditions and operation order. No library registration or
|
|
16
|
+
changes to AlgiViz are required. For example, reversing an array:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { defineArrayAlgorithm, createArrayPlayer, createArrayTimeline } from "@grundyjs/algiviz/array";
|
|
20
|
+
import { createArrayRenderer } from "@grundyjs/algiviz/canvas";
|
|
21
|
+
|
|
22
|
+
const reverse = defineArrayAlgorithm(function* (array) {
|
|
23
|
+
yield array.start();
|
|
24
|
+
for (let i = 0; i < Math.floor(array.length / 2); i++) {
|
|
25
|
+
yield array.highlight([i, array.length - 1 - i]);
|
|
26
|
+
yield array.swap(i, array.length - 1 - i);
|
|
27
|
+
}
|
|
28
|
+
yield array.done();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const options = { stepDurationMs: 350, finalHoldMs: 1000 };
|
|
32
|
+
const ctx = canvas.getContext("2d");
|
|
33
|
+
const renderer = createArrayRenderer({ theme: "dark" });
|
|
34
|
+
const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), {
|
|
35
|
+
...options,
|
|
36
|
+
render: frame => renderer.render(ctx, frame)
|
|
37
|
+
});
|
|
38
|
+
// Your animation loop calls player.advance(deltaMs * speed).
|
|
39
|
+
// No calls while paused; advance(0) redraws after resizing.
|
|
40
|
+
// On teardown: cancel your animation loop and call player.dispose().
|
|
41
|
+
|
|
42
|
+
// Alternatively, retain history and seek:
|
|
43
|
+
const timeline = createArrayTimeline(reverse.steps([1, 2, 3, 4]), options);
|
|
44
|
+
renderer.render(ctx, timeline.sample(500));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`defineArrayAlgorithm` returns `iterate(values)` and `steps(values)`. Each
|
|
48
|
+
iterator owns a separate workspace and copies/validates input on the first
|
|
49
|
+
`next()`. `steps` collects the iterator into a frozen array. Yield every
|
|
50
|
+
operation immediately, begin with `start`, and end with `done`. Each operation
|
|
51
|
+
updates the workspace and returns an immutable snapshot; it does not draw.
|
|
52
|
+
The player invokes the supplied `render` initially and on every `advance`.
|
|
53
|
+
Rendering errors close the iterator and propagate to the caller.
|
|
54
|
+
|
|
55
|
+
### Array operations
|
|
56
|
+
|
|
57
|
+
| Operation | Effect |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `at(index)`, `held`, `length` | Read a frozen item, the held item or array length |
|
|
60
|
+
| `start(regions?)` | Emit initial state; call once |
|
|
61
|
+
| `compare(left, right)` | Highlight two positions and count one comparison; either position can be `"held"` |
|
|
62
|
+
| `highlight(positions)` | Highlight items for this step without changing counters |
|
|
63
|
+
| `swap(left, right, regions?)` | Exchange occupied positions; count two writes (non-adjacent swaps are supported) |
|
|
64
|
+
| `select(from, regions?)` | Hold an item and leave a hole; no counted writes |
|
|
65
|
+
| `shift(from, to, regions?)` | Move an item into the hole; count one write |
|
|
66
|
+
| `insert(to, regions?)` | Fill the hole with the held item; count one write |
|
|
67
|
+
| `pass(end, regions?)` | Describe a completed pass without changing items or counters |
|
|
68
|
+
| `done(regions?)` | Finish; requires no held item; does not sort or infer sorted regions |
|
|
69
|
+
|
|
70
|
+
`regions` can contain `sortedPrefixLength` and `sortedSuffixLength`.
|
|
71
|
+
Omitted markers retain their previous values. The author decides when regions
|
|
72
|
+
are sorted; AlgiViz validates their bounds but does not prove that claim. Positions
|
|
73
|
+
are zero-based; invalid positions, occupied destinations and invalid operation
|
|
74
|
+
order throw before mutating the workspace. Read snapshots rather than modifying
|
|
75
|
+
items yourself. `createArrayTrace(values)` exposes the same operations directly,
|
|
76
|
+
with immediate input validation, for authors writing their own generator wrapper.
|
|
77
|
+
|
|
78
|
+
Full insertion/bubble implementations live in [examples/algorithms.mjs](examples/algorithms.mjs).
|
|
79
|
+
The browser demo imports those external algorithms. Deprecated `/core` exports
|
|
80
|
+
remain available through isolated compatibility implementations using the same
|
|
81
|
+
operations; they are retained in the package until a breaking release removes them.
|
|
82
|
+
Existing insertion/bubble traces remain unchanged. `ArrayEvent` also includes the
|
|
83
|
+
new `highlight` event, so exhaustive event switches must handle it.
|
|
84
|
+
|
|
85
|
+
### Custom structures and renderers
|
|
86
|
+
|
|
87
|
+
`/playback` does not know arrays, sorting, Canvas, or event names. A source yields
|
|
88
|
+
`Step<State, Event>` objects with contiguous zero-based `index`, immutable
|
|
89
|
+
`state` and `event`. Supply `isTerminal` to identify the final step and
|
|
90
|
+
`render` to draw each interpolated frame using any rendering technology:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { createPlayer, createTimeline } from "@grundyjs/algiviz/playback";
|
|
94
|
+
|
|
95
|
+
const player = createPlayer(myTraversal(), {
|
|
96
|
+
stepDurationMs: 300,
|
|
97
|
+
finalHoldMs: 1000,
|
|
98
|
+
isTerminal: step => step.event.kind === "finished",
|
|
99
|
+
render: frame => drawGraph(frame.previous, frame.current, frame.progress, frame.event)
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The render callback is optional. Without it, read `player.frame` and render
|
|
104
|
+
manually. `createTimeline(steps, timing)` supports seeking with the same generic
|
|
105
|
+
frame shape; call your renderer with its `sample(timeMs)` result. The generic
|
|
106
|
+
engine does not clone or freeze user states; that is the source's responsibility.
|
|
107
|
+
It retains adjacent snapshots only; the timeline retains the supplied history.
|
|
108
|
+
Custom array events or tree/graph operations can use this contract and a custom
|
|
109
|
+
renderer. The scene API below dispatches individual objects; there is no built-in
|
|
110
|
+
graph/tree algorithm or operation set.
|
|
111
|
+
|
|
112
|
+
### User-defined objects and visualizations
|
|
113
|
+
|
|
114
|
+
The `/scene` API is introduced in 0.4.0. Define a type map with your own data, then
|
|
115
|
+
provide a visualization for every type. AlgiViz matches identities and dispatches
|
|
116
|
+
objects; your handlers decide how data becomes geometry and how it interpolates.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { createSceneRenderer, type Scene } from "@grundyjs/algiviz/scene";
|
|
120
|
+
|
|
121
|
+
interface MyObjects {
|
|
122
|
+
badge: { x: number; y: number; label: string };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const renderer = createSceneRenderer<MyObjects, CanvasRenderingContext2D>({
|
|
126
|
+
badge(ctx, object) {
|
|
127
|
+
const from = (object.previous ?? object.current)!.data;
|
|
128
|
+
const to = (object.current ?? object.previous)!.data;
|
|
129
|
+
const x = from.x + (to.x - from.x) * object.progress;
|
|
130
|
+
const y = from.y + (to.y - from.y) * object.progress;
|
|
131
|
+
ctx.save();
|
|
132
|
+
try {
|
|
133
|
+
ctx.globalAlpha = object.presence;
|
|
134
|
+
ctx.fillText(to.label, x, y);
|
|
135
|
+
} finally { ctx.restore(); }
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const before: Scene<MyObjects> = {
|
|
140
|
+
objects: [{ id: "first", type: "badge", data: { x: 20, y: 30, label: "A" } }]
|
|
141
|
+
};
|
|
142
|
+
const after: Scene<MyObjects> = {
|
|
143
|
+
objects: [{ id: "first", type: "badge", data: { x: 120, y: 30, label: "A" } }]
|
|
144
|
+
};
|
|
145
|
+
// Keep these states immutable. A generic timeline/player can provide this frame.
|
|
146
|
+
renderer.render(ctx, {
|
|
147
|
+
previous: before, current: after, progress: 0.5, stepIndex: 1, event: "move"
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- An object has a non-empty stable `id`, a `type`, user-defined `data` and an
|
|
152
|
+
optional finite `zIndex` (default zero). IDs are unique within a snapshot. A
|
|
153
|
+
retained ID cannot change type; use a new ID to replace an object of another type.
|
|
154
|
+
- Handlers receive `previous` and `current` objects, with `null` for a missing
|
|
155
|
+
endpoint. `phase` is `enter`, `update` or `exit`. `progress` is the raw 0–1 frame
|
|
156
|
+
progress. `presence` is progress on entry, 1 on update and 1−progress on exit;
|
|
157
|
+
handlers may use it for opacity or scale. It is not applied automatically.
|
|
158
|
+
- Lower `zIndex` draws first. Ties follow the current snapshot's object order;
|
|
159
|
+
removed objects follow in their previous order. The current layer wins when it
|
|
160
|
+
changes. There is no implicit layout or coordinate system.
|
|
161
|
+
- The third handler argument exposes `scene.get(id)` for links, pointers and other
|
|
162
|
+
references. It returns the referenced object's full transition, including both
|
|
163
|
+
endpoints; missing references return `undefined`. The application decides how to
|
|
164
|
+
handle them and how to interpolate positions or changing targets.
|
|
165
|
+
- `createSceneFrame(frame)` exposes the same resolved transitions without drawing.
|
|
166
|
+
The registry is captured at renderer construction. Missing handlers, duplicate
|
|
167
|
+
IDs, invalid progress/layers and changed types fail before any handler is called.
|
|
168
|
+
- Scene processing is stateless, so arbitrary seeking is supported. Handlers may
|
|
169
|
+
see zero-presence objects. They are frame draws, **not guaranteed lifecycle
|
|
170
|
+
callbacks**: skipping steps can skip appearances/exits entirely. Clear Canvas
|
|
171
|
+
each frame; a retained SVG/DOM backend must reconcile its elements against the
|
|
172
|
+
current frame, removing stale IDs itself. Save/restore Canvas state inside each
|
|
173
|
+
handler. Exceptions propagate; there is no rollback of drawing already performed.
|
|
174
|
+
- Snapshots and user data are not cloned or deep-frozen. The author owns their
|
|
175
|
+
immutability, as with generic playback. Scene indexing uses O(n) additional
|
|
176
|
+
memory and layer ordering O(n log n) time per frame.
|
|
177
|
+
|
|
178
|
+
`arrayScene(snapshot)` from `/canvas` adapts an array to a `Scene<ArrayObjects>`
|
|
179
|
+
with `bar` objects whose data contains `item`, `index` and `held`. The built-in
|
|
180
|
+
array renderer uses this same dispatcher. You can provide a different `bar`
|
|
181
|
+
visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
|
|
182
|
+
|
|
183
|
+
See [examples/tree-scene.mjs](examples/tree-scene.mjs) and open
|
|
184
|
+
`examples/tree.html` after building and serving the repository. The application
|
|
185
|
+
defines its own tree traversal, `node`, `edge` and `pointer` types, appearance,
|
|
186
|
+
pointer movement and removal. No tree-specific code is added to AlgiViz.
|
|
187
|
+
|
|
188
|
+
## Migrating from 0.3.x
|
|
189
|
+
|
|
190
|
+
Existing `/core` imports continue to work in 0.4.0. Built-in insertion and bubble
|
|
191
|
+
sort exports are deprecated, but their traces remain unchanged. Migrate each
|
|
192
|
+
algorithm into your application using `defineArrayAlgorithm`; complete examples
|
|
193
|
+
are in [examples/algorithms.mjs](https://github.com/urffin/algiviz/blob/v0.4.0/examples/algorithms.mjs).
|
|
194
|
+
|
|
195
|
+
| Previous API | Application-owned API |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `insertionSortSteps(values)` | `insertion.steps(values)` on your algorithm definition |
|
|
198
|
+
| `iterateInsertionSortSteps(values)` | `insertion.iterate(values)` |
|
|
199
|
+
| `bubbleSortSteps(values)` / `iterateBubbleSortSteps(values)` | `bubble.steps(values)` / `bubble.iterate(values)` |
|
|
200
|
+
| `/core` `createSortTimeline` / `createSortPlayer` | `/array` `createArrayTimeline` / `createArrayPlayer` |
|
|
201
|
+
| `/canvas` `createSortRenderer` | `/canvas` `createArrayRenderer` |
|
|
202
|
+
| `/core` `SortEvent`, `SortSnapshot`, `SortStep`, `SortFrame` | `/array` `ArrayEvent`, `ArraySnapshot`, `ArrayStep`, `ArrayFrame` |
|
|
203
|
+
|
|
204
|
+
Timing options and array frames remain compatible. Exhaustive event switches
|
|
205
|
+
must handle the new `highlight` event, even though the legacy algorithms do not
|
|
206
|
+
emit it. An incomplete player source now reports a missing terminal step; avoid
|
|
207
|
+
depending on the old error text. Generic `/playback` requires an explicit
|
|
208
|
+
`isTerminal` predicate; `/array` players still recognize the `done` event.
|
|
209
|
+
|
|
210
|
+
Use `/scene` when you need your own object types and visualization handlers.
|
|
211
|
+
This is optional for array users: `createArrayRenderer` already uses scenes.
|
|
212
|
+
Generic states and custom scene data must remain immutable; unlike array
|
|
213
|
+
operations, the generic engine does not clone or deep-freeze them.
|
|
214
|
+
|
|
215
|
+
## Legacy sorting API
|
|
216
|
+
|
|
217
|
+
The following `/core` examples remain supported for compatibility. Prefer the
|
|
218
|
+
user-defined algorithm API above for new integrations.
|
|
4
219
|
|
|
5
220
|
## Install
|
|
6
221
|
|
|
@@ -42,6 +257,38 @@ The underlying sort uses O(n²) comparisons in the worst case and O(n) on alread
|
|
|
42
257
|
|
|
43
258
|
Tests replay every event independently, check item conservation, stability, prefix ordering, counters, frozen snapshots and 1,093 exhaustive inputs with values -1, 0 and 1 of lengths 0–6.
|
|
44
259
|
|
|
260
|
+
## Bubble sort
|
|
261
|
+
|
|
262
|
+
`bubbleSortSteps(values)` collects a frozen history; `iterateBubbleSortSteps(values)`
|
|
263
|
+
yields the same snapshots on demand. Both work with the existing timeline, player
|
|
264
|
+
and canvas renderer. Available since version 0.3.0.
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
import { bubbleSortSteps, iterateBubbleSortSteps, createSortPlayer } from "@grundyjs/algiviz/core";
|
|
268
|
+
|
|
269
|
+
const steps = bubbleSortSteps([5, 2, 4, 2, 1]);
|
|
270
|
+
const player = createSortPlayer(iterateBubbleSortSteps([5, 2, 4, 2, 1]), {
|
|
271
|
+
stepDurationMs: 350, finalHoldMs: 1000
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Each left-to-right pass compares adjacent items. `compare` identifies both items;
|
|
276
|
+
`swap` contains their pre-swap `leftId`, `rightId`, `left` and `right` positions,
|
|
277
|
+
and its snapshot is after the exchange. Each swap counts as two array writes.
|
|
278
|
+
Equal items are never swapped, so the sort is stable. `held` remains null and
|
|
279
|
+
`sortedPrefixLength` is zero.
|
|
280
|
+
|
|
281
|
+
`pass` marks a completed pass, with `end` identifying its last compared position.
|
|
282
|
+
The optional `sortedSuffixLength` snapshot field counts trailing items in their
|
|
283
|
+
final positions; renderers treat an omitted value as zero for older traces.
|
|
284
|
+
A pass with no swaps ends the sort early and marks the entire array sorted.
|
|
285
|
+
`done` always has `sortedSuffixLength` equal to the array length.
|
|
286
|
+
|
|
287
|
+
Validation, lazy input copying, freezing and memory costs match insertion sort.
|
|
288
|
+
The underlying algorithm performs O(n²) comparisons in the worst case and O(n)
|
|
289
|
+
on sorted input; full snapshots still make worst-case trace generation O(n³).
|
|
290
|
+
Consumers switching on `SortEvent.type` should handle the new `swap` and `pass` events.
|
|
291
|
+
|
|
45
292
|
## Lazy steps
|
|
46
293
|
|
|
47
294
|
Use `iterateInsertionSortSteps` to consume steps on demand without retaining the
|
|
@@ -140,7 +387,8 @@ Build, serve this project root using any static HTTP server, and open examples/i
|
|
|
140
387
|
The demo supports history (up to 64 items) and generator playback (up to 2,000).
|
|
141
388
|
Enter values or generate random, sorted or reversed arrays. Both modes support
|
|
142
389
|
pause, restart and speed changes; seeking is available only with full history.
|
|
143
|
-
|
|
390
|
+
Algorithm and mode changes restart the loaded array. Choose insertion or bubble
|
|
391
|
+
sort; both support both playback modes. These limits apply only to the demo.
|
|
144
392
|
Dense charts hide bar labels. Hidden tabs pause playback, and delayed frames cap
|
|
145
393
|
catch-up work to keep controls responsive.
|
|
146
394
|
|
|
@@ -148,21 +396,37 @@ catch-up work to keep controls responsive.
|
|
|
148
396
|
## API at a glance
|
|
149
397
|
|
|
150
398
|
- `insertionSortSteps(values)` returns immutable `SortStep[]` snapshots. Empty arrays are valid.
|
|
399
|
+
- `bubbleSortSteps(values)` returns the same snapshot format for stable bubble sort.
|
|
400
|
+
- `iterateInsertionSortSteps(values)` and `iterateBubbleSortSteps(values)` yield snapshots lazily.
|
|
401
|
+
- `createSortPlayer(iterable, { stepDurationMs, finalHoldMs })` provides forward-only playback with `frame`, `advance(deltaMs)`, `finished` and `dispose()`.
|
|
151
402
|
- `createSortTimeline(steps, { stepDurationMs, finalHoldMs })` returns `durationMs` and `sample(timeMs)`. Frames expose `previous`, `current`, `progress`, `stepIndex` and `event`.
|
|
152
403
|
- `createSortRenderer({ theme: "dark" | "light" }).render(ctx, frame)` draws into a browser 2D canvas context. Drawing and animation scheduling remain separate.
|
|
153
404
|
|
|
154
405
|
For playback, call `timeline.sample(elapsedMs)` in your own requestAnimationFrame loop. For recording, use a fixed-size export canvas. Negative values are shown by magnitude with signed labels; the chart is not a signed-axis plot.
|
|
155
406
|
|
|
407
|
+
## Adding an algorithm
|
|
408
|
+
|
|
409
|
+
Create an application-owned generator with `defineArrayAlgorithm`, as shown
|
|
410
|
+
above. Add it to your application's controls or the demo registry. AlgiViz needs
|
|
411
|
+
no change when the existing operations are sufficient. For a new kind of state
|
|
412
|
+
or event, implement the generic playback contract and its renderer.
|
|
413
|
+
|
|
156
414
|
## Release
|
|
157
415
|
|
|
158
|
-
Run `npm test
|
|
416
|
+
Run `npm test` and `npm run test:package`. The package check builds a real tarball,
|
|
417
|
+
installs it offline in a separate temporary consumer, compiles TypeScript against
|
|
418
|
+
the installed declarations and runs an external algorithm and custom scene with
|
|
419
|
+
both playback modes. It also checks the array adapter and legacy compatibility.
|
|
420
|
+
The temporary consumer is removed afterward. The release workflow runs this check
|
|
421
|
+
before publishing. Source and issues: [urffin/algiviz](https://github.com/urffin/algiviz).
|
|
422
|
+
Publishing is a separate maintainer action.
|
|
159
423
|
|
|
160
424
|
### Publishing from GitHub Releases
|
|
161
425
|
|
|
162
426
|
The `.github/workflows/npm-publish.yml` workflow publishes to npm when a stable
|
|
163
427
|
GitHub Release is published (`release: published`). Drafts, prereleases and tag
|
|
164
428
|
pushes alone do not publish a package. The release tag must be `v` followed by
|
|
165
|
-
the exact version in `package.json` and `package-lock.json` (for example `v0.
|
|
429
|
+
the exact version in `package.json` and `package-lock.json` (for example `v0.4.0`).
|
|
166
430
|
The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
|
|
167
431
|
build before publishing with provenance, using npm trusted publishing (OIDC).
|
|
168
432
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createArrayTrace, defineArrayAlgorithm } from "./trace.js";
|
|
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";
|
|
4
|
+
export { createSortPlayer as createArrayPlayer } from "../core/player.js";
|
|
5
|
+
export { createSortTimeline as createArrayTimeline } from "../core/timeline.js";
|
|
6
|
+
export type { SortPlayer as ArrayPlayer } from "../core/player.js";
|
|
7
|
+
export type { SortFrame as ArrayFrame, SortTimeline as ArrayTimeline } from "../core/timeline.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Item, SortEvent, SortStep } from "./types.js";
|
|
2
|
+
/** Internal mutable workspace; only emit() exposes immutable snapshots. */
|
|
3
|
+
export declare function createSortTrace(values: readonly number[]): {
|
|
4
|
+
state: {
|
|
5
|
+
slots: (Item | null)[];
|
|
6
|
+
held: Item | null;
|
|
7
|
+
sortedPrefixLength: number;
|
|
8
|
+
sortedSuffixLength?: number;
|
|
9
|
+
};
|
|
10
|
+
emit: (event: SortEvent) => SortStep;
|
|
11
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Internal mutable workspace; only emit() exposes immutable snapshots. */
|
|
2
|
+
export function createSortTrace(values) {
|
|
3
|
+
// Array.from exposes sparse holes as undefined so they fail validation too.
|
|
4
|
+
const input = Array.from(values);
|
|
5
|
+
const slots = input.map((value, index) => {
|
|
6
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
7
|
+
throw new TypeError(`Expected a finite number at index ${index}`);
|
|
8
|
+
}
|
|
9
|
+
return Object.freeze({ id: `item-${index}`, value });
|
|
10
|
+
});
|
|
11
|
+
const state = { slots, held: null, sortedPrefixLength: 0 };
|
|
12
|
+
let index = 0;
|
|
13
|
+
let comparisons = 0;
|
|
14
|
+
let writes = 0;
|
|
15
|
+
function emit(event) {
|
|
16
|
+
if (event.type === "compare")
|
|
17
|
+
comparisons++;
|
|
18
|
+
if (event.type === "shift" || event.type === "insert")
|
|
19
|
+
writes++;
|
|
20
|
+
if (event.type === "swap")
|
|
21
|
+
writes += 2;
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
index: index++,
|
|
24
|
+
event: Object.freeze(event),
|
|
25
|
+
state: Object.freeze({
|
|
26
|
+
...state,
|
|
27
|
+
slots: Object.freeze(state.slots.slice()),
|
|
28
|
+
comparisons,
|
|
29
|
+
writes
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { state, emit };
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Item, SortStep } from "./types.js";
|
|
2
|
+
export interface SortedRegions {
|
|
3
|
+
readonly sortedPrefixLength?: number;
|
|
4
|
+
readonly sortedSuffixLength?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ArrayTrace {
|
|
7
|
+
readonly length: number;
|
|
8
|
+
readonly held: Item | null;
|
|
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;
|
|
19
|
+
}
|
|
20
|
+
/** Operations own mutations, counters and immutable snapshots, but no algorithm. */
|
|
21
|
+
export declare function createArrayTrace(values: readonly number[]): ArrayTrace;
|
|
22
|
+
export interface ArrayAlgorithm {
|
|
23
|
+
iterate(values: readonly number[]): Generator<SortStep, void, unknown>;
|
|
24
|
+
steps(values: readonly number[]): readonly SortStep[];
|
|
25
|
+
}
|
|
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;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createSortTrace } from "./snapshot-builder.js";
|
|
2
|
+
/** Operations own mutations, counters and immutable snapshots, but no algorithm. */
|
|
3
|
+
export function createArrayTrace(values) {
|
|
4
|
+
const { state, emit } = createSortTrace(values);
|
|
5
|
+
const { slots } = state;
|
|
6
|
+
let phase = "new";
|
|
7
|
+
function index(value) {
|
|
8
|
+
if (!Number.isInteger(value) || value < 0 || value >= slots.length)
|
|
9
|
+
throw new RangeError(`Invalid array index: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
function active() {
|
|
12
|
+
if (phase !== "running")
|
|
13
|
+
throw new Error("Call start before operations; no operations are allowed after done");
|
|
14
|
+
}
|
|
15
|
+
function item(position) {
|
|
16
|
+
if (position !== "held")
|
|
17
|
+
index(position);
|
|
18
|
+
const value = position === "held" ? state.held : slots[position];
|
|
19
|
+
if (!value)
|
|
20
|
+
throw new Error(`No item at ${position}`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function regions(update = {}) {
|
|
24
|
+
for (const value of [update.sortedPrefixLength, update.sortedSuffixLength]) {
|
|
25
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0 || value > slots.length)) {
|
|
26
|
+
throw new RangeError("Sorted region length must be between zero and array length");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (update.sortedPrefixLength !== undefined)
|
|
30
|
+
state.sortedPrefixLength = update.sortedPrefixLength;
|
|
31
|
+
if (update.sortedSuffixLength !== undefined)
|
|
32
|
+
state.sortedSuffixLength = update.sortedSuffixLength;
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
get length() { return slots.length; },
|
|
36
|
+
get held() { return state.held; },
|
|
37
|
+
at(position) { index(position); return slots[position]; },
|
|
38
|
+
start(update) {
|
|
39
|
+
if (phase !== "new")
|
|
40
|
+
throw new Error("Trace has already started");
|
|
41
|
+
regions(update);
|
|
42
|
+
phase = "running";
|
|
43
|
+
return emit({ type: "start" });
|
|
44
|
+
},
|
|
45
|
+
compare(left, right) {
|
|
46
|
+
active();
|
|
47
|
+
const a = item(left), b = item(right);
|
|
48
|
+
return emit({ type: "compare", leftId: a.id, rightId: b.id });
|
|
49
|
+
},
|
|
50
|
+
highlight(positions) {
|
|
51
|
+
active();
|
|
52
|
+
const itemIds = Object.freeze(positions.map(position => item(position).id));
|
|
53
|
+
return emit({ type: "highlight", itemIds });
|
|
54
|
+
},
|
|
55
|
+
swap(left, right, update) {
|
|
56
|
+
active();
|
|
57
|
+
const a = item(left), b = item(right);
|
|
58
|
+
if (left === right)
|
|
59
|
+
throw new RangeError("Swap requires distinct positions");
|
|
60
|
+
regions(update);
|
|
61
|
+
slots[left] = b;
|
|
62
|
+
slots[right] = a;
|
|
63
|
+
return emit({ type: "swap", leftId: a.id, rightId: b.id, left, right });
|
|
64
|
+
},
|
|
65
|
+
select(from, update) {
|
|
66
|
+
active();
|
|
67
|
+
if (state.held)
|
|
68
|
+
throw new Error("An item is already held");
|
|
69
|
+
const selected = item(from);
|
|
70
|
+
regions(update);
|
|
71
|
+
state.held = selected;
|
|
72
|
+
slots[from] = null;
|
|
73
|
+
return emit({ type: "select", itemId: selected.id, from });
|
|
74
|
+
},
|
|
75
|
+
shift(from, to, update) {
|
|
76
|
+
active();
|
|
77
|
+
const moved = item(from);
|
|
78
|
+
index(to);
|
|
79
|
+
if (slots[to] !== null)
|
|
80
|
+
throw new Error("Shift destination must be empty");
|
|
81
|
+
regions(update);
|
|
82
|
+
slots[to] = moved;
|
|
83
|
+
slots[from] = null;
|
|
84
|
+
return emit({ type: "shift", itemId: moved.id, from, to });
|
|
85
|
+
},
|
|
86
|
+
insert(to, update) {
|
|
87
|
+
active();
|
|
88
|
+
const inserted = item("held");
|
|
89
|
+
index(to);
|
|
90
|
+
if (slots[to] !== null)
|
|
91
|
+
throw new Error("Insert destination must be empty");
|
|
92
|
+
regions(update);
|
|
93
|
+
slots[to] = inserted;
|
|
94
|
+
state.held = null;
|
|
95
|
+
return emit({ type: "insert", itemId: inserted.id, to });
|
|
96
|
+
},
|
|
97
|
+
pass(end, update) {
|
|
98
|
+
active();
|
|
99
|
+
index(end);
|
|
100
|
+
regions(update);
|
|
101
|
+
return emit({ type: "pass", end });
|
|
102
|
+
},
|
|
103
|
+
done(update) {
|
|
104
|
+
active();
|
|
105
|
+
if (state.held)
|
|
106
|
+
throw new Error("Insert the held item before completing the trace");
|
|
107
|
+
regions(update);
|
|
108
|
+
phase = "done";
|
|
109
|
+
return emit({ type: "done" });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Each iterator owns its workspace; input is read on its first next(). */
|
|
114
|
+
export function defineArrayAlgorithm(algorithm) {
|
|
115
|
+
function* iterate(values) {
|
|
116
|
+
yield* algorithm(createArrayTrace(values));
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({ iterate, steps: (values) => Object.freeze([...iterate(values)]) });
|
|
119
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Item = Readonly<{
|
|
2
|
+
id: string;
|
|
3
|
+
value: number;
|
|
4
|
+
}>;
|
|
5
|
+
export type SortEvent = Readonly<{
|
|
6
|
+
type: "start";
|
|
7
|
+
} | {
|
|
8
|
+
type: "select";
|
|
9
|
+
itemId: string;
|
|
10
|
+
from: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: "compare";
|
|
13
|
+
leftId: string;
|
|
14
|
+
rightId: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "shift";
|
|
17
|
+
itemId: string;
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
} | {
|
|
21
|
+
type: "insert";
|
|
22
|
+
itemId: string;
|
|
23
|
+
to: number;
|
|
24
|
+
} | {
|
|
25
|
+
type: "swap";
|
|
26
|
+
leftId: string;
|
|
27
|
+
rightId: string;
|
|
28
|
+
left: number;
|
|
29
|
+
right: number;
|
|
30
|
+
} | {
|
|
31
|
+
type: "pass";
|
|
32
|
+
end: number;
|
|
33
|
+
} | {
|
|
34
|
+
type: "highlight";
|
|
35
|
+
itemIds: readonly string[];
|
|
36
|
+
} | {
|
|
37
|
+
type: "done";
|
|
38
|
+
}>;
|
|
39
|
+
export interface SortSnapshot {
|
|
40
|
+
readonly slots: readonly (Item | null)[];
|
|
41
|
+
readonly held: Item | null;
|
|
42
|
+
/** Sorted leading occupied slots; during insertion it ends at the hole. */
|
|
43
|
+
readonly sortedPrefixLength: number;
|
|
44
|
+
/** Final sorted trailing slots. Omitted by older traces and insertion sort. */
|
|
45
|
+
readonly sortedSuffixLength?: number;
|
|
46
|
+
readonly comparisons: number;
|
|
47
|
+
/** Writes into array slots; selecting a key does not count as a write. */
|
|
48
|
+
readonly writes: number;
|
|
49
|
+
}
|
|
50
|
+
export interface SortStep {
|
|
51
|
+
readonly index: number;
|
|
52
|
+
readonly event: SortEvent;
|
|
53
|
+
/** Complete immutable state immediately after the event. */
|
|
54
|
+
readonly state: SortSnapshot;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/canvas/index.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import type { Item, SortSnapshot } from "../core/types.js";
|
|
1
2
|
import type { SortFrame } from "../core/timeline.js";
|
|
3
|
+
import { type Scene } from "../scene/index.js";
|
|
4
|
+
export type ArrayObjects = {
|
|
5
|
+
bar: {
|
|
6
|
+
item: Item;
|
|
7
|
+
index: number;
|
|
8
|
+
held: boolean;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
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 };
|
|
2
14
|
/** Stateless full-frame renderer. Bitmap dimensions determine layout and video size. */
|
|
3
15
|
export declare function createSortRenderer(options: {
|
|
4
16
|
theme: "light" | "dark";
|