@grundyjs/algiviz 0.3.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 +17 -0
- package/README.md +229 -38
- package/dist/array/index.d.ts +7 -0
- package/dist/array/index.js +3 -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 -25
- package/dist/compat/algorithms.d.ts +2 -0
- package/dist/compat/algorithms.js +36 -0
- package/dist/core/bubble-sort.d.ts +4 -5
- package/dist/core/bubble-sort.js +5 -33
- package/dist/core/insertion-sort.d.ts +4 -8
- package/dist/core/insertion-sort.js +5 -40
- 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 -52
- 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 +18 -2
- /package/dist/{core/sort-trace.d.ts → array/snapshot-builder.d.ts} +0 -0
- /package/dist/{core/sort-trace.js → array/snapshot-builder.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** State and event must remain immutable while playback uses them. */
|
|
2
|
+
export interface Step<S, E> {
|
|
3
|
+
readonly index: number;
|
|
4
|
+
readonly state: S;
|
|
5
|
+
readonly event: E;
|
|
6
|
+
}
|
|
7
|
+
export interface Frame<S, E> {
|
|
8
|
+
readonly previous: S;
|
|
9
|
+
readonly current: S;
|
|
10
|
+
readonly progress: number;
|
|
11
|
+
readonly stepIndex: number;
|
|
12
|
+
readonly event: E;
|
|
13
|
+
}
|
|
14
|
+
export interface Timeline<S, E> {
|
|
15
|
+
readonly durationMs: number;
|
|
16
|
+
sample(timeMs: number): Frame<S, E>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Frame } from "../playback/types.js";
|
|
2
|
+
/** Types maps user-defined type names to their data, e.g. { node: NodeData }. */
|
|
3
|
+
export type SceneObject<Types extends object> = {
|
|
4
|
+
[K in keyof Types & string]: Readonly<{
|
|
5
|
+
id: string;
|
|
6
|
+
type: K;
|
|
7
|
+
data: Types[K];
|
|
8
|
+
zIndex?: number;
|
|
9
|
+
}>;
|
|
10
|
+
}[keyof Types & string];
|
|
11
|
+
export interface Scene<Types extends object> {
|
|
12
|
+
readonly objects: readonly SceneObject<Types>[];
|
|
13
|
+
}
|
|
14
|
+
export interface ObjectTransition<Object> {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly previous: Object | null;
|
|
17
|
+
readonly current: Object | null;
|
|
18
|
+
readonly phase: "update" | "enter" | "exit";
|
|
19
|
+
readonly progress: number;
|
|
20
|
+
/** Suggested opacity/scale for entering and exiting objects. */
|
|
21
|
+
readonly presence: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SceneFrame<Types extends object, Event> {
|
|
24
|
+
readonly progress: number;
|
|
25
|
+
readonly stepIndex: number;
|
|
26
|
+
readonly event: Event;
|
|
27
|
+
/** Draw order: zIndex, then current snapshot order (removed objects follow). */
|
|
28
|
+
readonly objects: readonly ObjectTransition<SceneObject<Types>>[];
|
|
29
|
+
/** Includes entering/exiting objects, even when their presence is zero. */
|
|
30
|
+
get(id: string): ObjectTransition<SceneObject<Types>> | undefined;
|
|
31
|
+
}
|
|
32
|
+
export type SceneVisualizations<Types extends object, Context, Event> = {
|
|
33
|
+
readonly [K in keyof Types & string]: (context: Context, object: ObjectTransition<Extract<SceneObject<Types>, {
|
|
34
|
+
type: K;
|
|
35
|
+
}>>, scene: SceneFrame<Types, Event>) => void;
|
|
36
|
+
};
|
|
37
|
+
/** Matches stable identities. Data is not cloned: snapshots must be immutable. */
|
|
38
|
+
export declare function createSceneFrame<Types extends object, Event>(frame: Frame<Scene<Types>, Event>): SceneFrame<Types, Event>;
|
|
39
|
+
/** Backend-independent dispatch: Context can be Canvas, SVG, DOM or a custom target. */
|
|
40
|
+
export declare function createSceneRenderer<Types extends object, Context, Event = unknown>(visualizations: SceneVisualizations<Types, Context, Event>): Readonly<{
|
|
41
|
+
render(context: Context, frame: Frame<Scene<Types>, Event>): void;
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Matches stable identities. Data is not cloned: snapshots must be immutable. */
|
|
2
|
+
export function createSceneFrame(frame) {
|
|
3
|
+
if (!Number.isFinite(frame.progress) || frame.progress < 0 || frame.progress > 1) {
|
|
4
|
+
throw new RangeError("Scene progress must be between zero and one");
|
|
5
|
+
}
|
|
6
|
+
function index(scene) {
|
|
7
|
+
const result = new Map();
|
|
8
|
+
for (const object of scene.objects) {
|
|
9
|
+
if (typeof object.id !== "string" || !object.id || typeof object.type !== "string" || !object.type) {
|
|
10
|
+
throw new TypeError("Scene objects require non-empty string ids and types");
|
|
11
|
+
}
|
|
12
|
+
if (!Number.isFinite(object.zIndex ?? 0))
|
|
13
|
+
throw new RangeError("zIndex must be finite");
|
|
14
|
+
if (result.has(object.id))
|
|
15
|
+
throw new Error(`Duplicate scene object id: ${object.id}`);
|
|
16
|
+
result.set(object.id, object);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
const before = index(frame.previous), after = index(frame.current);
|
|
21
|
+
const byId = new Map();
|
|
22
|
+
for (const id of new Set([...after.keys(), ...before.keys()])) {
|
|
23
|
+
const previous = before.get(id) ?? null, current = after.get(id) ?? null;
|
|
24
|
+
if (previous && current && previous.type !== current.type) {
|
|
25
|
+
throw new Error(`Object ${id} changed type; use a new id for a different type`);
|
|
26
|
+
}
|
|
27
|
+
byId.set(id, Object.freeze({
|
|
28
|
+
id,
|
|
29
|
+
previous,
|
|
30
|
+
current,
|
|
31
|
+
progress: frame.progress,
|
|
32
|
+
phase: previous === null ? "enter" : current === null ? "exit" : "update",
|
|
33
|
+
presence: previous === null ? frame.progress : current === null ? 1 - frame.progress : 1
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
const objects = Object.freeze([...byId.values()].sort((a, b) => ((a.current ?? a.previous).zIndex ?? 0) - ((b.current ?? b.previous).zIndex ?? 0)));
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
progress: frame.progress,
|
|
39
|
+
stepIndex: frame.stepIndex,
|
|
40
|
+
event: frame.event,
|
|
41
|
+
objects,
|
|
42
|
+
get: (id) => byId.get(id)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Backend-independent dispatch: Context can be Canvas, SVG, DOM or a custom target. */
|
|
46
|
+
export function createSceneRenderer(visualizations) {
|
|
47
|
+
const registry = new Map(Object.entries(visualizations));
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
render(context, frame) {
|
|
50
|
+
const scene = createSceneFrame(frame);
|
|
51
|
+
// Validate the complete frame before dispatching any visualization.
|
|
52
|
+
for (const object of scene.objects) {
|
|
53
|
+
const type = (object.current ?? object.previous).type;
|
|
54
|
+
if (typeof registry.get(type) !== "function")
|
|
55
|
+
throw new Error(`No visualization for scene type: ${type}`);
|
|
56
|
+
}
|
|
57
|
+
for (const object of scene.objects) {
|
|
58
|
+
const type = (object.current ?? object.previous).type;
|
|
59
|
+
// The registry is keyed by the same discriminant checked during matching.
|
|
60
|
+
const draw = registry.get(type);
|
|
61
|
+
draw(context, object, scene);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grundyjs/algiviz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic algorithm steps for educational visualizations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
"CHANGELOG.md"
|
|
11
11
|
],
|
|
12
12
|
"exports": {
|
|
13
|
+
"./scene": {
|
|
14
|
+
"types": "./dist/scene/index.d.ts",
|
|
15
|
+
"import": "./dist/scene/index.js",
|
|
16
|
+
"default": "./dist/scene/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./array": {
|
|
19
|
+
"types": "./dist/array/index.d.ts",
|
|
20
|
+
"import": "./dist/array/index.js",
|
|
21
|
+
"default": "./dist/array/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./playback": {
|
|
24
|
+
"types": "./dist/playback/index.d.ts",
|
|
25
|
+
"import": "./dist/playback/index.js",
|
|
26
|
+
"default": "./dist/playback/index.js"
|
|
27
|
+
},
|
|
13
28
|
"./core": {
|
|
14
29
|
"types": "./dist/core/index.d.ts",
|
|
15
30
|
"import": "./dist/core/index.js",
|
|
@@ -23,7 +38,8 @@
|
|
|
23
38
|
},
|
|
24
39
|
"scripts": {
|
|
25
40
|
"build": "tsc -p tsconfig.json",
|
|
26
|
-
"test": "npm run build && node --test test/*.test.mjs",
|
|
41
|
+
"test": "npm run build && tsc -p test/tsconfig.json && node --test test/*.test.mjs",
|
|
42
|
+
"test:package": "node scripts/test-package.mjs",
|
|
27
43
|
"prepack": "npm run build",
|
|
28
44
|
"prepublishOnly": "npm test"
|
|
29
45
|
},
|
|
File without changes
|
|
File without changes
|