@markdy/renderer-dom 0.7.26 → 0.7.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1232 -94
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -37,6 +37,9 @@ function toEasing(val) {
37
37
  return "linear";
38
38
  }
39
39
 
40
+ // src/actors.ts
41
+ import { TECHNICAL_NODE_KINDS, VISUAL_PRIMITIVE_TYPES } from "@markdy/core";
42
+
40
43
  // src/figure.ts
41
44
  function createFigureEl(def) {
42
45
  const skinColor = def.args[0] || "#ffdbac";
@@ -309,6 +312,766 @@ function readRotation(el) {
309
312
  }
310
313
 
311
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
+ }
312
1075
  function createActorEl(name, def, assetDefs, assetOverrides) {
313
1076
  let el;
314
1077
  switch (def.type) {
@@ -377,75 +1140,43 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
377
1140
  el = createFigureEl(def);
378
1141
  break;
379
1142
  }
380
- case "service":
381
- case "client":
382
- case "db":
383
- case "queue": {
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);
384
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");
385
1167
  const label = document.createElement("div");
1168
+ label.className = "markdy-arch-node__label";
386
1169
  label.textContent = def.args[0] ?? "";
387
- Object.assign(label.style, {
388
- fontFamily: "system-ui, -apple-system, sans-serif",
389
- fontSize: `${def.size ?? 16}px`,
390
- fontWeight: "600",
391
- color: "#e2e8f0",
392
- textAlign: "center",
393
- lineHeight: "1.2",
394
- pointerEvents: "none",
395
- whiteSpace: "nowrap",
396
- overflow: "hidden",
397
- textOverflow: "ellipsis",
398
- maxWidth: "100%"
399
- });
400
- Object.assign(card.style, {
401
- width: "180px",
402
- height: "84px",
403
- boxSizing: "border-box",
404
- border: "1px solid #475569",
405
- background: "#0f172a",
406
- display: "grid",
407
- placeItems: "center",
408
- padding: "10px 12px"
409
- });
410
- if (def.type === "service") {
411
- card.style.borderRadius = "12px";
412
- card.style.background = "linear-gradient(180deg, #1e293b 0%, #0f172a 100%)";
413
- } else if (def.type === "client") {
414
- const bar = document.createElement("div");
415
- Object.assign(bar.style, {
416
- position: "absolute",
417
- top: "8px",
418
- left: "8px",
419
- right: "8px",
420
- height: "8px",
421
- borderRadius: "6px",
422
- background: "#334155"
423
- });
424
- card.style.position = "relative";
425
- card.style.borderRadius = "10px";
426
- card.style.paddingTop = "20px";
427
- card.appendChild(bar);
428
- } else if (def.type === "db") {
429
- card.style.borderRadius = "50% / 14%";
430
- card.style.background = "linear-gradient(180deg, #334155 0%, #0f172a 70%)";
431
- card.style.boxShadow = "inset 0 10px 0 rgba(226, 232, 240, 0.12)";
432
- } else if (def.type === "queue") {
433
- card.style.borderRadius = "8px";
434
- card.style.boxShadow = "-6px -6px 0 rgba(30, 41, 59, 0.9), -12px -12px 0 rgba(15, 23, 42, 0.9)";
435
- }
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);
436
1176
  card.appendChild(label);
437
1177
  el = card;
438
1178
  break;
439
1179
  }
440
- default: {
441
- const div = document.createElement("div");
442
- div.style.width = "100px";
443
- div.style.height = "100px";
444
- div.style.background = "#999";
445
- div.style.boxSizing = "border-box";
446
- el = div;
447
- break;
448
- }
449
1180
  }
450
1181
  el.dataset.markdyActor = name;
451
1182
  el.style.position = "absolute";
@@ -456,6 +1187,8 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
456
1187
  el.style.opacity = String(def.opacity ?? 1);
457
1188
  if (def.z !== void 0) el.style.zIndex = String(def.z);
458
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";
459
1192
  return el;
460
1193
  }
461
1194
 
@@ -776,27 +1509,190 @@ var bounce = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
776
1509
  anims.push(el.animate(keyframes, { ...baseOpts, easing: "ease-out" }));
