flowsvg 0.1.0 → 0.1.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/PATTERNS.md +12 -1
  3. package/README.md +139 -117
  4. package/README.zh-CN.md +139 -117
  5. package/dist/adapters/react.d.ts +48 -0
  6. package/dist/adapters/react.js +126 -0
  7. package/dist/adapters/vue.d.ts +132 -0
  8. package/dist/adapters/vue.js +161 -0
  9. package/dist/adapters/web-component.d.ts +99 -0
  10. package/dist/adapters/web-component.js +396 -0
  11. package/dist/browser/player.d.ts +69 -0
  12. package/dist/browser/player.js +353 -0
  13. package/dist/browser.d.ts +4 -69
  14. package/dist/browser.js +2 -353
  15. package/dist/core/color.d.ts +3 -0
  16. package/dist/core/color.js +18 -0
  17. package/dist/core/diagram.d.ts +38 -0
  18. package/dist/core/diagram.js +391 -0
  19. package/dist/core/edge-geometry.d.ts +32 -0
  20. package/dist/core/edge-geometry.js +382 -0
  21. package/dist/{core.d.ts → core/model.d.ts} +2 -52
  22. package/dist/core/model.js +1 -0
  23. package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
  24. package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
  25. package/dist/{node-content.d.ts → core/node-content.d.ts} +1 -1
  26. package/dist/core/svg-number.d.ts +5 -0
  27. package/dist/core/svg-number.js +7 -0
  28. package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
  29. package/dist/core/theme.js +17 -0
  30. package/dist/{timeline.d.ts → core/timeline.d.ts} +1 -3
  31. package/dist/{timeline.js → core/timeline.js} +1 -17
  32. package/dist/core/validation.d.ts +11 -0
  33. package/dist/core/validation.js +389 -0
  34. package/dist/index.d.ts +5 -1
  35. package/dist/index.js +2 -1
  36. package/dist/react.d.ts +3 -48
  37. package/dist/react.js +2 -126
  38. package/dist/vue.d.ts +3 -132
  39. package/dist/vue.js +2 -161
  40. package/dist/web-component.d.ts +3 -103
  41. package/dist/web-component.js +2 -396
  42. package/docs/images/entity-graph.png +0 -0
  43. package/docs/images/facts-timeline.png +0 -0
  44. package/docs/images/facts-timeline.zh-CN.png +0 -0
  45. package/docs/images/node-content.png +0 -0
  46. package/docs/images/numbered-list.png +0 -0
  47. package/docs/images/numbered-list.zh-CN.png +0 -0
  48. package/docs/images/quick-start.png +0 -0
  49. package/docs/images/record-tags.png +0 -0
  50. package/package.json +2 -1
  51. package/dist/core.js +0 -1149
  52. package/dist/theme.js +0 -6
  53. /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
  54. /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
  55. /package/dist/{player-controls.d.ts → browser/controls.d.ts} +0 -0
  56. /package/dist/{player-controls.js → browser/controls.js} +0 -0
  57. /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
  58. /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
  59. /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
  60. /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
  61. /package/dist/{node-content.js → core/node-content.js} +0 -0
