@markdy/renderer-dom 0.7.29 → 0.8.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/dist/index.js CHANGED
@@ -1,1698 +1,12 @@
1
1
  // src/player.ts
2
- import { parse } from "@markdy/core";
3
-
4
- // src/types.ts
5
- function stateFrom(def) {
6
- return {
7
- x: def.x,
8
- y: def.y,
9
- scale: def.scale ?? 1,
10
- rotate: def.rotate ?? 0,
11
- opacity: def.opacity ?? 1
12
- };
13
- }
14
- function tx(s) {
15
- return `translate(${s.x}px, ${s.y}px) scale(${s.scale}) rotate(${s.rotate}deg)`;
16
- }
17
- function txCaption(s) {
18
- return `translate(calc(${s.x}px - 50%), calc(${s.y}px - 50%)) scale(${s.scale}) rotate(${s.rotate}deg)`;
19
- }
20
- var EASE_MAP = {
21
- linear: "linear",
22
- in: "ease-in",
23
- out: "ease-out",
24
- inout: "ease-in-out",
25
- // Named cubic-bezier presets for authors who want a more polished feel
26
- // than the four raw CSS keywords above without hand-writing a curve.
27
- smooth: "cubic-bezier(0.4, 0, 0.2, 1)",
28
- snappy: "cubic-bezier(0.16, 1, 0.3, 1)",
29
- overshoot: "cubic-bezier(0.34, 1.56, 0.64, 1)",
30
- sharp: "cubic-bezier(0.4, 0, 1, 1)"
31
- };
32
- var CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*\)$/;
33
- function toEasing(val) {
34
- const str = String(val ?? "");
35
- if (EASE_MAP[str]) return EASE_MAP[str];
36
- if (CUBIC_BEZIER_RE.test(str)) return str;
37
- return "linear";
38
- }
39
-
40
- // src/actors.ts
41
- import { TECHNICAL_NODE_KINDS, VISUAL_PRIMITIVE_TYPES } from "@markdy/core";
42
-
43
- // src/figure.ts
44
- function createFigureEl(def) {
45
- const skinColor = def.args[0] || "#ffdbac";
46
- const isFemale = def.args[1] === "f";
47
- const startFace = def.args[2] || (isFemale ? "\u{1F642}" : "\u{1F636}");
48
- const ink = "currentColor";
49
- const WRAP_W = 80;
50
- const FACE_FS = 40;
51
- const SHIRT_FS = isFemale ? 48 : 44;
52
- const vShirtW = SHIRT_FS * 0.9;
53
- const shLx = (WRAP_W - vShirtW) / 2 + vShirtW * 0.18;
54
- const shRx = WRAP_W - shLx;
55
- const shY = Math.round(SHIRT_FS * 0.28);
56
- const ARM_W = 36, ARM_H = 22;
57
- const LEG_H = 54, LEG_STICK_H = 34;
58
- const JOINT_SIZE = 6;
59
- const wrap = document.createElement("div");
60
- Object.assign(wrap.style, {
61
- position: "relative",
62
- display: "flex",
63
- flexDirection: "column",
64
- alignItems: "center",
65
- width: `${WRAP_W}px`,
66
- overflow: "visible"
67
- });
68
- const faceEl = document.createElement("span");
69
- faceEl.dataset.figFace = "";
70
- faceEl.dataset.figHead = "";
71
- faceEl.textContent = startFace;
72
- Object.assign(faceEl.style, {
73
- fontSize: `${FACE_FS}px`,
74
- lineHeight: "1",
75
- flexShrink: "0",
76
- userSelect: "none",
77
- pointerEvents: "none",
78
- zIndex: "5"
79
- });
80
- const neck = document.createElement("div");
81
- Object.assign(neck.style, {
82
- width: "10px",
83
- height: "10px",
84
- background: skinColor,
85
- borderRadius: "4px",
86
- flexShrink: "0",
87
- marginTop: "-3px",
88
- marginBottom: "-3px",
89
- zIndex: "4"
90
- });
91
- const shirtRow = document.createElement("div");
92
- Object.assign(shirtRow.style, {
93
- position: "relative",
94
- width: `${WRAP_W}px`,
95
- height: `${SHIRT_FS}px`,
96
- textAlign: "center",
97
- flexShrink: "0",
98
- zIndex: "2",
99
- overflow: "visible"
100
- });
101
- const torso = document.createElement("span");
102
- torso.dataset.figBody = "";
103
- torso.textContent = isFemale ? "\u{1F457}" : "\u{1F455}";
104
- Object.assign(torso.style, {
105
- fontSize: `${SHIRT_FS}px`,
106
- lineHeight: "1",
107
- userSelect: "none",
108
- pointerEvents: "none"
109
- });
110
- shirtRow.appendChild(torso);
111
- const shoulderL = buildJoint("left", skinColor, JOINT_SIZE, shLx, shY, WRAP_W);
112
- const shoulderR = buildJoint("right", skinColor, JOINT_SIZE, shRx, shY, WRAP_W);
113
- shoulderL.dataset.figShoulderL = "";
114
- shoulderR.dataset.figShoulderR = "";
115
- shirtRow.appendChild(shoulderL);
116
- shirtRow.appendChild(shoulderR);
117
- const armHandEmoji = isFemale ? "\u{1F485}" : "\u{1F91C}";
118
- shirtRow.appendChild(
119
- buildArm("left", armHandEmoji, skinColor, {
120
- w: ARM_W,
121
- h: ARM_H,
122
- anchorX: WRAP_W - shLx,
123
- anchorY: shY,
124
- restDeg: 20,
125
- flipFist: !isFemale
126
- })
127
- );
128
- shirtRow.appendChild(
129
- buildArm("right", armHandEmoji, skinColor, {
130
- w: ARM_W,
131
- h: ARM_H,
132
- anchorX: shRx,
133
- anchorY: shY,
134
- restDeg: -20,
135
- flipFist: false
136
- })
137
- );
138
- const hipRow = document.createElement("div");
139
- Object.assign(hipRow.style, {
140
- position: "relative",
141
- display: "flex",
142
- flexDirection: "column",
143
- alignItems: "center",
144
- flexShrink: "0",
145
- overflow: "visible"
146
- });
147
- const hipBridge = document.createElement("div");
148
- Object.assign(hipBridge.style, {
149
- width: "28px",
150
- height: "4px",
151
- background: skinColor,
152
- borderRadius: "2px",
153
- marginBottom: "1px",
154
- zIndex: "3"
155
- });
156
- hipRow.appendChild(hipBridge);
157
- const legsRow = document.createElement("div");
158
- Object.assign(legsRow.style, {
159
- display: "flex",
160
- justifyContent: "center",
161
- gap: "10px",
162
- flexShrink: "0",
163
- overflow: "visible",
164
- position: "relative"
165
- });
166
- const legL = buildLeg(true, isFemale, ink, skinColor, LEG_H, LEG_STICK_H, JOINT_SIZE);
167
- const legR = buildLeg(false, isFemale, ink, skinColor, LEG_H, LEG_STICK_H, JOINT_SIZE);
168
- legsRow.append(legL, legR);
169
- hipRow.appendChild(legsRow);
170
- wrap.append(faceEl, neck, shirtRow, hipRow);
171
- return wrap;
172
- }
173
- function buildJoint(side, skinColor, size, anchorX, anchorY, wrapW) {
174
- const joint = document.createElement("div");
175
- const isLeft = side === "left";
176
- Object.assign(joint.style, {
177
- position: "absolute",
178
- width: `${size}px`,
179
- height: `${size}px`,
180
- borderRadius: "50%",
181
- background: skinColor,
182
- ...isLeft ? { right: `${wrapW - anchorX - size / 2}px` } : { left: `${anchorX - size / 2}px` },
183
- top: `${anchorY - size / 2}px`,
184
- zIndex: "5",
185
- pointerEvents: "none"
186
- });
187
- return joint;
188
- }
189
- function buildArm(side, handEmoji, skinColor, g) {
190
- const arm = document.createElement("div");
191
- const ds = arm.dataset;
192
- ds[side === "left" ? "figArmL" : "figArmR"] = "";
193
- const isLeft = side === "left";
194
- const origin = isLeft ? "right center" : "left center";
195
- Object.assign(arm.style, {
196
- position: "absolute",
197
- width: `${g.w}px`,
198
- height: `${g.h}px`,
199
- ...isLeft ? { right: `${g.anchorX}px` } : { left: `${g.anchorX}px` },
200
- top: `${g.anchorY - g.h / 2}px`,
201
- transformOrigin: origin,
202
- transform: `rotate(${g.restDeg}deg)`,
203
- zIndex: "4",
204
- overflow: "visible"
205
- });
206
- const stick = document.createElement("div");
207
- Object.assign(stick.style, {
208
- position: "absolute",
209
- ...isLeft ? { right: "5px" } : { left: "5px" },
210
- top: `${g.h / 2 - 2}px`,
211
- width: "18px",
212
- height: "4px",
213
- background: skinColor,
214
- borderRadius: "2px"
215
- });
216
- const fist = document.createElement("span");
217
- fist.textContent = handEmoji;
218
- Object.assign(fist.style, {
219
- position: "absolute",
220
- fontSize: "17px",
221
- lineHeight: "1",
222
- ...isLeft ? { left: "0" } : { right: "0" },
223
- top: `${g.h / 2 - 10}px`,
224
- ...g.flipFist ? { transform: "scaleX(-1)" } : {},
225
- userSelect: "none",
226
- pointerEvents: "none"
227
- });
228
- arm.append(stick, fist);
229
- return arm;
230
- }
231
- function buildLeg(isLeft, isFemale, ink, skinColor, legH, stickH, jointSize) {
232
- const leg = document.createElement("div");
233
- const ds = leg.dataset;
234
- ds[isLeft ? "figLegL" : "figLegR"] = "";
235
- Object.assign(leg.style, {
236
- position: "relative",
237
- width: "20px",
238
- height: `${legH}px`,
239
- transformOrigin: "top center",
240
- transform: "rotate(0deg)",
241
- overflow: "visible"
242
- });
243
- const hipJoint = document.createElement("div");
244
- hipJoint.dataset[isLeft ? "figHipL" : "figHipR"] = "";
245
- Object.assign(hipJoint.style, {
246
- position: "absolute",
247
- width: `${jointSize}px`,
248
- height: `${jointSize}px`,
249
- borderRadius: "50%",
250
- background: skinColor,
251
- left: "50%",
252
- top: `-${jointSize / 2}px`,
253
- transform: "translateX(-50%)",
254
- zIndex: "3",
255
- pointerEvents: "none"
256
- });
257
- const stick = document.createElement("div");
258
- Object.assign(stick.style, {
259
- position: "absolute",
260
- width: "4px",
261
- height: `${stickH}px`,
262
- background: ink,
263
- borderRadius: "2px",
264
- left: "50%",
265
- top: "0",
266
- transform: "translateX(-50%)"
267
- });
268
- const knee = document.createElement("div");
269
- Object.assign(knee.style, {
270
- position: "absolute",
271
- width: "4px",
272
- height: "4px",
273
- borderRadius: "50%",
274
- background: skinColor,
275
- left: "50%",
276
- top: `${stickH * 0.55}px`,
277
- transform: "translateX(-50%)",
278
- zIndex: "2",
279
- pointerEvents: "none"
280
- });
281
- const shoe = document.createElement("span");
282
- shoe.textContent = isFemale ? "\u{1F460}" : "\u{1F45F}";
283
- Object.assign(shoe.style, {
284
- position: "absolute",
285
- fontSize: "17px",
286
- lineHeight: "1",
287
- bottom: "0",
288
- userSelect: "none",
289
- pointerEvents: "none"
290
- });
291
- if (isLeft) {
292
- shoe.style.left = "0";
293
- shoe.style.transform = "scaleX(-1)";
294
- } else {
295
- shoe.style.right = "0";
296
- }
297
- leg.append(hipJoint, stick, knee, shoe);
298
- return leg;
299
- }
300
- var PART_SEL = {
301
- head: "[data-fig-head]",
302
- face: "[data-fig-face]",
303
- body: "[data-fig-body]",
304
- arm_left: "[data-fig-arm-l]",
305
- arm_right: "[data-fig-arm-r]",
306
- leg_left: "[data-fig-leg-l]",
307
- leg_right: "[data-fig-leg-r]"
308
- };
309
- function readRotation(el) {
310
- const m = /rotate\((-?[\d.]+)deg\)/.exec(el.style.transform ?? "");
311
- return m ? Number(m[1]) : 0;
312
- }
313
-
314
- // src/actors.ts
315
- var ARCHITECTURE_NODE_TYPES = new Set(Object.keys(TECHNICAL_NODE_KINDS));
316
- var LEGACY_VISUAL_PRIMITIVE_TYPES = ["parking_map", "ascii_map", "game_scene", "byte_viz"];
317
- var VISUAL_PRIMITIVE_TYPE_SET = /* @__PURE__ */ new Set([
318
- ...VISUAL_PRIMITIVE_TYPES,
319
- ...LEGACY_VISUAL_PRIMITIVE_TYPES
320
- ]);
321
- var ARCHITECTURE_STYLE_ID = "markdy-architecture-node-styles";
322
- var VISUAL_PRIMITIVE_STYLE_ID = "markdy-visual-primitive-styles";
323
- function ensureVisualPrimitiveStyles(doc) {
324
- if (doc.getElementById(VISUAL_PRIMITIVE_STYLE_ID)) return;
325
- const style = doc.createElement("style");
326
- style.id = VISUAL_PRIMITIVE_STYLE_ID;
327
- style.textContent = `
328
- .markdy-visual {
329
- --surface-a: rgba(15, 23, 42, 0.9);
330
- --accent: #38bdf8;
331
- --accent-2: #22c55e;
332
- position: relative;
333
- box-sizing: border-box;
334
- width: 390px;
335
- height: 250px;
336
- overflow: hidden;
337
- border: 1px solid rgba(148, 163, 184, 0.34);
338
- border-radius: 24px;
339
- color: #e5f3ff;
340
- background:
341
- radial-gradient(circle at 12% 0%, color-mix(in srgb, var(--accent) 32%, transparent), transparent 34%),
342
- radial-gradient(circle at 90% 15%, color-mix(in srgb, var(--accent-2) 26%, transparent), transparent 34%),
343
- linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.025) 32%, var(--surface-a));
344
- box-shadow:
345
- 0 26px 70px rgba(2, 6, 23, 0.42),
346
- 0 0 0 1px rgba(255, 255, 255, 0.06) inset,
347
- 0 1px 0 rgba(255, 255, 255, 0.18) inset,
348
- 0 0 44px color-mix(in srgb, var(--accent) 24%, transparent);
349
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
350
- line-height: 1.2;
351
- isolation: isolate;
352
- contain: layout paint style;
353
- backdrop-filter: blur(16px) saturate(1.2);
354
- -webkit-backdrop-filter: blur(16px) saturate(1.2);
355
- }
356
- .markdy-visual,
357
- .markdy-visual * {
358
- box-sizing: border-box;
359
- }
360
- .markdy-visual::before {
361
- content: "";
362
- position: absolute;
363
- inset: 0;
364
- z-index: -1;
365
- background:
366
- linear-gradient(90deg, transparent 0 48%, rgba(255, 255, 255, 0.09) 50%, transparent 52%) 0 0 / 34px 34px,
367
- linear-gradient(0deg, transparent 0 48%, rgba(255, 255, 255, 0.055) 50%, transparent 52%) 0 0 / 34px 34px;
368
- mask-image: linear-gradient(to bottom, rgba(0,0,0,0.85), transparent 82%);
369
- opacity: 0.48;
370
- animation: markdyGridDrift 7s linear infinite;
371
- }
372
- .markdy-visual::after {
373
- content: "";
374
- position: absolute;
375
- inset: -40% -20%;
376
- z-index: -1;
377
- background: conic-gradient(from 90deg, transparent, color-mix(in srgb, var(--accent) 20%, transparent), transparent 32%);
378
- opacity: 0.6;
379
- animation: markdyAurora 9s linear infinite;
380
- }
381
- .markdy-visual[data-tone="green"] { --accent: #22c55e; --accent-2: #38bdf8; }
382
- .markdy-visual[data-tone="amber"] { --accent: #f59e0b; --accent-2: #22c55e; }
383
- .markdy-visual[data-tone="purple"] { --accent: #a78bfa; --accent-2: #38bdf8; }
384
- .markdy-visual[data-tone="rose"] { --accent: #fb7185; --accent-2: #f59e0b; }
385
- .markdy-visual__top {
386
- display: flex;
387
- align-items: center;
388
- justify-content: space-between;
389
- gap: 12px;
390
- padding: 14px 16px 8px;
391
- }
392
- .markdy-visual__title {
393
- min-width: 0;
394
- overflow: hidden;
395
- text-overflow: ellipsis;
396
- white-space: nowrap;
397
- font-size: 15px;
398
- font-weight: 820;
399
- letter-spacing: 0.01em;
400
- color: #f8fafc;
401
- }
402
- .markdy-visual__pill {
403
- flex: 0 0 auto;
404
- border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
405
- border-radius: 999px;
406
- padding: 4px 8px;
407
- color: #bae6fd;
408
- background: rgba(8, 47, 73, 0.54);
409
- font-size: 10px;
410
- font-weight: 800;
411
- letter-spacing: 0.08em;
412
- text-transform: uppercase;
413
- box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 22%, transparent);
414
- }
415
- .markdy-visual__body { position: absolute; inset: 48px 14px 14px; }
416
- .markdy-visual--metric {
417
- width: 112px;
418
- height: 44px;
419
- border-radius: 14px;
420
- }
421
- .markdy-visual--grid {
422
- width: 190px;
423
- height: 92px;
424
- border-radius: 18px;
425
- }
426
- .markdy-visual--lane {
427
- width: 300px;
428
- height: 44px;
429
- border-radius: 999px;
430
- }
431
- .markdy-visual--marker {
432
- width: 50px;
433
- height: 24px;
434
- border-radius: 999px;
435
- }
436
- .markdy-visual--token_strip {
437
- width: 300px;
438
- height: 54px;
439
- border-radius: 16px;
440
- }
441
- .markdy-visual--glyph_card {
442
- width: 98px;
443
- height: 120px;
444
- border-radius: 22px;
445
- }
446
- .visual-panel__scan {
447
- position: absolute;
448
- left: 18px;
449
- top: 18px;
450
- width: 92px;
451
- height: 122px;
452
- border-radius: 24px;
453
- border: 1px solid color-mix(in srgb, var(--accent) 54%, transparent);
454
- background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 22%, transparent), transparent);
455
- box-shadow: 0 0 24px color-mix(in srgb, var(--accent) 28%, transparent);
456
- animation: markdyScanPulse 1.8s ease-in-out infinite;
457
- }
458
- .visual-panel__rails {
459
- position: absolute;
460
- left: 18px;
461
- right: 18px;
462
- bottom: 42px;
463
- height: 48px;
464
- border-radius: 999px;
465
- background:
466
- repeating-linear-gradient(90deg, rgba(226, 232, 240, 0.74) 0 18px, transparent 18px 34px) center / 100% 3px no-repeat,
467
- linear-gradient(90deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.88));
468
- box-shadow: 0 10px 28px rgba(2, 6, 23, 0.28) inset;
469
- }
470
- .visual-panel__pulse {
471
- position: absolute;
472
- left: 110px;
473
- right: 78px;
474
- bottom: 64px;
475
- height: 3px;
476
- border-radius: 999px;
477
- background: linear-gradient(90deg, transparent, var(--accent-2), var(--accent), transparent);
478
- filter: drop-shadow(0 0 8px var(--accent-2));
479
- animation: markdyRouteFlow 1.5s linear infinite;
480
- }
481
- .visual-panel__cells {
482
- position: absolute;
483
- right: 18px;
484
- top: 22px;
485
- display: grid;
486
- grid-template-columns: repeat(4, 42px);
487
- gap: 7px;
488
- }
489
- .visual-panel__cell {
490
- height: 30px;
491
- border-radius: 10px;
492
- border: 1px solid rgba(148, 163, 184, 0.34);
493
- background: rgba(15, 23, 42, 0.7);
494
- box-shadow: 0 0 0 1px rgba(255,255,255,0.035) inset;
495
- }
496
- .visual-panel__cell:nth-child(3),
497
- .visual-panel__cell:nth-child(6),
498
- .visual-panel__cell:nth-child(8) {
499
- border-color: color-mix(in srgb, var(--accent-2) 78%, transparent);
500
- background: color-mix(in srgb, var(--accent-2) 35%, rgba(15, 23, 42, 0.7));
501
- animation: markdySlotGlow 1.7s ease-in-out infinite;
502
- }
503
- .visual-terminal__chrome {
504
- height: 28px;
505
- padding-left: 12px;
506
- display: flex;
507
- align-items: center;
508
- gap: 6px;
509
- border-bottom: 1px solid rgba(148, 163, 184, 0.16);
510
- }
511
- .visual-terminal__chrome span {
512
- width: 8px;
513
- height: 8px;
514
- border-radius: 999px;
515
- background: #ef4444;
516
- box-shadow: 14px 0 #f59e0b, 28px 0 #22c55e;
517
- }
518
- .visual-terminal__lines {
519
- position: absolute;
520
- left: 16px;
521
- right: 16px;
522
- top: 46px;
523
- font-family: "SFMono-Regular", Consolas, monospace;
524
- font-size: 18px;
525
- line-height: 1.55;
526
- color: color-mix(in srgb, var(--accent-2) 62%, #f8fafc);
527
- text-shadow: 0 0 12px color-mix(in srgb, var(--accent-2) 38%, transparent);
528
- }
529
- .visual-terminal__line {
530
- overflow: hidden;
531
- text-overflow: ellipsis;
532
- white-space: nowrap;
533
- }
534
- .visual-metric {
535
- height: 100%;
536
- padding: 7px 9px;
537
- display: flex;
538
- align-items: center;
539
- justify-content: center;
540
- gap: 6px;
541
- min-width: 0;
542
- overflow: hidden;
543
- white-space: nowrap;
544
- }
545
- .visual-metric strong {
546
- flex: 0 0 auto;
547
- color: #f8fafc;
548
- font-size: 15px;
549
- line-height: 1;
550
- }
551
- .visual-metric span {
552
- min-width: 0;
553
- overflow: hidden;
554
- color: #94a3b8;
555
- font-size: 8px;
556
- font-weight: 840;
557
- letter-spacing: 0.08em;
558
- line-height: 1;
559
- text-overflow: ellipsis;
560
- text-transform: uppercase;
561
- }
562
- .visual-grid {
563
- position: absolute;
564
- inset: 10px;
565
- display: grid;
566
- gap: 7px;
567
- }
568
- .visual-grid__cell {
569
- border-radius: 8px;
570
- border: 1px solid rgba(148, 163, 184, 0.3);
571
- background: rgba(15, 23, 42, 0.74);
572
- }
573
- .visual-grid__cell.is-active {
574
- border-color: color-mix(in srgb, var(--accent-2) 78%, transparent);
575
- background: color-mix(in srgb, var(--accent-2) 35%, rgba(15, 23, 42, 0.74));
576
- box-shadow: 0 0 18px color-mix(in srgb, var(--accent-2) 26%, transparent);
577
- }
578
- .visual-lane {
579
- position: absolute;
580
- inset: 0;
581
- border-radius: inherit;
582
- background:
583
- repeating-linear-gradient(90deg, rgba(226, 232, 240, 0.76) 0 18px, transparent 18px 34px) center / 100% 3px no-repeat,
584
- linear-gradient(90deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.88));
585
- box-shadow: 0 10px 28px rgba(2, 6, 23, 0.28) inset, 0 0 18px color-mix(in srgb, var(--accent) 22%, transparent);
586
- }
587
- .visual-lane::after {
588
- content: "";
589
- position: absolute;
590
- left: 18%;
591
- right: 18%;
592
- top: 50%;
593
- height: 3px;
594
- border-radius: 999px;
595
- transform: translateY(-50%);
596
- background: linear-gradient(90deg, transparent, var(--accent-2), var(--accent), transparent);
597
- filter: drop-shadow(0 0 8px var(--accent-2));
598
- animation: markdyRouteFlow 1.5s linear infinite;
599
- }
600
- .visual-marker {
601
- position: absolute;
602
- inset: 0;
603
- border-radius: inherit;
604
- background: linear-gradient(90deg, var(--accent), #818cf8);
605
- box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 55%, transparent), 0 8px 18px rgba(2, 6, 23, 0.35);
606
- }
607
- .visual-marker::before,
608
- .visual-marker::after {
609
- content: "";
610
- position: absolute;
611
- bottom: -3px;
612
- width: 8px;
613
- height: 8px;
614
- border-radius: 999px;
615
- background: #020617;
616
- border: 2px solid #cbd5e1;
617
- }
618
- .visual-marker::before { left: 9px; }
619
- .visual-marker::after { right: 9px; }
620
- .visual-tokens {
621
- height: 100%;
622
- padding: 8px;
623
- display: flex;
624
- align-items: center;
625
- gap: 7px;
626
- }
627
- .visual-token {
628
- flex: 1 1 0;
629
- min-width: 0;
630
- border: 1px solid rgba(148, 163, 184, 0.25);
631
- border-radius: 12px;
632
- padding: 8px 10px;
633
- background: rgba(2, 6, 23, 0.42);
634
- color: color-mix(in srgb, var(--accent) 45%, #f8fafc);
635
- font-family: "SFMono-Regular", Consolas, monospace;
636
- font-size: 12px;
637
- font-weight: 800;
638
- overflow: hidden;
639
- text-overflow: ellipsis;
640
- white-space: nowrap;
641
- }
642
- .visual-glyph {
643
- height: 100%;
644
- display: grid;
645
- place-items: center;
646
- padding: 10px;
647
- text-align: center;
648
- }
649
- .visual-glyph strong {
650
- color: #f8fafc;
651
- font-size: 56px;
652
- line-height: 1;
653
- }
654
- .visual-glyph span {
655
- margin-top: 6px;
656
- color: #cbd5e1;
657
- font-size: 10px;
658
- font-weight: 800;
659
- letter-spacing: 0.08em;
660
- text-transform: uppercase;
661
- }
662
- @keyframes markdyGridDrift { to { background-position: 34px 34px, 34px 34px; } }
663
- @keyframes markdyAurora { to { transform: rotate(1turn); } }
664
- @keyframes markdyScanPulse { 50% { transform: translateX(18px); opacity: 0.72; } }
665
- @keyframes markdySlotGlow { 50% { box-shadow: 0 0 20px color-mix(in srgb, var(--accent-2) 32%, transparent); } }
666
- @keyframes markdyRouteFlow { to { filter: hue-rotate(70deg) drop-shadow(0 0 9px var(--accent)); } }
667
- `;
668
- doc.head.appendChild(style);
669
- }
670
- function escapeHtml(value) {
671
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
672
- }
673
- function toneArg(def, index, fallback = "cyan") {
674
- const tone = def.args[index]?.trim().toLowerCase() || fallback;
675
- return /^(cyan|green|amber|purple|rose)$/.test(tone) ? tone : fallback;
676
- }
677
- function parseGridSpec(spec) {
678
- const match = /^(\d+)x(\d+)$/i.exec(spec?.trim() ?? "");
679
- const cols = match ? Number(match[1]) : 4;
680
- const rows = match ? Number(match[2]) : 2;
681
- return {
682
- cols: Math.min(Math.max(cols, 1), 8),
683
- rows: Math.min(Math.max(rows, 1), 4)
684
- };
685
- }
686
- function activeCellSet(value) {
687
- return new Set(
688
- (value ?? "").split(/[|\s]+/).map((part) => Number(part.trim())).filter((part) => Number.isInteger(part) && part > 0)
689
- );
690
- }
691
- function canonicalVisualType(type) {
692
- switch (type) {
693
- case "surface":
694
- return "panel";
695
- case "stat":
696
- return "metric";
697
- case "matrix":
698
- return "grid";
699
- case "track":
700
- return "lane";
701
- case "dot":
702
- return "marker";
703
- case "chips":
704
- return "token_strip";
705
- case "glyph":
706
- return "glyph_card";
707
- case "parking_map":
708
- case "game_scene":
709
- case "byte_viz":
710
- return "panel";
711
- case "ascii_map":
712
- return "terminal";
713
- default:
714
- return type;
715
- }
716
- }
717
- function legacyVisualArgs(type, args) {
718
- const label = args[0] || type.replace("_", " ");
719
- switch (type) {
720
- case "parking_map":
721
- return [label, "live ops", "cyan"];
722
- case "ascii_map":
723
- return [label, "[P1][P2][ ][EV]", "[IN]===LANE===[OUT]", "0101 0000 0100 0001", "green"];
724
- case "game_scene":
725
- return [label, "60 fps", "amber"];
726
- case "byte_viz":
727
- return [label, "UTF-8", "purple"];
728
- default:
729
- return args;
730
- }
731
- }
732
- function createVisualPrimitiveEl(type, def) {
733
- ensureVisualPrimitiveStyles(document);
734
- const visualType = canonicalVisualType(type);
735
- const args = legacyVisualArgs(type, def.args);
736
- const label = args[0] || type.replace("_", " ");
737
- const root = document.createElement("div");
738
- root.className = `markdy-visual markdy-visual--${visualType}`;
739
- root.dataset.visualType = visualType;
740
- root.setAttribute("role", "img");
741
- root.setAttribute("aria-label", label);
742
- if (visualType === "panel") {
743
- const tag = escapeHtml(args[1] || "live");
744
- root.dataset.tone = toneArg({ ...def, args }, 2);
745
- root.innerHTML = `
746
- <div class="markdy-visual__top"><div class="markdy-visual__title">${escapeHtml(label)}</div><div class="markdy-visual__pill">${tag}</div></div>
747
- <div class="markdy-visual__body">
748
- <div class="visual-panel__scan"></div>
749
- <div class="visual-panel__cells">${Array.from({ length: 8 }, () => '<span class="visual-panel__cell"></span>').join("")}</div>
750
- <div class="visual-panel__rails"></div>
751
- <div class="visual-panel__pulse"></div>
752
- </div>`;
753
- } else if (visualType === "terminal") {
754
- root.dataset.tone = toneArg({ ...def, args }, 4, "green");
755
- const lines = [args[1], args[2], args[3]].filter((line) => typeof line === "string" && line.length > 0).map((line) => `<div class="visual-terminal__line">${escapeHtml(line)}</div>`).join("");
756
- root.innerHTML = `
757
- <div class="markdy-visual__top"><div class="markdy-visual__title">${escapeHtml(label)}</div><div class="markdy-visual__pill">terminal</div></div>
758
- <div class="markdy-visual__body"><div class="visual-terminal__chrome"><span></span></div><div class="visual-terminal__lines">${lines}</div></div>`;
759
- } else if (visualType === "metric") {
760
- root.dataset.tone = toneArg({ ...def, args }, 2);
761
- root.innerHTML = `<div class="visual-metric"><strong>${escapeHtml(args[1] || "\u2014")}</strong><span>${escapeHtml(label)}</span></div>`;
762
- } else if (visualType === "grid") {
763
- root.dataset.tone = toneArg({ ...def, args }, 3, "green");
764
- const { cols, rows } = parseGridSpec(args[1]);
765
- const active = activeCellSet(args[2]);
766
- root.innerHTML = `<div class="visual-grid" style="grid-template-columns: repeat(${cols}, 1fr);">${Array.from({ length: cols * rows }, (_, i) => `<span class="visual-grid__cell${active.has(i + 1) ? " is-active" : ""}"></span>`).join("")}</div>`;
767
- } else if (visualType === "lane") {
768
- root.dataset.tone = toneArg({ ...def, args }, 1);
769
- root.innerHTML = `<div class="visual-lane"></div>`;
770
- } else if (visualType === "marker") {
771
- root.dataset.tone = toneArg({ ...def, args }, 1);
772
- root.innerHTML = `<div class="visual-marker"></div>`;
773
- } else if (visualType === "token_strip") {
774
- root.dataset.tone = toneArg({ ...def, args }, 1, "purple");
775
- const tokens = (args[0] || "token").split("|").map((token) => token.trim()).filter(Boolean);
776
- root.innerHTML = `<div class="visual-tokens">${tokens.map((token) => `<span class="visual-token">${escapeHtml(token)}</span>`).join("")}</div>`;
777
- } else {
778
- root.dataset.tone = toneArg({ ...def, args }, 2, "purple");
779
- root.innerHTML = `<div class="visual-glyph"><div><strong>${escapeHtml(args[0] || "A")}</strong><span>${escapeHtml(args[1] || "glyph")}</span></div></div>`;
780
- }
781
- return root;
782
- }
783
- function ensureArchitectureStyles(doc) {
784
- if (doc.getElementById(ARCHITECTURE_STYLE_ID)) return;
785
- const style = doc.createElement("style");
786
- style.id = ARCHITECTURE_STYLE_ID;
787
- style.textContent = `
788
- .markdy-arch-node {
789
- --markdy-node-accent: #38bdf8;
790
- --markdy-node-accent-2: #22c55e;
791
- --markdy-node-surface: rgba(15, 23, 42, 0.82);
792
- --markdy-node-border: rgba(148, 163, 184, 0.34);
793
- --markdy-node-glow: rgba(56, 189, 248, 0.24);
794
- position: relative;
795
- isolation: isolate;
796
- width: 184px;
797
- min-height: 88px;
798
- box-sizing: border-box;
799
- display: grid;
800
- grid-template-columns: 34px minmax(0, 1fr);
801
- align-items: center;
802
- gap: 10px;
803
- padding: 13px 14px;
804
- border: 1px solid var(--markdy-node-border);
805
- border-radius: 14px;
806
- color: #e5eefb;
807
- background:
808
- linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.02) 34%, rgba(15, 23, 42, 0.88) 100%),
809
- radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--markdy-node-accent) 34%, transparent), transparent 42%),
810
- var(--markdy-node-surface);
811
- box-shadow:
812
- 0 16px 34px rgba(2, 6, 23, 0.28),
813
- 0 0 0 1px rgba(255, 255, 255, 0.04) inset,
814
- 0 1px 0 rgba(255, 255, 255, 0.12) inset,
815
- 0 0 28px var(--markdy-node-glow);
816
- overflow: hidden;
817
- contain: layout paint style;
818
- backdrop-filter: blur(14px) saturate(1.18);
819
- -webkit-backdrop-filter: blur(14px) saturate(1.18);
820
- }
821
- .markdy-arch-node::before {
822
- content: "";
823
- position: absolute;
824
- inset: 0;
825
- z-index: -1;
826
- background:
827
- linear-gradient(90deg, transparent 0 24%, rgba(255, 255, 255, 0.06) 50%, transparent 76%) -180px 0 / 180px 100% no-repeat,
828
- repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.055) 0 1px, transparent 1px 10px);
829
- opacity: 0.6;
830
- }
831
- .markdy-arch-node::after {
832
- content: "";
833
- position: absolute;
834
- left: 14px;
835
- right: 14px;
836
- bottom: 9px;
837
- height: 2px;
838
- border-radius: 999px;
839
- background: linear-gradient(90deg, transparent, var(--markdy-node-accent), var(--markdy-node-accent-2), transparent);
840
- opacity: 0.82;
841
- filter: drop-shadow(0 0 6px var(--markdy-node-accent));
842
- }
843
- .markdy-arch-node__icon {
844
- position: relative;
845
- width: 32px;
846
- height: 32px;
847
- border-radius: 11px;
848
- background:
849
- radial-gradient(circle at 35% 25%, rgba(255, 255, 255, 0.28), transparent 42%),
850
- linear-gradient(145deg, var(--markdy-node-accent), color-mix(in srgb, var(--markdy-node-accent) 38%, #020617));
851
- box-shadow:
852
- 0 0 0 1px rgba(255, 255, 255, 0.16) inset,
853
- 0 10px 20px color-mix(in srgb, var(--markdy-node-accent) 25%, transparent);
854
- }
855
- .markdy-arch-node__icon::before,
856
- .markdy-arch-node__icon::after {
857
- content: "";
858
- position: absolute;
859
- box-sizing: border-box;
860
- border-color: rgba(255, 255, 255, 0.9);
861
- }
862
- .markdy-arch-node__label {
863
- min-width: 0;
864
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
865
- font-size: 15px;
866
- font-weight: 720;
867
- line-height: 1.08;
868
- color: #f8fafc;
869
- text-shadow: 0 1px 12px rgba(15, 23, 42, 0.8);
870
- white-space: nowrap;
871
- overflow: hidden;
872
- text-overflow: ellipsis;
873
- }
874
- .markdy-arch-node__type {
875
- display: block;
876
- margin-top: 5px;
877
- font-size: 9px;
878
- font-weight: 740;
879
- letter-spacing: 0.08em;
880
- text-transform: uppercase;
881
- color: color-mix(in srgb, var(--markdy-node-accent) 70%, #e2e8f0);
882
- opacity: 0.86;
883
- }
884
- .markdy-arch-node[data-markdy-system-kind="compute"],
885
- .markdy-arch-node[data-markdy-system-kind="code"] {
886
- --markdy-node-accent: #38bdf8;
887
- --markdy-node-accent-2: #818cf8;
888
- }
889
- .markdy-arch-node[data-markdy-system-kind="client"] {
890
- --markdy-node-accent: #f59e0b;
891
- --markdy-node-accent-2: #fb7185;
892
- }
893
- .markdy-arch-node[data-markdy-system-kind="data"] {
894
- --markdy-node-accent: #22c55e;
895
- --markdy-node-accent-2: #14b8a6;
896
- }
897
- .markdy-arch-node[data-markdy-system-kind="messaging"] {
898
- --markdy-node-accent: #a78bfa;
899
- --markdy-node-accent-2: #38bdf8;
900
- }
901
- .markdy-arch-node[data-markdy-system-kind="network"] {
902
- --markdy-node-accent: #60a5fa;
903
- --markdy-node-accent-2: #67e8f9;
904
- }
905
- .markdy-arch-node[data-markdy-system-kind="platform"] {
906
- --markdy-node-accent: #c084fc;
907
- --markdy-node-accent-2: #f472b6;
908
- }
909
- .markdy-arch-node[data-markdy-system-kind="security"] {
910
- --markdy-node-accent: #fb7185;
911
- --markdy-node-accent-2: #f59e0b;
912
- }
913
- .markdy-arch-node[data-markdy-system-kind="delivery"] {
914
- --markdy-node-accent: #facc15;
915
- --markdy-node-accent-2: #22c55e;
916
- }
917
- .markdy-arch-node[data-markdy-system-kind="observability"] {
918
- --markdy-node-accent: #2dd4bf;
919
- --markdy-node-accent-2: #38bdf8;
920
- }
921
- .markdy-arch-node[data-markdy-system-kind="flow"] {
922
- --markdy-node-accent: #e879f9;
923
- --markdy-node-accent-2: #a78bfa;
924
- }
925
- .markdy-arch-node[data-markdy-system-kind="distributed"] {
926
- --markdy-node-accent: #94a3b8;
927
- --markdy-node-accent-2: #38bdf8;
928
- }
929
- .markdy-arch-node[data-markdy-system-kind="data"] {
930
- border-radius: 18px 18px 24px 24px;
931
- }
932
- .markdy-arch-node[data-markdy-system-kind="network"] {
933
- border-radius: 999px;
934
- }
935
- .markdy-arch-node[data-markdy-system-kind="platform"] {
936
- border-radius: 10px;
937
- }
938
- .markdy-arch-node[data-markdy-system-kind="security"] {
939
- border-style: solid;
940
- }
941
- .markdy-arch-node[data-markdy-system-kind="compute"] .markdy-arch-node__icon::before,
942
- .markdy-arch-node[data-markdy-system-kind="code"] .markdy-arch-node__icon::before {
943
- inset: 9px 7px;
944
- border-top: 3px solid rgba(255, 255, 255, 0.92);
945
- border-bottom: 3px solid rgba(255, 255, 255, 0.92);
946
- }
947
- .markdy-arch-node[data-markdy-system-kind="compute"] .markdy-arch-node__icon::after,
948
- .markdy-arch-node[data-markdy-system-kind="code"] .markdy-arch-node__icon::after {
949
- inset: 14px 7px auto;
950
- border-top: 3px solid rgba(255, 255, 255, 0.92);
951
- }
952
- .markdy-arch-node[data-markdy-system-kind="client"] .markdy-arch-node__icon::before {
953
- left: 8px;
954
- right: 8px;
955
- top: 8px;
956
- height: 13px;
957
- border: 2px solid rgba(255, 255, 255, 0.92);
958
- border-radius: 3px;
959
- }
960
- .markdy-arch-node[data-markdy-system-kind="client"] .markdy-arch-node__icon::after {
961
- left: 12px;
962
- right: 12px;
963
- bottom: 7px;
964
- border-top: 2px solid rgba(255, 255, 255, 0.92);
965
- }
966
- .markdy-arch-node[data-markdy-system-kind="data"] .markdy-arch-node__icon::before {
967
- inset: 7px 7px 9px;
968
- border: 2px solid rgba(255, 255, 255, 0.92);
969
- border-radius: 50% / 16%;
970
- }
971
- .markdy-arch-node[data-markdy-system-kind="data"] .markdy-arch-node__icon::after {
972
- left: 8px;
973
- right: 8px;
974
- top: 12px;
975
- border-top: 2px solid rgba(255, 255, 255, 0.72);
976
- }
977
- .markdy-arch-node[data-markdy-system-kind="messaging"] .markdy-arch-node__icon::before {
978
- inset: 9px 8px;
979
- border: 2px solid rgba(255, 255, 255, 0.92);
980
- border-radius: 999px;
981
- }
982
- .markdy-arch-node[data-markdy-system-kind="messaging"] .markdy-arch-node__icon::after {
983
- left: 13px;
984
- top: 9px;
985
- width: 8px;
986
- height: 14px;
987
- border-left: 2px solid rgba(255, 255, 255, 0.82);
988
- border-right: 2px solid rgba(255, 255, 255, 0.82);
989
- }
990
- .markdy-arch-node[data-markdy-system-kind="network"] .markdy-arch-node__icon::before {
991
- left: 7px;
992
- right: 7px;
993
- bottom: 9px;
994
- height: 12px;
995
- border: 2px solid rgba(255, 255, 255, 0.92);
996
- border-radius: 999px;
997
- }
998
- .markdy-arch-node[data-markdy-system-kind="network"] .markdy-arch-node__icon::after {
999
- left: 10px;
1000
- top: 7px;
1001
- width: 13px;
1002
- height: 13px;
1003
- border-top: 2px solid rgba(255, 255, 255, 0.92);
1004
- border-left: 2px solid rgba(255, 255, 255, 0.92);
1005
- border-radius: 999px 0 0 0;
1006
- }
1007
- .markdy-arch-node[data-markdy-system-kind="platform"] .markdy-arch-node__icon::before {
1008
- inset: 8px;
1009
- border: 2px solid rgba(255, 255, 255, 0.92);
1010
- border-radius: 4px;
1011
- }
1012
- .markdy-arch-node[data-markdy-system-kind="platform"] .markdy-arch-node__icon::after {
1013
- left: 10px;
1014
- right: 10px;
1015
- top: 14px;
1016
- border-top: 2px solid rgba(255, 255, 255, 0.72);
1017
- }
1018
- .markdy-arch-node[data-markdy-system-kind="security"] .markdy-arch-node__icon::before {
1019
- left: 9px;
1020
- right: 9px;
1021
- top: 7px;
1022
- bottom: 7px;
1023
- border: 2px solid rgba(255, 255, 255, 0.92);
1024
- border-radius: 10px 10px 14px 14px;
1025
- }
1026
- .markdy-arch-node[data-markdy-system-kind="security"] .markdy-arch-node__icon::after {
1027
- left: 14px;
1028
- right: 14px;
1029
- top: 14px;
1030
- border-top: 2px solid rgba(255, 255, 255, 0.82);
1031
- }
1032
- .markdy-arch-node[data-markdy-system-kind="delivery"] .markdy-arch-node__icon::before,
1033
- .markdy-arch-node[data-markdy-system-kind="observability"] .markdy-arch-node__icon::before,
1034
- .markdy-arch-node[data-markdy-system-kind="distributed"] .markdy-arch-node__icon::before {
1035
- inset: 8px;
1036
- border: 2px solid rgba(255, 255, 255, 0.92);
1037
- border-radius: 999px;
1038
- }
1039
- .markdy-arch-node[data-markdy-system-kind="delivery"] .markdy-arch-node__icon::after,
1040
- .markdy-arch-node[data-markdy-system-kind="observability"] .markdy-arch-node__icon::after,
1041
- .markdy-arch-node[data-markdy-system-kind="distributed"] .markdy-arch-node__icon::after {
1042
- left: 15px;
1043
- top: 7px;
1044
- bottom: 7px;
1045
- border-left: 2px solid rgba(255, 255, 255, 0.82);
1046
- }
1047
- .markdy-arch-node[data-markdy-system-kind="flow"] .markdy-arch-node__icon::before {
1048
- inset: 8px;
1049
- border: 2px solid rgba(255, 255, 255, 0.92);
1050
- transform: rotate(45deg);
1051
- }
1052
- .markdy-arch-node[data-markdy-system-kind="flow"] .markdy-arch-node__icon::after {
1053
- left: 14px;
1054
- right: 14px;
1055
- top: 15px;
1056
- border-top: 2px solid rgba(255, 255, 255, 0.82);
1057
- }
1058
- `;
1059
- doc.head.appendChild(style);
1060
- }
1061
- function isArchitectureNodeType(type) {
1062
- return ARCHITECTURE_NODE_TYPES.has(type);
1063
- }
1064
- function isVisualPrimitiveType(type) {
1065
- return VISUAL_PRIMITIVE_TYPE_SET.has(type);
1066
- }
1067
- function architectureTypeLabel(type) {
1068
- if (type === "db") return "database";
1069
- if (type === "api") return "service";
1070
- return type;
1071
- }
1072
- function architectureNodeKind(type) {
1073
- return TECHNICAL_NODE_KINDS[type] ?? "compute";
1074
- }
1075
- function createActorEl(name, def, assetDefs, assetOverrides) {
1076
- let el;
1077
- switch (def.type) {
1078
- case "sprite": {
1079
- const assetName = def.args[0] ?? "";
1080
- const assetDef = assetDefs[assetName];
1081
- if (assetDef?.type === "icon") {
1082
- const span = document.createElement("span");
1083
- span.className = "iconify";
1084
- span.style.display = "inline-block";
1085
- span.style.fontSize = `${def.size ?? 32}px`;
1086
- span.style.lineHeight = "1";
1087
- span.dataset.icon = assetDef.value;
1088
- span.setAttribute("aria-label", assetDef.value.split(":").pop() ?? "icon");
1089
- el = span;
1090
- } else {
1091
- const img = document.createElement("img");
1092
- img.src = assetOverrides[assetName] ?? assetDef?.value ?? "";
1093
- img.alt = assetName;
1094
- img.style.display = "block";
1095
- img.style.maxWidth = "100%";
1096
- img.style.maxHeight = "200px";
1097
- img.style.objectFit = "contain";
1098
- img.setAttribute("draggable", "false");
1099
- el = img;
1100
- }
1101
- break;
1102
- }
1103
- case "text": {
1104
- const div = document.createElement("div");
1105
- div.textContent = def.args[0] ?? "";
1106
- div.style.fontSize = `${def.size ?? 24}px`;
1107
- div.style.fontFamily = "sans-serif";
1108
- div.style.whiteSpace = "nowrap";
1109
- div.style.userSelect = "none";
1110
- div.style.pointerEvents = "none";
1111
- el = div;
1112
- break;
1113
- }
1114
- case "caption": {
1115
- const div = document.createElement("div");
1116
- div.textContent = def.args[0] ?? "";
1117
- div.dataset.markdyCaption = def.anchor ?? "top";
1118
- Object.assign(div.style, {
1119
- fontSize: `${def.size ?? 32}px`,
1120
- fontFamily: "system-ui, -apple-system, sans-serif",
1121
- fontWeight: "700",
1122
- whiteSpace: "nowrap",
1123
- textAlign: "center",
1124
- lineHeight: "1.1",
1125
- padding: "6px 14px",
1126
- borderRadius: "4px",
1127
- background: "rgba(0, 0, 0, 0.55)",
1128
- color: "#fff",
1129
- textShadow: "0 2px 6px rgba(0, 0, 0, 0.45)",
1130
- userSelect: "none",
1131
- pointerEvents: "none"
1132
- // Center the caption on its (x, y) point (x = sceneWidth/2).
1133
- // We combine translate-centering with the actor transform in the
1134
- // dataset below so the player can re-apply on state changes.
1135
- });
1136
- el = div;
1137
- break;
1138
- }
1139
- case "figure": {
1140
- el = createFigureEl(def);
1141
- break;
1142
- }
1143
- default: {
1144
- if (VISUAL_PRIMITIVE_TYPE_SET.has(def.type)) {
1145
- el = createVisualPrimitiveEl(def.type, def);
1146
- break;
1147
- }
1148
- if (!isArchitectureNodeType(def.type)) {
1149
- const div = document.createElement("div");
1150
- div.style.width = "100px";
1151
- div.style.height = "100px";
1152
- div.style.background = "#999";
1153
- div.style.boxSizing = "border-box";
1154
- el = div;
1155
- break;
1156
- }
1157
- ensureArchitectureStyles(document);
1158
- const card = document.createElement("div");
1159
- card.className = "markdy-arch-node";
1160
- card.dataset.markdySystemType = def.type;
1161
- card.dataset.markdySystemKind = architectureNodeKind(def.type);
1162
- card.setAttribute("role", "img");
1163
- card.setAttribute("aria-label", `${architectureTypeLabel(def.type)} ${def.args[0] ?? name}`);
1164
- const icon = document.createElement("span");
1165
- icon.className = "markdy-arch-node__icon";
1166
- icon.setAttribute("aria-hidden", "true");
1167
- const label = document.createElement("div");
1168
- label.className = "markdy-arch-node__label";
1169
- label.textContent = def.args[0] ?? "";
1170
- label.style.fontSize = `${def.size ?? 15}px`;
1171
- const typeLabel = document.createElement("span");
1172
- typeLabel.className = "markdy-arch-node__type";
1173
- typeLabel.textContent = architectureTypeLabel(def.type);
1174
- label.appendChild(typeLabel);
1175
- card.appendChild(icon);
1176
- card.appendChild(label);
1177
- el = card;
1178
- break;
1179
- }
1180
- }
1181
- el.dataset.markdyActor = name;
1182
- el.style.position = "absolute";
1183
- el.style.left = "0";
1184
- el.style.top = "0";
1185
- el.style.transformOrigin = "center center";
1186
- el.style.transform = def.type === "caption" ? txCaption(stateFrom(def)) : tx(stateFrom(def));
1187
- el.style.opacity = String(def.opacity ?? 1);
1188
- if (def.z !== void 0) el.style.zIndex = String(def.z);
1189
- else if (def.type === "caption") el.style.zIndex = "100";
1190
- else if (isVisualPrimitiveType(def.type)) el.style.zIndex = "25";
1191
- else if (isArchitectureNodeType(def.type)) el.style.zIndex = "10";
1192
- return el;
1193
- }
1194
-
1195
- // src/camera.ts
1196
- function freshCameraState() {
1197
- return { x: 0, y: 0, zoom: 1 };
1198
- }
1199
- function cameraTx(s) {
1200
- return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
1201
- }
1202
- var DEFAULT_SHAKE_INTENSITY = 8;
1203
- function buildCameraAction(ev, scene, ast, baseOpts, anims, state) {
1204
- switch (ev.action) {
1205
- case "pan": {
1206
- const to = ev.params.to;
1207
- if (!to) break;
1208
- const next = {
1209
- ...state,
1210
- x: to[0] - ast.meta.width / 2,
1211
- y: to[1] - ast.meta.height / 2
1212
- };
1213
- anims.push(
1214
- scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
1215
- );
1216
- state.x = next.x;
1217
- state.y = next.y;
1218
- break;
1219
- }
1220
- case "zoom": {
1221
- const next = {
1222
- ...state,
1223
- zoom: typeof ev.params.to === "number" ? ev.params.to : state.zoom
1224
- };
1225
- anims.push(
1226
- scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
1227
- );
1228
- state.zoom = next.zoom;
1229
- break;
1230
- }
1231
- case "shake": {
1232
- const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : DEFAULT_SHAKE_INTENSITY;
1233
- const at = (dx, dy, offset) => ({
1234
- transform: cameraTx({ ...state, x: state.x + dx, y: state.y + dy }),
1235
- offset
1236
- });
1237
- anims.push(
1238
- scene.animate(
1239
- [
1240
- at(0, 0, 0),
1241
- at(-mag, -mag * 0.4, 0.15),
1242
- at(mag, mag * 0.4, 0.35),
1243
- at(-mag * 0.6, mag * 0.3, 0.55),
1244
- at(mag * 0.5, -mag * 0.3, 0.75),
1245
- at(0, 0, 1)
1246
- ],
1247
- { ...baseOpts, easing: "linear" }
1248
- )
1249
- );
1250
- break;
1251
- }
1252
- default:
1253
- break;
1254
- }
1255
- }
1256
-
1257
- // src/stage.ts
1258
- function offscreenState(s, direction, sceneWidth, sceneHeight) {
1259
- const out = { ...s };
1260
- switch (direction) {
1261
- case "left":
1262
- out.x = -sceneWidth * 1.1;
1263
- break;
1264
- case "right":
1265
- out.x = sceneWidth * 2.1;
1266
- break;
1267
- case "top":
1268
- out.y = -sceneHeight * 1.1;
1269
- break;
1270
- case "bottom":
1271
- out.y = sceneHeight * 2.1;
1272
- break;
1273
- }
1274
- return out;
1275
- }
1276
- function preInitInlineStyles(ast, actorEls, states, events, txFor) {
1277
- const firstEventByActor = /* @__PURE__ */ new Map();
1278
- for (const ev of events) {
1279
- if (ev.actor === "camera") continue;
1280
- if (!firstEventByActor.has(ev.actor)) firstEventByActor.set(ev.actor, ev);
1281
- }
1282
- for (const [name, def] of Object.entries(ast.actors)) {
1283
- const el = actorEls.get(name);
1284
- const s = states.get(name);
1285
- if (!el || !s) continue;
1286
- const firstEv = firstEventByActor.get(name);
1287
- if (!firstEv) continue;
1288
- if (firstEv.action === "enter") {
1289
- const from = String(firstEv.params.from ?? "left");
1290
- el.style.transform = txFor(name)(offscreenState(s, from, ast.meta.width, ast.meta.height));
1291
- }
1292
- if (firstEv.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
1293
- el.style.opacity = "0";
1294
- }
1295
- }
1296
- }
1297
-
1298
- // src/actions/figure.ts
1299
- function sideOf(ctx) {
1300
- return String(ctx.ev.params.side ?? "right") === "left" ? "left" : "right";
1301
- }
1302
- function partEl(ctx, part) {
1303
- const selector = PART_SEL[part];
1304
- return selector ? ctx.el.querySelector(selector) : null;
1305
- }
1306
- function rotateKeyframe(deg, offset) {
1307
- return offset === void 0 ? { transform: `rotate(${deg}deg)` } : { transform: `rotate(${deg}deg)`, offset };
1308
- }
1309
- function swingLimb(ctx, part, extendDeg, peakOffset) {
1310
- const el = partEl(ctx, part);
1311
- if (!el) return;
1312
- const rest = readRotation(el);
1313
- ctx.anims.push(
1314
- el.animate([rotateKeyframe(rest), rotateKeyframe(extendDeg, peakOffset), rotateKeyframe(rest)], {
1315
- ...ctx.baseOpts,
1316
- easing: "ease-in-out",
1317
- fill: "forwards"
1318
- })
1319
- );
1320
- }
1321
- var punch = (ctx) => {
1322
- const side = sideOf(ctx);
1323
- swingLimb(ctx, side === "left" ? "arm_left" : "arm_right", side === "left" ? -75 : 75, 0.35);
1324
- };
1325
- var kick = (ctx) => {
1326
- const side = sideOf(ctx);
1327
- swingLimb(ctx, side === "left" ? "leg_left" : "leg_right", side === "left" ? -100 : 100, 0.38);
1328
- };
1329
- var wave = (ctx) => {
1330
- const side = sideOf(ctx);
1331
- const el = partEl(ctx, side === "left" ? "arm_left" : "arm_right");
1332
- if (!el) return;
1333
- const rest = readRotation(el);
1334
- const up = side === "left" ? 70 : -70;
1335
- const inward = side === "left" ? 50 : -50;
1336
- ctx.anims.push(
1337
- el.animate(
1338
- [
1339
- rotateKeyframe(rest, 0),
1340
- rotateKeyframe(up, 0.2),
1341
- rotateKeyframe(inward, 0.4),
1342
- rotateKeyframe(up, 0.55),
1343
- rotateKeyframe(inward, 0.7),
1344
- rotateKeyframe(up, 0.85),
1345
- rotateKeyframe(rest, 1)
1346
- ],
1347
- { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
1348
- )
1349
- );
1350
- };
1351
- var nod = (ctx) => {
1352
- const el = partEl(ctx, "head");
1353
- if (!el) return;
1354
- const rest = readRotation(el);
1355
- const down = 15;
1356
- ctx.anims.push(
1357
- el.animate(
1358
- [
1359
- rotateKeyframe(rest, 0),
1360
- rotateKeyframe(down, 0.35),
1361
- rotateKeyframe(rest, 0.65),
1362
- rotateKeyframe(down, 0.8),
1363
- rotateKeyframe(rest, 1)
1364
- ],
1365
- { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
1366
- )
1367
- );
1368
- };
1369
- function commitRotation(el, deg) {
1370
- el.style.transform = el.style.transform.replace(/rotate\([^)]*\)/, `rotate(${deg}deg)`);
1371
- }
1372
- var rotatePart = (ctx) => {
1373
- const el = partEl(ctx, String(ctx.ev.params.part ?? ""));
1374
- if (!el) return;
1375
- const from = readRotation(el);
1376
- const to = typeof ctx.ev.params.to === "number" ? ctx.ev.params.to : from;
1377
- ctx.anims.push(
1378
- el.animate([rotateKeyframe(from), rotateKeyframe(to)], { ...ctx.baseOpts, fill: "forwards" })
1379
- );
1380
- commitRotation(el, to);
1381
- };
1382
- var POSEABLE_PARTS = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
1383
- var pose = (ctx) => {
1384
- for (const part of POSEABLE_PARTS) {
1385
- const to = ctx.ev.params[part];
1386
- if (typeof to !== "number") continue;
1387
- const el = partEl(ctx, part);
1388
- if (!el) continue;
1389
- ctx.anims.push(
1390
- el.animate([rotateKeyframe(readRotation(el)), rotateKeyframe(to)], {
1391
- ...ctx.baseOpts,
1392
- fill: "forwards"
1393
- })
1394
- );
1395
- commitRotation(el, to);
1396
- }
1397
- };
1398
- var face = (ctx) => {
1399
- const el = ctx.el.querySelector("[data-fig-face]");
1400
- if (!el) return;
1401
- const emoji = String(ctx.ev.params.text ?? ctx.ev.params._0 ?? "");
1402
- if (emoji) ctx.faceSwaps.push({ timeMs: ctx.ev.time * 1e3, el, emoji });
1403
- };
1404
-
1405
- // src/actions/transform.ts
1406
- var move = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1407
- const to = ev.params.to;
1408
- const toX = to?.[0] ?? state.x;
1409
- const toY = to?.[1] ?? state.y;
1410
- anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, x: toX, y: toY }) }], baseOpts));
1411
- state.x = toX;
1412
- state.y = toY;
1413
- };
1414
- var enter = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
1415
- const from = String(ev.params.from ?? "left");
1416
- const fromState = offscreenState(state, from, ast.meta.width, ast.meta.height);
1417
- anims.push(
1418
- el.animate(
1419
- [
1420
- { transform: tx2(fromState), opacity: state.opacity },
1421
- { transform: tx2(state), opacity: 1 }
1422
- ],
1423
- baseOpts
1424
- )
1425
- );
1426
- state.opacity = 1;
1427
- };
1428
- var exit = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
1429
- const to = String(ev.params.to ?? "right");
1430
- const toState = offscreenState(state, to, ast.meta.width, ast.meta.height);
1431
- anims.push(
1432
- el.animate(
1433
- [
1434
- { transform: tx2(state), opacity: state.opacity },
1435
- { transform: tx2(toState), opacity: 0 }
1436
- ],
1437
- baseOpts
1438
- )
1439
- );
1440
- state.x = toState.x;
1441
- state.y = toState.y;
1442
- state.opacity = 0;
1443
- };
1444
- var fadeIn = ({ el, state, baseOpts, anims }) => {
1445
- anims.push(el.animate([{ opacity: 0 }, { opacity: 1 }], baseOpts));
1446
- state.opacity = 1;
1447
- };
1448
- var fadeOut = ({ el, state, baseOpts, anims }) => {
1449
- anims.push(el.animate([{ opacity: state.opacity }, { opacity: 0 }], baseOpts));
1450
- state.opacity = 0;
1451
- };
1452
- var scale = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1453
- const to = typeof ev.params.to === "number" ? ev.params.to : state.scale;
1454
- anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, scale: to }) }], baseOpts));
1455
- state.scale = to;
1456
- };
1457
- var rotate = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1458
- const to = typeof ev.params.to === "number" ? ev.params.to : state.rotate;
1459
- anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, rotate: to }) }], baseOpts));
1460
- state.rotate = to;
1461
- };
1462
- var shake = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1463
- const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 5;
1464
- anims.push(
1465
- el.animate(
1466
- [
1467
- { transform: tx2(state), offset: 0 },
1468
- { transform: tx2({ ...state, x: state.x + mag }), offset: 0.2 },
1469
- { transform: tx2({ ...state, x: state.x - mag }), offset: 0.4 },
1470
- { transform: tx2({ ...state, x: state.x + mag }), offset: 0.6 },
1471
- { transform: tx2({ ...state, x: state.x - mag }), offset: 0.8 },
1472
- { transform: tx2(state), offset: 1 }
1473
- ],
1474
- { ...baseOpts, easing: "linear" }
1475
- )
1476
- );
1477
- };
1478
- var jump = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1479
- const height = typeof ev.params.height === "number" ? ev.params.height : 30;
1480
- anims.push(
1481
- el.animate(
1482
- [
1483
- { transform: tx2(state), offset: 0 },
1484
- { transform: tx2({ ...state, scale: state.scale * 0.9 }), offset: 0.1 },
1485
- { transform: tx2({ ...state, y: state.y - height, scale: state.scale * 1.1 }), offset: 0.45 },
1486
- { transform: tx2({ ...state, y: state.y - height * 0.3, scale: state.scale * 1.05 }), offset: 0.7 },
1487
- { transform: tx2({ ...state, scale: state.scale * 0.92 }), offset: 0.88 },
1488
- { transform: tx2(state), offset: 1 }
1489
- ],
1490
- { ...baseOpts, easing: "ease-in-out" }
1491
- )
1492
- );
1493
- };
1494
- var bounce = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1495
- const intensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
1496
- const count = typeof ev.params.count === "number" ? ev.params.count : 3;
1497
- const keyframes = [{ transform: tx2(state), offset: 0 }];
1498
- for (let i = 0; i < count; i++) {
1499
- const amp = intensity * Math.pow(0.55, i);
1500
- const baseOffset = (i + 0.5) / (count + 0.5);
1501
- const peakOffset = Math.min(baseOffset, 0.98);
1502
- const valleyOffset = Math.min(baseOffset + 0.25 / (count + 0.5), 0.99);
1503
- keyframes.push({ transform: tx2({ ...state, y: state.y - amp }), offset: peakOffset });
1504
- if (i < count - 1) {
1505
- keyframes.push({ transform: tx2(state), offset: valleyOffset });
1506
- }
1507
- }
1508
- keyframes.push({ transform: tx2(state), offset: 1 });
1509
- anims.push(el.animate(keyframes, { ...baseOpts, easing: "ease-out" }));
1510
- };
1511
-
1512
- // src/actions/effects.ts
1513
- function numeric(value, fallback) {
1514
- return typeof value === "number" ? value : fallback;
1515
- }
1516
- function stringParam(value, fallback) {
1517
- return typeof value === "string" && value.length > 0 ? value : fallback;
1518
- }
1519
- var spring = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1520
- const to = ev.params.to;
1521
- const toX = to?.[0] ?? state.x;
1522
- const toY = to?.[1] ?? state.y;
1523
- const stiffness = numeric(ev.params.stiffness, 0.18);
1524
- const overshootX = toX + (toX - state.x) * stiffness;
1525
- const overshootY = toY + (toY - state.y) * stiffness;
1526
- anims.push(
1527
- el.animate(
1528
- [
1529
- { transform: tx2(state), offset: 0 },
1530
- { transform: tx2({ ...state, x: overshootX, y: overshootY }), offset: 0.72 },
1531
- { transform: tx2({ ...state, x: toX, y: toY }), offset: 1 }
1532
- ],
1533
- { ...baseOpts, easing: "cubic-bezier(.2,1.35,.35,1)" }
1534
- )
1535
- );
1536
- state.x = toX;
1537
- state.y = toY;
1538
- };
1539
- var followPath = ({ ev, el, baseOpts, anims }) => {
1540
- const path = stringParam(ev.params.path, "M 0 0 L 120 0");
1541
- const rotate2 = ev.params.rotate === false ? "0deg" : "auto";
1542
- anims.push(
1543
- el.animate(
1544
- [
1545
- { offsetPath: `path('${path}')`, offsetDistance: "0%", offsetRotate: rotate2 },
1546
- { offsetPath: `path('${path}')`, offsetDistance: "100%", offsetRotate: rotate2 }
1547
- ],
1548
- baseOpts
1549
- )
1550
- );
1551
- };
1552
- var pulse = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1553
- const amount = numeric(ev.params.amount, 1.08);
1554
- anims.push(
1555
- el.animate(
1556
- [
1557
- { transform: tx2(state), offset: 0 },
1558
- { transform: tx2({ ...state, scale: state.scale * amount }), offset: 0.48 },
1559
- { transform: tx2(state), offset: 1 }
1560
- ],
1561
- { ...baseOpts, easing: "ease-in-out" }
1562
- )
1563
- );
1564
- };
1565
- var glow = ({ ev, el, baseOpts, anims }) => {
1566
- const color = stringParam(ev.params.color, "#38bdf8");
1567
- const strength = numeric(ev.params.strength, 24);
1568
- anims.push(
1569
- el.animate(
1570
- [
1571
- { filter: "drop-shadow(0 0 0 transparent)", boxShadow: "0 0 0 rgba(0,0,0,0)" },
1572
- { filter: `drop-shadow(0 0 ${strength}px ${color})`, boxShadow: `0 0 ${strength}px ${color}` },
1573
- { filter: "drop-shadow(0 0 0 transparent)", boxShadow: "0 0 0 rgba(0,0,0,0)" }
1574
- ],
1575
- { ...baseOpts, easing: "ease-in-out" }
1576
- )
1577
- );
1578
- };
1579
- var ripple = ({ ev, el, delayMs, durMs, anims }) => {
1580
- const color = stringParam(ev.params.color, "#38bdf8");
1581
- const size = numeric(ev.params.size, 140);
1582
- const ring = document.createElement("span");
1583
- ring.setAttribute("data-markdy-ripple", "1");
1584
- Object.assign(ring.style, {
1585
- position: "absolute",
1586
- left: "50%",
1587
- top: "50%",
1588
- width: `${size}px`,
1589
- height: `${size}px`,
1590
- marginLeft: `${-size / 2}px`,
1591
- marginTop: `${-size / 2}px`,
1592
- border: `2px solid ${color}`,
1593
- borderRadius: "999px",
1594
- pointerEvents: "none",
1595
- opacity: "0"
1596
- });
1597
- el.appendChild(ring);
1598
- anims.push(
1599
- ring.animate(
1600
- [
1601
- { transform: "scale(0.2)", opacity: 0.6 },
1602
- { transform: "scale(1)", opacity: 0 }
1603
- ],
1604
- { delay: delayMs, duration: durMs, fill: "forwards", easing: "ease-out" }
1605
- )
1606
- );
1607
- };
1608
- var blur = ({ ev, el, baseOpts, anims }) => {
1609
- const from = numeric(ev.params.from, 0);
1610
- const to = numeric(ev.params.to, 8);
1611
- anims.push(el.animate([{ filter: `blur(${from}px)` }, { filter: `blur(${to}px)` }], baseOpts));
1612
- };
1613
- var lineReveal = ({ ev, el, baseOpts, anims }) => {
1614
- const from = stringParam(ev.params.from, "left");
1615
- const start = from === "right" ? "inset(0 0 0 100%)" : "inset(0 100% 0 0)";
1616
- anims.push(el.animate([{ clipPath: start }, { clipPath: "inset(0 0 0 0)" }], baseOpts));
1617
- };
1618
- var mask = ({ ev, el, baseOpts, anims }) => {
1619
- const from = numeric(ev.params.from, 0);
1620
- const to = numeric(ev.params.to, 120);
1621
- anims.push(
1622
- el.animate(
1623
- [
1624
- { clipPath: `circle(${from}% at 50% 50%)` },
1625
- { clipPath: `circle(${to}% at 50% 50%)` }
1626
- ],
1627
- baseOpts
1628
- )
1629
- );
1630
- };
1631
- var parallax = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1632
- const depth = numeric(ev.params.depth, 0.35);
1633
- const by = ev.params.by;
1634
- const dx = (by?.[0] ?? 24) * depth;
1635
- const dy = (by?.[1] ?? 0) * depth;
1636
- anims.push(
1637
- el.animate(
1638
- [
1639
- { transform: tx2(state) },
1640
- { transform: tx2({ ...state, x: state.x + dx, y: state.y + dy }) }
1641
- ],
1642
- baseOpts
1643
- )
1644
- );
1645
- };
2
+ import { parseAndCompile } from "@markdy/core";
1646
3
 