777
1510
  };
778
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
+ };
1646
+
779
1647
  // src/geometry/rect.ts
1648
+ import { TECHNICAL_NODE_TYPES } from "@markdy/core";
1649
+ var SYSTEM_NODE_SIZE = { width: 184, height: 88 };
780
1650
  var ACTOR_SIZES = {
781
- service: { width: 180, height: 84 },
782
- client: { width: 180, height: 84 },
783
- db: { width: 180, height: 84 },
784
- queue: { width: 180, height: 84 },
1651
+ ...Object.fromEntries(TECHNICAL_NODE_TYPES.map((type) => [type, SYSTEM_NODE_SIZE])),
785
1652
  box: { width: 100, height: 100 },
786
1653
  caption: { width: 260, height: 56 },
787
- figure: { width: 120, height: 170 }
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 }
788
1674
  };
789
1675
  var DEFAULT_ACTOR_SIZE = { width: 140, height: 42 };
790
1676
  function actorSizeByType(type) {
791
1677
  return ACTOR_SIZES[type] ?? DEFAULT_ACTOR_SIZE;
792
1678
  }
793
1679
  function actorCenter(state, actorType) {
794
- const { width, height } = actorSizeByType(actorType);
795
- return { x: state.x + width / 2, y: state.y + height / 2 };
1680
+ const rect = actorRect(state, actorType);
1681
+ return { x: rect.x1 + (rect.x2 - rect.x1) / 2, y: rect.y1 + (rect.y2 - rect.y1) / 2 };
796
1682
  }
797
1683
  function actorRect(state, actorType) {
798
1684
  const { width, height } = actorSizeByType(actorType);
799
- return { x1: state.x, y1: state.y, x2: state.x + width, y2: state.y + height };
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
+ };
800
1696
  }