package/dist/browser.js CHANGED
@@ -1,353 +1,2 @@
1
- /**
2
- * Browser player: own the clock, DOM, controls and input, forwarding time and visual options to the core.
3
- * Importing does not access the DOM; createPlayer allocates mount resources and destroy releases them.
4
- */
5
- import { paintContentHighlight } from "./node-content-paint.js";
6
- import { updateSvgDom } from "./svg-dom.js";
7
- import { attachPlayerLifecycle } from "./player-lifecycle.js";
8
- import { observeKeyboard, observeReducedMotion } from "./player-accessibility.js";
9
- import { PlayerControls } from "./player-controls.js";
10
- import { resolveThemePalette } from "./theme.js";
11
- import { createDiagram } from "./core.js";
12
- /**
13
- * Mount a player using the host document and observe browser motion preferences unless explicitly overridden.
14
- * Replaces host children; destroy an existing player before replacing it on the same host.
15
- * @throws {DiagramValidationError} For an invalid initial document, before an instance exists to emit error.
16
- */
17
- export function createPlayer(host, document, options = {}) {
18
- const initial = createDiagram(document);
19
- return new BrowserPlayer(host, initial, document, options);
20
- }
21
- /**
22
- * Own one compiled Diagram and advance time with RAF; core results drive the canvas, caption and controls.
23
- * Retain cleanup callbacks for lifecycle, media preferences and pointer listeners, releasing them on destroy.
24
- */
25
- class BrowserPlayer {
26
- element;
27
- diagram;
28
- options;
29
- timeMs = 0;
30
- speed = 1;
31
- playing = false;
32
- destroyed = false;
33
- frameHandle;
34
- lastTimestamp;
35
- frame;
36
- canvas;
37
- caption;
38
- controls;
39
- scenes;
40
- beatStarts;
41
- detachLifecycle;
42
- stopKeyboard;
43
- stopMotion;
44
- stopPointer;
45
- themeOverride;
46
- pointerPosition;
47
- hoveredHit = null;
48
- constructor(host, diagram, document, options) {
49
- this.element = host;
50
- this.diagram = diagram;
51
- this.beatStarts = collectBeatStarts(document);
52
- this.scenes = collectScenes(document);
53
- const owner = host.ownerDocument;
54
- const motion = observeReducedMotion(owner, options.reducedMotion, (reducedMotion) => {
55
- if (this.destroyed)
56
- return;
57
- this.options = { ...this.options, reducedMotion };
58
- this.updateFrame();
59
- });
60
- this.stopMotion = motion.cleanup;
61
- this.themeOverride = options.theme;
62
- this.options = { ...options, theme: options.theme ?? document.theme ?? "light", reducedMotion: motion.value };
63
- this.frame = diagram.evaluateAt(0, this.options);
64
- this.canvas = owner.createElement("div");
65
- this.canvas.className = "flowsvg-canvas";
66
- this.stopKeyboard = observeKeyboard(this.canvas, { toggle: this.togglePlayback, step: (direction) => this.step(direction) });
67
- this.caption = owner.createElement("p");
68
- this.caption.className = "flowsvg-caption";
69
- this.caption.setAttribute("aria-live", "polite");
70
- this.caption.setAttribute("aria-atomic", "true");
71
- if (options.controls !== false) {
72
- this.controls = new PlayerControls(owner, {
73
- toggle: this.togglePlayback,
74
- step: (direction) => this.step(direction),
75
- seek: (timeMs) => this.seek(timeMs),
76
- setSpeed: (speed) => this.setSpeed(speed)
77
- });
78
- }
79
- host.replaceChildren(this.canvas, this.caption, ...(this.controls ? [this.controls.element] : []));
80
- this.render();
81
- this.stopPointer = observePointer(this.canvas, {
82
- move: (event) => this.handlePointer("hover", event),
83
- leave: () => this.clearPointer(),
84
- click: (event) => this.handlePointer("click", event)
85
- });
86
- this.detachLifecycle = attachPlayerLifecycle(host, this.canvas, () => this.pause());
87
- }
88
- play() {
89
- if (this.destroyed || this.playing || this.element.ownerDocument.visibilityState === "hidden" || this.diagram.totalDuration <= this.timeMs)
90
- return;
91
- this.playing = true;
92
- this.lastTimestamp = undefined;
93
- this.renderControls();
94
- this.emit("playstatechange");
95
- this.scheduleFrame();
96
- }
97
- pause() {
98
- if (this.destroyed || !this.playing)
99
- return;
100
- this.playing = false;
101
- if (this.frameHandle !== undefined)
102
- cancelAnimationFrame(this.frameHandle);
103
- this.frameHandle = undefined;
104
- this.renderControls();
105
- this.emit("playstatechange");
106
- }
107
- seek(timeMs) {
108
- if (this.destroyed)
109
- return;
110
- this.timeMs = clamp(timeMs, 0, this.diagram.totalDuration);
111
- this.lastTimestamp = undefined;
112
- this.updateFrame();
113
- if (this.timeMs >= this.diagram.totalDuration)
114
- this.pause();
115
- }
116
- step(direction) {
117
- if (this.destroyed)
118
- return;
119
- this.pause();
120
- const starts = direction > 0 ? this.beatStarts : [...this.beatStarts].reverse();
121
- const target = starts.find((start) => direction > 0 ? start > this.timeMs : start < this.timeMs);
122
- this.seek(target ?? (direction > 0 ? this.diagram.totalDuration : 0));
123
- }
124
- setSpeed(speed) {
125
- if (this.destroyed || !Number.isFinite(speed) || speed <= 0)
126
- return;
127
- this.speed = speed;
128
- this.renderControls();
129
- }
130
- load(document) {
131
- if (this.destroyed)
132
- return;
133
- try {
134
- // Keep the old player intact until compilation succeeds so invalid loads do not interrupt valid playback.
135
- const diagram = createDiagram(document);
136
- this.pause();
137
- if (this.destroyed)
138
- return;
139
- this.diagram = diagram;
140
- this.options = { ...this.options, theme: this.themeOverride ?? document.theme ?? "light" };
141
- this.beatStarts = collectBeatStarts(document);
142
- this.scenes = collectScenes(document);
143
- this.timeMs = 0;
144
- // A replacement document owns new interaction targets, even when it reuses the same node IDs.
145
- this.pointerPosition = undefined;
146
- this.updateFrame();
147
- }
148
- catch (error) {
149
- this.emit("error", { ...this.state(), error: error instanceof Error ? error : new Error(String(error)) });
150
- }
151
- }
152
- getState() {
153
- return this.state();
154
- }
155
- destroy() {
156
- if (this.destroyed)
157
- return;
158
- // Block dispatch/rescheduling before releasing resources; do not call pause, which would emit an event.
159
- this.destroyed = true;
160
- this.playing = false;
161
- if (this.frameHandle !== undefined)
162
- cancelAnimationFrame(this.frameHandle);
163
- this.frameHandle = undefined;
164
- this.lastTimestamp = undefined;
165
- this.detachLifecycle();
166
- this.stopKeyboard();
167
- this.stopMotion();
168
- this.stopPointer();
169
- this.pointerPosition = undefined;
170
- this.hoveredHit = null;
171
- this.element.replaceChildren();
172
- }
173
- /** The first RAF establishes a baseline; seek clears it so elapsed time from the old position is not reused. */
174
- tick = (timestamp) => {
175
- if (!this.playing || this.destroyed)
176
- return;
177
- this.frameHandle = undefined;
178
- const previous = this.lastTimestamp ?? timestamp;
179
- this.lastTimestamp = timestamp;
180
- this.timeMs += (timestamp - previous) * this.speed;
181
- if (this.timeMs >= this.diagram.totalDuration) {
182
- this.timeMs = this.diagram.totalDuration;
183
- this.updateFrame();
184
- this.pause();
185
- return;
186
- }
187
- this.updateFrame();
188
- this.scheduleFrame();
189
- };
190
- /** Event listeners may restart playback synchronously; never queue a second frame owned by the same player. */
191
- scheduleFrame() {
192
- if (this.destroyed || !this.playing || this.frameHandle !== undefined)
193
- return;
194
- this.frameHandle = requestAnimationFrame(this.tick);
195
- }
196
- /** Update the frame before notifying consumers so listeners observe the rendered position. */
197
- updateFrame() {
198
- const previousBeat = this.frame.beatId;
199
- const previousScene = this.frame.sceneId;
200
- this.frame = this.diagram.evaluateAt(this.timeMs, this.options);
201
- this.render();
202
- if (previousBeat !== this.frame.beatId || previousScene !== this.frame.sceneId)
203
- this.emit("stepchange");
204
- this.emit("timeupdate");
205
- }
206
- togglePlayback = () => {
207
- if (this.playing)
208
- this.pause();
209
- else
210
- this.play();
211
- };
212
- render() {
213
- updateSvgDom(this.canvas, this.diagram.renderSvg(this.timeMs, this.options));
214
- this.canvas.style.aspectRatio = `${this.frame.camera.width} / ${this.frame.camera.height}`;
215
- this.updateHover();
216
- this.caption.textContent = this.frame.caption ?? "";
217
- this.renderControls();
218
- }
219
- renderControls() {
220
- this.controls?.render({ ...this.state(), sceneId: this.frame.sceneId, scenes: this.scenes });
221
- }
222
- state() {
223
- return { timeMs: this.timeMs, totalDuration: this.diagram.totalDuration, playing: this.playing, speed: this.speed, frame: this.frame };
224
- }
225
- handlePointer(type, event) {
226
- if (this.destroyed)
227
- return;
228
- if (type === "hover") {
229
- this.pointerPosition = { clientX: event.clientX, clientY: event.clientY };
230
- updateSvgDom(this.canvas, this.diagram.renderSvg(this.timeMs, this.options));
231
- this.updateHover(true);
232
- return;
233
- }
234
- if (type === "click")
235
- event.stopPropagation();
236
- const point = pointerToDiagramPoint(this.canvas, this.frame.camera, event);
237
- const hit = this.diagram.hitTest(this.timeMs, point, this.options);
238
- const detail = hit ? { ...hit, point, timeMs: this.timeMs, sceneId: this.frame.sceneId, beatId: this.frame.beatId } : null;
239
- this.emit("click", detail);
240
- this.emit("objectclick", detail);
241
- }
242
- /** Re-hit the saved screen position after camera/geometry updates; hover never changes the core hit geometry. */
243
- updateHover(emitAlways = false) {
244
- const point = this.pointerPosition ? pointerToDiagramPoint(this.canvas, this.frame.camera, this.pointerPosition) : undefined;
245
- const hit = point ? this.diagram.hitTest(this.timeMs, point, this.options) : null;
246
- if (hit?.type === "node") {
247
- const shape = Array.from(this.canvas.querySelectorAll("[data-id]")).find((element) => element.getAttribute("data-id") === hit.id);
248
- const color = resolveThemePalette(this.options.theme).highlight;
249
- const node = this.frame.nodes.find((candidate) => candidate.id === hit.id);
250
- // Border-only hover preserves all current timeline and content paint unless fill feedback is explicitly enabled.
251
- if (this.options.hoverFill)
252
- shape?.setAttribute("fill", color);
253
- shape?.setAttribute("stroke", color);
254
- const layout = node?.contentLayout;
255
- const content = Array.from(this.canvas.querySelectorAll("[data-content-id]")).find((element) => element.getAttribute("data-content-id") === hit.id);
256
- if (this.options.hoverFill && content && layout)
257
- paintHoveredContent(content, paintContentHighlight(layout, 1, color));
258
- }
259
- const changed = hit?.id !== this.hoveredHit?.id || hit?.type !== this.hoveredHit?.type;
260
- this.hoveredHit = hit;
261
- if (!emitAlways && !changed)
262
- return;
263
- const detail = hit && point ? { ...hit, point, timeMs: this.timeMs, sceneId: this.frame.sceneId, beatId: this.frame.beatId } : null;
264
- this.emit("hover", detail);
265
- this.emit("objecthover", detail);
266
- }
267
- clearPointer() {
268
- if (this.destroyed)
269
- return;
270
- this.pointerPosition = undefined;
271
- updateSvgDom(this.canvas, this.diagram.renderSvg(this.timeMs, this.options));
272
- this.updateHover(true);
273
- }
274
- emit(type, detail) {
275
- if (!this.destroyed)
276
- this.element.dispatchEvent(new CustomEvent(type, { detail: detail === undefined ? this.state() : detail }));
277
- }
278
- }
279
- /** Apply the shared paint result to current decorative children; no child receives a separate interaction identity. */
280
- function paintHoveredContent(content, layout) {
281
- const tags = content.querySelectorAll("rect");
282
- layout.tags.forEach((tag, index) => tags[index]?.setAttribute("fill", tag.fill));
283
- const texts = content.querySelectorAll("text");
284
- layout.lines.forEach((line, index) => {
285
- const text = texts[index];
286
- text?.setAttribute("fill", line.color);
287
- const spans = text?.querySelectorAll("tspan");
288
- line.spans.forEach((span, spanIndex) => spans?.[spanIndex]?.setAttribute("fill", span.color));
289
- });
290
- const graphs = content.querySelectorAll("[data-content-graph]");
291
- layout.graphs?.forEach((graph, index) => {
292
- const circles = graphs[index]?.querySelectorAll("circle");
293
- const edges = graphs[index]?.querySelectorAll("line");
294
- graph.nodes.forEach((node, nodeIndex) => circles?.[nodeIndex]?.setAttribute("fill", node.fill));
295
- graph.edges.forEach((edge, edgeIndex) => edges?.[edgeIndex]?.setAttribute("stroke", edge.color));
296
- });
297
- }
298
- function clamp(value, min, max) {
299
- return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : min;
300
- }
301
- function collectBeatStarts(document) {
302
- let startMs = 0;
303
- return document.scenes.flatMap((scene) => scene.beats.map((beat) => {
304
- const start = startMs;
305
- startMs += beat.durationMs;
306
- return start;
307
- }));
308
- }
309
- function collectScenes(document) {
310
- let startMs = 0;
311
- return document.scenes.map((scene) => {
312
- const choice = { id: scene.id, label: scene.label, startMs };
313
- startMs += scene.beats.reduce((duration, beat) => duration + beat.durationMs, 0);
314
- return choice;
315
- });
316
- }
317
- function observePointer(canvas, handlers) {
318
- const onMove = (event) => handlers.move(event);
319
- const onLeave = () => handlers.leave();
320
- const onClick = (event) => handlers.click(event);
321
- canvas.addEventListener("pointermove", onMove);
322
- canvas.addEventListener("pointerleave", onLeave);
323
- canvas.addEventListener("pointercancel", onLeave);
324
- canvas.addEventListener("click", onClick);
325
- return () => {
326
- canvas.removeEventListener("pointermove", onMove);
327
- canvas.removeEventListener("pointerleave", onLeave);
328
- canvas.removeEventListener("pointercancel", onLeave);
329
- canvas.removeEventListener("click", onClick);
330
- };
331
- }
332
- /**
333
- * Prefer the inverse SVG screen transform for scaling/letterboxing; otherwise map the element rectangle to the camera.
334
- * The rectangle fallback lacks full SVG transform information and is only an approximation.
335
- */
336
- function pointerToDiagramPoint(canvas, camera, event) {
337
- const svg = canvas.querySelector("svg");
338
- const matrix = svg?.getScreenCTM?.();
339
- if (matrix) {
340
- const inverse = matrix.inverse();
341
- return {
342
- x: event.clientX * inverse.a + event.clientY * inverse.c + inverse.e,
343
- y: event.clientX * inverse.b + event.clientY * inverse.d + inverse.f
344
- };
345
- }
346
- const bounds = svg?.getBoundingClientRect() ?? canvas.getBoundingClientRect();
347
- const width = bounds.width || 1;
348
- const height = bounds.height || 1;
349
- return {
350
- x: camera.x + ((event.clientX - bounds.left) / width) * camera.width,
351
- y: camera.y + ((event.clientY - bounds.top) / height) * camera.height
352
- };
353
- }
1
+ /** Public browser entry: expose the player contract while keeping its implementation under the browser module. */
2
+ export { createPlayer } from "./browser/player.js";
@@ -0,0 +1,3 @@
1
+ /** Pure color interpolation shared by timeline visuals and node-content painting. */
2
+ /** Interpolate validated hexadecimal RGB/RGBA channels; preserve endpoint strings and normalize intermediate colors. */
3
+ export declare function mixColor(source: string, target: string, progress: number): string;
@@ -0,0 +1,18 @@
1
+ /** Pure color interpolation shared by timeline visuals and node-content painting. */
2
+ /** Interpolate validated hexadecimal RGB/RGBA channels; preserve endpoint strings and normalize intermediate colors. */
3
+ export function mixColor(source, target, progress) {
4
+ if (progress === 0)
5
+ return source;
6
+ if (progress === 1)
7
+ return target;
8
+ const channels = (color) => {
9
+ const hex = color.slice(1);
10
+ const expanded = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
11
+ return [0, 2, 4, 6].map((offset) => offset < expanded.length ? parseInt(expanded.slice(offset, offset + 2), 16) : 255);
12
+ };
13
+ const from = channels(source);
14
+ const to = channels(target);
15
+ const values = from.map((value, index) => Math.round(value + (to[index] - value) * progress));
16
+ const hex = values.map((value) => value.toString(16).padStart(2, "0"));
17
+ return `#${hex.slice(0, values[3] === 255 ? 3 : 4).join("")}`;
18
+ }
@@ -0,0 +1,38 @@
1
+ import type { DiagramDocument, EvaluateOptions, HitObject, DiagramPoint, DiagramFrameState } from "./model.js";
2
+ /**
3
+ * Validate and copy a document, then compile its timeline; later input mutations cannot affect evaluation.
4
+ * @throws {DiagramValidationError} If document structure, values or references are invalid.
5
+ */
6
+ export declare function createDiagram(input: DiagramDocument): Diagram;
7
+ /** One-shot static SVG export; use createDiagram to reuse compilation across repeated evaluations or exports. */
8
+ export declare function renderSvg(document: DiagramDocument, timeMs?: number, options?: EvaluateOptions): string;
9
+ /** Compiled diagram independent of the DOM, network and clock; each query evaluates from its time and options. */
10
+ export interface Diagram {
11
+ /** Global timeline duration in milliseconds; zero for static diagrams. */
12
+ readonly totalDuration: number;
13
+ /**
14
+ * Evaluate a frame; finite times clamp to [0, totalDuration], and non-finite times become zero.
15
+ * Beat intervals include their start and exclude their end; totalDuration returns the completed final beat.
16
+ */
17
+ evaluateAt(timeMs: number, options?: EvaluateOptions): DiagramFrameState;
18
+ /** Serialize the same frame as self-contained static SVG with escaped text and resolved colors, without controls. */
19
+ renderSvg(timeMs?: number, options?: EvaluateOptions): string;
20
+ /**
21
+ * Hit-test document coordinates in paint order: ordinary nodes, then edges, then group backgrounds.
22
+ * Objects within each layer are searched in reverse document order.
23
+ * Node hits include their visible label area. Hidden/zero-opacity objects are excluded.
24
+ * Return null for no hit or non-finite coordinates.
25
+ */
26
+ hitTest(timeMs: number, point: DiagramPoint, options?: EvaluateOptions): HitObject | null;
27
+ }
28
+ interface Typography {
29
+ fontSize: number;
30
+ lineHeight: number;
31
+ charWidth: number;
32
+ }
33
+ /**
34
+ * Estimate wrapping with fixed code-point widths, preserving explicit empty lines and preferring spaces as breaks.
35
+ * No font/DOM measurement is used; this handles width only, while frame evaluation reports height overflow.
36
+ */
37
+ export declare function wrapText(label: string, maxWidth: number, typography?: Typography): string[];
38
+ export {};