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.
- package/CHANGELOG.md +26 -1
- package/PATTERNS.md +12 -1
- package/README.md +139 -117
- package/README.zh-CN.md +139 -117
- package/dist/adapters/react.d.ts +48 -0
- package/dist/adapters/react.js +126 -0
- package/dist/adapters/vue.d.ts +132 -0
- package/dist/adapters/vue.js +161 -0
- package/dist/adapters/web-component.d.ts +99 -0
- package/dist/adapters/web-component.js +396 -0
- package/dist/browser/player.d.ts +69 -0
- package/dist/browser/player.js +353 -0
- package/dist/browser.d.ts +4 -69
- package/dist/browser.js +2 -353
- package/dist/core/color.d.ts +3 -0
- package/dist/core/color.js +18 -0
- package/dist/core/diagram.d.ts +38 -0
- package/dist/core/diagram.js +391 -0
- package/dist/core/edge-geometry.d.ts +32 -0
- package/dist/core/edge-geometry.js +382 -0
- package/dist/{core.d.ts → core/model.d.ts} +2 -52
- package/dist/core/model.js +1 -0
- package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
- package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
- package/dist/{node-content.d.ts → core/node-content.d.ts} +1 -1
- package/dist/core/svg-number.d.ts +5 -0
- package/dist/core/svg-number.js +7 -0
- package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
- package/dist/core/theme.js +17 -0
- package/dist/{timeline.d.ts → core/timeline.d.ts} +1 -3
- package/dist/{timeline.js → core/timeline.js} +1 -17
- package/dist/core/validation.d.ts +11 -0
- package/dist/core/validation.js +389 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -1
- package/dist/react.d.ts +3 -48
- package/dist/react.js +2 -126
- package/dist/vue.d.ts +3 -132
- package/dist/vue.js +2 -161
- package/dist/web-component.d.ts +3 -103
- package/dist/web-component.js +2 -396
- package/docs/images/entity-graph.png +0 -0
- package/docs/images/facts-timeline.png +0 -0
- package/docs/images/facts-timeline.zh-CN.png +0 -0
- package/docs/images/node-content.png +0 -0
- package/docs/images/numbered-list.png +0 -0
- package/docs/images/numbered-list.zh-CN.png +0 -0
- package/docs/images/quick-start.png +0 -0
- package/docs/images/record-tags.png +0 -0
- package/package.json +2 -1
- package/dist/core.js +0 -1149
- package/dist/theme.js +0 -6
- /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
- /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
- /package/dist/{player-controls.d.ts → browser/controls.d.ts} +0 -0
- /package/dist/{player-controls.js → browser/controls.js} +0 -0
- /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
- /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
- /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
- /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
- /package/dist/{node-content.js → core/node-content.js} +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
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 "../core/node-content-paint.js";
|
|
6
|
+
import { updateSvgDom } from "./svg-dom.js";
|
|
7
|
+
import { attachPlayerLifecycle } from "./lifecycle.js";
|
|
8
|
+
import { observeKeyboard, observeReducedMotion } from "./accessibility.js";
|
|
9
|
+
import { PlayerControls } from "./controls.js";
|
|
10
|
+
import { resolveThemePalette } from "../core/theme.js";
|
|
11
|
+
import { createDiagram } from "../core/diagram.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 palette = resolveThemePalette(this.options.theme);
|
|
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", palette.highlightFill);
|
|
253
|
+
shape?.setAttribute("stroke", palette.highlight);
|
|
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, palette.highlightFill));
|
|
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
|
+
}
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,69 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
/** Hover highlights borders by default; true also paints the background and adjusts content ink. Applies to groups too. */
|
|
6
|
-
hoverFill?: boolean;
|
|
7
|
-
}
|
|
8
|
-
/** Player snapshot: times are milliseconds, speed is a positive multiplier, and frame is the core result without hover. */
|
|
9
|
-
export interface PlayerState {
|
|
10
|
-
timeMs: number;
|
|
11
|
-
totalDuration: number;
|
|
12
|
-
playing: boolean;
|
|
13
|
-
speed: number;
|
|
14
|
-
frame: DiagramFrameState;
|
|
15
|
-
}
|
|
16
|
-
/** Object interaction detail; point uses document coordinates and time/beat IDs reflect the event-time player state. */
|
|
17
|
-
export interface PlayerInteractionDetail extends HitObject {
|
|
18
|
-
point: DiagramPoint;
|
|
19
|
-
timeMs: number;
|
|
20
|
-
sceneId: string | null;
|
|
21
|
-
beatId: string | null;
|
|
22
|
-
}
|
|
23
|
-
/** Imperative browser player; creation/successful load pause at zero, and controls become inert after destruction. */
|
|
24
|
-
export interface FlowPlayer {
|
|
25
|
-
/** The supplied host, also used as the CustomEvent dispatch target. */
|
|
26
|
-
readonly element: HTMLElement;
|
|
27
|
-
/** Play from the current position; do not start for static diagrams, at the end, or while the page is hidden. */
|
|
28
|
-
play(): void;
|
|
29
|
-
/** Stop time advancement, retaining the current frame. */
|
|
30
|
-
pause(): void;
|
|
31
|
-
/** Seek to a clamped millisecond time; preserve playback except at the end. Non-finite times become zero. */
|
|
32
|
-
seek(timeMs: number): void;
|
|
33
|
-
/** Pause and jump to the adjacent start; backward goes to this beat's start from inside it, or the preceding start. */
|
|
34
|
-
step(direction: 1 | -1): void;
|
|
35
|
-
/** Set a finite positive speed multiplier; ignore invalid values. */
|
|
36
|
-
setSpeed(speed: number): void;
|
|
37
|
-
/** Compile before replacing; reset/pause at zero on success, or emit error and retain the old document/time/playback. */
|
|
38
|
-
load(document: DiagramDocument): void;
|
|
39
|
-
/** Read current player state; the last state remains readable after destruction. */
|
|
40
|
-
getState(): PlayerState;
|
|
41
|
-
/** Silently and idempotently release frames/listeners/host content; no further player events are dispatched. */
|
|
42
|
-
destroy(): void;
|
|
43
|
-
}
|
|
44
|
-
/** Retained state and original load error; DiagramValidationError.issues provides field paths for validation failures. */
|
|
45
|
-
export interface PlayerErrorDetail extends PlayerState {
|
|
46
|
-
error: Error;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Host events and their detail types. Frame updates emit stepchange when needed, then timeupdate.
|
|
50
|
-
* hover/click use null for no hit; objecthover/objectclick report the same interactions under alternate names.
|
|
51
|
-
* Hover also changes when a frame update changes the object under a stationary pointer.
|
|
52
|
-
*/
|
|
53
|
-
export type PlayerEventMap = {
|
|
54
|
-
stepchange: CustomEvent<PlayerState>;
|
|
55
|
-
timeupdate: CustomEvent<PlayerState>;
|
|
56
|
-
playstatechange: CustomEvent<PlayerState>;
|
|
57
|
-
error: CustomEvent<PlayerErrorDetail>;
|
|
58
|
-
hover: CustomEvent<PlayerInteractionDetail | null>;
|
|
59
|
-
click: CustomEvent<PlayerInteractionDetail | null>;
|
|
60
|
-
objecthover: CustomEvent<PlayerInteractionDetail | null>;
|
|
61
|
-
objectclick: CustomEvent<PlayerInteractionDetail | null>;
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Mount a player using the host document and observe browser motion preferences unless explicitly overridden.
|
|
65
|
-
* Replaces host children; destroy an existing player before replacing it on the same host.
|
|
66
|
-
* @throws {DiagramValidationError} For an invalid initial document, before an instance exists to emit error.
|
|
67
|
-
*/
|
|
68
|
-
export declare function createPlayer(host: HTMLElement, document: DiagramDocument, options?: PlayerOptions): FlowPlayer;
|
|
69
|
-
export type { ThemeName };
|
|
1
|
+
/** Public browser entry: expose the player contract while keeping its implementation under the browser module. */
|
|
2
|
+
export { createPlayer } from "./browser/player.js";
|
|
3
|
+
export type { FlowPlayer, PlayerErrorDetail, PlayerEventMap, PlayerInteractionDetail, PlayerOptions, PlayerState } from "./browser/player.js";
|
|
4
|
+
export type { ThemeName } from "./core/model.js";
|