801
1697
  function inflateRect(rect, pad) {
802
1698
  return {
@@ -849,19 +1745,58 @@ function polylineLength(points) {
849
1745
  }
850
1746
  return Math.max(1, total);
851
1747
  }
852
- function pointAtDistance(points, dist) {
853
- let remain = Math.max(0, dist);
1748
+ function segmentLength(a, b) {
1749
+ return Math.hypot(b.x - a.x, b.y - a.y);
1750
+ }
1751
+ function labelPointForPath(points, lane) {
1752
+ if (points.length < 2) return points[0] ?? { x: 0, y: 0 };
1753
+ let bestIndex = 0;
1754
+ let bestLength = -1;
854
1755
  for (let i = 0; i < points.length - 1; i++) {
855
- const a = points[i];
856
- const b = points[i + 1];
857
- const seg = Math.hypot(b.x - a.x, b.y - a.y);
858
- if (remain <= seg || i === points.length - 2) {
859
- const t = seg <= 0 ? 0 : Math.min(1, remain / seg);
860
- return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
1756
+ const len = segmentLength(points[i], points[i + 1]);
1757
+ if (len > bestLength) {
1758
+ bestIndex = i;
1759
+ bestLength = len;
861
1760
  }
862
- remain -= seg;
863
1761
  }
864
- return points[0];
1762
+ const a = points[bestIndex];
1763
+ const b = points[bestIndex + 1];
1764
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1765
+ const offset = lane * 8;
1766
+ if (Math.abs(a.x - b.x) >= Math.abs(a.y - b.y)) {
1767
+ return { x: mid.x, y: mid.y - 10 - offset };
1768
+ }
1769
+ return { x: mid.x + 10 + offset, y: mid.y };
1770
+ }
1771
+ function clamp(n, min, max) {
1772
+ if (max < min) return n;
1773
+ return Math.min(max, Math.max(min, n));
1774
+ }
1775
+ function clampPointToScene(point, ast) {
1776
+ const pad = 14;
1777
+ return {
1778
+ x: round1(clamp(point.x, pad, ast.meta.width - pad)),
1779
+ y: round1(clamp(point.y, pad, ast.meta.height - pad))
1780
+ };
1781
+ }
1782
+ function routeLength(points) {
1783
+ let total = 0;
1784
+ for (let i = 0; i < points.length - 1; i++) total += segmentLength(points[i], points[i + 1]);
1785
+ return total;
1786
+ }
1787
+ function routeBends(points) {
1788
+ let bends = 0;
1789
+ for (let i = 1; i < points.length - 1; i++) {
1790
+ const prev = points[i - 1];
1791
+ const cur = points[i];
1792
+ const next = points[i + 1];
1793
+ const dx1 = Math.sign(cur.x - prev.x);
1794
+ const dy1 = Math.sign(cur.y - prev.y);
1795
+ const dx2 = Math.sign(next.x - cur.x);
1796
+ const dy2 = Math.sign(next.y - cur.y);
1797
+ if (dx1 !== dx2 || dy1 !== dy2) bends++;
1798
+ }
1799
+ return bends;
865
1800
  }
866
1801
  function routeFlowPath(sourceName, targetName, sourceState, targetState, states, ast, lane) {
867
1802
  const sourceType = ast.actors[sourceName]?.type ?? "box";
@@ -898,17 +1833,25 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
898
1833
  [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
899
1834
  [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
900
1835
  );
1836
+ const minObstacleX = obstacles.length ? Math.min(...obstacles.map((o) => o.x1)) : Math.min(source.x, target.x);
1837
+ const maxObstacleX = obstacles.length ? Math.max(...obstacles.map((o) => o.x2)) : Math.max(source.x, target.x);
1838
+ const leftLane = Math.max(16, minObstacleX - 24 - lane * 12);
1839
+ const rightLane = Math.min(ast.meta.width - 16, maxObstacleX + 24 + lane * 12);
1840
+ candidates.push(
1841
+ [source, { x: leftLane, y: source.y }, { x: leftLane, y: target.y }, target],
1842
+ [source, { x: rightLane, y: source.y }, { x: rightLane, y: target.y }, target]
1843
+ );
901
1844
  let best = candidates[0];
902
- let bestHits = Number.POSITIVE_INFINITY;
1845
+ let bestScore = Number.POSITIVE_INFINITY;
903
1846
  for (const candidate of candidates) {
904
1847
  const hits = countPathIntersections(candidate, obstacles);
905
- if (hits < bestHits) {
1848
+ const score = hits * 1e5 + routeBends(candidate) * 800 + routeLength(candidate);
1849
+ if (score < bestScore) {
906
1850
  best = candidate;
907
- bestHits = hits;
908
- if (hits === 0) break;
1851
+ bestScore = score;
909
1852
  }
910
1853
  }
911
- return best;
1854
+ return best.map((point) => clampPointToScene(point, ast));
912
1855
  }
913
1856
 
914
1857
  // src/actions/flow.ts
@@ -938,6 +1881,41 @@ function ensureEdgeLayer(scene) {
938
1881
  scene.appendChild(svg);
939
1882
  return svg;
940
1883
  }
1884
+ function ensureEdgeDefs(svg) {
1885
+ if (svg.querySelector("defs[data-markdy-flow-defs='1']")) return;
1886
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1887
+ defs.setAttribute("data-markdy-flow-defs", "1");
1888
+ const glow2 = document.createElementNS("http://www.w3.org/2000/svg", "filter");
1889
+ glow2.setAttribute("id", "markdy-flow-glow");
1890
+ glow2.setAttribute("x", "-40%");
1891
+ glow2.setAttribute("y", "-40%");
1892
+ glow2.setAttribute("width", "180%");
1893
+ glow2.setAttribute("height", "180%");
1894
+ const blur2 = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
1895
+ blur2.setAttribute("stdDeviation", "3");
1896
+ blur2.setAttribute("result", "coloredBlur");
1897
+ const merge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
1898
+ const glowNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1899
+ glowNode.setAttribute("in", "coloredBlur");
1900
+ const sourceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1901
+ sourceNode.setAttribute("in", "SourceGraphic");
1902
+ merge.append(glowNode, sourceNode);
1903
+ glow2.append(blur2, merge);
1904
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
1905
+ arrow.setAttribute("id", "markdy-flow-arrow");
1906
+ arrow.setAttribute("viewBox", "0 0 10 10");
1907
+ arrow.setAttribute("refX", "8");
1908
+ arrow.setAttribute("refY", "5");
1909
+ arrow.setAttribute("markerWidth", "6");
1910
+ arrow.setAttribute("markerHeight", "6");
1911
+ arrow.setAttribute("orient", "auto-start-reverse");
1912
+ const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
1913
+ arrowPath.setAttribute("d", "M 1 1 L 9 5 L 1 9 z");
1914
+ arrowPath.setAttribute("fill", "context-stroke");
1915
+ arrow.appendChild(arrowPath);
1916
+ defs.append(glow2, arrow);
1917
+ svg.prepend(defs);
1918
+ }
941
1919
  function isDashed(ctx) {
942
1920
  const style = String(ctx.ev.params.style ?? "");
943
1921
  return style === "dashed" || style === "fire_and_forget" || ctx.ev.action === "response";
@@ -951,38 +1929,51 @@ function buildEdge(ctx, targetName) {
951
1929
  const length = polylineLength(points);
952
1930
  const pathD = toPathD(points);
953
1931
  const stroke = STROKE_BY_ACTION[ev.action] ?? DEFAULT_STROKE;
1932
+ const svg = ensureEdgeLayer(scene);
1933
+ ensureEdgeDefs(svg);
954
1934
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
955
1935
  group.setAttribute("data-markdy-flow-edge", "1");
956
1936
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
957
1937
  path.setAttribute("d", pathD);
958
1938
  path.setAttribute("fill", "none");
959
1939
  path.setAttribute("stroke", stroke);
960
- path.setAttribute("stroke-width", "2.5");
1940
+ path.setAttribute("stroke-width", "3");
1941
+ path.setAttribute("stroke-linecap", "round");
1942
+ path.setAttribute("stroke-linejoin", "round");
1943
+ path.setAttribute("marker-end", "url(#markdy-flow-arrow)");
1944
+ path.setAttribute("filter", "url(#markdy-flow-glow)");
961
1945
  path.setAttribute("data-markdy-flow-action", ev.action);
962
1946
  path.style.strokeDasharray = isDashed(ctx) ? "8 6" : `${length}`;
963
1947
  path.style.strokeDashoffset = `${length}`;
964
1948
  group.appendChild(path);
965
1949
  const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
966
- marker.setAttribute("r", "3");
1950
+ marker.setAttribute("r", "4.5");
967
1951
  marker.setAttribute("fill", stroke);
1952
+ marker.setAttribute("filter", "url(#markdy-flow-glow)");
968
1953
  marker.style.offsetPath = `path('${pathD}')`;
969
1954
  marker.style.offsetDistance = "0%";
970
1955
  marker.style.opacity = "0";
971
1956
  group.appendChild(marker);
972
1957
  const labelText = String(ev.params.label ?? "");
973
1958
  if (labelText) {
974
- const midPoint = pointAtDistance(points, length * 0.5);
1959
+ const midPoint = labelPointForPath(points, lane);
975
1960
  const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
976
1961
  label.setAttribute("x", `${round1(midPoint.x)}`);
977
1962
  label.setAttribute("y", `${round1(midPoint.y - 8)}`);
978
1963
  label.setAttribute("text-anchor", "middle");
979
1964
  label.setAttribute("font-size", "12");
1965
+ label.setAttribute("font-weight", "700");
1966
+ label.setAttribute("paint-order", "stroke");
1967
+ label.setAttribute("stroke", "rgba(2, 6, 23, 0.88)");
1968
+ label.setAttribute("stroke-width", "5");
1969
+ label.setAttribute("stroke-linejoin", "round");
980
1970
  label.setAttribute("fill", "#cbd5e1");
1971
+ label.setAttribute("filter", "url(#markdy-flow-glow)");
981
1972
  label.setAttribute("data-full-label", labelText);
982
1973
  label.textContent = labelText.length > LABEL_MAX_CHARS ? `${labelText.slice(0, LABEL_MAX_CHARS - 1)}\u2026` : labelText;
983
1974
  group.appendChild(label);
984
1975
  }
985
- ensureEdgeLayer(scene).appendChild(group);
1976
+ svg.appendChild(group);
986
1977
  anims.push(
987
1978
  path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts),
988
1979
  marker.animate(
@@ -1164,9 +2155,18 @@ var ACTION_HANDLERS = Object.freeze({
1164
2155
  exit,
1165
2156
  fade_in: fadeIn,
1166
2157
  fade_out: fadeOut,
2158
+ spring,
2159
+ follow_path: followPath,
1167
2160
  scale,
1168
2161
  rotate,
1169
2162
  shake,
2163
+ pulse,
2164
+ glow,
2165
+ ripple,
2166
+ blur,
2167
+ line_reveal: lineReveal,
2168
+ mask,
2169
+ parallax,
1170
2170
  jump,
1171
2171
  bounce,
1172
2172
  say,
@@ -1196,7 +2196,7 @@ var DEFAULT_EASE_BY_ACTION = {
1196
2196
  exit: "in",
1197
2197
  fade_out: "in"
1198
2198
  };
1199
- function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
2199
+ function buildAnimations(ast, actorEls, actorLayer, cameraLayer, assetOverrides, faceSwaps) {
1200
2200
  const anims = [];
1201
2201
  const states = /* @__PURE__ */ new Map();
1202
2202
  for (const [name, def] of Object.entries(ast.actors)) {
@@ -1222,7 +2222,7 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
1222
2222
  easing: toEasing(ev.params.ease ?? DEFAULT_EASE_BY_ACTION[ev.action])
1223
2223
  };
1224
2224
  if (ev.actor === "camera") {
1225
- buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState);
2225
+ buildCameraAction(ev, cameraLayer, ast, baseOpts, anims, cameraState);
1226
2226
  continue;
1227
2227
  }
1228
2228
  const el = actorEls.get(ev.actor);
@@ -1239,7 +2239,7 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
1239
2239
  ast,
1240
2240
  states,
1241
2241
  actorEls,
1242
- scene,
2242
+ scene: actorLayer,
1243
2243
  assetOverrides,
1244
2244
  faceSwaps,
1245
2245
  anims,
@@ -1251,6 +2251,134 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
1251
2251
  }
1252
2252
 
1253
2253
  // src/player.ts
2254
+ var SCENE_STYLE_ID = "markdy-scene-ambience-styles";
2255
+ var SAFE_FRAME_PADDING = 28;
2256
+ function ensureSceneStyles(doc) {
2257
+ if (doc.getElementById(SCENE_STYLE_ID)) return;
2258
+ const style = doc.createElement("style");
2259
+ style.id = SCENE_STYLE_ID;
2260
+ style.textContent = `
2261
+ .markdy-scene-root {
2262
+ --markdy-scene-grid: rgba(148, 163, 184, 0.13);
2263
+ --markdy-scene-grid-strong: rgba(148, 163, 184, 0.2);
2264
+ --markdy-scene-vignette: rgba(2, 6, 23, 0.72);
2265
+ --markdy-scene-accent: rgba(56, 189, 248, 0.16);
2266
+ isolation: isolate;
2267
+ }
2268
+ .markdy-scene-root::before,
2269
+ .markdy-scene-root::after {
2270
+ content: "";
2271
+ position: absolute;
2272
+ inset: 0;
2273
+ pointer-events: none;
2274
+ }
2275
+ .markdy-scene-root::before {
2276
+ z-index: 0;
2277
+ background:
2278
+ linear-gradient(var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2279
+ linear-gradient(90deg, var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2280
+ linear-gradient(var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px,
2281
+ linear-gradient(90deg, var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px;
2282
+ mask-image: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
2283
+ opacity: 0.82;
2284
+ }
2285
+ .markdy-scene-root::after {
2286
+ z-index: 1;
2287
+ background:
2288
+ radial-gradient(ellipse at 50% 0%, var(--markdy-scene-accent), transparent 42%),
2289
+ linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--markdy-scene-vignette) 100%),
2290
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035), transparent 18%, transparent 82%, rgba(255, 255, 255, 0.035));
2291
+ mix-blend-mode: screen;
2292
+ opacity: 0.8;
2293
+ }
2294
+ .markdy-scene-root[data-markdy-scene-tone="light"] {
2295
+ --markdy-scene-grid: rgba(15, 23, 42, 0.09);
2296
+ --markdy-scene-grid-strong: rgba(15, 23, 42, 0.14);
2297
+ --markdy-scene-vignette: rgba(241, 245, 249, 0.74);
2298
+ --markdy-scene-accent: rgba(14, 165, 233, 0.12);
2299
+ }
2300
+ .markdy-scene-content {
2301
+ z-index: 2;
2302
+ }
2303
+ .markdy-scene-actor-layer {
2304
+ position: absolute;
2305
+ inset: 0;
2306
+ overflow: visible;
2307
+ transform-origin: 0 0;
2308
+ will-change: transform;
2309
+ }
2310
+ `;
2311
+ doc.head.appendChild(style);
2312
+ }
2313
+ function unionRect(a, b) {
2314
+ if (!a) return b;
2315
+ return {
2316
+ x1: Math.min(a.x1, b.x1),
2317
+ y1: Math.min(a.y1, b.y1),
2318
+ x2: Math.max(a.x2, b.x2),
2319
+ y2: Math.max(a.y2, b.y2)
2320
+ };
2321
+ }
2322
+ function numericPair(value) {
2323
+ if (!Array.isArray(value) || value.length < 2) return null;
2324
+ const [x, y] = value;
2325
+ return typeof x === "number" && typeof y === "number" ? [x, y] : null;
2326
+ }
2327
+ function computeContentBounds(ast) {
2328
+ let bounds = null;
2329
+ const maxScaleByActor = /* @__PURE__ */ new Map();
2330
+ const positionsByActor = /* @__PURE__ */ new Map();
2331
+ for (const [name, def] of Object.entries(ast.actors)) {
2332
+ maxScaleByActor.set(name, Math.max(1e-3, def.scale ?? 1));
2333
+ positionsByActor.set(name, [[def.x, def.y]]);
2334
+ }
2335
+ for (const ev of ast.events) {
2336
+ if (ev.actor === "camera") continue;
2337
+ if (ev.action === "scale" && typeof ev.params.to === "number") {
2338
+ maxScaleByActor.set(ev.actor, Math.max(maxScaleByActor.get(ev.actor) ?? 1, ev.params.to));
2339
+ }
2340
+ if (ev.action === "move" || ev.action === "spring") {
2341
+ const to = numericPair(ev.params.to);
2342
+ if (to) positionsByActor.get(ev.actor)?.push(to);
2343
+ }
2344
+ }
2345
+ for (const [name, def] of Object.entries(ast.actors)) {
2346
+ const positions = positionsByActor.get(name) ?? [[def.x, def.y]];
2347
+ const scale2 = maxScaleByActor.get(name) ?? def.scale ?? 1;
2348
+ for (const [x, y] of positions) {
2349
+ bounds = unionRect(bounds, actorRect({ ...stateFrom(def), x, y, scale: scale2 }, def.type));
2350
+ }
2351
+ }
2352
+ return bounds;
2353
+ }
2354
+ function computeSafeFrame(ast) {
2355
+ const bounds = computeContentBounds(ast);
2356
+ if (!bounds) return { x: 0, y: 0, scale: 1 };
2357
+ const boundsWidth = Math.max(1, bounds.x2 - bounds.x1);
2358
+ const boundsHeight = Math.max(1, bounds.y2 - bounds.y1);
2359
+ const availableWidth = Math.max(1, ast.meta.width - SAFE_FRAME_PADDING * 2);
2360
+ const availableHeight = Math.max(1, ast.meta.height - SAFE_FRAME_PADDING * 2);
2361
+ const scale2 = Math.min(1, availableWidth / boundsWidth, availableHeight / boundsHeight);
2362
+ const scaledWidth = boundsWidth * scale2;
2363
+ const scaledHeight = boundsHeight * scale2;
2364
+ function axisOffset(min, max, sceneSize, scaledSize) {
2365
+ const paddedMin = SAFE_FRAME_PADDING;
2366
+ const paddedMax = sceneSize - SAFE_FRAME_PADDING;
2367
+ const scaledMin = min * scale2;
2368
+ const scaledMax = max * scale2;
2369
+ if (scaledSize > paddedMax - paddedMin) return (sceneSize - scaledSize) / 2 - scaledMin;
2370
+ if (scaledMin < paddedMin) return paddedMin - scaledMin;
2371
+ if (scaledMax > paddedMax) return paddedMax - scaledMax;
2372
+ return 0;
2373
+ }
2374
+ const x = axisOffset(bounds.x1, bounds.x2, ast.meta.width, scaledWidth);
2375
+ const y = axisOffset(bounds.y1, bounds.y2, ast.meta.height, scaledHeight);
2376
+ return {
2377
+ x: Math.round(x * 1e3) / 1e3,
2378
+ y: Math.round(y * 1e3) / 1e3,
2379
+ scale: Math.round(scale2 * 1e3) / 1e3
2380
+ };
2381
+ }
1254
2382
  function bgToTextColor(bg) {
1255
2383
  let hex = bg.trim().replace(/^#/, "");
1256
2384
  if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
@@ -1350,6 +2478,9 @@ function createPlayer(opts) {
1350
2478
  }
1351
2479
  }
1352
2480
  const scene = document.createElement("div");
2481
+ ensureSceneStyles(document);
2482
+ scene.className = "markdy-scene-root";
2483
+ scene.dataset.markdySceneTone = bgToTextColor(ast.meta.bg) === "#1a1a1a" ? "light" : "dark";
1353
2484
  Object.assign(scene.style, {
1354
2485
  position: "absolute",
1355
2486
  top: "0",
@@ -1364,6 +2495,7 @@ function createPlayer(opts) {
1364
2495
  });
1365
2496
  viewport.appendChild(scene);
1366
2497
  const sceneContent = document.createElement("div");
2498
+ sceneContent.className = "markdy-scene-content";
1367
2499
  Object.assign(sceneContent.style, {
1368
2500
  position: "absolute",
1369
2501
  top: "0",
@@ -1374,6 +2506,12 @@ function createPlayer(opts) {
1374
2506
  willChange: "transform"
1375
2507
  });
1376
2508
  scene.appendChild(sceneContent);
2509
+ const actorLayer = document.createElement("div");
2510
+ actorLayer.className = "markdy-scene-actor-layer";
2511
+ const safeFrame = computeSafeFrame(ast);
2512
+ actorLayer.dataset.markdySafeFrame = `${safeFrame.x},${safeFrame.y},${safeFrame.scale}`;
2513
+ actorLayer.style.transform = `translate(${safeFrame.x}px, ${safeFrame.y}px) scale(${safeFrame.scale})`;
2514
+ sceneContent.appendChild(actorLayer);
1377
2515
  function scaleScene() {
1378
2516
  const s = viewport.clientWidth / ast.meta.width;
1379
2517
  scene.style.transform = `scale(${s})`;
@@ -1384,11 +2522,11 @@ function createPlayer(opts) {
1384
2522
  const actorEls = /* @__PURE__ */ new Map();
1385
2523
  for (const [name, def] of Object.entries(ast.actors)) {
1386
2524
  const el = createActorEl(name, def, ast.assets, assetOverrides);
1387
- sceneContent.appendChild(el);
2525
+ actorLayer.appendChild(el);
1388
2526
  actorEls.set(name, el);
1389
2527
  }
1390
2528
  const faceSwaps = [];
1391
- const allAnims = buildAnimations(ast, actorEls, sceneContent, assetOverrides, faceSwaps);
2529
+ const allAnims = buildAnimations(ast, actorEls, actorLayer, sceneContent, assetOverrides, faceSwaps);
1392
2530
  faceSwaps.sort((a, b) => a.timeMs - b.timeMs);
1393
2531
  for (const anim of allAnims) {
1394
2532
  anim.pause();