@markdy/renderer-dom 0.8.0 → 0.8.2
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/README.md +7 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +258 -31
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
|
|
|
8
8
|
- **Auto-layout diagrams** — renders positioned nodes and orthogonal, obstacle-aware edges from a compiled `RenderPlan`
|
|
9
9
|
- **Flow edges** — `->` request, `<-` response, `~>` event, `--` dependency, each with its own stroke, plus a pulse that travels the edge as it draws
|
|
10
10
|
- **Beat-driven cues** — `show`, `hide`, `glow`, and `focus`, sequenced by named beats
|
|
11
|
+
- **Semantic node cards** — compact SVG glyphs for browsers, services, gateways, queues, workers, databases, storage, CDN, security, platform, and more
|
|
11
12
|
- **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
|
|
13
|
+
- **Playback-rate controls** — set timeline speed to slow down or speed up diagrams without rebuilding animations
|
|
12
14
|
- **Semantic themes** — `midnight` and `paper`, with per-role node colors
|
|
13
15
|
- **Single dependency** — only `@markdy/core`
|
|
14
16
|
|
|
@@ -68,7 +70,9 @@ player.destroy(); // clean up DOM + cancel animations
|
|
|
68
70
|
| `autoplay` | `boolean` | `true` | Start playing immediately |
|
|
69
71
|
| `loop` | `boolean` | `true` | Loop the animation when it reaches the end |
|
|
70
72
|
| `copyright` | `boolean` | `true` | Show a small "Powered by Markdy" badge below the animation |
|
|
71
|
-
| `progressBar` | `boolean` | `true` |
|
|
73
|
+
| `progressBar` | `boolean` | `true` | Deprecated compatibility flag for the rainbow scene-boundary progress bar |
|
|
74
|
+
| `sceneBoundaryProgress` | `boolean` | `progressBar ?? true` | Preferred flag for the rainbow scene-boundary progress bar |
|
|
75
|
+
| `playbackRate` | `number` | `1` | Timeline speed multiplier |
|
|
72
76
|
| `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
|
|
73
77
|
| `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
|
|
74
78
|
| `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
|
|
@@ -80,6 +84,8 @@ player.destroy(); // clean up DOM + cancel animations
|
|
|
80
84
|
| `play()` | Start or resume playback |
|
|
81
85
|
| `pause()` | Pause at current position |
|
|
82
86
|
| `seek(seconds)` | Jump to a specific time |
|
|
87
|
+
| `setPlaybackRate(rate)` | Change timeline speed; ignores non-positive or non-finite values |
|
|
88
|
+
| `playbackRate()` | Current timeline speed multiplier |
|
|
83
89
|
| `currentTime()` | Current playback position in seconds |
|
|
84
90
|
| `duration()` | Total scene duration in seconds |
|
|
85
91
|
| `isPlaying()` | Whether the scene is currently playing |
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,12 @@ interface PlayerOptions {
|
|
|
10
10
|
autoplay?: boolean;
|
|
11
11
|
loop?: boolean;
|
|
12
12
|
copyright?: boolean;
|
|
13
|
+
/** @deprecated Prefer sceneBoundaryProgress. */
|
|
13
14
|
progressBar?: boolean;
|
|
15
|
+
/** Show rainbow progress around scene boundary. Defaults to true. */
|
|
16
|
+
sceneBoundaryProgress?: boolean;
|
|
17
|
+
/** Playback speed multiplier. Defaults to 1. */
|
|
18
|
+
playbackRate?: number;
|
|
14
19
|
onWarning?: (warning: Diagnostic) => void;
|
|
15
20
|
onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
|
|
16
21
|
onPlayStateChange?: (playing: boolean) => void;
|
|
@@ -19,6 +24,8 @@ interface Player {
|
|
|
19
24
|
play(): void;
|
|
20
25
|
pause(): void;
|
|
21
26
|
seek(seconds: number): void;
|
|
27
|
+
setPlaybackRate(rate: number): void;
|
|
28
|
+
playbackRate(): number;
|
|
22
29
|
currentTime(): number;
|
|
23
30
|
duration(): number;
|
|
24
31
|
isPlaying(): boolean;
|
package/dist/index.js
CHANGED
|
@@ -62,25 +62,57 @@ function polylineLength(points) {
|
|
|
62
62
|
function segmentLength(a, b) {
|
|
63
63
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
var LABEL_BOX_HEIGHT = 16;
|
|
66
|
+
function rectsOverlap(a, b) {
|
|
67
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
68
|
+
}
|
|
69
|
+
function overlapCount(rect, obstacles) {
|
|
70
|
+
let hits = 0;
|
|
71
|
+
for (const o of obstacles) if (rectsOverlap(rect, o)) hits++;
|
|
72
|
+
return hits;
|
|
73
|
+
}
|
|
74
|
+
function placeFlowLabel(points, textWidth, obstacles, bounds) {
|
|
67
75
|
let bestIndex = 0;
|
|
68
76
|
let bestLength = -1;
|
|
69
77
|
for (let i = 0; i < points.length - 1; i++) {
|
|
70
78
|
const len = segmentLength(points[i], points[i + 1]);
|
|
71
79
|
if (len > bestLength) {
|
|
72
|
-
bestIndex = i;
|
|
73
80
|
bestLength = len;
|
|
81
|
+
bestIndex = i;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
|
-
const a = points[bestIndex];
|
|
77
|
-
const b = points[bestIndex + 1];
|
|
84
|
+
const a = points[bestIndex] ?? { x: 0, y: 0 };
|
|
85
|
+
const b = points[bestIndex + 1] ?? a;
|
|
78
86
|
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
|
|
88
|
+
const half = textWidth / 2;
|
|
89
|
+
const halfH = LABEL_BOX_HEIGHT / 2;
|
|
90
|
+
const pad = 8;
|
|
91
|
+
const base = horizontal ? 14 : half + 12;
|
|
92
|
+
const step = horizontal ? LABEL_BOX_HEIGHT + 4 : textWidth + 12;
|
|
93
|
+
const order = horizontal ? [-1, 1] : [1, -1];
|
|
94
|
+
const offsets = [];
|
|
95
|
+
for (let k = 0; k < 8; k++) {
|
|
96
|
+
for (const sign of order) offsets.push(sign * (base + k * step));
|
|
97
|
+
}
|
|
98
|
+
let fallback = null;
|
|
99
|
+
let fallbackHits = Number.POSITIVE_INFINITY;
|
|
100
|
+
for (const off of offsets) {
|
|
101
|
+
const cx = clamp(horizontal ? mid.x : mid.x + off, pad + half, bounds.width - pad - half);
|
|
102
|
+
const cy = clamp(horizontal ? mid.y + off : mid.y, pad + halfH, bounds.height - pad - halfH);
|
|
103
|
+
const rect = { x1: cx - half, y1: cy - halfH, x2: cx + half, y2: cy + halfH };
|
|
104
|
+
const hits = overlapCount(rect, obstacles);
|
|
105
|
+
if (hits === 0) return { x: round1(cx), y: round1(cy), rect };
|
|
106
|
+
if (hits < fallbackHits) {
|
|
107
|
+
fallbackHits = hits;
|
|
108
|
+
fallback = { x: round1(cx), y: round1(cy), rect };
|
|
109
|
+
}
|
|
82
110
|
}
|
|
83
|
-
return
|
|
111
|
+
return fallback ?? {
|
|
112
|
+
x: round1(mid.x),
|
|
113
|
+
y: round1(mid.y),
|
|
114
|
+
rect: { x1: mid.x - half, y1: mid.y - halfH, x2: mid.x + half, y2: mid.y + halfH }
|
|
115
|
+
};
|
|
84
116
|
}
|
|
85
117
|
function clamp(n, min, max) {
|
|
86
118
|
if (max < min) return n;
|
|
@@ -214,11 +246,11 @@ function ensureDefs(svg, theme, id) {
|
|
|
214
246
|
}
|
|
215
247
|
svg.prepend(defs);
|
|
216
248
|
}
|
|
217
|
-
function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId,
|
|
249
|
+
function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane) {
|
|
218
250
|
ensureDefs(svg, theme, sceneId);
|
|
219
251
|
const color = theme.edges[kind];
|
|
220
252
|
const style = EDGE_STYLES[kind];
|
|
221
|
-
const points = dedupePoints(routeOrthogonal(boxRect(from), boxRect(to),
|
|
253
|
+
const points = dedupePoints(routeOrthogonal(boxRect(from), boxRect(to), routeObstacles, bounds, lane));
|
|
222
254
|
const d = toPathD(points);
|
|
223
255
|
const len = polylineLength(points);
|
|
224
256
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
@@ -243,21 +275,29 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, obstacles
|
|
|
243
275
|
dot.style.filter = `drop-shadow(0 0 6px ${color})`;
|
|
244
276
|
group.append(path, dot);
|
|
245
277
|
let labelEl;
|
|
278
|
+
let labelRect;
|
|
246
279
|
if (label) {
|
|
247
|
-
const
|
|
280
|
+
const textWidth = label.length * 6.6 + 10;
|
|
281
|
+
const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
|
|
282
|
+
labelRect = placement.rect;
|
|
248
283
|
labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
249
|
-
labelEl.setAttribute("x", String(
|
|
250
|
-
labelEl.setAttribute("y", String(
|
|
284
|
+
labelEl.setAttribute("x", String(placement.x));
|
|
285
|
+
labelEl.setAttribute("y", String(placement.y));
|
|
251
286
|
labelEl.setAttribute("text-anchor", "middle");
|
|
287
|
+
labelEl.setAttribute("dominant-baseline", "middle");
|
|
252
288
|
labelEl.setAttribute("font-size", "11");
|
|
253
289
|
labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
|
|
254
290
|
labelEl.setAttribute("fill", theme.textMuted);
|
|
291
|
+
labelEl.setAttribute("stroke", theme.canvas);
|
|
292
|
+
labelEl.setAttribute("stroke-width", "3");
|
|
293
|
+
labelEl.setAttribute("paint-order", "stroke");
|
|
294
|
+
labelEl.setAttribute("stroke-linejoin", "round");
|
|
255
295
|
labelEl.textContent = label;
|
|
256
296
|
labelEl.style.opacity = "0";
|
|
257
297
|
group.appendChild(labelEl);
|
|
258
298
|
}
|
|
259
299
|
svg.appendChild(group);
|
|
260
|
-
return { group, path, label: labelEl, dot, pathLen: len, points };
|
|
300
|
+
return { group, path, label: labelEl, dot, pathLen: len, points, labelRect };
|
|
261
301
|
}
|
|
262
302
|
function dotTravelKeyframes(points) {
|
|
263
303
|
const total = polylineLength(points);
|
|
@@ -309,6 +349,9 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds)
|
|
|
309
349
|
const anims = [];
|
|
310
350
|
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
311
351
|
const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
|
|
352
|
+
const allNodeRects = [...rectById.values()];
|
|
353
|
+
const placedLabels = [];
|
|
354
|
+
const laneByPair = /* @__PURE__ */ new Map();
|
|
312
355
|
const svg = ensureEdgeLayer(scene);
|
|
313
356
|
const sceneId = `md-${Math.random().toString(36).slice(2, 8)}`;
|
|
314
357
|
anims.push(
|
|
@@ -376,11 +419,16 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds)
|
|
|
376
419
|
const from = nodeById.get(seg.from);
|
|
377
420
|
const to = nodeById.get(seg.to);
|
|
378
421
|
if (!from || !to) continue;
|
|
379
|
-
const
|
|
422
|
+
const routeObstacles = [];
|
|
380
423
|
for (const [id, rect] of rectById) {
|
|
381
|
-
if (id !== seg.from && id !== seg.to)
|
|
424
|
+
if (id !== seg.from && id !== seg.to) routeObstacles.push(rect);
|
|
382
425
|
}
|
|
383
|
-
const
|
|
426
|
+
const pairKey = [seg.from, seg.to].sort().join("|");
|
|
427
|
+
const lane = laneByPair.get(pairKey) ?? 0;
|
|
428
|
+
laneByPair.set(pairKey, lane + 1);
|
|
429
|
+
const labelObstacles = [...allNodeRects, ...placedLabels];
|
|
430
|
+
const runtime = createEdgeRuntime(svg, from, to, seg.op, seg.label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane);
|
|
431
|
+
if (runtime.labelRect) placedLabels.push(runtime.labelRect);
|
|
384
432
|
anims.push(...animateEdgeReveal(runtime, startMs, durMs));
|
|
385
433
|
}
|
|
386
434
|
}
|
|
@@ -400,7 +448,7 @@ function ensureNodeStyles(doc) {
|
|
|
400
448
|
box-sizing: border-box;
|
|
401
449
|
width: var(--md-node-w, 184px);
|
|
402
450
|
height: var(--md-node-h, 88px);
|
|
403
|
-
border-radius:
|
|
451
|
+
border-radius: 14px;
|
|
404
452
|
border: 1px solid var(--md-border);
|
|
405
453
|
background: linear-gradient(145deg, var(--md-surface-raised), var(--md-surface));
|
|
406
454
|
color: var(--md-text);
|
|
@@ -431,15 +479,41 @@ function ensureNodeStyles(doc) {
|
|
|
431
479
|
border-radius: 16px 0 0 16px;
|
|
432
480
|
}
|
|
433
481
|
.markdy-node__type {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
482
|
+
display: none;
|
|
483
|
+
}
|
|
484
|
+
.markdy-node__body {
|
|
485
|
+
height: 100%;
|
|
486
|
+
padding: 0 14px 0 18px;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
gap: 10px;
|
|
490
|
+
min-width: 0;
|
|
491
|
+
}
|
|
492
|
+
.markdy-node__icon {
|
|
493
|
+
flex: 0 0 auto;
|
|
494
|
+
width: 30px;
|
|
495
|
+
height: 30px;
|
|
496
|
+
border-radius: 10px;
|
|
497
|
+
display: flex;
|
|
498
|
+
align-items: center;
|
|
499
|
+
justify-content: center;
|
|
500
|
+
color: var(--md-role-color, var(--md-accent));
|
|
501
|
+
background: color-mix(in srgb, var(--md-role-color, var(--md-accent)) 16%, transparent);
|
|
502
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 28%, transparent) inset;
|
|
503
|
+
}
|
|
504
|
+
.markdy-node__icon svg {
|
|
505
|
+
width: 18px;
|
|
506
|
+
height: 18px;
|
|
507
|
+
display: block;
|
|
508
|
+
stroke: currentColor;
|
|
509
|
+
}
|
|
510
|
+
.markdy-node[data-icon="decision"] .markdy-node__icon,
|
|
511
|
+
.markdy-node[data-icon="flow"] .markdy-node__icon {
|
|
512
|
+
border-radius: 999px;
|
|
440
513
|
}
|
|
441
514
|
.markdy-node__label {
|
|
442
|
-
|
|
515
|
+
min-width: 0;
|
|
516
|
+
padding: 0;
|
|
443
517
|
font-size: 15px;
|
|
444
518
|
font-weight: 650;
|
|
445
519
|
line-height: 1.25;
|
|
@@ -471,6 +545,139 @@ function ensureNodeStyles(doc) {
|
|
|
471
545
|
`;
|
|
472
546
|
doc.head.appendChild(style);
|
|
473
547
|
}
|
|
548
|
+
var ICONS = {
|
|
549
|
+
compute: [
|
|
550
|
+
["rect", { x: "5", y: "5", width: "14", height: "14", rx: "3" }],
|
|
551
|
+
["path", { d: "M9 9h6v6H9zM9 2.5v2.5M15 2.5v2.5M9 19v2.5M15 19v2.5M2.5 9h2.5M2.5 15h2.5M19 9h2.5M19 15h2.5" }]
|
|
552
|
+
],
|
|
553
|
+
user: [
|
|
554
|
+
["circle", { cx: "12", cy: "8", r: "3.4" }],
|
|
555
|
+
["path", { d: "M5.5 20a6.5 6.5 0 0 1 13 0" }]
|
|
556
|
+
],
|
|
557
|
+
browser: [
|
|
558
|
+
["rect", { x: "3", y: "5", width: "18", height: "14", rx: "2.5" }],
|
|
559
|
+
["path", { d: "M3 9h18" }],
|
|
560
|
+
["path", { d: "M7 7h.01M10 7h.01" }]
|
|
561
|
+
],
|
|
562
|
+
service: [
|
|
563
|
+
["path", { d: "M12 3 4.5 7.2 12 11.4l7.5-4.2L12 3Z" }],
|
|
564
|
+
["path", { d: "M4.5 12 12 16.2 19.5 12" }],
|
|
565
|
+
["path", { d: "M4.5 16.8 12 21l7.5-4.2" }]
|
|
566
|
+
],
|
|
567
|
+
gateway: [
|
|
568
|
+
["circle", { cx: "5", cy: "12", r: "2" }],
|
|
569
|
+
["circle", { cx: "19", cy: "6", r: "2" }],
|
|
570
|
+
["circle", { cx: "19", cy: "18", r: "2" }],
|
|
571
|
+
["path", { d: "M7 12h4l5.5-5" }],
|
|
572
|
+
["path", { d: "M11 12l5.5 5" }]
|
|
573
|
+
],
|
|
574
|
+
queue: [
|
|
575
|
+
["rect", { x: "5", y: "5", width: "14", height: "4", rx: "1.4" }],
|
|
576
|
+
["rect", { x: "5", y: "10", width: "14", height: "4", rx: "1.4" }],
|
|
577
|
+
["rect", { x: "5", y: "15", width: "14", height: "4", rx: "1.4" }]
|
|
578
|
+
],
|
|
579
|
+
worker: [
|
|
580
|
+
["circle", { cx: "12", cy: "12", r: "3.2" }],
|
|
581
|
+
["path", { d: "M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" }]
|
|
582
|
+
],
|
|
583
|
+
database: [
|
|
584
|
+
["ellipse", { cx: "12", cy: "5.5", rx: "7", ry: "3" }],
|
|
585
|
+
["path", { d: "M5 5.5v10c0 1.7 3.1 3 7 3s7-1.3 7-3v-10" }],
|
|
586
|
+
["path", { d: "M5 10.5c0 1.7 3.1 3 7 3s7-1.3 7-3" }]
|
|
587
|
+
],
|
|
588
|
+
storage: [
|
|
589
|
+
["path", { d: "M4 8.5 12 4l8 4.5v7L12 20l-8-4.5v-7Z" }],
|
|
590
|
+
["path", { d: "M4 8.5 12 13l8-4.5" }],
|
|
591
|
+
["path", { d: "M12 13v7" }]
|
|
592
|
+
],
|
|
593
|
+
cdn: [
|
|
594
|
+
["circle", { cx: "12", cy: "12", r: "8" }],
|
|
595
|
+
["path", { d: "M4 12h16M12 4c2.2 2.1 3.3 4.8 3.3 8S14.2 17.9 12 20M12 4c-2.2 2.1-3.3 4.8-3.3 8S9.8 17.9 12 20" }]
|
|
596
|
+
],
|
|
597
|
+
cache: [
|
|
598
|
+
["path", { d: "M13 2 5 13h6l-1 9 9-13h-6l0-7Z" }]
|
|
599
|
+
],
|
|
600
|
+
code: [
|
|
601
|
+
["path", { d: "m9 18-6-6 6-6" }],
|
|
602
|
+
["path", { d: "m15 6 6 6-6 6" }],
|
|
603
|
+
["path", { d: "m14 4-4 16" }]
|
|
604
|
+
],
|
|
605
|
+
messaging: [
|
|
606
|
+
["path", { d: "M4 6h16v10H8l-4 4V6Z" }],
|
|
607
|
+
["path", { d: "M8 10h8M8 13h5" }]
|
|
608
|
+
],
|
|
609
|
+
network: [
|
|
610
|
+
["circle", { cx: "6", cy: "7", r: "2" }],
|
|
611
|
+
["circle", { cx: "18", cy: "7", r: "2" }],
|
|
612
|
+
["circle", { cx: "12", cy: "18", r: "2" }],
|
|
613
|
+
["path", { d: "M8 8.5 11 16M16 8.5 13 16M8 7h8" }]
|
|
614
|
+
],
|
|
615
|
+
platform: [
|
|
616
|
+
["rect", { x: "4", y: "5", width: "16", height: "14", rx: "2.5" }],
|
|
617
|
+
["path", { d: "M8 9h8M8 13h8M8 17h4" }]
|
|
618
|
+
],
|
|
619
|
+
security: [
|
|
620
|
+
["path", { d: "M12 3 5 6v5c0 4.5 3 7.7 7 10 4-2.3 7-5.5 7-10V6l-7-3Z" }],
|
|
621
|
+
["path", { d: "M9.5 12.5 11.2 14 15 10" }]
|
|
622
|
+
],
|
|
623
|
+
delivery: [
|
|
624
|
+
["path", { d: "M4 7h10" }],
|
|
625
|
+
["path", { d: "M4 12h16" }],
|
|
626
|
+
["path", { d: "M4 17h10" }],
|
|
627
|
+
["path", { d: "m16 7 4 5-4 5" }]
|
|
628
|
+
],
|
|
629
|
+
observability: [
|
|
630
|
+
["path", { d: "M4 14s2.5-5 8-5 8 5 8 5-2.5 5-8 5-8-5-8-5Z" }],
|
|
631
|
+
["circle", { cx: "12", cy: "14", r: "2.5" }]
|
|
632
|
+
],
|
|
633
|
+
distributed: [
|
|
634
|
+
["circle", { cx: "6", cy: "12", r: "2.5" }],
|
|
635
|
+
["circle", { cx: "18", cy: "6", r: "2.5" }],
|
|
636
|
+
["circle", { cx: "18", cy: "18", r: "2.5" }],
|
|
637
|
+
["path", { d: "M8.4 11 15.6 7M8.4 13 15.6 17" }]
|
|
638
|
+
],
|
|
639
|
+
flow: [
|
|
640
|
+
["path", { d: "M5 12h14" }],
|
|
641
|
+
["path", { d: "m13 6 6 6-6 6" }]
|
|
642
|
+
],
|
|
643
|
+
decision: [
|
|
644
|
+
["path", { d: "M12 3 21 12 12 21 3 12 12 3Z" }],
|
|
645
|
+
["path", { d: "M12 8v4" }],
|
|
646
|
+
["path", { d: "M12 16h.01" }]
|
|
647
|
+
]
|
|
648
|
+
};
|
|
649
|
+
function iconKeyForNode(node) {
|
|
650
|
+
if (node.kind === "api_gateway" || node.kind === "gateway" || node.kind === "load_balancer" || node.kind === "ingress") return "gateway";
|
|
651
|
+
if (node.kind === "db" || node.kind === "database" || node.kind === "sql" || node.kind === "nosql" || node.kind === "warehouse") return "database";
|
|
652
|
+
if (node.kind === "bucket" || node.kind === "object_store" || node.kind === "blob" || node.kind === "volume" || node.kind === "disk") return "storage";
|
|
653
|
+
if (node.kind === "cdn" || node.kind === "dns" || node.kind === "internet") return "cdn";
|
|
654
|
+
if (node.kind === "queue" || node.kind === "topic" || node.kind === "stream" || node.kind === "event_bus" || node.kind === "broker") return "queue";
|
|
655
|
+
if (node.kind === "worker" || node.kind === "job" || node.kind === "scheduler" || node.kind === "cron" || node.kind === "batch") return "worker";
|
|
656
|
+
if (node.kind === "browser" || node.kind === "web" || node.kind === "frontend" || node.kind === "app") return "browser";
|
|
657
|
+
if (node.kind === "user" || node.kind === "client") return "user";
|
|
658
|
+
if (node.kind === "decision" || node.kind === "condition") return "decision";
|
|
659
|
+
if (node.kind === "cache") return "cache";
|
|
660
|
+
return ICONS[node.kind] ? node.kind : node.role;
|
|
661
|
+
}
|
|
662
|
+
function createIconEl(doc, node) {
|
|
663
|
+
const wrap = doc.createElement("div");
|
|
664
|
+
wrap.className = "markdy-node__icon";
|
|
665
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
666
|
+
const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
667
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
668
|
+
svg.setAttribute("fill", "none");
|
|
669
|
+
svg.setAttribute("stroke-width", "2");
|
|
670
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
671
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
672
|
+
const spec = ICONS[iconKeyForNode(node)] ?? ICONS.service;
|
|
673
|
+
for (const [tag, attrs] of spec) {
|
|
674
|
+
const child = doc.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
675
|
+
for (const [name, value] of Object.entries(attrs)) child.setAttribute(name, value);
|
|
676
|
+
svg.appendChild(child);
|
|
677
|
+
}
|
|
678
|
+
wrap.appendChild(svg);
|
|
679
|
+
return wrap;
|
|
680
|
+
}
|
|
474
681
|
function createNodeEl(node, theme) {
|
|
475
682
|
const el = document.createElement("div");
|
|
476
683
|
el.className = "markdy-node markdy-scene-actor";
|
|
@@ -482,15 +689,24 @@ function createNodeEl(node, theme) {
|
|
|
482
689
|
el.style.setProperty("--md-node-h", `${node.height}px`);
|
|
483
690
|
const roleColor = theme.roles[node.role] ?? theme.accent;
|
|
484
691
|
el.style.setProperty("--md-role-color", roleColor);
|
|
692
|
+
const typeText = node.kind.replace(/_/g, " ");
|
|
693
|
+
el.dataset.kind = node.kind;
|
|
694
|
+
el.dataset.icon = iconKeyForNode(node);
|
|
695
|
+
el.title = `${node.label} (${typeText})`;
|
|
696
|
+
el.setAttribute("aria-label", el.title);
|
|
485
697
|
const rail = document.createElement("div");
|
|
486
698
|
rail.className = "markdy-node__rail";
|
|
487
699
|
const type = document.createElement("div");
|
|
488
700
|
type.className = "markdy-node__type";
|
|
489
|
-
type.textContent =
|
|
701
|
+
type.textContent = typeText;
|
|
702
|
+
const body = document.createElement("div");
|
|
703
|
+
body.className = "markdy-node__body";
|
|
704
|
+
const icon = createIconEl(document, node);
|
|
490
705
|
const label = document.createElement("div");
|
|
491
706
|
label.className = "markdy-node__label";
|
|
492
707
|
label.textContent = node.label;
|
|
493
|
-
|
|
708
|
+
body.append(icon, label);
|
|
709
|
+
el.append(rail, type, body);
|
|
494
710
|
return el;
|
|
495
711
|
}
|
|
496
712
|
function createTitleEl(title) {
|
|
@@ -567,11 +783,14 @@ function createPlayer(opts) {
|
|
|
567
783
|
autoplay = true,
|
|
568
784
|
loop = true,
|
|
569
785
|
copyright = true,
|
|
570
|
-
progressBar
|
|
786
|
+
progressBar,
|
|
787
|
+
sceneBoundaryProgress,
|
|
788
|
+
playbackRate: initialPlaybackRate = 1,
|
|
571
789
|
onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
|
|
572
790
|
onTimeUpdate,
|
|
573
791
|
onPlayStateChange
|
|
574
792
|
} = opts;
|
|
793
|
+
const showSceneBoundaryProgress = sceneBoundaryProgress ?? progressBar ?? true;
|
|
575
794
|
const { ast, plan } = parseAndCompile(code);
|
|
576
795
|
for (const w of ast.diagnostics) {
|
|
577
796
|
if (w.severity === "warning") onWarning(w);
|
|
@@ -587,7 +806,7 @@ function createPlayer(opts) {
|
|
|
587
806
|
});
|
|
588
807
|
container.appendChild(viewport);
|
|
589
808
|
let progressEl = null;
|
|
590
|
-
if (
|
|
809
|
+
if (showSceneBoundaryProgress) {
|
|
591
810
|
progressEl = document.createElement("div");
|
|
592
811
|
Object.assign(progressEl.style, {
|
|
593
812
|
position: "absolute",
|
|
@@ -678,6 +897,7 @@ function createPlayer(opts) {
|
|
|
678
897
|
anim.currentTime = 0;
|
|
679
898
|
}
|
|
680
899
|
let sceneMs = 0;
|
|
900
|
+
let playbackRate = Number.isFinite(initialPlaybackRate) && initialPlaybackRate > 0 ? initialPlaybackRate : 1;
|
|
681
901
|
let lastRafTs = null;
|
|
682
902
|
let isPlaying = false;
|
|
683
903
|
let rafId = null;
|
|
@@ -686,7 +906,7 @@ function createPlayer(opts) {
|
|
|
686
906
|
onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
|
|
687
907
|
}
|
|
688
908
|
function rafTick(timestamp) {
|
|
689
|
-
if (lastRafTs !== null) sceneMs += timestamp - lastRafTs;
|
|
909
|
+
if (lastRafTs !== null) sceneMs += (timestamp - lastRafTs) * playbackRate;
|
|
690
910
|
lastRafTs = timestamp;
|
|
691
911
|
if (totalDurationMs > 0 && sceneMs >= totalDurationMs) {
|
|
692
912
|
if (loop) sceneMs = sceneMs % totalDurationMs;
|
|
@@ -725,6 +945,13 @@ function createPlayer(opts) {
|
|
|
725
945
|
applyCurrentTime();
|
|
726
946
|
if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
|
|
727
947
|
},
|
|
948
|
+
setPlaybackRate(rate) {
|
|
949
|
+
if (!Number.isFinite(rate) || rate <= 0) return;
|
|
950
|
+
playbackRate = rate;
|
|
951
|
+
},
|
|
952
|
+
playbackRate() {
|
|
953
|
+
return playbackRate;
|
|
954
|
+
},
|
|
728
955
|
currentTime() {
|
|
729
956
|
return sceneMs / 1e3;
|
|
730
957
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "Browser renderer for animated MarkdyScript architecture diagrams, built on the Web Animations API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"markdy",
|
|
23
|
-
"
|
|
23
|
+
"animated-diagram",
|
|
24
|
+
"architecture-diagram",
|
|
25
|
+
"diagram-as-code",
|
|
24
26
|
"web-animations-api",
|
|
25
27
|
"renderer",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"motion"
|
|
29
|
-
"framer-motion-alternative",
|
|
30
|
-
"gsap-alternative"
|
|
28
|
+
"mermaid-alternative",
|
|
29
|
+
"svg",
|
|
30
|
+
"motion"
|
|
31
31
|
],
|
|
32
32
|
"author": "Hoang Yell <hoangyell@gmail.com> (https://hoangyell.com)",
|
|
33
33
|
"homepage": "https://markdy.com",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@markdy/core": "0.8.
|
|
46
|
+
"@markdy/core": "0.8.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"jsdom": "^29.1.1",
|
|
50
50
|
"tsup": "^8.5.1",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
52
|
"vitest": "^4.1.7",
|
|
53
|
-
"@markdy/stdlib-systems": "0.8.
|
|
53
|
+
"@markdy/stdlib-systems": "0.8.2"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsup",
|