1647
4
  // src/geometry/rect.ts
1648
- import { TECHNICAL_NODE_TYPES } from "@markdy/core";
1649
- var SYSTEM_NODE_SIZE = { width: 184, height: 88 };
1650
- var ACTOR_SIZES = {
1651
- ...Object.fromEntries(TECHNICAL_NODE_TYPES.map((type) => [type, SYSTEM_NODE_SIZE])),
1652
- box: { width: 100, height: 100 },
1653
- caption: { width: 260, height: 56 },
1654
- figure: { width: 120, height: 170 },
1655
- panel: { width: 390, height: 250 },
1656
- surface: { width: 390, height: 250 },
1657
- terminal: { width: 390, height: 250 },
1658
- metric: { width: 112, height: 44 },
1659
- stat: { width: 112, height: 44 },
1660
- grid: { width: 190, height: 92 },
1661
- matrix: { width: 190, height: 92 },
1662
- lane: { width: 300, height: 44 },
1663
- track: { width: 300, height: 44 },
1664
- marker: { width: 50, height: 24 },
1665
- dot: { width: 50, height: 24 },
1666
- token_strip: { width: 300, height: 54 },
1667
- chips: { width: 300, height: 54 },
1668
- glyph_card: { width: 98, height: 120 },
1669
- glyph: { width: 98, height: 120 },
1670
- parking_map: { width: 390, height: 250 },
1671
- ascii_map: { width: 390, height: 250 },
1672
- game_scene: { width: 390, height: 250 },
1673
- byte_viz: { width: 390, height: 250 }
1674
- };
1675
- var DEFAULT_ACTOR_SIZE = { width: 140, height: 42 };
1676
- function actorSizeByType(type) {
1677
- return ACTOR_SIZES[type] ?? DEFAULT_ACTOR_SIZE;
5
+ function boxRect(box) {
6
+ return { x1: box.x, y1: box.y, x2: box.x + box.width, y2: box.y + box.height };
1678
7
  }
1679
- function actorCenter(state, actorType) {
1680
- const rect = actorRect(state, actorType);
1681
- return { x: rect.x1 + (rect.x2 - rect.x1) / 2, y: rect.y1 + (rect.y2 - rect.y1) / 2 };
1682
- }
1683
- function actorRect(state, actorType) {
1684
- const { width, height } = actorSizeByType(actorType);
1685
- const scale2 = Math.max(1e-3, state.scale);
1686
- const scaledWidth = width * scale2;
1687
- const scaledHeight = height * scale2;
1688
- const dx = (scaledWidth - width) / 2;
1689
- const dy = (scaledHeight - height) / 2;
1690
- return {
1691
- x1: state.x - dx,
1692
- y1: state.y - dy,
1693
- x2: state.x + width + dx,
1694
- y2: state.y + height + dy
1695
- };
8
+ function rectCenter(rect) {
9
+ return { x: (rect.x1 + rect.x2) / 2, y: (rect.y1 + rect.y2) / 2 };
1696
10
  }
1697
11
  function inflateRect(rect, pad) {
1698
12
  return {
@@ -1748,69 +62,35 @@ function polylineLength(points) {
1748
62
  function segmentLength(a, b) {
1749
63
  return Math.hypot(b.x - a.x, b.y - a.y);
1750
64
  }
1751
- function rectsOverlap(a, b) {
1752
- return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
1753
- }
1754
- function overlapCount(rect, obstacles) {
1755
- let hits = 0;
1756
- for (const o of obstacles) if (rectsOverlap(rect, o)) hits++;
1757
- return hits;
1758
- }
1759
- var LABEL_BOX_HEIGHT = 18;
1760
- function placeFlowLabel(points, textWidth, obstacles, ast) {
65
+ function labelPointForPath(points, lane) {
66
+ if (points.length < 2) return points[0] ?? { x: 0, y: 0 };
1761
67
  let bestIndex = 0;
1762
68
  let bestLength = -1;
1763
69
  for (let i = 0; i < points.length - 1; i++) {
1764
70
  const len = segmentLength(points[i], points[i + 1]);
1765
71
  if (len > bestLength) {
1766
- bestLength = len;
1767
72
  bestIndex = i;
73
+ bestLength = len;
1768
74
  }
1769
75
  }
1770
- const a = points[bestIndex] ?? { x: 0, y: 0 };
1771
- const b = points[bestIndex + 1] ?? a;
76
+ const a = points[bestIndex];
77
+ const b = points[bestIndex + 1];
1772
78
  const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1773
- const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
1774
- const half = textWidth / 2;
1775
- const halfH = LABEL_BOX_HEIGHT / 2;
1776
- const pad = 8;
1777
- const base = horizontal ? 15 : half + 12;
1778
- const step = horizontal ? LABEL_BOX_HEIGHT + 3 : textWidth + 12;
1779
- const order = horizontal ? [-1, 1] : [1, -1];
1780
- const offsets = [];
1781
- for (let k = 0; k < 7; k++) {
1782
- for (const sign of order) offsets.push(sign * (base + k * step));
1783
- }
1784
- let fallback = null;
1785
- let fallbackHits = Number.POSITIVE_INFINITY;
1786
- for (const off of offsets) {
1787
- let cx = horizontal ? mid.x : mid.x + off;
1788
- let cy = horizontal ? mid.y + off : mid.y;
1789
- cx = clamp(cx, pad + half, ast.meta.width - pad - half);
1790
- cy = clamp(cy, pad + halfH, ast.meta.height - pad - halfH);
1791
- const rect = { x1: cx - half, y1: cy - halfH, x2: cx + half, y2: cy + halfH };
1792
- const hits = overlapCount(rect, obstacles);
1793
- if (hits === 0) return { x: round1(cx), y: round1(cy), rect };
1794
- if (hits < fallbackHits) {
1795
- fallbackHits = hits;
1796
- fallback = { x: round1(cx), y: round1(cy), rect };
1797
- }
79
+ const offset = lane * 8;
80
+ if (Math.abs(a.x - b.x) >= Math.abs(a.y - b.y)) {
81
+ return { x: round1(mid.x), y: round1(mid.y - 10 - offset) };
1798
82
  }
1799
- return fallback ?? {
1800
- x: round1(mid.x),
1801
- y: round1(mid.y),
1802
- rect: { x1: mid.x - half, y1: mid.y - halfH, x2: mid.x + half, y2: mid.y + halfH }
1803
- };
83
+ return { x: round1(mid.x + 10 + offset), y: round1(mid.y) };
1804
84
  }
1805
85
  function clamp(n, min, max) {
1806
86
  if (max < min) return n;
1807
87
  return Math.min(max, Math.max(min, n));
1808
88
  }
1809
- function clampPointToScene(point, ast) {
89
+ function clampPointToScene(point, bounds) {
1810
90
  const pad = 14;
1811
91
  return {
1812
- x: round1(clamp(point.x, pad, ast.meta.width - pad)),
1813
- y: round1(clamp(point.y, pad, ast.meta.height - pad))
92
+ x: round1(clamp(point.x, pad, bounds.width - pad)),
93
+ y: round1(clamp(point.y, pad, bounds.height - pad))
1814
94
  };
1815
95
  }
1816
96
  function routeLength(points) {
@@ -1832,23 +112,14 @@ function routeBends(points) {
1832
112
  }
1833
113
  return bends;
1834
114
  }
1835
- function routeFlowPath(sourceName, targetName, sourceState, targetState, states, ast, lane) {
1836
- const sourceType = ast.actors[sourceName]?.type ?? "box";
1837
- const targetType = ast.actors[targetName]?.type ?? "box";
1838
- const sourceRect = actorRect(sourceState, sourceType);
1839
- const targetRect = actorRect(targetState, targetType);
115
+ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
1840
116
  const laneShift = lane * 18;
1841
- const sourceCenter = actorCenter(sourceState, sourceType);
1842
- const targetCenter = actorCenter(targetState, targetType);
117
+ const sourceCenter = rectCenter(sourceRect);
118
+ const targetCenter = rectCenter(targetRect);
1843
119
  const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
1844
120
  const source = horizontalPrimary ? { x: targetCenter.x >= sourceCenter.x ? sourceRect.x2 : sourceRect.x1, y: sourceCenter.y } : { x: sourceCenter.x, y: targetCenter.y >= sourceCenter.y ? sourceRect.y2 : sourceRect.y1 };
1845
121
  const target = horizontalPrimary ? { x: targetCenter.x >= sourceCenter.x ? targetRect.x1 : targetRect.x2, y: targetCenter.y } : { x: targetCenter.x, y: targetCenter.y >= sourceCenter.y ? targetRect.y1 : targetRect.y2 };
1846
- const obstacles = [];
1847
- for (const [name, state] of states.entries()) {
1848
- if (name === sourceName || name === targetName) continue;
1849
- const type = ast.actors[name]?.type ?? "box";
1850
- obstacles.push(inflateRect(actorRect(state, type), 8));
1851
- }
122
+ const infl = obstacles.map((o) => inflateRect(o, 8));
1852
123
  const candidates = [];
1853
124
  if (Math.abs(source.y - target.y) < 1e-3 || Math.abs(source.x - target.x) < 1e-3) {
1854
125
  candidates.push([source, target]);
@@ -1859,18 +130,18 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
1859
130
  [source, { x: midX, y: source.y }, { x: midX, y: target.y }, target],
1860
131
  [source, { x: source.x, y: midY }, { x: target.x, y: midY }, target]
1861
132
  );
1862
- const minObstacleY = obstacles.length ? Math.min(...obstacles.map((o) => o.y1)) : Math.min(source.y, target.y);
1863
- const maxObstacleY = obstacles.length ? Math.max(...obstacles.map((o) => o.y2)) : Math.max(source.y, target.y);
133
+ const minObstacleY = infl.length ? Math.min(...infl.map((o) => o.y1)) : Math.min(source.y, target.y);
134
+ const maxObstacleY = infl.length ? Math.max(...infl.map((o) => o.y2)) : Math.max(source.y, target.y);
1864
135
  const topLane = Math.max(16, minObstacleY - 24 - lane * 12);
1865
- const bottomLane = Math.min(ast.meta.height - 16, maxObstacleY + 24 + lane * 12);
136
+ const bottomLane = Math.min(bounds.height - 16, maxObstacleY + 24 + lane * 12);
1866
137
  candidates.push(
1867
138
  [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
1868
139
  [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
1869
140
  );
1870
- const minObstacleX = obstacles.length ? Math.min(...obstacles.map((o) => o.x1)) : Math.min(source.x, target.x);
1871
- const maxObstacleX = obstacles.length ? Math.max(...obstacles.map((o) => o.x2)) : Math.max(source.x, target.x);
141
+ const minObstacleX = infl.length ? Math.min(...infl.map((o) => o.x1)) : Math.min(source.x, target.x);
142
+ const maxObstacleX = infl.length ? Math.max(...infl.map((o) => o.x2)) : Math.max(source.x, target.x);
1872
143
  const leftLane = Math.max(16, minObstacleX - 24 - lane * 12);
1873
- const rightLane = Math.min(ast.meta.width - 16, maxObstacleX + 24 + lane * 12);
144
+ const rightLane = Math.min(bounds.width - 16, maxObstacleX + 24 + lane * 12);
1874
145
  candidates.push(
1875
146
  [source, { x: leftLane, y: source.y }, { x: leftLane, y: target.y }, target],
1876
147
  [source, { x: rightLane, y: source.y }, { x: rightLane, y: target.y }, target]
@@ -1878,28 +149,32 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
1878
149
  let best = candidates[0];
1879
150
  let bestScore = Number.POSITIVE_INFINITY;
1880
151
  for (const candidate of candidates) {
1881
- const hits = countPathIntersections(candidate, obstacles);
152
+ const hits = countPathIntersections(candidate, infl);
1882
153
  const score = hits * 1e5 + routeBends(candidate) * 800 + routeLength(candidate);
1883
154
  if (score < bestScore) {
1884
155
  best = candidate;
1885
156
  bestScore = score;
1886
157
  }
1887
158
  }
1888
- return best.map((point) => clampPointToScene(point, ast));
159
+ return best.map((point) => clampPointToScene(point, bounds));
1889
160
  }
1890
161
 
1891
- // src/actions/flow.ts
162
+ // src/edges.ts
1892
163
  var EDGE_LAYER_ATTR = "data-markdy-edge-layer";
1893
- var STROKE_BY_ACTION = {
1894
- request: "#38bdf8",
1895
- response: "#a78bfa",
1896
- emit: "#f59e0b"
164
+ var EDGE_STYLES = {
165
+ request: { dash: "", marker: "arrow" },
166
+ response: { dash: "6 4", marker: "arrow-open" },
167
+ event: { dash: "2 6", marker: "dot" },
168
+ dependency: { dash: "4 4", marker: "none" }
1897
169
  };
1898
- var DEFAULT_STROKE = "#38bdf8";
1899
- var LABEL_MAX_CHARS = 28;
1900
- var LABEL_CHAR_PX = 6.8;
1901
- var EDGE_REVEAL_MS = 220;
1902
- var LABEL_RECTS = /* @__PURE__ */ new WeakMap();
170
+ function dedupePoints(points) {
171
+ const out = [];
172
+ for (const p of points) {
173
+ const last = out[out.length - 1];
174
+ if (!last || Math.abs(last.x - p.x) > 0.01 || Math.abs(last.y - p.y) > 0.01) out.push(p);
175
+ }
176
+ return out.length >= 2 ? out : points;
177
+ }
1903
178
  function ensureEdgeLayer(scene) {
1904
179
  const existing = scene.querySelector(`svg[${EDGE_LAYER_ATTR}='1']`);
1905
180
  if (existing) return existing;
@@ -1917,412 +192,322 @@ function ensureEdgeLayer(scene) {
1917
192
  scene.appendChild(svg);
1918
193
  return svg;
1919
194
  }
1920
- function ensureEdgeDefs(svg) {
1921
- if (svg.querySelector("defs[data-markdy-flow-defs='1']")) return;
195
+ function ensureDefs(svg, theme, id) {
196
+ const key = `data-markdy-defs-${id}`;
197
+ if (svg.querySelector(`defs[${key}='1']`)) return;
1922
198
  const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1923
- defs.setAttribute("data-markdy-flow-defs", "1");
1924
- const glow2 = document.createElementNS("http://www.w3.org/2000/svg", "filter");
1925
- glow2.setAttribute("id", "markdy-flow-glow");
1926
- glow2.setAttribute("x", "-40%");
1927
- glow2.setAttribute("y", "-40%");
1928
- glow2.setAttribute("width", "180%");
1929
- glow2.setAttribute("height", "180%");
1930
- const blur2 = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
1931
- blur2.setAttribute("stdDeviation", "3");
1932
- blur2.setAttribute("result", "coloredBlur");
1933
- const merge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
1934
- const glowNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1935
- glowNode.setAttribute("in", "coloredBlur");
1936
- const sourceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1937
- sourceNode.setAttribute("in", "SourceGraphic");
1938
- merge.append(glowNode, sourceNode);
1939
- glow2.append(blur2, merge);
1940
- const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
1941
- arrow.setAttribute("id", "markdy-flow-arrow");
1942
- arrow.setAttribute("viewBox", "0 0 10 10");
1943
- arrow.setAttribute("refX", "8");
1944
- arrow.setAttribute("refY", "5");
1945
- arrow.setAttribute("markerWidth", "6");
1946
- arrow.setAttribute("markerHeight", "6");
1947
- arrow.setAttribute("orient", "auto-start-reverse");
1948
- const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
1949
- arrowPath.setAttribute("d", "M 1 1 L 9 5 L 1 9 z");
1950
- arrowPath.setAttribute("fill", "context-stroke");
1951
- arrow.appendChild(arrowPath);
1952
- defs.append(glow2, arrow);
199
+ defs.setAttribute(key, "1");
200
+ for (const [kind, color] of Object.entries(theme.edges)) {
201
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
202
+ arrow.setAttribute("id", `${id}-arrow-${kind}`);
203
+ arrow.setAttribute("viewBox", "0 0 10 10");
204
+ arrow.setAttribute("refX", "8");
205
+ arrow.setAttribute("refY", "5");
206
+ arrow.setAttribute("markerWidth", "6");
207
+ arrow.setAttribute("markerHeight", "6");
208
+ arrow.setAttribute("orient", "auto-start-reverse");
209
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
210
+ path.setAttribute("d", "M 1 1 L 9 5 L 1 9 z");
211
+ path.setAttribute("fill", color);
212
+ arrow.appendChild(path);
213
+ defs.appendChild(arrow);
214
+ }
1953
215
  svg.prepend(defs);
1954
216
  }
1955
- function isDashed(ctx) {
1956
- const style = String(ctx.ev.params.style ?? "");
1957
- return style === "dashed" || style === "fire_and_forget" || ctx.ev.action === "response";
1958
- }
1959
- function nodeObstacleRect(state, type, sceneWidth) {
1960
- const rect = actorRect(state, type);
1961
- if (type !== "caption") return rect;
1962
- const halfH = (rect.y2 - rect.y1) / 2 + 4;
1963
- return { x1: 0, y1: state.y - halfH, x2: sceneWidth, y2: state.y + halfH };
1964
- }
1965
- function buildEdge(ctx, targetName) {
1966
- const { ev, state, states, ast, scene, baseOpts, anims } = ctx;
1967
- const targetState = states.get(targetName);
1968
- if (!targetState) return;
1969
- const lane = ev.line % 5 - 2;
1970
- const points = routeFlowPath(ev.actor, targetName, state, targetState, states, ast, lane);
1971
- const length = polylineLength(points);
1972
- const pathD = toPathD(points);
1973
- const stroke = STROKE_BY_ACTION[ev.action] ?? DEFAULT_STROKE;
1974
- const svg = ensureEdgeLayer(scene);
1975
- ensureEdgeDefs(svg);
1976
- const startMs = Number(baseOpts.delay ?? 0);
1977
- const durationMs = Math.max(1, Number(baseOpts.duration ?? 500));
1978
- const revealMs = Math.min(EDGE_REVEAL_MS, durationMs);
217
+ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, obstacles, bounds) {
218
+ ensureDefs(svg, theme, sceneId);
219
+ const color = theme.edges[kind];
220
+ const style = EDGE_STYLES[kind];
221
+ const points = dedupePoints(routeOrthogonal(boxRect(from), boxRect(to), obstacles, bounds));
222
+ const d = toPathD(points);
223
+ const len = polylineLength(points);
1979
224
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
1980
- group.setAttribute("data-markdy-flow-edge", "1");
225
+ group.setAttribute("data-edge-kind", kind);
1981
226
  group.style.opacity = "0";
1982
227
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1983
- path.setAttribute("d", pathD);
228
+ path.setAttribute("d", d);
1984
229
  path.setAttribute("fill", "none");
1985
- path.setAttribute("stroke", stroke);
1986
- path.setAttribute("stroke-width", "3");
1987
- path.setAttribute("stroke-linecap", "round");
230
+ path.setAttribute("stroke", color);
231
+ path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2.5");
1988
232
  path.setAttribute("stroke-linejoin", "round");
1989
- path.setAttribute("marker-end", "url(#markdy-flow-arrow)");
1990
- path.setAttribute("filter", "url(#markdy-flow-glow)");
1991
- path.setAttribute("data-markdy-flow-action", ev.action);
1992
- path.style.strokeDasharray = isDashed(ctx) ? "8 6" : `${length}`;
1993
- path.style.strokeDashoffset = `${length}`;
1994
- group.appendChild(path);
1995
- const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
1996
- marker.setAttribute("r", "4.5");
1997
- marker.setAttribute("fill", stroke);
1998
- marker.setAttribute("filter", "url(#markdy-flow-glow)");
1999
- marker.style.offsetPath = `path('${pathD}')`;
2000
- marker.style.offsetDistance = "0%";
2001
- marker.style.opacity = "0";
2002
- group.appendChild(marker);
2003
- const labelText = String(ev.params.label ?? "");
2004
- if (labelText) {
2005
- const shown = labelText.length > LABEL_MAX_CHARS ? `${labelText.slice(0, LABEL_MAX_CHARS - 1)}\u2026` : labelText;
2006
- const textWidth = shown.length * LABEL_CHAR_PX + 12;
2007
- const placed = LABEL_RECTS.get(svg) ?? (LABEL_RECTS.set(svg, []), LABEL_RECTS.get(svg));
2008
- const obstacles = [...placed];
2009
- for (const [name, nodeState] of states.entries()) {
2010
- const type = ast.actors[name]?.type ?? "box";
2011
- obstacles.push(inflateRect(nodeObstacleRect(nodeState, type, ast.meta.width), 2));
2012
- }
2013
- const spot = placeFlowLabel(points, textWidth, obstacles, ast);
2014
- placed.push(spot.rect);
2015
- const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
2016
- label.setAttribute("x", `${spot.x}`);
2017
- label.setAttribute("y", `${spot.y}`);
2018
- label.setAttribute("text-anchor", "middle");
2019
- label.setAttribute("dominant-baseline", "middle");
2020
- label.setAttribute("font-size", "12");
2021
- label.setAttribute("font-weight", "700");
2022
- label.setAttribute("paint-order", "stroke");
2023
- label.setAttribute("stroke", "rgba(2, 6, 23, 0.9)");
2024
- label.setAttribute("stroke-width", "5");
2025
- label.setAttribute("stroke-linejoin", "round");
2026
- label.setAttribute("fill", "#dbe4f0");
2027
- label.setAttribute("data-full-label", labelText);
2028
- label.textContent = shown;
2029
- group.appendChild(label);
233
+ path.setAttribute("stroke-linecap", "round");
234
+ if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
235
+ if (style.marker !== "none") {
236
+ path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
237
+ }
238
+ path.style.filter = `drop-shadow(0 0 4px ${color}55)`;
239
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
240
+ dot.setAttribute("r", "4");
241
+ dot.setAttribute("fill", color);
242
+ dot.style.opacity = "0";
243
+ dot.style.filter = `drop-shadow(0 0 6px ${color})`;
244
+ group.append(path, dot);
245
+ let labelEl;
246
+ if (label) {
247
+ const mid = labelPointForPath(points, 0);
248
+ labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
249
+ labelEl.setAttribute("x", String(mid.x));
250
+ labelEl.setAttribute("y", String(mid.y));
251
+ labelEl.setAttribute("text-anchor", "middle");
252
+ labelEl.setAttribute("font-size", "11");
253
+ labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
254
+ labelEl.setAttribute("fill", theme.textMuted);
255
+ labelEl.textContent = label;
256
+ labelEl.style.opacity = "0";
257
+ group.appendChild(labelEl);
2030
258
  }
2031
259
  svg.appendChild(group);
2032
- anims.push(
2033
- path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts),
2034
- // Travel the dot along the edge, then fade it at arrival so the finished
2035
- // edge is a clean line + arrowhead rather than a dot resting on the tip.
2036
- marker.animate(
2037
- [
2038
- { offsetDistance: "0%", opacity: 1, offset: 0 },
2039
- { offsetDistance: "100%", opacity: 1, offset: 0.82 },
2040
- { offsetDistance: "100%", opacity: 0, offset: 1 }
2041
- ],
2042
- baseOpts
2043
- ),
2044
- // Reveal the edge at its scheduled time and keep it (persist). `fill: both`
2045
- // holds opacity 0 during the delay, so nothing appears before its turn.
2046
- group.animate([{ opacity: 0 }, { opacity: 1 }], {
2047
- delay: startMs,
2048
- duration: revealMs,
2049
- fill: "both"
2050
- })
2051
- );
2052
- }
2053
- var flowEdge = (ctx) => {
2054
- const targetName = String(ctx.ev.params.to ?? "");
2055
- if (targetName) buildEdge(ctx, targetName);
2056
- };
2057
-
2058
- // src/theme.ts
2059
- var DARK_LUMINANCE_THRESHOLD = 140;
2060
- var NAMED_DARK = {
2061
- black: true,
2062
- "#000": true,
2063
- "#000000": true
2064
- };
2065
- function isSceneDark(scene) {
2066
- const bg = scene.style.background || "white";
2067
- let hex = bg.trim().replace(/^#/, "");
2068
- if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
2069
- if (hex.length === 6) {
2070
- const r = parseInt(hex.slice(0, 2), 16);
2071
- const g = parseInt(hex.slice(2, 4), 16);
2072
- const b = parseInt(hex.slice(4, 6), 16);
2073
- return 0.299 * r + 0.587 * g + 0.114 * b <= DARK_LUMINANCE_THRESHOLD;
2074
- }
2075
- return NAMED_DARK[bg.toLowerCase()] ?? false;
2076
- }
2077
- function speechBubbleTheme(dark) {
2078
- return dark ? {
2079
- background: "#1e2530",
2080
- border: "#475569",
2081
- text: "#e2e8f0",
2082
- shadow: "0 2px 8px rgba(0,0,0,0.35)"
2083
- } : {
2084
- background: "white",
2085
- border: "#222",
2086
- text: "#222",
2087
- shadow: "0 2px 8px rgba(0,0,0,0.12)"
2088
- };
2089
- }
2090
-
2091
- // src/actions/speech.ts
2092
- var MAX_FADE_MS = 200;
2093
- var FADE_FRACTION = 0.15;
2094
- var say = ({ ev, el, state, delayMs, durMs, scene, anims }) => {
2095
- const text = String(ev.params.text ?? "");
2096
- const theme = speechBubbleTheme(isSceneDark(scene));
2097
- const inverseScale = 1 / (state.scale || 1);
2098
- const bubble = document.createElement("div");
2099
- bubble.textContent = text;
2100
- Object.assign(bubble.style, {
2101
- position: "absolute",
2102
- bottom: "calc(100% + 10px)",
2103
- left: "50%",
2104
- transform: `translateX(-50%) scale(${inverseScale})`,
2105
- transformOrigin: "center bottom",
2106
- background: theme.background,
2107
- border: `2px solid ${theme.border}`,
2108
- color: theme.text,
2109
- borderRadius: "12px",
2110
- padding: "6px 14px",
2111
- fontFamily: "system-ui, sans-serif",
2112
- fontSize: "15px",
2113
- lineHeight: "1.3",
2114
- whiteSpace: "nowrap",
2115
- maxWidth: "220px",
2116
- overflow: "hidden",
2117
- textOverflow: "ellipsis",
2118
- pointerEvents: "none",
2119
- zIndex: "10",
2120
- boxShadow: theme.shadow,
2121
- opacity: "0"
2122
- });
2123
- const tail = document.createElement("span");
2124
- Object.assign(tail.style, {
2125
- position: "absolute",
2126
- bottom: "-10px",
2127
- left: "50%",
2128
- transform: "translateX(-50%)",
2129
- width: "0",
2130
- height: "0",
2131
- borderLeft: "7px solid transparent",
2132
- borderRight: "7px solid transparent",
2133
- borderTop: `10px solid ${theme.border}`
260
+ return { group, path, label: labelEl, dot, pathLen: len, points };
261
+ }
262
+ function dotTravelKeyframes(points) {
263
+ const total = polylineLength(points);
264
+ const frames = [];
265
+ let acc = 0;
266
+ let lastOffset = -1;
267
+ points.forEach((p, i) => {
268
+ if (i > 0) acc += Math.hypot(p.x - points[i - 1].x, p.y - points[i - 1].y);
269
+ let offset = total > 0 ? acc / total : i === points.length - 1 ? 1 : 0;
270
+ if (offset <= lastOffset) offset = Math.min(1, lastOffset + 1e-4);
271
+ lastOffset = offset;
272
+ frames.push({
273
+ offset,
274
+ transform: `translate(${p.x}px, ${p.y}px)`,
275
+ opacity: i === 0 || i === points.length - 1 ? 0 : 1
276
+ });
2134
277
  });
2135
- bubble.appendChild(tail);
2136
- el.style.overflow = "visible";
2137
- el.appendChild(bubble);
2138
- const fadeMs = Math.min(MAX_FADE_MS, durMs * FADE_FRACTION);
278
+ return frames;
279
+ }
280
+ function animateEdgeReveal(runtime, startMs, durMs) {
281
+ const anims = [];
282
+ const { path, dot, label, group, pathLen, points } = runtime;
283
+ path.style.strokeDasharray = String(pathLen);
284
+ path.style.strokeDashoffset = String(pathLen);
285
+ const drawMs = Math.min(220, durMs * 0.5);
2139
286
  anims.push(
2140
- bubble.animate([{ opacity: 0 }, { opacity: 1 }], {
2141
- delay: delayMs,
2142
- duration: fadeMs,
2143
- fill: "forwards"
2144
- }),
2145
- bubble.animate([{ opacity: 1 }, { opacity: 0 }], {
2146
- delay: delayMs + durMs - fadeMs,
2147
- duration: fadeMs,
2148
- fill: "forwards"
2149
- })
287
+ group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 120, delay: startMs, fill: "forwards" }),
288
+ path.animate(
289
+ [{ strokeDashoffset: pathLen }, { strokeDashoffset: 0 }],
290
+ { duration: drawMs, delay: startMs, fill: "forwards", easing: "ease-out" }
291
+ )
2150
292
  );
2151
- };
2152
-
2153
- // src/actions/projectile.ts
2154
- var PROJECTILE_SIZE_PX = 32;
2155
- function createProjectile(assetName, assetDef, assetOverrides) {
2156
- if (assetDef.type === "image") {
2157
- const img = document.createElement("img");
2158
- img.src = assetOverrides[assetName] ?? assetDef.value;
2159
- img.alt = assetName;
2160
- img.setAttribute("draggable", "false");
2161
- img.style.width = `${PROJECTILE_SIZE_PX}px`;
2162
- img.style.height = `${PROJECTILE_SIZE_PX}px`;
2163
- return img;
293
+ if (label) {
294
+ anims.push(label.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
295
+ }
296
+ if (points.length >= 2) {
297
+ anims.push(
298
+ dot.animate(dotTravelKeyframes(points), {
299
+ duration: Math.max(drawMs, durMs),
300
+ delay: startMs,
301
+ fill: "forwards",
302
+ easing: "ease-in-out"
303
+ })
304
+ );
2164
305
  }
2165
- const span = document.createElement("span");
2166
- span.className = "iconify";
2167
- span.dataset.icon = assetDef.value;
2168
- span.style.fontSize = `${PROJECTILE_SIZE_PX}px`;
2169
- span.style.lineHeight = "1";
2170
- span.style.display = "inline-block";
2171
- return span;
2172
- }
2173
- var throwAsset = ({
2174
- ev,
2175
- state,
2176
- baseOpts,
2177
- ast,
2178
- states,
2179
- scene,
2180
- assetOverrides,
2181
- anims
2182
- }) => {
2183
- const assetName = String(ev.params.asset ?? "");
2184
- const targetState = states.get(String(ev.params.to ?? ""));
2185
- const assetDef = ast.assets[assetName];
2186
- if (!assetDef || !targetState) return;
2187
- const projectile = createProjectile(assetName, assetDef, assetOverrides);
2188
- Object.assign(projectile.style, {
2189
- position: "absolute",
2190
- left: "0",
2191
- top: "0",
2192
- pointerEvents: "none",
2193
- zIndex: "9",
2194
- opacity: "0"
2195
- });
2196
- scene.appendChild(projectile);
2197
- const anim = projectile.animate(
2198
- [
2199
- { transform: tx(state), opacity: 1 },
2200
- { transform: tx(targetState), opacity: 0 }
2201
- ],
2202
- { ...baseOpts, easing: "ease-in" }
2203
- );
2204
- anim.addEventListener("finish", () => {
2205
- if (projectile.parentNode === scene) scene.removeChild(projectile);
2206
- });
2207
- anims.push(anim);
2208
- };
2209
-
2210
- // src/actions/registry.ts
2211
- var ACTION_HANDLERS = Object.freeze({
2212
- // Universal — every actor type
2213
- move,
2214
- enter,
2215
- exit,
2216
- fade_in: fadeIn,
2217
- fade_out: fadeOut,
2218
- spring,
2219
- follow_path: followPath,
2220
- scale,
2221
- rotate,
2222
- shake,
2223
- pulse,
2224
- glow,
2225
- ripple,
2226
- blur,
2227
- line_reveal: lineReveal,
2228
- mask,
2229
- parallax,
2230
- jump,
2231
- bounce,
2232
- say,
2233
- throw: throwAsset,
2234
- // Figure-only — the parser rejects these on other actor types
2235
- punch,
2236
- kick,
2237
- wave,
2238
- nod,
2239
- rotate_part: rotatePart,
2240
- pose,
2241
- face,
2242
- // System-diagram flow edges (@markdy/stdlib-systems)
2243
- request: flowEdge,
2244
- response: flowEdge,
2245
- emit: flowEdge
2246
- });
2247
- function handlerFor(action) {
2248
- return ACTION_HANDLERS[action];
306
+ return anims;
2249
307
  }
2250
-
2251
- // src/animations.ts
2252
- var DEFAULT_DURATION_S = 0.5;
2253
- var DEFAULT_EASE_BY_ACTION = {
2254
- enter: "out",
2255
- fade_in: "out",
2256
- exit: "in",
2257
- fade_out: "in"
2258
- };
2259
- function buildAnimations(ast, actorEls, actorLayer, cameraLayer, assetOverrides, faceSwaps) {
308
+ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds) {
2260
309
  const anims = [];
2261
- const states = /* @__PURE__ */ new Map();
2262
- for (const [name, def] of Object.entries(ast.actors)) {
2263
- states.set(name, stateFrom(def));
2264
- }
2265
- const captionActors = new Set(
2266
- Object.entries(ast.actors).filter(([, def]) => def.type === "caption").map(([name]) => name)
310
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
311
+ const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
312
+ const svg = ensureEdgeLayer(scene);
313
+ const sceneId = `md-${Math.random().toString(36).slice(2, 8)}`;
314
+ anims.push(
315
+ titleEl.animate(
316
+ [{ opacity: 0, transform: "translateY(-6px)" }, { opacity: 1, transform: "translateY(0)" }],
317
+ { duration: 420, delay: 0, fill: "forwards", easing: "ease-out" }
318
+ )
2267
319
  );
2268
- const txFor = (actorName) => captionActors.has(actorName) ? txCaption : tx;
2269
- const events = [...ast.events].sort((a, b) => a.time - b.time);
2270
- preInitInlineStyles(ast, actorEls, states, events, txFor);
2271
- const cameraState = freshCameraState();
2272
- for (const ev of events) {
2273
- const delayMs = ev.time * 1e3;
2274
- const durMs = Math.max(
2275
- 1,
2276
- (typeof ev.params.dur === "number" ? ev.params.dur : DEFAULT_DURATION_S) * 1e3
2277
- );
2278
- const baseOpts = {
2279
- delay: delayMs,
2280
- duration: durMs,
2281
- fill: "forwards",
2282
- easing: toEasing(ev.params.ease ?? DEFAULT_EASE_BY_ACTION[ev.action])
2283
- };
2284
- if (ev.actor === "camera") {
2285
- buildCameraAction(ev, cameraLayer, ast, baseOpts, anims, cameraState);
320
+ for (const cue of cues) {
321
+ const startMs = cue.start * 1e3;
322
+ const durMs = cue.duration * 1e3;
323
+ if (cue.kind === "show") {
324
+ cue.targets.forEach((id, idx) => {
325
+ const el = nodeEls.get(id);
326
+ if (!el) return;
327
+ const delay = startMs + (typeof cue.params.stagger === "number" ? cue.params.stagger * 1e3 * idx : 0);
328
+ anims.push(
329
+ el.animate(
330
+ [{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "translateY(0)" }],
331
+ { duration: durMs, delay, fill: "forwards", easing: "ease-out" }
332
+ )
333
+ );
334
+ });
335
+ continue;
336
+ }
337
+ if (cue.kind === "hide") {
338
+ for (const id of cue.targets) {
339
+ const el = nodeEls.get(id);
340
+ if (!el) continue;
341
+ anims.push(el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: durMs, delay: startMs, fill: "forwards" }));
342
+ }
343
+ continue;
344
+ }
345
+ if (cue.kind === "glow") {
346
+ for (const id of cue.targets) {
347
+ const el = nodeEls.get(id);
348
+ if (!el) continue;
349
+ if (cue.params.color) el.style.setProperty("--md-glow-color", String(cue.params.color));
350
+ el.dataset.glow = "1";
351
+ anims.push(
352
+ el.animate(
353
+ [{ filter: "brightness(1)" }, { filter: "brightness(1.15)" }, { filter: "brightness(1)" }],
354
+ { duration: durMs, delay: startMs, fill: "forwards" }
355
+ )
356
+ );
357
+ }
2286
358
  continue;
2287
359
  }
2288
- const el = actorEls.get(ev.actor);
2289
- const state = states.get(ev.actor);
2290
- const handler = handlerFor(ev.action);
2291
- if (!el || !state || !handler) continue;
2292
- const ctx = {
2293
- ev,
2294
- el,
2295
- state,
2296
- baseOpts,
2297
- delayMs,
2298
- durMs,
2299
- ast,
2300
- states,
2301
- actorEls,
2302
- scene: actorLayer,
2303
- assetOverrides,
2304
- faceSwaps,
2305
- anims,
2306
- tx: txFor(ev.actor)
2307
- };
2308
- handler(ctx);
360
+ if (cue.kind === "focus") {
361
+ for (const id of cue.targets) {
362
+ const el = nodeEls.get(id);
363
+ if (!el) continue;
364
+ el.dataset.focused = "1";
365
+ anims.push(
366
+ el.animate(
367
+ [{ transform: "scale(1)" }, { transform: "scale(1.03)" }, { transform: "scale(1)" }],
368
+ { duration: durMs, delay: startMs, fill: "forwards" }
369
+ )
370
+ );
371
+ }
372
+ continue;
373
+ }
374
+ if (cue.kind === "flow" && cue.segments?.[0]) {
375
+ const seg = cue.segments[0];
376
+ const from = nodeById.get(seg.from);
377
+ const to = nodeById.get(seg.to);
378
+ if (!from || !to) continue;
379
+ const obstacles = [];
380
+ for (const [id, rect] of rectById) {
381
+ if (id !== seg.from && id !== seg.to) obstacles.push(rect);
382
+ }
383
+ const runtime = createEdgeRuntime(svg, from, to, seg.op, seg.label, theme, sceneId, obstacles, bounds);
384
+ anims.push(...animateEdgeReveal(runtime, startMs, durMs));
385
+ }
2309
386
  }
387
+ for (const a of anims) a.pause();
2310
388
  return anims;
2311
389
  }
2312
390
 
2313
- // src/player.ts
391
+ // src/nodes.ts
392
+ var STYLE_ID = "markdy-diagram-node-styles";
393
+ function ensureNodeStyles(doc) {
394
+ if (doc.getElementById(STYLE_ID)) return;
395
+ const style = doc.createElement("style");
396
+ style.id = STYLE_ID;
397
+ style.textContent = `
398
+ .markdy-node {
399
+ position: absolute;
400
+ box-sizing: border-box;
401
+ width: var(--md-node-w, 184px);
402
+ height: var(--md-node-h, 88px);
403
+ border-radius: 16px;
404
+ border: 1px solid var(--md-border);
405
+ background: linear-gradient(145deg, var(--md-surface-raised), var(--md-surface));
406
+ color: var(--md-text);
407
+ box-shadow: 0 12px 32px rgba(2, 6, 23, 0.28), 0 0 0 1px rgba(255,255,255,0.04) inset;
408
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
409
+ overflow: hidden;
410
+ opacity: 0;
411
+ transform: translateY(8px);
412
+ transition: box-shadow 0.3s ease;
413
+ }
414
+ .markdy-node[data-visible="1"] {
415
+ opacity: 1;
416
+ transform: translateY(0);
417
+ }
418
+ .markdy-node[data-focused="1"] {
419
+ box-shadow: 0 0 0 2px var(--md-accent), 0 16px 40px rgba(2, 6, 23, 0.35);
420
+ }
421
+ .markdy-node[data-glow="1"] {
422
+ box-shadow: 0 0 24px var(--md-glow-color, var(--md-accent)), 0 0 0 1px var(--md-glow-color, var(--md-accent)) inset;
423
+ }
424
+ .markdy-node__rail {
425
+ position: absolute;
426
+ left: 0;
427
+ top: 0;
428
+ bottom: 0;
429
+ width: 4px;
430
+ background: var(--md-role-color, var(--md-accent));
431
+ border-radius: 16px 0 0 16px;
432
+ }
433
+ .markdy-node__type {
434
+ padding: 10px 14px 0 18px;
435
+ font-size: 10px;
436
+ font-weight: 700;
437
+ letter-spacing: 0.08em;
438
+ text-transform: uppercase;
439
+ color: var(--md-text-muted);
440
+ }
441
+ .markdy-node__label {
442
+ padding: 2px 14px 0 18px;
443
+ font-size: 15px;
444
+ font-weight: 650;
445
+ line-height: 1.25;
446
+ white-space: nowrap;
447
+ overflow: hidden;
448
+ text-overflow: ellipsis;
449
+ }
450
+ .markdy-node[data-role="client"] { border-radius: 20px 20px 8px 8px; }
451
+ .markdy-node[data-role="data"] { border-radius: 16px 16px 28px 28px; }
452
+ .markdy-node[data-role="flow"] { transform: rotate(0deg); clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
453
+ .markdy-node[data-role="network"] { clip-path: polygon(8% 0%, 92% 0%, 100% 50%, 92% 100%, 8% 100%, 0% 50%); }
454
+ .markdy-scene-title {
455
+ position: absolute;
456
+ left: 64px;
457
+ top: 28px;
458
+ right: 64px;
459
+ font-size: 32px;
460
+ font-weight: 700;
461
+ line-height: 1.2;
462
+ color: var(--md-text);
463
+ opacity: 0;
464
+ transform: translateY(-6px);
465
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
466
+ }
467
+ .markdy-scene-title[data-visible="1"] {
468
+ opacity: 1;
469
+ transform: translateY(0);
470
+ }
471
+ `;
472
+ doc.head.appendChild(style);
473
+ }
474
+ function createNodeEl(node, theme) {
475
+ const el = document.createElement("div");
476
+ el.className = "markdy-node markdy-scene-actor";
477
+ el.dataset.actor = node.id;
478
+ el.dataset.role = node.role;
479
+ el.style.left = `${node.x}px`;
480
+ el.style.top = `${node.y}px`;
481
+ el.style.setProperty("--md-node-w", `${node.width}px`);
482
+ el.style.setProperty("--md-node-h", `${node.height}px`);
483
+ const roleColor = theme.roles[node.role] ?? theme.accent;
484
+ el.style.setProperty("--md-role-color", roleColor);
485
+ const rail = document.createElement("div");
486
+ rail.className = "markdy-node__rail";
487
+ const type = document.createElement("div");
488
+ type.className = "markdy-node__type";
489
+ type.textContent = node.kind.replace(/_/g, " ");
490
+ const label = document.createElement("div");
491
+ label.className = "markdy-node__label";
492
+ label.textContent = node.label;
493
+ el.append(rail, type, label);
494
+ return el;
495
+ }
496
+ function createTitleEl(title) {
497
+ const el = document.createElement("div");
498
+ el.className = "markdy-scene-title";
499
+ el.textContent = title;
500
+ return el;
501
+ }
502
+
503
+ // src/theme.ts
2314
504
  var SCENE_STYLE_ID = "markdy-scene-ambience-styles";
2315
- var SAFE_FRAME_PADDING = 28;
2316
505
  function ensureSceneStyles(doc) {
2317
506
  if (doc.getElementById(SCENE_STYLE_ID)) return;
2318
507
  const style = doc.createElement("style");
2319
508
  style.id = SCENE_STYLE_ID;
2320
509
  style.textContent = `
2321
510
  .markdy-scene-root {
2322
- --markdy-scene-grid: rgba(148, 163, 184, 0.13);
2323
- --markdy-scene-grid-strong: rgba(148, 163, 184, 0.2);
2324
- --markdy-scene-vignette: rgba(2, 6, 23, 0.72);
2325
- --markdy-scene-accent: rgba(56, 189, 248, 0.16);
2326
511
  isolation: isolate;
2327
512
  }
2328
513
  .markdy-scene-root::before,
@@ -2335,149 +520,69 @@ function ensureSceneStyles(doc) {
2335
520
  .markdy-scene-root::before {
2336
521
  z-index: 0;
2337
522
  background:
2338
- linear-gradient(var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2339
- linear-gradient(90deg, var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2340
- linear-gradient(var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px,
2341
- linear-gradient(90deg, var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px;
523
+ linear-gradient(var(--md-grid-minor) 1px, transparent 1px) 0 0 / 32px 32px,
524
+ linear-gradient(90deg, var(--md-grid-minor) 1px, transparent 1px) 0 0 / 32px 32px,
525
+ linear-gradient(var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px,
526
+ linear-gradient(90deg, var(--md-grid-major) 1px, transparent 1px) 0 0 / 160px 160px;
2342
527
  mask-image: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
2343
528
  opacity: 0.82;
2344
529
  }
2345
530
  .markdy-scene-root::after {
2346
531
  z-index: 1;
2347
532
  background:
2348
- radial-gradient(ellipse at 50% 0%, var(--markdy-scene-accent), transparent 42%),
2349
- linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--markdy-scene-vignette) 100%),
2350
- linear-gradient(90deg, rgba(255, 255, 255, 0.035), transparent 18%, transparent 82%, rgba(255, 255, 255, 0.035));
2351
- mix-blend-mode: screen;
533
+ radial-gradient(ellipse at 50% 0%, color-mix(in srgb, var(--md-accent) 16%, transparent), transparent 42%),
534
+ linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--md-vignette) 100%);
2352
535
  opacity: 0.8;
2353
536
  }
2354
- .markdy-scene-root[data-markdy-scene-tone="light"] {
2355
- --markdy-scene-grid: rgba(15, 23, 42, 0.09);
2356
- --markdy-scene-grid-strong: rgba(15, 23, 42, 0.14);
2357
- --markdy-scene-vignette: rgba(241, 245, 249, 0.74);
2358
- --markdy-scene-accent: rgba(14, 165, 233, 0.12);
2359
- }
2360
- .markdy-scene-content {
2361
- z-index: 2;
2362
- }
537
+ .markdy-scene-content { z-index: 2; }
2363
538
  .markdy-scene-actor-layer {
2364
539
  position: absolute;
2365
540
  inset: 0;
2366
541
  overflow: visible;
2367
- transform-origin: 0 0;
2368
- will-change: transform;
2369
542
  }
2370
543
  `;
2371
544
  doc.head.appendChild(style);
2372
545
  }
2373
- function unionRect(a, b) {
2374
- if (!a) return b;
2375
- return {
2376
- x1: Math.min(a.x1, b.x1),
2377
- y1: Math.min(a.y1, b.y1),
2378
- x2: Math.max(a.x2, b.x2),
2379
- y2: Math.max(a.y2, b.y2)
2380
- };
2381
- }
2382
- function numericPair(value) {
2383
- if (!Array.isArray(value) || value.length < 2) return null;
2384
- const [x, y] = value;
2385
- return typeof x === "number" && typeof y === "number" ? [x, y] : null;
2386
- }
2387
- function computeContentBounds(ast) {
2388
- let bounds = null;
2389
- const maxScaleByActor = /* @__PURE__ */ new Map();
2390
- const positionsByActor = /* @__PURE__ */ new Map();
2391
- for (const [name, def] of Object.entries(ast.actors)) {
2392
- maxScaleByActor.set(name, Math.max(1e-3, def.scale ?? 1));
2393
- positionsByActor.set(name, [[def.x, def.y]]);
2394
- }
2395
- for (const ev of ast.events) {
2396
- if (ev.actor === "camera") continue;
2397
- if (ev.action === "scale" && typeof ev.params.to === "number") {
2398
- maxScaleByActor.set(ev.actor, Math.max(maxScaleByActor.get(ev.actor) ?? 1, ev.params.to));
2399
- }
2400
- if (ev.action === "move" || ev.action === "spring") {
2401
- const to = numericPair(ev.params.to);
2402
- if (to) positionsByActor.get(ev.actor)?.push(to);
2403
- }
2404
- }
2405
- for (const [name, def] of Object.entries(ast.actors)) {
2406
- const positions = positionsByActor.get(name) ?? [[def.x, def.y]];
2407
- const scale2 = maxScaleByActor.get(name) ?? def.scale ?? 1;
2408
- for (const [x, y] of positions) {
2409
- bounds = unionRect(bounds, actorRect({ ...stateFrom(def), x, y, scale: scale2 }, def.type));
2410
- }
2411
- }
2412
- return bounds;
2413
- }
2414
- function computeSafeFrame(ast) {
2415
- const bounds = computeContentBounds(ast);
2416
- if (!bounds) return { x: 0, y: 0, scale: 1 };
2417
- const boundsWidth = Math.max(1, bounds.x2 - bounds.x1);
2418
- const boundsHeight = Math.max(1, bounds.y2 - bounds.y1);
2419
- const availableWidth = Math.max(1, ast.meta.width - SAFE_FRAME_PADDING * 2);
2420
- const availableHeight = Math.max(1, ast.meta.height - SAFE_FRAME_PADDING * 2);
2421
- const scale2 = Math.min(1, availableWidth / boundsWidth, availableHeight / boundsHeight);
2422
- const scaledWidth = boundsWidth * scale2;
2423
- const scaledHeight = boundsHeight * scale2;
2424
- function axisOffset(min, max, sceneSize, scaledSize) {
2425
- const paddedMin = SAFE_FRAME_PADDING;
2426
- const paddedMax = sceneSize - SAFE_FRAME_PADDING;
2427
- const scaledMin = min * scale2;
2428
- const scaledMax = max * scale2;
2429
- if (scaledSize > paddedMax - paddedMin) return (sceneSize - scaledSize) / 2 - scaledMin;
2430
- if (scaledMin < paddedMin) return paddedMin - scaledMin;
2431
- if (scaledMax > paddedMax) return paddedMax - scaledMax;
2432
- return 0;
2433
- }
2434
- const x = axisOffset(bounds.x1, bounds.x2, ast.meta.width, scaledWidth);
2435
- const y = axisOffset(bounds.y1, bounds.y2, ast.meta.height, scaledHeight);
2436
- return {
2437
- x: Math.round(x * 1e3) / 1e3,
2438
- y: Math.round(y * 1e3) / 1e3,
2439
- scale: Math.round(scale2 * 1e3) / 1e3
2440
- };
2441
- }
2442
- function bgToTextColor(bg) {
2443
- let hex = bg.trim().replace(/^#/, "");
2444
- if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
2445
- if (hex.length === 6) {
2446
- const r = parseInt(hex.slice(0, 2), 16);
2447
- const g = parseInt(hex.slice(2, 4), 16);
2448
- const b = parseInt(hex.slice(4, 6), 16);
2449
- return 0.299 * r + 0.587 * g + 0.114 * b > 140 ? "#1a1a1a" : "#f0f0f0";
2450
- }
2451
- const named = {
2452
- white: "#1a1a1a",
2453
- black: "#f0f0f0",
2454
- transparent: "#1a1a1a"
2455
- };
2456
- return named[bg.toLowerCase()] ?? "#1a1a1a";
546
+ function applyThemeToScene(scene, theme) {
547
+ scene.style.background = theme.canvas;
548
+ scene.style.color = theme.text;
549
+ scene.style.setProperty("--md-canvas", theme.canvas);
550
+ scene.style.setProperty("--md-surface", theme.surface);
551
+ scene.style.setProperty("--md-surface-raised", theme.surfaceRaised);
552
+ scene.style.setProperty("--md-border", theme.border);
553
+ scene.style.setProperty("--md-text", theme.text);
554
+ scene.style.setProperty("--md-text-muted", theme.textMuted);
555
+ scene.style.setProperty("--md-grid-minor", theme.gridMinor);
556
+ scene.style.setProperty("--md-grid-major", theme.gridMajor);
557
+ scene.style.setProperty("--md-vignette", theme.vignette);
558
+ scene.style.setProperty("--md-accent", theme.accent);
559
+ scene.dataset.markdyTheme = theme.name;
2457
560
  }
561
+
562
+ // src/player.ts
2458
563
  function createPlayer(opts) {
2459
564
  const {
2460
565
  container,
2461
566
  code,
2462
- assets: assetOverrides = {},
2463
- imports,
2464
567
  autoplay = true,
2465
568
  loop = true,
2466
569
  copyright = true,
2467
570
  progressBar = true,
2468
- onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message} (${w.kind})`),
571
+ onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
2469
572
  onTimeUpdate,
2470
573
  onPlayStateChange
2471
574
  } = opts;
2472
- const ast = parse(code, imports ? { imports } : void 0);
2473
- const totalDurationMs = (ast.meta.duration ?? 0) * 1e3;
2474
- const durationSeconds = totalDurationMs / 1e3;
2475
- for (const w of ast.warnings) onWarning(w);
575
+ const { ast, plan } = parseAndCompile(code);
576
+ for (const w of ast.diagnostics) {
577
+ if (w.severity === "warning") onWarning(w);
578
+ }
579
+ const totalDurationMs = plan.duration * 1e3;
580
+ const durationSeconds = plan.duration;
2476
581
  const viewport = document.createElement("div");
2477
582
  Object.assign(viewport.style, {
2478
583
  position: "relative",
2479
584
  width: "100%",
2480
- aspectRatio: `${ast.meta.width} / ${ast.meta.height}`,
585
+ aspectRatio: `${plan.meta.width} / ${plan.meta.height}`,
2481
586
  overflow: "hidden"
2482
587
  });
2483
588
  container.appendChild(viewport);
@@ -2493,13 +598,14 @@ function createPlayer(opts) {
2493
598
  });
2494
599
  viewport.appendChild(progressEl);
2495
600
  }
2496
- const tlAngle = (Math.atan2(-ast.meta.width / 2, ast.meta.height / 2) * 180 / Math.PI + 360) % 360;
601
+ const tlAngle = Math.atan2(-plan.meta.width / 2, plan.meta.height / 2) * 180 / Math.PI;
602
+ const tlAngleNorm = (tlAngle % 360 + 360) % 360;
2497
603
  function updateProgressBar(pct) {
2498
604
  if (!progressEl) return;
2499
605
  const deg = pct * 360;
2500
606
  const rainbow = "hsl(0,90%,60%), hsl(45,90%,55%), hsl(90,80%,50%), hsl(180,80%,50%), hsl(270,80%,55%), hsl(330,90%,60%)";
2501
- progressEl.style.background = `conic-gradient(from ${tlAngle}deg, ${rainbow} ${deg}deg, transparent ${deg}deg)`;
2502
- progressEl.style.mask = `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`;
607
+ progressEl.style.background = `conic-gradient(from ${tlAngleNorm}deg, ${rainbow} ${deg}deg, transparent ${deg}deg)`;
608
+ progressEl.style.mask = "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)";
2503
609
  progressEl.style.webkitMask = progressEl.style.mask;
2504
610
  progressEl.style.maskComposite = "exclude";
2505
611
  progressEl.style.webkitMaskComposite = "xor";
@@ -2516,78 +622,57 @@ function createPlayer(opts) {
2516
622
  display: "block",
2517
623
  textAlign: "right",
2518
624
  fontSize: "10px",
2519
- fontFamily: "system-ui, -apple-system, sans-serif",
625
+ fontFamily: "system-ui, sans-serif",
2520
626
  color: "#999",
2521
627
  textDecoration: "none",
2522
628
  padding: "3px 6px 0",
2523
- opacity: "0.7",
2524
- transition: "opacity 0.2s",
2525
- maxWidth: container.style.maxWidth || "100%",
2526
- width: "100%"
2527
- });
2528
- badge.addEventListener("mouseenter", () => {
2529
- badge.style.opacity = "1";
629
+ opacity: "0.7"
2530
630
  });
2531
- badge.addEventListener("mouseleave", () => {
2532
- badge.style.opacity = "0.7";
2533
- });
2534
- if (container.parentNode) {
2535
- container.parentNode.insertBefore(badge, container.nextSibling);
2536
- } else {
2537
- container.appendChild(badge);
2538
- }
631
+ if (container.parentNode) container.parentNode.insertBefore(badge, container.nextSibling);
632
+ else container.appendChild(badge);
2539
633
  }
2540
- const scene = document.createElement("div");
2541
634
  ensureSceneStyles(document);
635
+ ensureNodeStyles(document);
636
+ const scene = document.createElement("div");
2542
637
  scene.className = "markdy-scene-root";
2543
- scene.dataset.markdySceneTone = bgToTextColor(ast.meta.bg) === "#1a1a1a" ? "light" : "dark";
2544
638
  Object.assign(scene.style, {
2545
639
  position: "absolute",
2546
640
  top: "0",
2547
641
  left: "0",
2548
- width: `${ast.meta.width}px`,
2549
- height: `${ast.meta.height}px`,
2550
- background: ast.meta.bg,
2551
- color: bgToTextColor(ast.meta.bg),
642
+ width: `${plan.meta.width}px`,
643
+ height: `${plan.meta.height}px`,
2552
644
  overflow: "hidden",
2553
645
  userSelect: "none",
2554
646
  transformOrigin: "0 0"
2555
647
  });
648
+ applyThemeToScene(scene, plan.theme);
2556
649
  viewport.appendChild(scene);
2557
650
  const sceneContent = document.createElement("div");
2558
651
  sceneContent.className = "markdy-scene-content";
2559
- Object.assign(sceneContent.style, {
2560
- position: "absolute",
2561
- top: "0",
2562
- left: "0",
2563
- width: "100%",
2564
- height: "100%",
2565
- transformOrigin: "50% 50%",
2566
- willChange: "transform"
2567
- });
652
+ Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
2568
653
  scene.appendChild(sceneContent);
2569
654
  const actorLayer = document.createElement("div");
2570
655
  actorLayer.className = "markdy-scene-actor-layer";
2571
- const safeFrame = computeSafeFrame(ast);
2572
- actorLayer.dataset.markdySafeFrame = `${safeFrame.x},${safeFrame.y},${safeFrame.scale}`;
2573
- actorLayer.style.transform = `translate(${safeFrame.x}px, ${safeFrame.y}px) scale(${safeFrame.scale})`;
2574
656
  sceneContent.appendChild(actorLayer);
657
+ const titleEl = createTitleEl(plan.title);
658
+ actorLayer.appendChild(titleEl);
659
+ const nodeEls = /* @__PURE__ */ new Map();
660
+ for (const node of plan.nodes) {
661
+ const el = createNodeEl(node, plan.theme);
662
+ actorLayer.appendChild(el);
663
+ nodeEls.set(node.id, el);
664
+ }
2575
665
  function scaleScene() {
2576
- const s = viewport.clientWidth / ast.meta.width;
666
+ const s = viewport.clientWidth / plan.meta.width;
2577
667
  scene.style.transform = `scale(${s})`;
2578
668
  }
2579
669
  scaleScene();
2580
670
  const resizeObserver = new ResizeObserver(scaleScene);
2581
671
  resizeObserver.observe(viewport);
2582
- const actorEls = /* @__PURE__ */ new Map();
2583
- for (const [name, def] of Object.entries(ast.actors)) {
2584
- const el = createActorEl(name, def, ast.assets, assetOverrides);
2585
- actorLayer.appendChild(el);
2586
- actorEls.set(name, el);
2587
- }
2588
- const faceSwaps = [];
2589
- const allAnims = buildAnimations(ast, actorEls, actorLayer, sceneContent, assetOverrides, faceSwaps);
2590
- faceSwaps.sort((a, b) => a.timeMs - b.timeMs);
672
+ const allAnims = buildCueAnimations(plan.cues, nodeEls, plan.nodes, plan.theme, sceneContent, titleEl, {
673
+ width: plan.meta.width,
674
+ height: plan.meta.height
675
+ });
2591
676
  for (const anim of allAnims) {
2592
677
  anim.pause();
2593
678
  anim.currentTime = 0;
@@ -2596,47 +681,16 @@ function createPlayer(opts) {
2596
681
  let lastRafTs = null;
2597
682
  let isPlaying = false;
2598
683
  let rafId = null;
2599
- for (const { el } of faceSwaps) {
2600
- if (!el.dataset["figFaceInitial"]) {
2601
- el.dataset["figFaceInitial"] = el.textContent ?? "";
2602
- }
2603
- }
2604
684
  function applyCurrentTime() {
2605
- for (const anim of allAnims) {
2606
- anim.currentTime = sceneMs;
2607
- }
2608
- applyFaceSwaps();
685
+ for (const anim of allAnims) anim.currentTime = sceneMs;
2609
686
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
2610
687
  }
2611
- function applyFaceSwaps() {
2612
- if (faceSwaps.length === 0) return;
2613
- const elEmoji = /* @__PURE__ */ new Map();
2614
- for (const { timeMs, el, emoji } of faceSwaps) {
2615
- if (timeMs <= sceneMs) elEmoji.set(el, emoji);
2616
- }
2617
- for (const [el, emoji] of elEmoji) {
2618
- if (el.textContent !== emoji) el.textContent = emoji;
2619
- }
2620
- const elFirst = /* @__PURE__ */ new Map();
2621
- for (const { el, emoji } of faceSwaps) {
2622
- if (!elFirst.has(el)) elFirst.set(el, emoji);
2623
- }
2624
- for (const [el, firstEmoji] of elFirst) {
2625
- if (!elEmoji.has(el)) {
2626
- const initial = el.dataset["figFaceInitial"] ?? firstEmoji;
2627
- if (el.textContent !== initial) el.textContent = initial;
2628
- }
2629
- }
2630
- }
2631
688
  function rafTick(timestamp) {
2632
- if (lastRafTs !== null) {
2633
- sceneMs += timestamp - lastRafTs;
2634
- }
689
+ if (lastRafTs !== null) sceneMs += timestamp - lastRafTs;
2635
690
  lastRafTs = timestamp;
2636
691
  if (totalDurationMs > 0 && sceneMs >= totalDurationMs) {
2637
- if (loop) {
2638
- sceneMs = sceneMs % totalDurationMs;
2639
- } else {
692
+ if (loop) sceneMs = sceneMs % totalDurationMs;
693
+ else {
2640
694
  sceneMs = totalDurationMs;
2641
695
  applyCurrentTime();
2642
696
  isPlaying = false;
@@ -2662,15 +716,12 @@ function createPlayer(opts) {
2662
716
  if (!isPlaying) return;
2663
717
  isPlaying = false;
2664
718
  onPlayStateChange?.(false);
2665
- if (rafId !== null) {
2666
- cancelAnimationFrame(rafId);
2667
- rafId = null;
2668
- }
719
+ if (rafId !== null) cancelAnimationFrame(rafId);
720
+ rafId = null;
2669
721
  lastRafTs = null;
2670
722
  },
2671
723
  seek(seconds) {
2672
- const nextMs = seconds * 1e3;
2673
- sceneMs = totalDurationMs > 0 ? Math.min(Math.max(nextMs, 0), totalDurationMs) : Math.max(nextMs, 0);
724
+ sceneMs = totalDurationMs > 0 ? Math.min(Math.max(seconds * 1e3, 0), totalDurationMs) : Math.max(seconds * 1e3, 0);
2674
725
  applyCurrentTime();
2675
726
  if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
2676
727
  },
@@ -2683,13 +734,12 @@ function createPlayer(opts) {
2683
734
  isPlaying() {
2684
735
  return isPlaying;
2685
736
  },
2686
- chapters() {
2687
- return ast.chapters;
737
+ beats() {
738
+ return plan.beats;
2688
739
  },
2689
- seekToChapter(name) {
2690
- const chapter = ast.chapters.find((c) => c.name === name);
2691
- if (!chapter) return;
2692
- player.seek(chapter.startTime);
740
+ seekToBeat(name) {
741
+ const beat = plan.beats.find((b) => b.name === name);
742
+ if (beat) player.seek(beat.start);
2693
743
  },
2694
744
  destroy() {
2695
745
  player.pause();
@@ -2702,12 +752,9 @@ function createPlayer(opts) {
2702
752
  };
2703
753
  viewport.style.cursor = "pointer";
2704
754
  viewport.addEventListener("click", () => {
2705
- if (isPlaying) {
2706
- player.pause();
2707
- } else {
2708
- if (!loop && sceneMs >= totalDurationMs) {
2709
- sceneMs = 0;
2710
- }
755
+ if (isPlaying) player.pause();
756
+ else {
757
+ if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
2711
758
  player.play();
2712
759
  }
2713
760
  });