@markdy/renderer-dom 0.7.25 → 0.7.27

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 (3) hide show
  1. package/README.md +30 -5
  2. package/dist/index.js +1762 -721
  3. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -309,6 +309,730 @@ function readRotation(el) {
309
309
  }
310
310
 
311
311
  // src/actors.ts
312
+ var ARCHITECTURE_NODE_TYPES = /* @__PURE__ */ new Set([
313
+ "service",
314
+ "api",
315
+ "microservice",
316
+ "client",
317
+ "user",
318
+ "db",
319
+ "database",
320
+ "queue",
321
+ "cache",
322
+ "cloud",
323
+ "region",
324
+ "container",
325
+ "cluster"
326
+ ]);
327
+ var SHOWCASE_SURFACE_TYPES = /* @__PURE__ */ new Set(["parking_map", "ascii_map", "game_scene", "byte_viz"]);
328
+ var ARCHITECTURE_STYLE_ID = "markdy-architecture-node-styles";
329
+ var SHOWCASE_STYLE_ID = "markdy-showcase-surface-styles";
330
+ function ensureShowcaseStyles(doc) {
331
+ if (doc.getElementById(SHOWCASE_STYLE_ID)) return;
332
+ const style = doc.createElement("style");
333
+ style.id = SHOWCASE_STYLE_ID;
334
+ style.textContent = `
335
+ .markdy-showcase {
336
+ --surface-a: rgba(15, 23, 42, 0.9);
337
+ --accent: #38bdf8;
338
+ --accent-2: #22c55e;
339
+ position: relative;
340
+ box-sizing: border-box;
341
+ width: 390px;
342
+ height: 250px;
343
+ overflow: hidden;
344
+ border: 1px solid rgba(148, 163, 184, 0.34);
345
+ border-radius: 24px;
346
+ color: #e5f3ff;
347
+ background:
348
+ radial-gradient(circle at 12% 0%, color-mix(in srgb, var(--accent) 32%, transparent), transparent 34%),
349
+ radial-gradient(circle at 90% 15%, color-mix(in srgb, var(--accent-2) 26%, transparent), transparent 34%),
350
+ linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.025) 32%, var(--surface-a));
351
+ box-shadow:
352
+ 0 26px 70px rgba(2, 6, 23, 0.42),
353
+ 0 0 0 1px rgba(255, 255, 255, 0.06) inset,
354
+ 0 1px 0 rgba(255, 255, 255, 0.18) inset,
355
+ 0 0 44px color-mix(in srgb, var(--accent) 24%, transparent);
356
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
357
+ isolation: isolate;
358
+ contain: layout paint style;
359
+ backdrop-filter: blur(16px) saturate(1.2);
360
+ -webkit-backdrop-filter: blur(16px) saturate(1.2);
361
+ }
362
+ .markdy-showcase::before {
363
+ content: "";
364
+ position: absolute;
365
+ inset: 0;
366
+ z-index: -1;
367
+ background:
368
+ linear-gradient(90deg, transparent 0 48%, rgba(255, 255, 255, 0.09) 50%, transparent 52%) 0 0 / 34px 34px,
369
+ linear-gradient(0deg, transparent 0 48%, rgba(255, 255, 255, 0.055) 50%, transparent 52%) 0 0 / 34px 34px;
370
+ mask-image: linear-gradient(to bottom, rgba(0,0,0,0.85), transparent 82%);
371
+ opacity: 0.48;
372
+ animation: markdyGridDrift 7s linear infinite;
373
+ }
374
+ .markdy-showcase::after {
375
+ content: "";
376
+ position: absolute;
377
+ inset: -40% -20%;
378
+ z-index: -1;
379
+ background: conic-gradient(from 90deg, transparent, color-mix(in srgb, var(--accent) 20%, transparent), transparent 32%);
380
+ opacity: 0.6;
381
+ animation: markdyAurora 9s linear infinite;
382
+ }
383
+ .markdy-showcase--byte_viz { width: 420px; }
384
+ .markdy-showcase__top {
385
+ display: flex;
386
+ align-items: center;
387
+ justify-content: space-between;
388
+ gap: 12px;
389
+ padding: 14px 16px 8px;
390
+ }
391
+ .markdy-showcase__title {
392
+ min-width: 0;
393
+ overflow: hidden;
394
+ text-overflow: ellipsis;
395
+ white-space: nowrap;
396
+ font-size: 15px;
397
+ font-weight: 820;
398
+ letter-spacing: 0.01em;
399
+ color: #f8fafc;
400
+ }
401
+ .markdy-showcase__pill {
402
+ flex: 0 0 auto;
403
+ border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
404
+ border-radius: 999px;
405
+ padding: 4px 8px;
406
+ color: #bae6fd;
407
+ background: rgba(8, 47, 73, 0.54);
408
+ font-size: 10px;
409
+ font-weight: 800;
410
+ letter-spacing: 0.08em;
411
+ text-transform: uppercase;
412
+ box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 22%, transparent);
413
+ }
414
+ .markdy-showcase__body { position: absolute; inset: 48px 14px 14px; }
415
+ .markdy-showcase--parking_map { --accent: #38bdf8; --accent-2: #22c55e; }
416
+ .parking-map__road {
417
+ position: absolute;
418
+ left: 14px;
419
+ right: 14px;
420
+ top: 78px;
421
+ height: 50px;
422
+ border-radius: 999px;
423
+ background:
424
+ repeating-linear-gradient(90deg, rgba(226, 232, 240, 0.78) 0 18px, transparent 18px 34px) center / 100% 3px no-repeat,
425
+ linear-gradient(90deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.88));
426
+ box-shadow: 0 10px 28px rgba(2, 6, 23, 0.28) inset;
427
+ }
428
+ .parking-map__scan {
429
+ position: absolute;
430
+ left: 28px;
431
+ top: 42px;
432
+ width: 66px;
433
+ height: 124px;
434
+ border-radius: 18px;
435
+ border: 1px solid rgba(56, 189, 248, 0.55);
436
+ background: linear-gradient(180deg, rgba(56, 189, 248, 0.22), transparent);
437
+ box-shadow: 0 0 24px rgba(56, 189, 248, 0.28);
438
+ animation: markdyScanPulse 1.8s ease-in-out infinite;
439
+ }
440
+ .parking-map__car {
441
+ position: absolute;
442
+ left: 48px;
443
+ top: 86px;
444
+ width: 46px;
445
+ height: 22px;
446
+ border-radius: 11px 15px 15px 11px;
447
+ background: linear-gradient(90deg, #22d3ee, #818cf8);
448
+ box-shadow: 0 0 18px rgba(56, 189, 248, 0.55), 0 8px 18px rgba(2, 6, 23, 0.35);
449
+ animation: markdyCarGlide 3.8s cubic-bezier(.4,0,.2,1) infinite;
450
+ }
451
+ .parking-map__car::before,
452
+ .parking-map__car::after {
453
+ content: "";
454
+ position: absolute;
455
+ bottom: -4px;
456
+ width: 8px;
457
+ height: 8px;
458
+ border-radius: 999px;
459
+ background: #020617;
460
+ border: 2px solid #cbd5e1;
461
+ }
462
+ .parking-map__car::before { left: 7px; }
463
+ .parking-map__car::after { right: 7px; }
464
+ .parking-map__slots {
465
+ position: absolute;
466
+ right: 12px;
467
+ top: 24px;
468
+ display: grid;
469
+ grid-template-columns: repeat(4, 44px);
470
+ gap: 8px;
471
+ }
472
+ .parking-map__slot {
473
+ height: 34px;
474
+ border-radius: 10px;
475
+ border: 1px solid rgba(148, 163, 184, 0.34);
476
+ background: rgba(15, 23, 42, 0.7);
477
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.035) inset;
478
+ }
479
+ .parking-map__slot:nth-child(3),
480
+ .parking-map__slot:nth-child(6),
481
+ .parking-map__slot:nth-child(8) {
482
+ border-color: rgba(34, 197, 94, 0.78);
483
+ background: rgba(20, 83, 45, 0.5);
484
+ animation: markdySlotGlow 1.7s ease-in-out infinite;
485
+ }
486
+ .parking-map__route {
487
+ position: absolute;
488
+ left: 91px;
489
+ right: 95px;
490
+ top: 109px;
491
+ height: 3px;
492
+ border-radius: 999px;
493
+ background: linear-gradient(90deg, transparent, #22c55e, #38bdf8, transparent);
494
+ filter: drop-shadow(0 0 8px #22c55e);
495
+ animation: markdyRouteFlow 1.5s linear infinite;
496
+ }
497
+ .parking-map__stats {
498
+ position: absolute;
499
+ left: 12px;
500
+ right: 12px;
501
+ bottom: 5px;
502
+ display: grid;
503
+ grid-template-columns: repeat(3, 1fr);
504
+ gap: 8px;
505
+ }
506
+ .parking-map__stat,
507
+ .ascii-map__badge,
508
+ .game-scene__hud,
509
+ .byte-viz__badge {
510
+ border: 1px solid rgba(148, 163, 184, 0.24);
511
+ border-radius: 12px;
512
+ padding: 8px;
513
+ background: rgba(2, 6, 23, 0.42);
514
+ color: #cbd5e1;
515
+ font-size: 10px;
516
+ font-weight: 760;
517
+ }
518
+ .parking-map__stat strong,
519
+ .byte-viz__badge strong {
520
+ display: block;
521
+ margin-top: 2px;
522
+ color: #f8fafc;
523
+ font-size: 16px;
524
+ }
525
+ .markdy-showcase--ascii_map { --accent: #22c55e; --accent-2: #38bdf8; }
526
+ .ascii-map__terminal {
527
+ position: absolute;
528
+ inset: 0;
529
+ border-radius: 18px;
530
+ border: 1px solid rgba(34, 197, 94, 0.34);
531
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.72), rgba(2, 6, 23, 0.36));
532
+ box-shadow: 0 0 28px rgba(34, 197, 94, 0.16) inset;
533
+ }
534
+ .ascii-map__chrome {
535
+ height: 28px;
536
+ padding-left: 12px;
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 6px;
540
+ border-bottom: 1px solid rgba(148, 163, 184, 0.16);
541
+ }
542
+ .ascii-map__chrome span {
543
+ width: 8px;
544
+ height: 8px;
545
+ border-radius: 999px;
546
+ background: #ef4444;
547
+ box-shadow: 14px 0 #f59e0b, 28px 0 #22c55e;
548
+ }
549
+ .ascii-map__code {
550
+ position: absolute;
551
+ left: 16px;
552
+ top: 46px;
553
+ font-family: "SFMono-Regular", Consolas, monospace;
554
+ font-size: 19px;
555
+ line-height: 1.55;
556
+ color: #bbf7d0;
557
+ text-shadow: 0 0 12px rgba(34, 197, 94, 0.45);
558
+ }
559
+ .ascii-map__code b {
560
+ color: #67e8f9;
561
+ animation: markdyBlink 1.2s steps(2, end) infinite;
562
+ }
563
+ .ascii-map__minimap {
564
+ position: absolute;
565
+ right: 18px;
566
+ top: 48px;
567
+ width: 102px;
568
+ height: 122px;
569
+ display: grid;
570
+ grid-template-columns: repeat(3, 1fr);
571
+ gap: 7px;
572
+ }
573
+ .ascii-map__mini-slot {
574
+ border-radius: 7px;
575
+ border: 1px solid rgba(148, 163, 184, 0.28);
576
+ background: rgba(15, 23, 42, 0.72);
577
+ }
578
+ .ascii-map__mini-slot:nth-child(2),
579
+ .ascii-map__mini-slot:nth-child(6) {
580
+ border-color: rgba(34, 197, 94, 0.85);
581
+ background: rgba(20, 83, 45, 0.56);
582
+ }
583
+ .ascii-map__transform {
584
+ position: absolute;
585
+ left: 16px;
586
+ right: 16px;
587
+ bottom: 10px;
588
+ display: grid;
589
+ grid-template-columns: 1fr 1fr 1fr;
590
+ gap: 8px;
591
+ }
592
+ .markdy-showcase--game_scene { --accent: #f59e0b; --accent-2: #22c55e; }
593
+ .game-scene__world {
594
+ position: absolute;
595
+ inset: 0;
596
+ border-radius: 18px;
597
+ overflow: hidden;
598
+ background:
599
+ radial-gradient(circle at 80% 18%, rgba(245, 158, 11, 0.22), transparent 26%),
600
+ linear-gradient(160deg, #172554, #020617 72%);
601
+ }
602
+ .game-scene__world::before {
603
+ content: "";
604
+ position: absolute;
605
+ inset: 0;
606
+ background: repeating-linear-gradient(115deg, transparent 0 22px, rgba(255,255,255,0.06) 22px 24px);
607
+ animation: markdyRoadMove 4s linear infinite;
608
+ }
609
+ .game-scene__road {
610
+ position: absolute;
611
+ left: -35px;
612
+ right: -35px;
613
+ top: 94px;
614
+ height: 76px;
615
+ transform: rotate(-8deg);
616
+ background:
617
+ repeating-linear-gradient(90deg, #e2e8f0 0 24px, transparent 24px 46px) center / 100% 4px no-repeat,
618
+ linear-gradient(180deg, #334155, #0f172a);
619
+ box-shadow: 0 18px 32px rgba(2, 6, 23, 0.42);
620
+ }
621
+ .game-scene__car {
622
+ position: absolute;
623
+ left: 88px;
624
+ top: 126px;
625
+ width: 58px;
626
+ height: 30px;
627
+ border-radius: 12px 18px 18px 12px;
628
+ background: linear-gradient(90deg, #fb7185, #f59e0b);
629
+ transform: rotate(-8deg);
630
+ box-shadow: 0 0 24px rgba(245, 158, 11, 0.52);
631
+ animation: markdyCarPark 3.4s cubic-bezier(.2,1,.3,1) infinite;
632
+ }
633
+ .game-scene__spot {
634
+ position: absolute;
635
+ right: 54px;
636
+ top: 85px;
637
+ width: 90px;
638
+ height: 68px;
639
+ border: 3px solid rgba(34, 197, 94, 0.78);
640
+ border-left-style: dashed;
641
+ border-radius: 10px;
642
+ filter: drop-shadow(0 0 12px rgba(34, 197, 94, 0.48));
643
+ }
644
+ .game-scene__cones {
645
+ position: absolute;
646
+ right: 80px;
647
+ bottom: 34px;
648
+ display: flex;
649
+ gap: 14px;
650
+ }
651
+ .game-scene__cone {
652
+ width: 0;
653
+ height: 0;
654
+ border-left: 10px solid transparent;
655
+ border-right: 10px solid transparent;
656
+ border-bottom: 26px solid #f97316;
657
+ filter: drop-shadow(0 5px 8px rgba(2, 6, 23, 0.35));
658
+ }
659
+ .game-scene__hud {
660
+ position: absolute;
661
+ left: 14px;
662
+ top: 14px;
663
+ min-width: 120px;
664
+ border-color: rgba(245, 158, 11, 0.38);
665
+ }
666
+ .game-scene__hud strong { color: #fed7aa; font-size: 18px; }
667
+ .game-scene__perfect {
668
+ position: absolute;
669
+ left: 132px;
670
+ top: 54px;
671
+ color: #fef3c7;
672
+ font-size: 21px;
673
+ font-weight: 900;
674
+ letter-spacing: 0.05em;
675
+ text-shadow: 0 0 18px rgba(245, 158, 11, 0.7);
676
+ animation: markdyPerfectPop 1.6s ease-in-out infinite;
677
+ }
678
+ .markdy-showcase--byte_viz { --accent: #a78bfa; --accent-2: #38bdf8; }
679
+ .byte-viz__pipeline {
680
+ position: absolute;
681
+ inset: 4px;
682
+ display: grid;
683
+ grid-template-columns: 98px 1fr 122px;
684
+ gap: 12px;
685
+ align-items: stretch;
686
+ }
687
+ .byte-viz__glyph {
688
+ display: grid;
689
+ place-items: center;
690
+ border-radius: 22px;
691
+ border: 1px solid rgba(167, 139, 250, 0.45);
692
+ background: radial-gradient(circle at 30% 20%, rgba(167, 139, 250, 0.42), rgba(15, 23, 42, 0.78));
693
+ color: #fff;
694
+ font-size: 72px;
695
+ font-weight: 920;
696
+ box-shadow: 0 0 34px rgba(167, 139, 250, 0.32) inset;
697
+ }
698
+ .byte-viz__middle { display: grid; gap: 10px; }
699
+ .byte-viz__row {
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 7px;
703
+ }
704
+ .byte-viz__chip {
705
+ flex: 1 1 0;
706
+ border: 1px solid rgba(148, 163, 184, 0.25);
707
+ border-radius: 12px;
708
+ padding: 8px 10px;
709
+ background: rgba(2, 6, 23, 0.42);
710
+ color: #ddd6fe;
711
+ font-family: "SFMono-Regular", Consolas, monospace;
712
+ font-size: 12px;
713
+ font-weight: 800;
714
+ }
715
+ .byte-viz__arrow {
716
+ color: #67e8f9;
717
+ filter: drop-shadow(0 0 8px #38bdf8);
718
+ animation: markdyArrowPulse 1.1s ease-in-out infinite;
719
+ }
720
+ .byte-viz__bits {
721
+ display: grid;
722
+ grid-template-columns: repeat(8, 1fr);
723
+ gap: 5px;
724
+ }
725
+ .byte-viz__bit {
726
+ display: grid;
727
+ place-items: center;
728
+ height: 31px;
729
+ border-radius: 8px;
730
+ background: rgba(15, 23, 42, 0.82);
731
+ border: 1px solid rgba(56, 189, 248, 0.28);
732
+ color: #bae6fd;
733
+ font-family: "SFMono-Regular", Consolas, monospace;
734
+ font-size: 12px;
735
+ font-weight: 900;
736
+ animation: markdyBitFlip 1.9s ease-in-out infinite;
737
+ }
738
+ .byte-viz__bit:nth-child(2n) { animation-delay: 0.12s; }
739
+ .byte-viz__bit:nth-child(3n) { animation-delay: 0.24s; }
740
+ .byte-viz__side { display: grid; gap: 9px; }
741
+ @keyframes markdyGridDrift { to { background-position: 34px 34px, 34px 34px; } }
742
+ @keyframes markdyAurora { to { transform: rotate(1turn); } }
743
+ @keyframes markdyScanPulse { 50% { transform: translateX(18px); opacity: 0.72; } }
744
+ @keyframes markdyCarGlide { 0%, 12% { transform: translateX(0); } 68%, 100% { transform: translateX(198px); } }
745
+ @keyframes markdySlotGlow { 50% { box-shadow: 0 0 20px rgba(34, 197, 94, 0.38); } }
746
+ @keyframes markdyRouteFlow { to { filter: hue-rotate(70deg) drop-shadow(0 0 9px #38bdf8); } }
747
+ @keyframes markdyBlink { 50% { opacity: 0.25; } }
748
+ @keyframes markdyRoadMove { to { background-position: 44px 0; } }
749
+ @keyframes markdyCarPark { 0%, 12% { transform: translateX(0) rotate(-8deg); } 70%, 100% { transform: translateX(164px) translateY(-31px) rotate(-1deg); } }
750
+ @keyframes markdyPerfectPop { 0%, 100% { transform: scale(0.92); opacity: 0.72; } 50% { transform: scale(1.05); opacity: 1; } }
751
+ @keyframes markdyArrowPulse { 50% { transform: translateX(3px); opacity: 0.72; } }
752
+ @keyframes markdyBitFlip { 50% { transform: translateY(-2px); border-color: rgba(167, 139, 250, 0.75); } }
753
+ `;
754
+ doc.head.appendChild(style);
755
+ }
756
+ function createShowcaseSurfaceEl(type, def) {
757
+ ensureShowcaseStyles(document);
758
+ const label = def.args[0] || type.replace("_", " ");
759
+ const root = document.createElement("div");
760
+ root.className = `markdy-showcase markdy-showcase--${type}`;
761
+ root.setAttribute("role", "img");
762
+ root.setAttribute("aria-label", label);
763
+ if (type === "parking_map") {
764
+ root.innerHTML = `
765
+ <div class="markdy-showcase__top"><div class="markdy-showcase__title"></div><div class="markdy-showcase__pill">live ops</div></div>
766
+ <div class="markdy-showcase__body">
767
+ <div class="parking-map__scan"></div><div class="parking-map__road"></div><div class="parking-map__route"></div><div class="parking-map__car"></div>
768
+ <div class="parking-map__slots"><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span><span class="parking-map__slot"></span></div>
769
+ <div class="parking-map__stats"><div class="parking-map__stat">occupancy<strong>87%</strong></div><div class="parking-map__stat">OCR<strong>18ms</strong></div><div class="parking-map__stat">route<strong>A-17</strong></div></div>
770
+ </div>`;
771
+ } else if (type === "ascii_map") {
772
+ root.innerHTML = `
773
+ <div class="markdy-showcase__top"><div class="markdy-showcase__title"></div><div class="markdy-showcase__pill">parser</div></div>
774
+ <div class="markdy-showcase__body">
775
+ <div class="ascii-map__terminal"><div class="ascii-map__chrome"><span></span></div><div class="ascii-map__code">[P1][P2][<b> </b>][EV]<br />[IN]===LANE===[OUT]<br />0101 0000 0100 0001</div><div class="ascii-map__minimap"><i class="ascii-map__mini-slot"></i><i class="ascii-map__mini-slot"></i><i class="ascii-map__mini-slot"></i><i class="ascii-map__mini-slot"></i><i class="ascii-map__mini-slot"></i><i class="ascii-map__mini-slot"></i></div><div class="ascii-map__transform"><div class="ascii-map__badge">tokens</div><div class="ascii-map__badge">graph</div><div class="ascii-map__badge">slots</div></div></div>
776
+ </div>`;
777
+ } else if (type === "game_scene") {
778
+ root.innerHTML = `
779
+ <div class="markdy-showcase__top"><div class="markdy-showcase__title"></div><div class="markdy-showcase__pill">60 fps</div></div>
780
+ <div class="markdy-showcase__body">
781
+ <div class="game-scene__world"><div class="game-scene__hud">combo<br /><strong>x4.2</strong></div><div class="game-scene__perfect">PERFECT PARK</div><div class="game-scene__road"></div><div class="game-scene__spot"></div><div class="game-scene__car"></div><div class="game-scene__cones"><i class="game-scene__cone"></i><i class="game-scene__cone"></i><i class="game-scene__cone"></i></div></div>
782
+ </div>`;
783
+ } else {
784
+ root.innerHTML = `
785
+ <div class="markdy-showcase__top"><div class="markdy-showcase__title"></div><div class="markdy-showcase__pill">UTF-8</div></div>
786
+ <div class="markdy-showcase__body">
787
+ <div class="byte-viz__pipeline"><div class="byte-viz__glyph">A</div><div class="byte-viz__middle"><div class="byte-viz__row"><div class="byte-viz__chip">char</div><span class="byte-viz__arrow">-></span><div class="byte-viz__chip">U+0041</div></div><div class="byte-viz__row"><div class="byte-viz__chip">0x41</div><span class="byte-viz__arrow">-></span><div class="byte-viz__chip">01000001</div></div><div class="byte-viz__bits"><span class="byte-viz__bit">0</span><span class="byte-viz__bit">1</span><span class="byte-viz__bit">0</span><span class="byte-viz__bit">0</span><span class="byte-viz__bit">0</span><span class="byte-viz__bit">0</span><span class="byte-viz__bit">0</span><span class="byte-viz__bit">1</span></div></div><div class="byte-viz__side"><div class="byte-viz__badge">bytes<strong>1</strong></div><div class="byte-viz__badge">planes<strong>BMP</strong></div><div class="byte-viz__badge">glyph<strong>paint</strong></div></div></div>
788
+ </div>`;
789
+ }
790
+ const title = root.querySelector(".markdy-showcase__title");
791
+ if (title) title.textContent = label;
792
+ return root;
793
+ }
794
+ function ensureArchitectureStyles(doc) {
795
+ if (doc.getElementById(ARCHITECTURE_STYLE_ID)) return;
796
+ const style = doc.createElement("style");
797
+ style.id = ARCHITECTURE_STYLE_ID;
798
+ style.textContent = `
799
+ .markdy-arch-node {
800
+ --markdy-node-accent: #38bdf8;
801
+ --markdy-node-accent-2: #22c55e;
802
+ --markdy-node-surface: rgba(15, 23, 42, 0.82);
803
+ --markdy-node-border: rgba(148, 163, 184, 0.34);
804
+ --markdy-node-glow: rgba(56, 189, 248, 0.24);
805
+ position: relative;
806
+ isolation: isolate;
807
+ width: 184px;
808
+ min-height: 88px;
809
+ box-sizing: border-box;
810
+ display: grid;
811
+ grid-template-columns: 34px minmax(0, 1fr);
812
+ align-items: center;
813
+ gap: 10px;
814
+ padding: 13px 14px;
815
+ border: 1px solid var(--markdy-node-border);
816
+ border-radius: 14px;
817
+ color: #e5eefb;
818
+ background:
819
+ linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.02) 34%, rgba(15, 23, 42, 0.88) 100%),
820
+ radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--markdy-node-accent) 34%, transparent), transparent 42%),
821
+ var(--markdy-node-surface);
822
+ box-shadow:
823
+ 0 16px 34px rgba(2, 6, 23, 0.28),
824
+ 0 0 0 1px rgba(255, 255, 255, 0.04) inset,
825
+ 0 1px 0 rgba(255, 255, 255, 0.12) inset,
826
+ 0 0 28px var(--markdy-node-glow);
827
+ overflow: hidden;
828
+ contain: layout paint style;
829
+ backdrop-filter: blur(14px) saturate(1.18);
830
+ -webkit-backdrop-filter: blur(14px) saturate(1.18);
831
+ }
832
+ .markdy-arch-node::before {
833
+ content: "";
834
+ position: absolute;
835
+ inset: 0;
836
+ z-index: -1;
837
+ background:
838
+ linear-gradient(90deg, transparent 0 24%, rgba(255, 255, 255, 0.06) 50%, transparent 76%) -180px 0 / 180px 100% no-repeat,
839
+ repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.055) 0 1px, transparent 1px 10px);
840
+ opacity: 0.6;
841
+ }
842
+ .markdy-arch-node::after {
843
+ content: "";
844
+ position: absolute;
845
+ left: 14px;
846
+ right: 14px;
847
+ bottom: 9px;
848
+ height: 2px;
849
+ border-radius: 999px;
850
+ background: linear-gradient(90deg, transparent, var(--markdy-node-accent), var(--markdy-node-accent-2), transparent);
851
+ opacity: 0.82;
852
+ filter: drop-shadow(0 0 6px var(--markdy-node-accent));
853
+ }
854
+ .markdy-arch-node__icon {
855
+ position: relative;
856
+ width: 32px;
857
+ height: 32px;
858
+ border-radius: 11px;
859
+ background:
860
+ radial-gradient(circle at 35% 25%, rgba(255, 255, 255, 0.28), transparent 42%),
861
+ linear-gradient(145deg, var(--markdy-node-accent), color-mix(in srgb, var(--markdy-node-accent) 38%, #020617));
862
+ box-shadow:
863
+ 0 0 0 1px rgba(255, 255, 255, 0.16) inset,
864
+ 0 10px 20px color-mix(in srgb, var(--markdy-node-accent) 25%, transparent);
865
+ }
866
+ .markdy-arch-node__icon::before,
867
+ .markdy-arch-node__icon::after {
868
+ content: "";
869
+ position: absolute;
870
+ box-sizing: border-box;
871
+ border-color: rgba(255, 255, 255, 0.9);
872
+ }
873
+ .markdy-arch-node__label {
874
+ min-width: 0;
875
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
876
+ font-size: 15px;
877
+ font-weight: 720;
878
+ line-height: 1.08;
879
+ color: #f8fafc;
880
+ text-shadow: 0 1px 12px rgba(15, 23, 42, 0.8);
881
+ white-space: nowrap;
882
+ overflow: hidden;
883
+ text-overflow: ellipsis;
884
+ }
885
+ .markdy-arch-node__type {
886
+ display: block;
887
+ margin-top: 5px;
888
+ font-size: 9px;
889
+ font-weight: 740;
890
+ letter-spacing: 0.08em;
891
+ text-transform: uppercase;
892
+ color: color-mix(in srgb, var(--markdy-node-accent) 70%, #e2e8f0);
893
+ opacity: 0.86;
894
+ }
895
+ .markdy-arch-node[data-markdy-system-type="service"],
896
+ .markdy-arch-node[data-markdy-system-type="api"],
897
+ .markdy-arch-node[data-markdy-system-type="microservice"] {
898
+ --markdy-node-accent: #38bdf8;
899
+ --markdy-node-accent-2: #818cf8;
900
+ }
901
+ .markdy-arch-node[data-markdy-system-type="client"],
902
+ .markdy-arch-node[data-markdy-system-type="user"] {
903
+ --markdy-node-accent: #f59e0b;
904
+ --markdy-node-accent-2: #fb7185;
905
+ }
906
+ .markdy-arch-node[data-markdy-system-type="database"],
907
+ .markdy-arch-node[data-markdy-system-type="db"] {
908
+ --markdy-node-accent: #22c55e;
909
+ --markdy-node-accent-2: #14b8a6;
910
+ border-radius: 18px 18px 24px 24px;
911
+ }
912
+ .markdy-arch-node[data-markdy-system-type="cache"] {
913
+ --markdy-node-accent: #a3e635;
914
+ --markdy-node-accent-2: #22c55e;
915
+ border-style: dashed;
916
+ }
917
+ .markdy-arch-node[data-markdy-system-type="queue"] {
918
+ --markdy-node-accent: #a78bfa;
919
+ --markdy-node-accent-2: #38bdf8;
920
+ }
921
+ .markdy-arch-node[data-markdy-system-type="cloud"],
922
+ .markdy-arch-node[data-markdy-system-type="region"] {
923
+ --markdy-node-accent: #60a5fa;
924
+ --markdy-node-accent-2: #67e8f9;
925
+ border-radius: 999px;
926
+ }
927
+ .markdy-arch-node[data-markdy-system-type="container"],
928
+ .markdy-arch-node[data-markdy-system-type="cluster"] {
929
+ --markdy-node-accent: #c084fc;
930
+ --markdy-node-accent-2: #f472b6;
931
+ border-radius: 10px;
932
+ }
933
+ .markdy-arch-node[data-markdy-system-type="service"] .markdy-arch-node__icon::before,
934
+ .markdy-arch-node[data-markdy-system-type="api"] .markdy-arch-node__icon::before,
935
+ .markdy-arch-node[data-markdy-system-type="microservice"] .markdy-arch-node__icon::before {
936
+ inset: 9px 7px;
937
+ border-top: 3px solid rgba(255, 255, 255, 0.92);
938
+ border-bottom: 3px solid rgba(255, 255, 255, 0.92);
939
+ }
940
+ .markdy-arch-node[data-markdy-system-type="service"] .markdy-arch-node__icon::after,
941
+ .markdy-arch-node[data-markdy-system-type="api"] .markdy-arch-node__icon::after,
942
+ .markdy-arch-node[data-markdy-system-type="microservice"] .markdy-arch-node__icon::after {
943
+ inset: 14px 7px auto;
944
+ border-top: 3px solid rgba(255, 255, 255, 0.92);
945
+ }
946
+ .markdy-arch-node[data-markdy-system-type="client"] .markdy-arch-node__icon::before,
947
+ .markdy-arch-node[data-markdy-system-type="user"] .markdy-arch-node__icon::before {
948
+ left: 8px;
949
+ right: 8px;
950
+ top: 8px;
951
+ height: 13px;
952
+ border: 2px solid rgba(255, 255, 255, 0.92);
953
+ border-radius: 3px;
954
+ }
955
+ .markdy-arch-node[data-markdy-system-type="client"] .markdy-arch-node__icon::after,
956
+ .markdy-arch-node[data-markdy-system-type="user"] .markdy-arch-node__icon::after {
957
+ left: 12px;
958
+ right: 12px;
959
+ bottom: 7px;
960
+ border-top: 2px solid rgba(255, 255, 255, 0.92);
961
+ }
962
+ .markdy-arch-node[data-markdy-system-type="database"] .markdy-arch-node__icon::before,
963
+ .markdy-arch-node[data-markdy-system-type="db"] .markdy-arch-node__icon::before,
964
+ .markdy-arch-node[data-markdy-system-type="cache"] .markdy-arch-node__icon::before {
965
+ inset: 7px 7px 9px;
966
+ border: 2px solid rgba(255, 255, 255, 0.92);
967
+ border-radius: 50% / 16%;
968
+ }
969
+ .markdy-arch-node[data-markdy-system-type="database"] .markdy-arch-node__icon::after,
970
+ .markdy-arch-node[data-markdy-system-type="db"] .markdy-arch-node__icon::after,
971
+ .markdy-arch-node[data-markdy-system-type="cache"] .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-type="queue"] .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-type="queue"] .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-type="cloud"] .markdy-arch-node__icon::before,
991
+ .markdy-arch-node[data-markdy-system-type="region"] .markdy-arch-node__icon::before {
992
+ left: 7px;
993
+ right: 7px;
994
+ bottom: 9px;
995
+ height: 12px;
996
+ border: 2px solid rgba(255, 255, 255, 0.92);
997
+ border-radius: 999px;
998
+ }
999
+ .markdy-arch-node[data-markdy-system-type="cloud"] .markdy-arch-node__icon::after,
1000
+ .markdy-arch-node[data-markdy-system-type="region"] .markdy-arch-node__icon::after {
1001
+ left: 10px;
1002
+ top: 7px;
1003
+ width: 13px;
1004
+ height: 13px;
1005
+ border-top: 2px solid rgba(255, 255, 255, 0.92);
1006
+ border-left: 2px solid rgba(255, 255, 255, 0.92);
1007
+ border-radius: 999px 0 0 0;
1008
+ }
1009
+ .markdy-arch-node[data-markdy-system-type="container"] .markdy-arch-node__icon::before,
1010
+ .markdy-arch-node[data-markdy-system-type="cluster"] .markdy-arch-node__icon::before {
1011
+ inset: 8px;
1012
+ border: 2px solid rgba(255, 255, 255, 0.92);
1013
+ border-radius: 4px;
1014
+ }
1015
+ .markdy-arch-node[data-markdy-system-type="container"] .markdy-arch-node__icon::after,
1016
+ .markdy-arch-node[data-markdy-system-type="cluster"] .markdy-arch-node__icon::after {
1017
+ left: 10px;
1018
+ right: 10px;
1019
+ top: 14px;
1020
+ border-top: 2px solid rgba(255, 255, 255, 0.72);
1021
+ }
1022
+ `;
1023
+ doc.head.appendChild(style);
1024
+ }
1025
+ function isArchitectureNodeType(type) {
1026
+ return ARCHITECTURE_NODE_TYPES.has(type);
1027
+ }
1028
+ function isShowcaseSurfaceType(type) {
1029
+ return SHOWCASE_SURFACE_TYPES.has(type);
1030
+ }
1031
+ function architectureTypeLabel(type) {
1032
+ if (type === "db") return "database";
1033
+ if (type === "api") return "service";
1034
+ return type;
1035
+ }
312
1036
  function createActorEl(name, def, assetDefs, assetOverrides) {
313
1037
  let el;
314
1038
  switch (def.type) {
@@ -377,62 +1101,44 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
377
1101
  el = createFigureEl(def);
378
1102
  break;
379
1103
  }
1104
+ case "parking_map":
1105
+ case "ascii_map":
1106
+ case "game_scene":
1107
+ case "byte_viz": {
1108
+ el = createShowcaseSurfaceEl(def.type, def);
1109
+ break;
1110
+ }
380
1111
  case "service":
1112
+ case "api":
1113
+ case "microservice":
381
1114
  case "client":
1115
+ case "user":
382
1116
  case "db":
383
- case "queue": {
1117
+ case "database":
1118
+ case "queue":
1119
+ case "cache":
1120
+ case "cloud":
1121
+ case "region":
1122
+ case "container":
1123
+ case "cluster": {
1124
+ ensureArchitectureStyles(document);
384
1125
  const card = document.createElement("div");
1126
+ card.className = "markdy-arch-node";
1127
+ card.dataset.markdySystemType = def.type;
1128
+ card.setAttribute("role", "img");
1129
+ card.setAttribute("aria-label", `${architectureTypeLabel(def.type)} ${def.args[0] ?? name}`);
1130
+ const icon = document.createElement("span");
1131
+ icon.className = "markdy-arch-node__icon";
1132
+ icon.setAttribute("aria-hidden", "true");
385
1133
  const label = document.createElement("div");
1134
+ label.className = "markdy-arch-node__label";
386
1135
  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
- }
1136
+ label.style.fontSize = `${def.size ?? 15}px`;
1137
+ const typeLabel = document.createElement("span");
1138
+ typeLabel.className = "markdy-arch-node__type";
1139
+ typeLabel.textContent = architectureTypeLabel(def.type);
1140
+ label.appendChild(typeLabel);
1141
+ card.appendChild(icon);
436
1142
  card.appendChild(label);
437
1143
  el = card;
438
1144
  break;
@@ -456,82 +1162,74 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
456
1162
  el.style.opacity = String(def.opacity ?? 1);
457
1163
  if (def.z !== void 0) el.style.zIndex = String(def.z);
458
1164
  else if (def.type === "caption") el.style.zIndex = "100";
1165
+ else if (isShowcaseSurfaceType(def.type)) el.style.zIndex = "25";
1166
+ else if (isArchitectureNodeType(def.type)) el.style.zIndex = "10";
459
1167
  return el;
460
1168
  }
461
1169
 
462
- // src/animations.ts
463
- var DEFAULT_EASE_BY_ACTION = {
464
- enter: "out",
465
- fade_in: "out",
466
- exit: "in",
467
- fade_out: "in"
468
- };
469
- function isSceneDark(scene) {
470
- const bg = scene.style.background || "white";
471
- let hex = bg.trim().replace(/^#/, "");
472
- if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
473
- if (hex.length === 6) {
474
- const r = parseInt(hex.slice(0, 2), 16);
475
- const g = parseInt(hex.slice(2, 4), 16);
476
- const b = parseInt(hex.slice(4, 6), 16);
477
- return 0.299 * r + 0.587 * g + 0.114 * b <= 140;
478
- }
479
- const dark = { black: true, "#000": true, "#000000": true };
480
- return dark[bg.toLowerCase()] ?? false;
1170
+ // src/camera.ts
1171
+ function freshCameraState() {
1172
+ return { x: 0, y: 0, zoom: 1 };
481
1173
  }
482
- function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
483
- const anims = [];
484
- const states = /* @__PURE__ */ new Map();
485
- for (const [name, def] of Object.entries(ast.actors)) {
486
- states.set(name, stateFrom(def));
487
- }
488
- const captionActors = /* @__PURE__ */ new Set();
489
- for (const [name, def] of Object.entries(ast.actors)) {
490
- if (def.type === "caption") captionActors.add(name);
491
- }
492
- const txFor = (actorName) => captionActors.has(actorName) ? txCaption : tx;
493
- const events = [...ast.events].sort((a, b) => a.time - b.time);
494
- preInitInlineStyles(ast, actorEls, states, events, txFor);
495
- const cameraState = freshCameraState();
496
- for (const ev of events) {
497
- const delayMs = ev.time * 1e3;
498
- const durMs = Math.max(
499
- 1,
500
- (typeof ev.params.dur === "number" ? ev.params.dur : 0.5) * 1e3
501
- );
502
- const easing = toEasing(ev.params.ease ?? DEFAULT_EASE_BY_ACTION[ev.action]);
503
- const baseOpts = {
504
- delay: delayMs,
505
- duration: durMs,
506
- fill: "forwards",
507
- easing
508
- };
509
- if (ev.actor === "camera") {
510
- buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState);
511
- continue;
1174
+ function cameraTx(s) {
1175
+ return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
1176
+ }
1177
+ var DEFAULT_SHAKE_INTENSITY = 8;
1178
+ function buildCameraAction(ev, scene, ast, baseOpts, anims, state) {
1179
+ switch (ev.action) {
1180
+ case "pan": {
1181
+ const to = ev.params.to;
1182
+ if (!to) break;
1183
+ const next = {
1184
+ ...state,
1185
+ x: to[0] - ast.meta.width / 2,
1186
+ y: to[1] - ast.meta.height / 2
1187
+ };
1188
+ anims.push(
1189
+ scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
1190
+ );
1191
+ state.x = next.x;
1192
+ state.y = next.y;
1193
+ break;
512
1194
  }
513
- const el = actorEls.get(ev.actor);
514
- const s = states.get(ev.actor);
515
- if (!el || !s) continue;
516
- buildAction(
517
- ev,
518
- el,
519
- s,
520
- baseOpts,
521
- delayMs,
522
- durMs,
523
- ast,
524
- states,
525
- actorEls,
526
- scene,
527
- assetOverrides,
528
- faceSwaps,
529
- anims,
530
- txFor(ev.actor)
531
- );
1195
+ case "zoom": {
1196
+ const next = {
1197
+ ...state,
1198
+ zoom: typeof ev.params.to === "number" ? ev.params.to : state.zoom
1199
+ };
1200
+ anims.push(
1201
+ scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
1202
+ );
1203
+ state.zoom = next.zoom;
1204
+ break;
1205
+ }
1206
+ case "shake": {
1207
+ const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : DEFAULT_SHAKE_INTENSITY;
1208
+ const at = (dx, dy, offset) => ({
1209
+ transform: cameraTx({ ...state, x: state.x + dx, y: state.y + dy }),
1210
+ offset
1211
+ });
1212
+ anims.push(
1213
+ scene.animate(
1214
+ [
1215
+ at(0, 0, 0),
1216
+ at(-mag, -mag * 0.4, 0.15),
1217
+ at(mag, mag * 0.4, 0.35),
1218
+ at(-mag * 0.6, mag * 0.3, 0.55),
1219
+ at(mag * 0.5, -mag * 0.3, 0.75),
1220
+ at(0, 0, 1)
1221
+ ],
1222
+ { ...baseOpts, easing: "linear" }
1223
+ )
1224
+ );
1225
+ break;
1226
+ }
1227
+ default:
1228
+ break;
532
1229
  }
533
- return anims;
534
1230
  }
1231
+
1232
+ // src/stage.ts
535
1233
  function offscreenState(s, direction, sceneWidth, sceneHeight) {
536
1234
  const out = { ...s };
537
1235
  switch (direction) {
@@ -561,113 +1259,409 @@ function preInitInlineStyles(ast, actorEls, states, events, txFor) {
561
1259
  const s = states.get(name);
562
1260
  if (!el || !s) continue;
563
1261
  const firstEv = firstEventByActor.get(name);
564
- const txFn = txFor(name);
565
- if (firstEv?.action === "enter") {
1262
+ if (!firstEv) continue;
1263
+ if (firstEv.action === "enter") {
566
1264
  const from = String(firstEv.params.from ?? "left");
567
- const offscreen = offscreenState(s, from, ast.meta.width, ast.meta.height);
568
- el.style.transform = txFn(offscreen);
1265
+ el.style.transform = txFor(name)(offscreenState(s, from, ast.meta.width, ast.meta.height));
569
1266
  }
570
- if (firstEv?.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
1267
+ if (firstEv.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
571
1268
  el.style.opacity = "0";
572
1269
  }
573
1270
  }
574
1271
  }
575
- function freshCameraState() {
576
- return { x: 0, y: 0, zoom: 1 };
1272
+
1273
+ // src/actions/figure.ts
1274
+ function sideOf(ctx) {
1275
+ return String(ctx.ev.params.side ?? "right") === "left" ? "left" : "right";
577
1276
  }
578
- function cameraTx(s) {
579
- return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
1277
+ function partEl(ctx, part) {
1278
+ const selector = PART_SEL[part];
1279
+ return selector ? ctx.el.querySelector(selector) : null;
580
1280
  }
581
- function buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState) {
582
- const s = cameraState;
583
- switch (ev.action) {
584
- case "pan": {
585
- const to = ev.params.to;
586
- if (!to) break;
587
- const sceneW = ast.meta.width;
588
- const sceneH = ast.meta.height;
589
- const targetX = to[0] - sceneW / 2;
590
- const targetY = to[1] - sceneH / 2;
591
- const next = { ...s, x: targetX, y: targetY };
592
- anims.push(
593
- scene.animate(
594
- [{ transform: cameraTx(s) }, { transform: cameraTx(next) }],
595
- baseOpts
596
- )
597
- );
598
- s.x = next.x;
599
- s.y = next.y;
600
- break;
601
- }
602
- case "zoom": {
603
- const to = typeof ev.params.to === "number" ? ev.params.to : s.zoom;
604
- const next = { ...s, zoom: to };
605
- anims.push(
606
- scene.animate(
607
- [{ transform: cameraTx(s) }, { transform: cameraTx(next) }],
608
- baseOpts
609
- )
610
- );
611
- s.zoom = next.zoom;
612
- break;
613
- }
614
- case "shake": {
615
- const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 8;
616
- anims.push(
617
- scene.animate(
618
- [
619
- { transform: cameraTx(s), offset: 0 },
620
- { transform: cameraTx({ ...s, x: s.x - mag, y: s.y - mag * 0.4 }), offset: 0.15 },
621
- { transform: cameraTx({ ...s, x: s.x + mag, y: s.y + mag * 0.4 }), offset: 0.35 },
622
- { transform: cameraTx({ ...s, x: s.x - mag * 0.6, y: s.y + mag * 0.3 }), offset: 0.55 },
623
- { transform: cameraTx({ ...s, x: s.x + mag * 0.5, y: s.y - mag * 0.3 }), offset: 0.75 },
624
- { transform: cameraTx(s), offset: 1 }
625
- ],
626
- { ...baseOpts, easing: "linear" }
627
- )
628
- );
629
- break;
1281
+ function rotateKeyframe(deg, offset) {
1282
+ return offset === void 0 ? { transform: `rotate(${deg}deg)` } : { transform: `rotate(${deg}deg)`, offset };
1283
+ }
1284
+ function swingLimb(ctx, part, extendDeg, peakOffset) {
1285
+ const el = partEl(ctx, part);
1286
+ if (!el) return;
1287
+ const rest = readRotation(el);
1288
+ ctx.anims.push(
1289
+ el.animate([rotateKeyframe(rest), rotateKeyframe(extendDeg, peakOffset), rotateKeyframe(rest)], {
1290
+ ...ctx.baseOpts,
1291
+ easing: "ease-in-out",
1292
+ fill: "forwards"
1293
+ })
1294
+ );
1295
+ }
1296
+ var punch = (ctx) => {
1297
+ const side = sideOf(ctx);
1298
+ swingLimb(ctx, side === "left" ? "arm_left" : "arm_right", side === "left" ? -75 : 75, 0.35);
1299
+ };
1300
+ var kick = (ctx) => {
1301
+ const side = sideOf(ctx);
1302
+ swingLimb(ctx, side === "left" ? "leg_left" : "leg_right", side === "left" ? -100 : 100, 0.38);
1303
+ };
1304
+ var wave = (ctx) => {
1305
+ const side = sideOf(ctx);
1306
+ const el = partEl(ctx, side === "left" ? "arm_left" : "arm_right");
1307
+ if (!el) return;
1308
+ const rest = readRotation(el);
1309
+ const up = side === "left" ? 70 : -70;
1310
+ const inward = side === "left" ? 50 : -50;
1311
+ ctx.anims.push(
1312
+ el.animate(
1313
+ [
1314
+ rotateKeyframe(rest, 0),
1315
+ rotateKeyframe(up, 0.2),
1316
+ rotateKeyframe(inward, 0.4),
1317
+ rotateKeyframe(up, 0.55),
1318
+ rotateKeyframe(inward, 0.7),
1319
+ rotateKeyframe(up, 0.85),
1320
+ rotateKeyframe(rest, 1)
1321
+ ],
1322
+ { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
1323
+ )
1324
+ );
1325
+ };
1326
+ var nod = (ctx) => {
1327
+ const el = partEl(ctx, "head");
1328
+ if (!el) return;
1329
+ const rest = readRotation(el);
1330
+ const down = 15;
1331
+ ctx.anims.push(
1332
+ el.animate(
1333
+ [
1334
+ rotateKeyframe(rest, 0),
1335
+ rotateKeyframe(down, 0.35),
1336
+ rotateKeyframe(rest, 0.65),
1337
+ rotateKeyframe(down, 0.8),
1338
+ rotateKeyframe(rest, 1)
1339
+ ],
1340
+ { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
1341
+ )
1342
+ );
1343
+ };
1344
+ function commitRotation(el, deg) {
1345
+ el.style.transform = el.style.transform.replace(/rotate\([^)]*\)/, `rotate(${deg}deg)`);
1346
+ }
1347
+ var rotatePart = (ctx) => {
1348
+ const el = partEl(ctx, String(ctx.ev.params.part ?? ""));
1349
+ if (!el) return;
1350
+ const from = readRotation(el);
1351
+ const to = typeof ctx.ev.params.to === "number" ? ctx.ev.params.to : from;
1352
+ ctx.anims.push(
1353
+ el.animate([rotateKeyframe(from), rotateKeyframe(to)], { ...ctx.baseOpts, fill: "forwards" })
1354
+ );
1355
+ commitRotation(el, to);
1356
+ };
1357
+ var POSEABLE_PARTS = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
1358
+ var pose = (ctx) => {
1359
+ for (const part of POSEABLE_PARTS) {
1360
+ const to = ctx.ev.params[part];
1361
+ if (typeof to !== "number") continue;
1362
+ const el = partEl(ctx, part);
1363
+ if (!el) continue;
1364
+ ctx.anims.push(
1365
+ el.animate([rotateKeyframe(readRotation(el)), rotateKeyframe(to)], {
1366
+ ...ctx.baseOpts,
1367
+ fill: "forwards"
1368
+ })
1369
+ );
1370
+ commitRotation(el, to);
1371
+ }
1372
+ };
1373
+ var face = (ctx) => {
1374
+ const el = ctx.el.querySelector("[data-fig-face]");
1375
+ if (!el) return;
1376
+ const emoji = String(ctx.ev.params.text ?? ctx.ev.params._0 ?? "");
1377
+ if (emoji) ctx.faceSwaps.push({ timeMs: ctx.ev.time * 1e3, el, emoji });
1378
+ };
1379
+
1380
+ // src/actions/transform.ts
1381
+ var move = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1382
+ const to = ev.params.to;
1383
+ const toX = to?.[0] ?? state.x;
1384
+ const toY = to?.[1] ?? state.y;
1385
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, x: toX, y: toY }) }], baseOpts));
1386
+ state.x = toX;
1387
+ state.y = toY;
1388
+ };
1389
+ var enter = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
1390
+ const from = String(ev.params.from ?? "left");
1391
+ const fromState = offscreenState(state, from, ast.meta.width, ast.meta.height);
1392
+ anims.push(
1393
+ el.animate(
1394
+ [
1395
+ { transform: tx2(fromState), opacity: state.opacity },
1396
+ { transform: tx2(state), opacity: 1 }
1397
+ ],
1398
+ baseOpts
1399
+ )
1400
+ );
1401
+ state.opacity = 1;
1402
+ };
1403
+ var exit = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
1404
+ const to = String(ev.params.to ?? "right");
1405
+ const toState = offscreenState(state, to, ast.meta.width, ast.meta.height);
1406
+ anims.push(
1407
+ el.animate(
1408
+ [
1409
+ { transform: tx2(state), opacity: state.opacity },
1410
+ { transform: tx2(toState), opacity: 0 }
1411
+ ],
1412
+ baseOpts
1413
+ )
1414
+ );
1415
+ state.x = toState.x;
1416
+ state.y = toState.y;
1417
+ state.opacity = 0;
1418
+ };
1419
+ var fadeIn = ({ el, state, baseOpts, anims }) => {
1420
+ anims.push(el.animate([{ opacity: 0 }, { opacity: 1 }], baseOpts));
1421
+ state.opacity = 1;
1422
+ };
1423
+ var fadeOut = ({ el, state, baseOpts, anims }) => {
1424
+ anims.push(el.animate([{ opacity: state.opacity }, { opacity: 0 }], baseOpts));
1425
+ state.opacity = 0;
1426
+ };
1427
+ var scale = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1428
+ const to = typeof ev.params.to === "number" ? ev.params.to : state.scale;
1429
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, scale: to }) }], baseOpts));
1430
+ state.scale = to;
1431
+ };
1432
+ var rotate = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1433
+ const to = typeof ev.params.to === "number" ? ev.params.to : state.rotate;
1434
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, rotate: to }) }], baseOpts));
1435
+ state.rotate = to;
1436
+ };
1437
+ var shake = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1438
+ const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 5;
1439
+ anims.push(
1440
+ el.animate(
1441
+ [
1442
+ { transform: tx2(state), offset: 0 },
1443
+ { transform: tx2({ ...state, x: state.x + mag }), offset: 0.2 },
1444
+ { transform: tx2({ ...state, x: state.x - mag }), offset: 0.4 },
1445
+ { transform: tx2({ ...state, x: state.x + mag }), offset: 0.6 },
1446
+ { transform: tx2({ ...state, x: state.x - mag }), offset: 0.8 },
1447
+ { transform: tx2(state), offset: 1 }
1448
+ ],
1449
+ { ...baseOpts, easing: "linear" }
1450
+ )
1451
+ );
1452
+ };
1453
+ var jump = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1454
+ const height = typeof ev.params.height === "number" ? ev.params.height : 30;
1455
+ anims.push(
1456
+ el.animate(
1457
+ [
1458
+ { transform: tx2(state), offset: 0 },
1459
+ { transform: tx2({ ...state, scale: state.scale * 0.9 }), offset: 0.1 },
1460
+ { transform: tx2({ ...state, y: state.y - height, scale: state.scale * 1.1 }), offset: 0.45 },
1461
+ { transform: tx2({ ...state, y: state.y - height * 0.3, scale: state.scale * 1.05 }), offset: 0.7 },
1462
+ { transform: tx2({ ...state, scale: state.scale * 0.92 }), offset: 0.88 },
1463
+ { transform: tx2(state), offset: 1 }
1464
+ ],
1465
+ { ...baseOpts, easing: "ease-in-out" }
1466
+ )
1467
+ );
1468
+ };
1469
+ var bounce = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1470
+ const intensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
1471
+ const count = typeof ev.params.count === "number" ? ev.params.count : 3;
1472
+ const keyframes = [{ transform: tx2(state), offset: 0 }];
1473
+ for (let i = 0; i < count; i++) {
1474
+ const amp = intensity * Math.pow(0.55, i);
1475
+ const baseOffset = (i + 0.5) / (count + 0.5);
1476
+ const peakOffset = Math.min(baseOffset, 0.98);
1477
+ const valleyOffset = Math.min(baseOffset + 0.25 / (count + 0.5), 0.99);
1478
+ keyframes.push({ transform: tx2({ ...state, y: state.y - amp }), offset: peakOffset });
1479
+ if (i < count - 1) {
1480
+ keyframes.push({ transform: tx2(state), offset: valleyOffset });
630
1481
  }
631
- default:
632
- break;
633
1482
  }
1483
+ keyframes.push({ transform: tx2(state), offset: 1 });
1484
+ anims.push(el.animate(keyframes, { ...baseOpts, easing: "ease-out" }));
1485
+ };
1486
+
1487
+ // src/actions/effects.ts
1488
+ function numeric(value, fallback) {
1489
+ return typeof value === "number" ? value : fallback;
634
1490
  }
635
- var FLOW_STROKE_BY_ACTION = {
636
- request: "#38bdf8",
637
- response: "#a78bfa",
638
- emit: "#f59e0b"
1491
+ function stringParam(value, fallback) {
1492
+ return typeof value === "string" && value.length > 0 ? value : fallback;
1493
+ }
1494
+ var spring = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1495
+ const to = ev.params.to;
1496
+ const toX = to?.[0] ?? state.x;
1497
+ const toY = to?.[1] ?? state.y;
1498
+ const stiffness = numeric(ev.params.stiffness, 0.18);
1499
+ const overshootX = toX + (toX - state.x) * stiffness;
1500
+ const overshootY = toY + (toY - state.y) * stiffness;
1501
+ anims.push(
1502
+ el.animate(
1503
+ [
1504
+ { transform: tx2(state), offset: 0 },
1505
+ { transform: tx2({ ...state, x: overshootX, y: overshootY }), offset: 0.72 },
1506
+ { transform: tx2({ ...state, x: toX, y: toY }), offset: 1 }
1507
+ ],
1508
+ { ...baseOpts, easing: "cubic-bezier(.2,1.35,.35,1)" }
1509
+ )
1510
+ );
1511
+ state.x = toX;
1512
+ state.y = toY;
639
1513
  };
1514
+ var followPath = ({ ev, el, baseOpts, anims }) => {
1515
+ const path = stringParam(ev.params.path, "M 0 0 L 120 0");
1516
+ const rotate2 = ev.params.rotate === false ? "0deg" : "auto";
1517
+ anims.push(
1518
+ el.animate(
1519
+ [
1520
+ { offsetPath: `path('${path}')`, offsetDistance: "0%", offsetRotate: rotate2 },
1521
+ { offsetPath: `path('${path}')`, offsetDistance: "100%", offsetRotate: rotate2 }
1522
+ ],
1523
+ baseOpts
1524
+ )
1525
+ );
1526
+ };
1527
+ var pulse = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1528
+ const amount = numeric(ev.params.amount, 1.08);
1529
+ anims.push(
1530
+ el.animate(
1531
+ [
1532
+ { transform: tx2(state), offset: 0 },
1533
+ { transform: tx2({ ...state, scale: state.scale * amount }), offset: 0.48 },
1534
+ { transform: tx2(state), offset: 1 }
1535
+ ],
1536
+ { ...baseOpts, easing: "ease-in-out" }
1537
+ )
1538
+ );
1539
+ };
1540
+ var glow = ({ ev, el, baseOpts, anims }) => {
1541
+ const color = stringParam(ev.params.color, "#38bdf8");
1542
+ const strength = numeric(ev.params.strength, 24);
1543
+ anims.push(
1544
+ el.animate(
1545
+ [
1546
+ { filter: "drop-shadow(0 0 0 transparent)", boxShadow: "0 0 0 rgba(0,0,0,0)" },
1547
+ { filter: `drop-shadow(0 0 ${strength}px ${color})`, boxShadow: `0 0 ${strength}px ${color}` },
1548
+ { filter: "drop-shadow(0 0 0 transparent)", boxShadow: "0 0 0 rgba(0,0,0,0)" }
1549
+ ],
1550
+ { ...baseOpts, easing: "ease-in-out" }
1551
+ )
1552
+ );
1553
+ };
1554
+ var ripple = ({ ev, el, delayMs, durMs, anims }) => {
1555
+ const color = stringParam(ev.params.color, "#38bdf8");
1556
+ const size = numeric(ev.params.size, 140);
1557
+ const ring = document.createElement("span");
1558
+ ring.setAttribute("data-markdy-ripple", "1");
1559
+ Object.assign(ring.style, {
1560
+ position: "absolute",
1561
+ left: "50%",
1562
+ top: "50%",
1563
+ width: `${size}px`,
1564
+ height: `${size}px`,
1565
+ marginLeft: `${-size / 2}px`,
1566
+ marginTop: `${-size / 2}px`,
1567
+ border: `2px solid ${color}`,
1568
+ borderRadius: "999px",
1569
+ pointerEvents: "none",
1570
+ opacity: "0"
1571
+ });
1572
+ el.appendChild(ring);
1573
+ anims.push(
1574
+ ring.animate(
1575
+ [
1576
+ { transform: "scale(0.2)", opacity: 0.6 },
1577
+ { transform: "scale(1)", opacity: 0 }
1578
+ ],
1579
+ { delay: delayMs, duration: durMs, fill: "forwards", easing: "ease-out" }
1580
+ )
1581
+ );
1582
+ };
1583
+ var blur = ({ ev, el, baseOpts, anims }) => {
1584
+ const from = numeric(ev.params.from, 0);
1585
+ const to = numeric(ev.params.to, 8);
1586
+ anims.push(el.animate([{ filter: `blur(${from}px)` }, { filter: `blur(${to}px)` }], baseOpts));
1587
+ };
1588
+ var lineReveal = ({ ev, el, baseOpts, anims }) => {
1589
+ const from = stringParam(ev.params.from, "left");
1590
+ const start = from === "right" ? "inset(0 0 0 100%)" : "inset(0 100% 0 0)";
1591
+ anims.push(el.animate([{ clipPath: start }, { clipPath: "inset(0 0 0 0)" }], baseOpts));
1592
+ };
1593
+ var mask = ({ ev, el, baseOpts, anims }) => {
1594
+ const from = numeric(ev.params.from, 0);
1595
+ const to = numeric(ev.params.to, 120);
1596
+ anims.push(
1597
+ el.animate(
1598
+ [
1599
+ { clipPath: `circle(${from}% at 50% 50%)` },
1600
+ { clipPath: `circle(${to}% at 50% 50%)` }
1601
+ ],
1602
+ baseOpts
1603
+ )
1604
+ );
1605
+ };
1606
+ var parallax = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
1607
+ const depth = numeric(ev.params.depth, 0.35);
1608
+ const by = ev.params.by;
1609
+ const dx = (by?.[0] ?? 24) * depth;
1610
+ const dy = (by?.[1] ?? 0) * depth;
1611
+ anims.push(
1612
+ el.animate(
1613
+ [
1614
+ { transform: tx2(state) },
1615
+ { transform: tx2({ ...state, x: state.x + dx, y: state.y + dy }) }
1616
+ ],
1617
+ baseOpts
1618
+ )
1619
+ );
1620
+ };
1621
+
1622
+ // src/geometry/rect.ts
1623
+ var ACTOR_SIZES = {
1624
+ service: { width: 184, height: 88 },
1625
+ api: { width: 184, height: 88 },
1626
+ microservice: { width: 184, height: 88 },
1627
+ client: { width: 184, height: 88 },
1628
+ user: { width: 184, height: 88 },
1629
+ db: { width: 184, height: 88 },
1630
+ database: { width: 184, height: 88 },
1631
+ queue: { width: 184, height: 88 },
1632
+ cache: { width: 184, height: 88 },
1633
+ cloud: { width: 184, height: 88 },
1634
+ region: { width: 184, height: 88 },
1635
+ container: { width: 184, height: 88 },
1636
+ cluster: { width: 184, height: 88 },
1637
+ box: { width: 100, height: 100 },
1638
+ caption: { width: 260, height: 56 },
1639
+ figure: { width: 120, height: 170 },
1640
+ parking_map: { width: 390, height: 250 },
1641
+ ascii_map: { width: 390, height: 250 },
1642
+ game_scene: { width: 390, height: 250 },
1643
+ byte_viz: { width: 420, height: 250 }
1644
+ };
1645
+ var DEFAULT_ACTOR_SIZE = { width: 140, height: 42 };
640
1646
  function actorSizeByType(type) {
641
- switch (type) {
642
- case "service":
643
- case "client":
644
- case "db":
645
- case "queue":
646
- return { width: 180, height: 84 };
647
- case "box":
648
- return { width: 100, height: 100 };
649
- case "caption":
650
- return { width: 260, height: 56 };
651
- case "figure":
652
- return { width: 120, height: 170 };
653
- default:
654
- return { width: 140, height: 42 };
655
- }
1647
+ return ACTOR_SIZES[type] ?? DEFAULT_ACTOR_SIZE;
656
1648
  }
657
1649
  function actorCenter(state, actorType) {
658
- const { width, height } = actorSizeByType(actorType);
659
- return {
660
- x: state.x + width / 2,
661
- y: state.y + height / 2
662
- };
1650
+ const rect = actorRect(state, actorType);
1651
+ return { x: rect.x1 + (rect.x2 - rect.x1) / 2, y: rect.y1 + (rect.y2 - rect.y1) / 2 };
663
1652
  }
664
1653
  function actorRect(state, actorType) {
665
1654
  const { width, height } = actorSizeByType(actorType);
1655
+ const scale2 = Math.max(1e-3, state.scale);
1656
+ const scaledWidth = width * scale2;
1657
+ const scaledHeight = height * scale2;
1658
+ const dx = (scaledWidth - width) / 2;
1659
+ const dy = (scaledHeight - height) / 2;
666
1660
  return {
667
- x1: state.x,
668
- y1: state.y,
669
- x2: state.x + width,
670
- y2: state.y + height
1661
+ x1: state.x - dx,
1662
+ y1: state.y - dy,
1663
+ x2: state.x + width + dx,
1664
+ y2: state.y + height + dy
671
1665
  };
672
1666
  }
673
1667
  function inflateRect(rect, pad) {
@@ -706,12 +1700,14 @@ function countPathIntersections(points, obstacles) {
706
1700
  }
707
1701
  return hits;
708
1702
  }
709
- function toPathD(points) {
710
- return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
711
- }
1703
+
1704
+ // src/geometry/path.ts
712
1705
  function round1(n) {
713
1706
  return Math.round(n * 10) / 10;
714
1707
  }
1708
+ function toPathD(points) {
1709
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
1710
+ }
715
1711
  function polylineLength(points) {
716
1712
  let total = 0;
717
1713
  for (let i = 0; i < points.length - 1; i++) {
@@ -719,26 +1715,67 @@ function polylineLength(points) {
719
1715
  }
720
1716
  return Math.max(1, total);
721
1717
  }
722
- function pointAtDistance(points, dist) {
723
- let remain = dist;
1718
+ function segmentLength(a, b) {
1719
+ return Math.hypot(b.x - a.x, b.y - a.y);
1720
+ }
1721
+ function labelPointForPath(points, lane) {
1722
+ if (points.length < 2) return points[0] ?? { x: 0, y: 0 };
1723
+ let bestIndex = 0;
1724
+ let bestLength = -1;
724
1725
  for (let i = 0; i < points.length - 1; i++) {
725
- const a = points[i];
726
- const b = points[i + 1];
727
- const seg = Math.hypot(b.x - a.x, b.y - a.y);
728
- if (remain <= seg || i === points.length - 2) {
729
- const t = seg <= 0 ? 0 : remain / seg;
730
- return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
1726
+ const len = segmentLength(points[i], points[i + 1]);
1727
+ if (len > bestLength) {
1728
+ bestIndex = i;
1729
+ bestLength = len;
731
1730
  }
732
- remain -= seg;
733
1731
  }
734
- return points[0];
1732
+ const a = points[bestIndex];
1733
+ const b = points[bestIndex + 1];
1734
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1735
+ const offset = lane * 8;
1736
+ if (Math.abs(a.x - b.x) >= Math.abs(a.y - b.y)) {
1737
+ return { x: mid.x, y: mid.y - 10 - offset };
1738
+ }
1739
+ return { x: mid.x + 10 + offset, y: mid.y };
1740
+ }
1741
+ function clamp(n, min, max) {
1742
+ if (max < min) return n;
1743
+ return Math.min(max, Math.max(min, n));
1744
+ }
1745
+ function clampPointToScene(point, ast) {
1746
+ const pad = 14;
1747
+ return {
1748
+ x: round1(clamp(point.x, pad, ast.meta.width - pad)),
1749
+ y: round1(clamp(point.y, pad, ast.meta.height - pad))
1750
+ };
1751
+ }
1752
+ function routeLength(points) {
1753
+ let total = 0;
1754
+ for (let i = 0; i < points.length - 1; i++) total += segmentLength(points[i], points[i + 1]);
1755
+ return total;
1756
+ }
1757
+ function routeBends(points) {
1758
+ let bends = 0;
1759
+ for (let i = 1; i < points.length - 1; i++) {
1760
+ const prev = points[i - 1];
1761
+ const cur = points[i];
1762
+ const next = points[i + 1];
1763
+ const dx1 = Math.sign(cur.x - prev.x);
1764
+ const dy1 = Math.sign(cur.y - prev.y);
1765
+ const dx2 = Math.sign(next.x - cur.x);
1766
+ const dy2 = Math.sign(next.y - cur.y);
1767
+ if (dx1 !== dx2 || dy1 !== dy2) bends++;
1768
+ }
1769
+ return bends;
735
1770
  }
736
1771
  function routeFlowPath(sourceName, targetName, sourceState, targetState, states, ast, lane) {
737
- const sourceRect = actorRect(sourceState, ast.actors[sourceName]?.type ?? "box");
738
- const targetRect = actorRect(targetState, ast.actors[targetName]?.type ?? "box");
1772
+ const sourceType = ast.actors[sourceName]?.type ?? "box";
1773
+ const targetType = ast.actors[targetName]?.type ?? "box";
1774
+ const sourceRect = actorRect(sourceState, sourceType);
1775
+ const targetRect = actorRect(targetState, targetType);
739
1776
  const laneShift = lane * 18;
740
- const sourceCenter = actorCenter(sourceState, ast.actors[sourceName]?.type ?? "box");
741
- const targetCenter = actorCenter(targetState, ast.actors[targetName]?.type ?? "box");
1777
+ const sourceCenter = actorCenter(sourceState, sourceType);
1778
+ const targetCenter = actorCenter(targetState, targetType);
742
1779
  const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
743
1780
  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 };
744
1781
  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 };
@@ -766,23 +1803,42 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
766
1803
  [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
767
1804
  [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
768
1805
  );
1806
+ const minObstacleX = obstacles.length ? Math.min(...obstacles.map((o) => o.x1)) : Math.min(source.x, target.x);
1807
+ const maxObstacleX = obstacles.length ? Math.max(...obstacles.map((o) => o.x2)) : Math.max(source.x, target.x);
1808
+ const leftLane = Math.max(16, minObstacleX - 24 - lane * 12);
1809
+ const rightLane = Math.min(ast.meta.width - 16, maxObstacleX + 24 + lane * 12);
1810
+ candidates.push(
1811
+ [source, { x: leftLane, y: source.y }, { x: leftLane, y: target.y }, target],
1812
+ [source, { x: rightLane, y: source.y }, { x: rightLane, y: target.y }, target]
1813
+ );
769
1814
  let best = candidates[0];
770
- let bestHits = Number.POSITIVE_INFINITY;
1815
+ let bestScore = Number.POSITIVE_INFINITY;
771
1816
  for (const candidate of candidates) {
772
1817
  const hits = countPathIntersections(candidate, obstacles);
773
- if (hits < bestHits) {
1818
+ const score = hits * 1e5 + routeBends(candidate) * 800 + routeLength(candidate);
1819
+ if (score < bestScore) {
774
1820
  best = candidate;
775
- bestHits = hits;
776
- if (hits === 0) break;
1821
+ bestScore = score;
777
1822
  }
778
1823
  }
779
- return best;
1824
+ return best.map((point) => clampPointToScene(point, ast));
780
1825
  }
1826
+
1827
+ // src/actions/flow.ts
1828
+ var EDGE_LAYER_ATTR = "data-markdy-edge-layer";
1829
+ var STROKE_BY_ACTION = {
1830
+ request: "#38bdf8",
1831
+ response: "#a78bfa",
1832
+ emit: "#f59e0b"
1833
+ };
1834
+ var DEFAULT_STROKE = "#38bdf8";
1835
+ var LABEL_MAX_CHARS = 28;
1836
+ var EDGE_FADE_MS = 140;
781
1837
  function ensureEdgeLayer(scene) {
782
- const existing = scene.querySelector("svg[data-markdy-edge-layer='1']");
1838
+ const existing = scene.querySelector(`svg[${EDGE_LAYER_ATTR}='1']`);
783
1839
  if (existing) return existing;
784
1840
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
785
- svg.setAttribute("data-markdy-edge-layer", "1");
1841
+ svg.setAttribute(EDGE_LAYER_ATTR, "1");
786
1842
  Object.assign(svg.style, {
787
1843
  position: "absolute",
788
1844
  inset: "0",
@@ -795,529 +1851,504 @@ function ensureEdgeLayer(scene) {
795
1851
  scene.appendChild(svg);
796
1852
  return svg;
797
1853
  }
798
- function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, states, ast, lane, scene, baseOpts, anims) {
799
- const styleToken = String(ev.params.style ?? "");
800
- const isDashed = styleToken === "dashed" || styleToken === "fire_and_forget" || ev.action === "response";
801
- const stroke = FLOW_STROKE_BY_ACTION[ev.action] ?? "#38bdf8";
802
- const points = routeFlowPath(
803
- sourceName,
804
- targetName,
805
- sourceState,
806
- targetState,
807
- states,
808
- ast,
809
- lane
810
- );
1854
+ function ensureEdgeDefs(svg) {
1855
+ if (svg.querySelector("defs[data-markdy-flow-defs='1']")) return;
1856
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1857
+ defs.setAttribute("data-markdy-flow-defs", "1");
1858
+ const glow2 = document.createElementNS("http://www.w3.org/2000/svg", "filter");
1859
+ glow2.setAttribute("id", "markdy-flow-glow");
1860
+ glow2.setAttribute("x", "-40%");
1861
+ glow2.setAttribute("y", "-40%");
1862
+ glow2.setAttribute("width", "180%");
1863
+ glow2.setAttribute("height", "180%");
1864
+ const blur2 = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
1865
+ blur2.setAttribute("stdDeviation", "3");
1866
+ blur2.setAttribute("result", "coloredBlur");
1867
+ const merge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
1868
+ const glowNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1869
+ glowNode.setAttribute("in", "coloredBlur");
1870
+ const sourceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
1871
+ sourceNode.setAttribute("in", "SourceGraphic");
1872
+ merge.append(glowNode, sourceNode);
1873
+ glow2.append(blur2, merge);
1874
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
1875
+ arrow.setAttribute("id", "markdy-flow-arrow");
1876
+ arrow.setAttribute("viewBox", "0 0 10 10");
1877
+ arrow.setAttribute("refX", "8");
1878
+ arrow.setAttribute("refY", "5");
1879
+ arrow.setAttribute("markerWidth", "6");
1880
+ arrow.setAttribute("markerHeight", "6");
1881
+ arrow.setAttribute("orient", "auto-start-reverse");
1882
+ const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
1883
+ arrowPath.setAttribute("d", "M 1 1 L 9 5 L 1 9 z");
1884
+ arrowPath.setAttribute("fill", "context-stroke");
1885
+ arrow.appendChild(arrowPath);
1886
+ defs.append(glow2, arrow);
1887
+ svg.prepend(defs);
1888
+ }
1889
+ function isDashed(ctx) {
1890
+ const style = String(ctx.ev.params.style ?? "");
1891
+ return style === "dashed" || style === "fire_and_forget" || ctx.ev.action === "response";
1892
+ }
1893
+ function buildEdge(ctx, targetName) {
1894
+ const { ev, state, states, ast, scene, baseOpts, anims } = ctx;
1895
+ const targetState = states.get(targetName);
1896
+ if (!targetState) return;
1897
+ const lane = ev.line % 5 - 2;
1898
+ const points = routeFlowPath(ev.actor, targetName, state, targetState, states, ast, lane);
811
1899
  const length = polylineLength(points);
812
- const midPoint = pointAtDistance(points, length * 0.5);
813
1900
  const pathD = toPathD(points);
1901
+ const stroke = STROKE_BY_ACTION[ev.action] ?? DEFAULT_STROKE;
814
1902
  const svg = ensureEdgeLayer(scene);
1903
+ ensureEdgeDefs(svg);
815
1904
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
816
1905
  group.setAttribute("data-markdy-flow-edge", "1");
817
1906
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
818
1907
  path.setAttribute("d", pathD);
819
1908
  path.setAttribute("fill", "none");
820
1909
  path.setAttribute("stroke", stroke);
821
- path.setAttribute("stroke-width", "2.5");
1910
+ path.setAttribute("stroke-width", "3");
1911
+ path.setAttribute("stroke-linecap", "round");
1912
+ path.setAttribute("stroke-linejoin", "round");
1913
+ path.setAttribute("marker-end", "url(#markdy-flow-arrow)");
1914
+ path.setAttribute("filter", "url(#markdy-flow-glow)");
822
1915
  path.setAttribute("data-markdy-flow-action", ev.action);
823
- path.style.strokeDasharray = isDashed ? "8 6" : `${length}`;
1916
+ path.style.strokeDasharray = isDashed(ctx) ? "8 6" : `${length}`;
824
1917
  path.style.strokeDashoffset = `${length}`;
825
1918
  group.appendChild(path);
826
1919
  const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
827
- marker.setAttribute("r", "3");
1920
+ marker.setAttribute("r", "4.5");
828
1921
  marker.setAttribute("fill", stroke);
1922
+ marker.setAttribute("filter", "url(#markdy-flow-glow)");
829
1923
  marker.style.offsetPath = `path('${pathD}')`;
830
1924
  marker.style.offsetDistance = "0%";
831
1925
  marker.style.opacity = "0";
832
1926
  group.appendChild(marker);
833
- const labelRaw = String(ev.params.label ?? "");
834
- if (labelRaw) {
835
- const label = labelRaw.length > 28 ? `${labelRaw.slice(0, 27)}\u2026` : labelRaw;
836
- const labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
837
- labelEl.setAttribute("x", `${round1(midPoint.x)}`);
838
- labelEl.setAttribute("y", `${round1(midPoint.y - 8)}`);
839
- labelEl.setAttribute("text-anchor", "middle");
840
- labelEl.setAttribute("font-size", "12");
841
- labelEl.setAttribute("fill", "#cbd5e1");
842
- labelEl.textContent = label;
843
- labelEl.setAttribute("data-full-label", labelRaw);
844
- group.appendChild(labelEl);
1927
+ const labelText = String(ev.params.label ?? "");
1928
+ if (labelText) {
1929
+ const midPoint = labelPointForPath(points, lane);
1930
+ const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
1931
+ label.setAttribute("x", `${round1(midPoint.x)}`);
1932
+ label.setAttribute("y", `${round1(midPoint.y - 8)}`);
1933
+ label.setAttribute("text-anchor", "middle");
1934
+ label.setAttribute("font-size", "12");
1935
+ label.setAttribute("font-weight", "700");
1936
+ label.setAttribute("paint-order", "stroke");
1937
+ label.setAttribute("stroke", "rgba(2, 6, 23, 0.88)");
1938
+ label.setAttribute("stroke-width", "5");
1939
+ label.setAttribute("stroke-linejoin", "round");
1940
+ label.setAttribute("fill", "#cbd5e1");
1941
+ label.setAttribute("filter", "url(#markdy-flow-glow)");
1942
+ label.setAttribute("data-full-label", labelText);
1943
+ label.textContent = labelText.length > LABEL_MAX_CHARS ? `${labelText.slice(0, LABEL_MAX_CHARS - 1)}\u2026` : labelText;
1944
+ group.appendChild(label);
845
1945
  }
846
1946
  svg.appendChild(group);
847
- anims.push(path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts));
848
1947
  anims.push(
1948
+ path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts),
849
1949
  marker.animate(
850
- [{ offsetDistance: "0%", opacity: 1 }, { offsetDistance: "100%", opacity: 1 }],
1950
+ [
1951
+ { offsetDistance: "0%", opacity: 1 },
1952
+ { offsetDistance: "100%", opacity: 1 }
1953
+ ],
851
1954
  baseOpts
852
- )
1955
+ ),
1956
+ group.animate([{ opacity: 1 }, { opacity: 0 }], {
1957
+ delay: Number(baseOpts.delay ?? 0) + Number(baseOpts.duration ?? 0),
1958
+ duration: EDGE_FADE_MS,
1959
+ fill: "forwards"
1960
+ })
853
1961
  );
854
- const fadeOutDelay = Number(baseOpts.delay ?? 0) + Number(baseOpts.duration ?? 0);
1962
+ }
1963
+ var flowEdge = (ctx) => {
1964
+ const targetName = String(ctx.ev.params.to ?? "");
1965
+ if (targetName) buildEdge(ctx, targetName);
1966
+ };
1967
+
1968
+ // src/theme.ts
1969
+ var DARK_LUMINANCE_THRESHOLD = 140;
1970
+ var NAMED_DARK = {
1971
+ black: true,
1972
+ "#000": true,
1973
+ "#000000": true
1974
+ };
1975
+ function isSceneDark(scene) {
1976
+ const bg = scene.style.background || "white";
1977
+ let hex = bg.trim().replace(/^#/, "");
1978
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
1979
+ if (hex.length === 6) {
1980
+ const r = parseInt(hex.slice(0, 2), 16);
1981
+ const g = parseInt(hex.slice(2, 4), 16);
1982
+ const b = parseInt(hex.slice(4, 6), 16);
1983
+ return 0.299 * r + 0.587 * g + 0.114 * b <= DARK_LUMINANCE_THRESHOLD;
1984
+ }
1985
+ return NAMED_DARK[bg.toLowerCase()] ?? false;
1986
+ }
1987
+ function speechBubbleTheme(dark) {
1988
+ return dark ? {
1989
+ background: "#1e2530",
1990
+ border: "#475569",
1991
+ text: "#e2e8f0",
1992
+ shadow: "0 2px 8px rgba(0,0,0,0.35)"
1993
+ } : {
1994
+ background: "white",
1995
+ border: "#222",
1996
+ text: "#222",
1997
+ shadow: "0 2px 8px rgba(0,0,0,0.12)"
1998
+ };
1999
+ }
2000
+
2001
+ // src/actions/speech.ts
2002
+ var MAX_FADE_MS = 200;
2003
+ var FADE_FRACTION = 0.15;
2004
+ var say = ({ ev, el, state, delayMs, durMs, scene, anims }) => {
2005
+ const text = String(ev.params.text ?? "");
2006
+ const theme = speechBubbleTheme(isSceneDark(scene));
2007
+ const inverseScale = 1 / (state.scale || 1);
2008
+ const bubble = document.createElement("div");
2009
+ bubble.textContent = text;
2010
+ Object.assign(bubble.style, {
2011
+ position: "absolute",
2012
+ bottom: "calc(100% + 10px)",
2013
+ left: "50%",
2014
+ transform: `translateX(-50%) scale(${inverseScale})`,
2015
+ transformOrigin: "center bottom",
2016
+ background: theme.background,
2017
+ border: `2px solid ${theme.border}`,
2018
+ color: theme.text,
2019
+ borderRadius: "12px",
2020
+ padding: "6px 14px",
2021
+ fontFamily: "system-ui, sans-serif",
2022
+ fontSize: "15px",
2023
+ lineHeight: "1.3",
2024
+ whiteSpace: "nowrap",
2025
+ maxWidth: "220px",
2026
+ overflow: "hidden",
2027
+ textOverflow: "ellipsis",
2028
+ pointerEvents: "none",
2029
+ zIndex: "10",
2030
+ boxShadow: theme.shadow,
2031
+ opacity: "0"
2032
+ });
2033
+ const tail = document.createElement("span");
2034
+ Object.assign(tail.style, {
2035
+ position: "absolute",
2036
+ bottom: "-10px",
2037
+ left: "50%",
2038
+ transform: "translateX(-50%)",
2039
+ width: "0",
2040
+ height: "0",
2041
+ borderLeft: "7px solid transparent",
2042
+ borderRight: "7px solid transparent",
2043
+ borderTop: `10px solid ${theme.border}`
2044
+ });
2045
+ bubble.appendChild(tail);
2046
+ el.style.overflow = "visible";
2047
+ el.appendChild(bubble);
2048
+ const fadeMs = Math.min(MAX_FADE_MS, durMs * FADE_FRACTION);
855
2049
  anims.push(
856
- group.animate([{ opacity: 1 }, { opacity: 0 }], {
857
- delay: fadeOutDelay,
858
- duration: 140,
2050
+ bubble.animate([{ opacity: 0 }, { opacity: 1 }], {
2051
+ delay: delayMs,
2052
+ duration: fadeMs,
2053
+ fill: "forwards"
2054
+ }),
2055
+ bubble.animate([{ opacity: 1 }, { opacity: 0 }], {
2056
+ delay: delayMs + durMs - fadeMs,
2057
+ duration: fadeMs,
859
2058
  fill: "forwards"
860
2059
  })
861
2060
  );
2061
+ };
2062
+
2063
+ // src/actions/projectile.ts
2064
+ var PROJECTILE_SIZE_PX = 32;
2065
+ function createProjectile(assetName, assetDef, assetOverrides) {
2066
+ if (assetDef.type === "image") {
2067
+ const img = document.createElement("img");
2068
+ img.src = assetOverrides[assetName] ?? assetDef.value;
2069
+ img.alt = assetName;
2070
+ img.setAttribute("draggable", "false");
2071
+ img.style.width = `${PROJECTILE_SIZE_PX}px`;
2072
+ img.style.height = `${PROJECTILE_SIZE_PX}px`;
2073
+ return img;
2074
+ }
2075
+ const span = document.createElement("span");
2076
+ span.className = "iconify";
2077
+ span.dataset.icon = assetDef.value;
2078
+ span.style.fontSize = `${PROJECTILE_SIZE_PX}px`;
2079
+ span.style.lineHeight = "1";
2080
+ span.style.display = "inline-block";
2081
+ return span;
862
2082
  }
863
- function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, _actorEls, scene, assetOverrides, faceSwaps, anims, txFn) {
864
- switch (ev.action) {
865
- case "request":
866
- case "response":
867
- case "emit": {
868
- const targetActorName = String(ev.params.to ?? "");
869
- if (!targetActorName) break;
870
- const targetState = states.get(targetActorName);
871
- if (!targetState) break;
872
- const lane = ev.line % 5 - 2;
873
- renderFlowEdge(
874
- ev,
875
- ev.actor,
876
- targetActorName,
877
- s,
878
- targetState,
879
- states,
880
- ast,
881
- lane,
882
- scene,
883
- baseOpts,
884
- anims
885
- );
886
- break;
887
- }
888
- // ── move ────────────────────────────────────────────────────────────────
889
- case "move": {
890
- const toArr = ev.params.to;
891
- const toX = toArr?.[0] ?? s.x;
892
- const toY = toArr?.[1] ?? s.y;
893
- anims.push(
894
- el.animate(
895
- [{ transform: txFn(s) }, { transform: txFn({ ...s, x: toX, y: toY }) }],
896
- baseOpts
897
- )
898
- );
899
- s.x = toX;
900
- s.y = toY;
901
- break;
902
- }
903
- // ── enter ───────────────────────────────────────────────────────────────
904
- // Slides the actor from the given screen edge into its current (s)
905
- // position while also restoring opacity to 1. The opacity restore makes
906
- // `enter` symmetric with `exit` so actors can re-enter after exiting
907
- // without needing a separate fade_in.
908
- case "enter": {
909
- const from = String(ev.params.from ?? "left");
910
- const fromState = offscreenState(s, from, ast.meta.width, ast.meta.height);
911
- anims.push(
912
- el.animate(
913
- [
914
- { transform: txFn(fromState), opacity: s.opacity },
915
- { transform: txFn(s), opacity: 1 }
916
- ],
917
- baseOpts
918
- )
919
- );
920
- s.opacity = 1;
921
- break;
922
- }
923
- // ── exit ────────────────────────────────────────────────────────────────
924
- // Mirror of enter: slides off-screen in the given direction while also
925
- // fading opacity to 0. Universal — works on any actor type (caption, text,
926
- // figure, box, sprite). Leaves `s` at the off-screen state since the
927
- // actor is conceptually gone after this.
928
- case "exit": {
929
- const to = String(ev.params.to ?? "right");
930
- const toState = offscreenState(s, to, ast.meta.width, ast.meta.height);
931
- anims.push(
932
- el.animate(
933
- [
934
- { transform: txFn(s), opacity: s.opacity },
935
- { transform: txFn(toState), opacity: 0 }
936
- ],
937
- baseOpts
938
- )
939
- );
940
- s.x = toState.x;
941
- s.y = toState.y;
942
- s.opacity = 0;
943
- break;
944
- }
945
- // ── fade_in ─────────────────────────────────────────────────────────────
946
- case "fade_in": {
947
- anims.push(el.animate([{ opacity: 0 }, { opacity: 1 }], baseOpts));
948
- s.opacity = 1;
949
- break;
950
- }
951
- // ── fade_out ────────────────────────────────────────────────────────────
952
- case "fade_out": {
953
- anims.push(
954
- el.animate([{ opacity: s.opacity }, { opacity: 0 }], baseOpts)
955
- );
956
- s.opacity = 0;
957
- break;
958
- }
959
- // ── scale ───────────────────────────────────────────────────────────────
960
- case "scale": {
961
- const toScale = typeof ev.params.to === "number" ? ev.params.to : s.scale;
962
- anims.push(
963
- el.animate(
964
- [{ transform: txFn(s) }, { transform: txFn({ ...s, scale: toScale }) }],
965
- baseOpts
966
- )
967
- );
968
- s.scale = toScale;
969
- break;
970
- }
971
- // ── rotate ──────────────────────────────────────────────────────────────
972
- case "rotate": {
973
- const toDeg = typeof ev.params.to === "number" ? ev.params.to : s.rotate;
974
- anims.push(
975
- el.animate(
976
- [{ transform: txFn(s) }, { transform: txFn({ ...s, rotate: toDeg }) }],
977
- baseOpts
978
- )
979
- );
980
- s.rotate = toDeg;
981
- break;
982
- }
983
- // ── shake ───────────────────────────────────────────────────────────────
984
- case "shake": {
985
- const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 5;
986
- anims.push(
987
- el.animate(
988
- [
989
- { transform: txFn(s), offset: 0 },
990
- { transform: txFn({ ...s, x: s.x + mag }), offset: 0.2 },
991
- { transform: txFn({ ...s, x: s.x - mag }), offset: 0.4 },
992
- { transform: txFn({ ...s, x: s.x + mag }), offset: 0.6 },
993
- { transform: txFn({ ...s, x: s.x - mag }), offset: 0.8 },
994
- { transform: txFn(s), offset: 1 }
995
- ],
996
- { ...baseOpts, easing: "linear" }
997
- )
998
- );
999
- break;
1000
- }
1001
- // ── punch ───────────────────────────────────────────────────────────────
1002
- case "punch": {
1003
- const pSide = String(ev.params.side ?? "right");
1004
- const pArmEl = el.querySelector(
1005
- pSide === "left" ? "[data-fig-arm-l]" : "[data-fig-arm-r]"
1006
- );
1007
- if (!pArmEl) break;
1008
- const pRest = readRotation(pArmEl);
1009
- const pExtend = pSide === "left" ? -75 : 75;
1010
- anims.push(
1011
- pArmEl.animate(
1012
- [
1013
- { transform: `rotate(${pRest}deg)` },
1014
- { transform: `rotate(${pExtend}deg)`, offset: 0.35 },
1015
- { transform: `rotate(${pRest}deg)` }
1016
- ],
1017
- { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
1018
- )
1019
- );
1020
- break;
1021
- }
1022
- // ── kick ────────────────────────────────────────────────────────────────
1023
- case "kick": {
1024
- const kSide = String(ev.params.side ?? "right");
1025
- const kLegEl = el.querySelector(
1026
- kSide === "left" ? "[data-fig-leg-l]" : "[data-fig-leg-r]"
1027
- );
1028
- if (!kLegEl) break;
1029
- const kRest = readRotation(kLegEl);
1030
- const kExtend = kSide === "left" ? -100 : 100;
1031
- anims.push(
1032
- kLegEl.animate(
1033
- [
1034
- { transform: `rotate(${kRest}deg)` },
1035
- { transform: `rotate(${kExtend}deg)`, offset: 0.38 },
1036
- { transform: `rotate(${kRest}deg)` }
1037
- ],
1038
- { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
1039
- )
1040
- );
1041
- break;
1042
- }
1043
- // ── rotate_part ─────────────────────────────────────────────────────────
1044
- case "rotate_part": {
1045
- const rpName = String(ev.params.part ?? "");
1046
- const rpSel = PART_SEL[rpName];
1047
- if (!rpSel) break;
1048
- const rpEl = el.querySelector(rpSel);
1049
- if (!rpEl) break;
1050
- const rpFrom = readRotation(rpEl);
1051
- const rpTo = typeof ev.params.to === "number" ? ev.params.to : rpFrom;
1052
- anims.push(
1053
- rpEl.animate(
1054
- [
1055
- { transform: `rotate(${rpFrom}deg)` },
1056
- { transform: `rotate(${rpTo}deg)` }
1057
- ],
1058
- { ...baseOpts, fill: "forwards" }
1059
- )
1060
- );
1061
- rpEl.style.transform = rpEl.style.transform.replace(
1062
- /rotate\([^)]*\)/,
1063
- `rotate(${rpTo}deg)`
1064
- );
1065
- break;
1066
- }
1067
- // ── face ────────────────────────────────────────────────────────────────
1068
- case "face": {
1069
- const fEl = el.querySelector("[data-fig-face]");
1070
- if (!fEl) break;
1071
- const emoji = String(ev.params.text ?? ev.params._0 ?? "");
1072
- if (emoji) faceSwaps.push({ timeMs: ev.time * 1e3, el: fEl, emoji });
1073
- break;
1074
- }
1075
- // ── say ─────────────────────────────────────────────────────────────────
1076
- case "say": {
1077
- const text = String(ev.params.text ?? "");
1078
- const inverseScale = 1 / (s.scale || 1);
1079
- const sceneDark = isSceneDark(scene);
1080
- const bubbleBg = sceneDark ? "#1e2530" : "white";
1081
- const bubbleBorder = sceneDark ? "#475569" : "#222";
1082
- const bubbleText = sceneDark ? "#e2e8f0" : "#222";
1083
- const bubbleShadow = sceneDark ? "0 2px 8px rgba(0,0,0,0.35)" : "0 2px 8px rgba(0,0,0,0.12)";
1084
- const bubble = document.createElement("div");
1085
- bubble.textContent = text;
1086
- bubble.style.opacity = "0";
1087
- Object.assign(bubble.style, {
1088
- position: "absolute",
1089
- bottom: "calc(100% + 10px)",
1090
- left: "50%",
1091
- transform: `translateX(-50%) scale(${inverseScale})`,
1092
- transformOrigin: "center bottom",
1093
- background: bubbleBg,
1094
- border: `2px solid ${bubbleBorder}`,
1095
- color: bubbleText,
1096
- borderRadius: "12px",
1097
- padding: "6px 14px",
1098
- fontFamily: "system-ui, sans-serif",
1099
- fontSize: "15px",
1100
- lineHeight: "1.3",
1101
- whiteSpace: "nowrap",
1102
- maxWidth: "220px",
1103
- overflow: "hidden",
1104
- textOverflow: "ellipsis",
1105
- pointerEvents: "none",
1106
- zIndex: "10",
1107
- boxShadow: bubbleShadow
1108
- });
1109
- const tail = document.createElement("span");
1110
- Object.assign(tail.style, {
1111
- position: "absolute",
1112
- bottom: "-10px",
1113
- left: "50%",
1114
- transform: "translateX(-50%)",
1115
- width: "0",
1116
- height: "0",
1117
- borderLeft: "7px solid transparent",
1118
- borderRight: "7px solid transparent",
1119
- borderTop: `10px solid ${bubbleBorder}`
1120
- });
1121
- bubble.appendChild(tail);
1122
- el.style.overflow = "visible";
1123
- el.appendChild(bubble);
1124
- const fadeDur = Math.min(200, durMs * 0.15);
1125
- anims.push(
1126
- bubble.animate([{ opacity: 0 }, { opacity: 1 }], {
1127
- delay: delayMs,
1128
- duration: fadeDur,
1129
- fill: "forwards"
1130
- }),
1131
- bubble.animate([{ opacity: 1 }, { opacity: 0 }], {
1132
- delay: delayMs + durMs - fadeDur,
1133
- duration: fadeDur,
1134
- fill: "forwards"
1135
- })
1136
- );
1137
- break;
1138
- }
1139
- // ── pose ────────────────────────────────────────────────────────────────
1140
- // Sets multiple body parts to target angles simultaneously.
1141
- // Usage: @1.0: hero.pose(arm_left=45, arm_right=-45, leg_left=10, dur=0.4)
1142
- case "pose": {
1143
- const poseParts = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
1144
- for (const partName of poseParts) {
1145
- if (typeof ev.params[partName] !== "number") continue;
1146
- const pSel = PART_SEL[partName];
1147
- if (!pSel) continue;
1148
- const pEl = el.querySelector(pSel);
1149
- if (!pEl) continue;
1150
- const fromDeg = readRotation(pEl);
1151
- const toDeg = ev.params[partName];
1152
- anims.push(
1153
- pEl.animate(
1154
- [
1155
- { transform: `rotate(${fromDeg}deg)` },
1156
- { transform: `rotate(${toDeg}deg)` }
1157
- ],
1158
- { ...baseOpts, fill: "forwards" }
1159
- )
1160
- );
1161
- pEl.style.transform = pEl.style.transform.replace(
1162
- /rotate\([^)]*\)/,
1163
- `rotate(${toDeg}deg)`
1164
- );
1165
- }
1166
- break;
1167
- }
1168
- // ── wave ────────────────────────────────────────────────────────────────
1169
- // Built-in wave gesture: raises arm, oscillates, returns.
1170
- // Usage: @2.0: hero.wave(side=right, dur=0.8)
1171
- case "wave": {
1172
- const wSide = String(ev.params.side ?? "right");
1173
- const wArmEl = el.querySelector(
1174
- wSide === "left" ? "[data-fig-arm-l]" : "[data-fig-arm-r]"
1175
- );
1176
- if (!wArmEl) break;
1177
- const wRest = readRotation(wArmEl);
1178
- const wUp = wSide === "left" ? 70 : -70;
1179
- const wMid1 = wSide === "left" ? 50 : -50;
1180
- const wMid2 = wSide === "left" ? 70 : -70;
1181
- anims.push(
1182
- wArmEl.animate(
1183
- [
1184
- { transform: `rotate(${wRest}deg)`, offset: 0 },
1185
- { transform: `rotate(${wUp}deg)`, offset: 0.2 },
1186
- { transform: `rotate(${wMid1}deg)`, offset: 0.4 },
1187
- { transform: `rotate(${wMid2}deg)`, offset: 0.55 },
1188
- { transform: `rotate(${wMid1}deg)`, offset: 0.7 },
1189
- { transform: `rotate(${wMid2}deg)`, offset: 0.85 },
1190
- { transform: `rotate(${wRest}deg)`, offset: 1 }
1191
- ],
1192
- { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
1193
- )
1194
- );
1195
- break;
1196
- }
1197
- // ── jump ────────────────────────────────────────────────────────────────
1198
- // Jumps the actor up with squash/stretch effect.
1199
- // Usage: @3.0: hero.jump(height=30, dur=0.5)
1200
- case "jump": {
1201
- const jHeight = typeof ev.params.height === "number" ? ev.params.height : 30;
1202
- anims.push(
1203
- el.animate(
1204
- [
1205
- { transform: txFn(s), offset: 0 },
1206
- { transform: txFn({ ...s, scale: s.scale * 0.9 }), offset: 0.1 },
1207
- { transform: txFn({ ...s, y: s.y - jHeight, scale: s.scale * 1.1 }), offset: 0.45 },
1208
- { transform: txFn({ ...s, y: s.y - jHeight * 0.3, scale: s.scale * 1.05 }), offset: 0.7 },
1209
- { transform: txFn({ ...s, scale: s.scale * 0.92 }), offset: 0.88 },
1210
- { transform: txFn(s), offset: 1 }
1211
- ],
1212
- { ...baseOpts, easing: "ease-in-out" }
1213
- )
1214
- );
1215
- break;
2083
+ var throwAsset = ({
2084
+ ev,
2085
+ state,
2086
+ baseOpts,
2087
+ ast,
2088
+ states,
2089
+ scene,
2090
+ assetOverrides,
2091
+ anims
2092
+ }) => {
2093
+ const assetName = String(ev.params.asset ?? "");
2094
+ const targetState = states.get(String(ev.params.to ?? ""));
2095
+ const assetDef = ast.assets[assetName];
2096
+ if (!assetDef || !targetState) return;
2097
+ const projectile = createProjectile(assetName, assetDef, assetOverrides);
2098
+ Object.assign(projectile.style, {
2099
+ position: "absolute",
2100
+ left: "0",
2101
+ top: "0",
2102
+ pointerEvents: "none",
2103
+ zIndex: "9",
2104
+ opacity: "0"
2105
+ });
2106
+ scene.appendChild(projectile);
2107
+ const anim = projectile.animate(
2108
+ [
2109
+ { transform: tx(state), opacity: 1 },
2110
+ { transform: tx(targetState), opacity: 0 }
2111
+ ],
2112
+ { ...baseOpts, easing: "ease-in" }
2113
+ );
2114
+ anim.addEventListener("finish", () => {
2115
+ if (projectile.parentNode === scene) scene.removeChild(projectile);
2116
+ });
2117
+ anims.push(anim);
2118
+ };
2119
+
2120
+ // src/actions/registry.ts
2121
+ var ACTION_HANDLERS = Object.freeze({
2122
+ // Universal — every actor type
2123
+ move,
2124
+ enter,
2125
+ exit,
2126
+ fade_in: fadeIn,
2127
+ fade_out: fadeOut,
2128
+ spring,
2129
+ follow_path: followPath,
2130
+ scale,
2131
+ rotate,
2132
+ shake,
2133
+ pulse,
2134
+ glow,
2135
+ ripple,
2136
+ blur,
2137
+ line_reveal: lineReveal,
2138
+ mask,
2139
+ parallax,
2140
+ jump,
2141
+ bounce,
2142
+ say,
2143
+ throw: throwAsset,
2144
+ // Figure-only the parser rejects these on other actor types
2145
+ punch,
2146
+ kick,
2147
+ wave,
2148
+ nod,
2149
+ rotate_part: rotatePart,
2150
+ pose,
2151
+ face,
2152
+ // System-diagram flow edges (@markdy/stdlib-systems)
2153
+ request: flowEdge,
2154
+ response: flowEdge,
2155
+ emit: flowEdge
2156
+ });
2157
+ function handlerFor(action) {
2158
+ return ACTION_HANDLERS[action];
2159
+ }
2160
+
2161
+ // src/animations.ts
2162
+ var DEFAULT_DURATION_S = 0.5;
2163
+ var DEFAULT_EASE_BY_ACTION = {
2164
+ enter: "out",
2165
+ fade_in: "out",
2166
+ exit: "in",
2167
+ fade_out: "in"
2168
+ };
2169
+ function buildAnimations(ast, actorEls, actorLayer, cameraLayer, assetOverrides, faceSwaps) {
2170
+ const anims = [];
2171
+ const states = /* @__PURE__ */ new Map();
2172
+ for (const [name, def] of Object.entries(ast.actors)) {
2173
+ states.set(name, stateFrom(def));
2174
+ }
2175
+ const captionActors = new Set(
2176
+ Object.entries(ast.actors).filter(([, def]) => def.type === "caption").map(([name]) => name)
2177
+ );
2178
+ const txFor = (actorName) => captionActors.has(actorName) ? txCaption : tx;
2179
+ const events = [...ast.events].sort((a, b) => a.time - b.time);
2180
+ preInitInlineStyles(ast, actorEls, states, events, txFor);
2181
+ const cameraState = freshCameraState();
2182
+ for (const ev of events) {
2183
+ const delayMs = ev.time * 1e3;
2184
+ const durMs = Math.max(
2185
+ 1,
2186
+ (typeof ev.params.dur === "number" ? ev.params.dur : DEFAULT_DURATION_S) * 1e3
2187
+ );
2188
+ const baseOpts = {
2189
+ delay: delayMs,
2190
+ duration: durMs,
2191
+ fill: "forwards",
2192
+ easing: toEasing(ev.params.ease ?? DEFAULT_EASE_BY_ACTION[ev.action])
2193
+ };
2194
+ if (ev.actor === "camera") {
2195
+ buildCameraAction(ev, cameraLayer, ast, baseOpts, anims, cameraState);
2196
+ continue;
1216
2197
  }
1217
- // ── nod ─────────────────────────────────────────────────────────────────
1218
- // Nods the head down and back up. Figure actors only.
1219
- // Usage: @2.0: hero.nod(dur=0.4)
1220
- case "nod": {
1221
- const nHeadEl = el.querySelector("[data-fig-head]");
1222
- if (!nHeadEl) break;
1223
- const nRest = readRotation(nHeadEl);
1224
- const nDown = 15;
1225
- anims.push(
1226
- nHeadEl.animate(
1227
- [
1228
- { transform: `rotate(${nRest}deg)`, offset: 0 },
1229
- { transform: `rotate(${nDown}deg)`, offset: 0.35 },
1230
- { transform: `rotate(${nRest}deg)`, offset: 0.65 },
1231
- { transform: `rotate(${nDown}deg)`, offset: 0.8 },
1232
- { transform: `rotate(${nRest}deg)`, offset: 1 }
1233
- ],
1234
- { ...baseOpts, easing: "ease-in-out", fill: "forwards" }
1235
- )
1236
- );
1237
- break;
2198
+ const el = actorEls.get(ev.actor);
2199
+ const state = states.get(ev.actor);
2200
+ const handler = handlerFor(ev.action);
2201
+ if (!el || !state || !handler) continue;
2202
+ const ctx = {
2203
+ ev,
2204
+ el,
2205
+ state,
2206
+ baseOpts,
2207
+ delayMs,
2208
+ durMs,
2209
+ ast,
2210
+ states,
2211
+ actorEls,
2212
+ scene: actorLayer,
2213
+ assetOverrides,
2214
+ faceSwaps,
2215
+ anims,
2216
+ tx: txFor(ev.actor)
2217
+ };
2218
+ handler(ctx);
2219
+ }
2220
+ return anims;
2221
+ }
2222
+
2223
+ // src/player.ts
2224
+ var SCENE_STYLE_ID = "markdy-scene-ambience-styles";
2225
+ var SAFE_FRAME_PADDING = 28;
2226
+ function ensureSceneStyles(doc) {
2227
+ if (doc.getElementById(SCENE_STYLE_ID)) return;
2228
+ const style = doc.createElement("style");
2229
+ style.id = SCENE_STYLE_ID;
2230
+ style.textContent = `
2231
+ .markdy-scene-root {
2232
+ --markdy-scene-grid: rgba(148, 163, 184, 0.13);
2233
+ --markdy-scene-grid-strong: rgba(148, 163, 184, 0.2);
2234
+ --markdy-scene-vignette: rgba(2, 6, 23, 0.72);
2235
+ --markdy-scene-accent: rgba(56, 189, 248, 0.16);
2236
+ isolation: isolate;
2237
+ }
2238
+ .markdy-scene-root::before,
2239
+ .markdy-scene-root::after {
2240
+ content: "";
2241
+ position: absolute;
2242
+ inset: 0;
2243
+ pointer-events: none;
2244
+ }
2245
+ .markdy-scene-root::before {
2246
+ z-index: 0;
2247
+ background:
2248
+ linear-gradient(var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2249
+ linear-gradient(90deg, var(--markdy-scene-grid) 1px, transparent 1px) 0 0 / 32px 32px,
2250
+ linear-gradient(var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px,
2251
+ linear-gradient(90deg, var(--markdy-scene-grid-strong) 1px, transparent 1px) 0 0 / 160px 160px;
2252
+ mask-image: linear-gradient(to bottom, transparent, #000 10%, #000 90%, transparent);
2253
+ opacity: 0.82;
2254
+ }
2255
+ .markdy-scene-root::after {
2256
+ z-index: 1;
2257
+ background:
2258
+ radial-gradient(ellipse at 50% 0%, var(--markdy-scene-accent), transparent 42%),
2259
+ linear-gradient(180deg, transparent 0%, rgba(2, 6, 23, 0.08) 58%, var(--markdy-scene-vignette) 100%),
2260
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035), transparent 18%, transparent 82%, rgba(255, 255, 255, 0.035));
2261
+ mix-blend-mode: screen;
2262
+ opacity: 0.8;
2263
+ }
2264
+ .markdy-scene-root[data-markdy-scene-tone="light"] {
2265
+ --markdy-scene-grid: rgba(15, 23, 42, 0.09);
2266
+ --markdy-scene-grid-strong: rgba(15, 23, 42, 0.14);
2267
+ --markdy-scene-vignette: rgba(241, 245, 249, 0.74);
2268
+ --markdy-scene-accent: rgba(14, 165, 233, 0.12);
2269
+ }
2270
+ .markdy-scene-content {
2271
+ z-index: 2;
2272
+ }
2273
+ .markdy-scene-actor-layer {
2274
+ position: absolute;
2275
+ inset: 0;
2276
+ overflow: visible;
2277
+ transform-origin: 0 0;
2278
+ will-change: transform;
2279
+ }
2280
+ `;
2281
+ doc.head.appendChild(style);
2282
+ }
2283
+ function unionRect(a, b) {
2284
+ if (!a) return b;
2285
+ return {
2286
+ x1: Math.min(a.x1, b.x1),
2287
+ y1: Math.min(a.y1, b.y1),
2288
+ x2: Math.max(a.x2, b.x2),
2289
+ y2: Math.max(a.y2, b.y2)
2290
+ };
2291
+ }
2292
+ function numericPair(value) {
2293
+ if (!Array.isArray(value) || value.length < 2) return null;
2294
+ const [x, y] = value;
2295
+ return typeof x === "number" && typeof y === "number" ? [x, y] : null;
2296
+ }
2297
+ function computeContentBounds(ast) {
2298
+ let bounds = null;
2299
+ const maxScaleByActor = /* @__PURE__ */ new Map();
2300
+ const positionsByActor = /* @__PURE__ */ new Map();
2301
+ for (const [name, def] of Object.entries(ast.actors)) {
2302
+ maxScaleByActor.set(name, Math.max(1e-3, def.scale ?? 1));
2303
+ positionsByActor.set(name, [[def.x, def.y]]);
2304
+ }
2305
+ for (const ev of ast.events) {
2306
+ if (ev.actor === "camera") continue;
2307
+ if (ev.action === "scale" && typeof ev.params.to === "number") {
2308
+ maxScaleByActor.set(ev.actor, Math.max(maxScaleByActor.get(ev.actor) ?? 1, ev.params.to));
1238
2309
  }
1239
- // ── bounce ──────────────────────────────────────────────────────────────
1240
- // Bounces the actor vertically with diminishing amplitude.
1241
- // Usage: @1.0: hero.bounce(intensity=15, count=3, dur=0.6)
1242
- case "bounce": {
1243
- const bIntensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
1244
- const bCount = typeof ev.params.count === "number" ? ev.params.count : 3;
1245
- const keyframes = [{ transform: txFn(s), offset: 0 }];
1246
- for (let bi = 0; bi < bCount; bi++) {
1247
- const amp = bIntensity * Math.pow(0.55, bi);
1248
- const baseOffset = (bi + 0.5) / (bCount + 0.5);
1249
- const peakOffset = Math.min(baseOffset, 0.98);
1250
- const valleyOffset = Math.min(baseOffset + 0.25 / (bCount + 0.5), 0.99);
1251
- keyframes.push({
1252
- transform: txFn({ ...s, y: s.y - amp }),
1253
- offset: peakOffset
1254
- });
1255
- if (bi < bCount - 1) {
1256
- keyframes.push({
1257
- transform: txFn(s),
1258
- offset: valleyOffset
1259
- });
1260
- }
1261
- }
1262
- keyframes.push({ transform: txFn(s), offset: 1 });
1263
- anims.push(
1264
- el.animate(keyframes, { ...baseOpts, easing: "ease-out" })
1265
- );
1266
- break;
2310
+ if (ev.action === "move" || ev.action === "spring") {
2311
+ const to = numericPair(ev.params.to);
2312
+ if (to) positionsByActor.get(ev.actor)?.push(to);
1267
2313
  }
1268
- // ── throw ───────────────────────────────────────────────────────────────
1269
- case "throw": {
1270
- const assetName = String(ev.params.asset ?? "");
1271
- const targetActorName = String(ev.params.to ?? "");
1272
- const targetState = states.get(targetActorName);
1273
- const assetDef = ast.assets[assetName];
1274
- if (!assetDef || !targetState) break;
1275
- let projectile;
1276
- if (assetDef.type === "image") {
1277
- const img = document.createElement("img");
1278
- img.src = assetOverrides[assetName] ?? assetDef.value;
1279
- img.alt = assetName;
1280
- img.setAttribute("draggable", "false");
1281
- img.style.width = "32px";
1282
- img.style.height = "32px";
1283
- projectile = img;
1284
- } else {
1285
- const span = document.createElement("span");
1286
- span.className = "iconify";
1287
- span.dataset.icon = assetDef.value;
1288
- span.style.fontSize = "32px";
1289
- span.style.lineHeight = "1";
1290
- span.style.display = "inline-block";
1291
- projectile = span;
1292
- }
1293
- Object.assign(projectile.style, {
1294
- position: "absolute",
1295
- left: "0",
1296
- top: "0",
1297
- pointerEvents: "none",
1298
- zIndex: "9",
1299
- opacity: "0"
1300
- });
1301
- scene.appendChild(projectile);
1302
- const throwAnim = projectile.animate(
1303
- [
1304
- { transform: tx(s), opacity: 1 },
1305
- { transform: tx(targetState), opacity: 0 }
1306
- ],
1307
- { ...baseOpts, easing: "ease-in" }
1308
- );
1309
- throwAnim.addEventListener("finish", () => {
1310
- if (projectile.parentNode === scene) scene.removeChild(projectile);
1311
- });
1312
- anims.push(throwAnim);
1313
- break;
2314
+ }
2315
+ for (const [name, def] of Object.entries(ast.actors)) {
2316
+ const positions = positionsByActor.get(name) ?? [[def.x, def.y]];
2317
+ const scale2 = maxScaleByActor.get(name) ?? def.scale ?? 1;
2318
+ for (const [x, y] of positions) {
2319
+ bounds = unionRect(bounds, actorRect({ ...stateFrom(def), x, y, scale: scale2 }, def.type));
1314
2320
  }
1315
- default:
1316
- break;
1317
2321
  }
2322
+ return bounds;
2323
+ }
2324
+ function computeSafeFrame(ast) {
2325
+ const bounds = computeContentBounds(ast);
2326
+ if (!bounds) return { x: 0, y: 0, scale: 1 };
2327
+ const boundsWidth = Math.max(1, bounds.x2 - bounds.x1);
2328
+ const boundsHeight = Math.max(1, bounds.y2 - bounds.y1);
2329
+ const availableWidth = Math.max(1, ast.meta.width - SAFE_FRAME_PADDING * 2);
2330
+ const availableHeight = Math.max(1, ast.meta.height - SAFE_FRAME_PADDING * 2);
2331
+ const scale2 = Math.min(1, availableWidth / boundsWidth, availableHeight / boundsHeight);
2332
+ const scaledWidth = boundsWidth * scale2;
2333
+ const scaledHeight = boundsHeight * scale2;
2334
+ function axisOffset(min, max, sceneSize, scaledSize) {
2335
+ const paddedMin = SAFE_FRAME_PADDING;
2336
+ const paddedMax = sceneSize - SAFE_FRAME_PADDING;
2337
+ const scaledMin = min * scale2;
2338
+ const scaledMax = max * scale2;
2339
+ if (scaledSize > paddedMax - paddedMin) return (sceneSize - scaledSize) / 2 - scaledMin;
2340
+ if (scaledMin < paddedMin) return paddedMin - scaledMin;
2341
+ if (scaledMax > paddedMax) return paddedMax - scaledMax;
2342
+ return 0;
2343
+ }
2344
+ const x = axisOffset(bounds.x1, bounds.x2, ast.meta.width, scaledWidth);
2345
+ const y = axisOffset(bounds.y1, bounds.y2, ast.meta.height, scaledHeight);
2346
+ return {
2347
+ x: Math.round(x * 1e3) / 1e3,
2348
+ y: Math.round(y * 1e3) / 1e3,
2349
+ scale: Math.round(scale2 * 1e3) / 1e3
2350
+ };
1318
2351
  }
1319
-
1320
- // src/player.ts
1321
2352
  function bgToTextColor(bg) {
1322
2353
  let hex = bg.trim().replace(/^#/, "");
1323
2354
  if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
@@ -1417,6 +2448,9 @@ function createPlayer(opts) {
1417
2448
  }
1418
2449
  }
1419
2450
  const scene = document.createElement("div");
2451
+ ensureSceneStyles(document);
2452
+ scene.className = "markdy-scene-root";
2453
+ scene.dataset.markdySceneTone = bgToTextColor(ast.meta.bg) === "#1a1a1a" ? "light" : "dark";
1420
2454
  Object.assign(scene.style, {
1421
2455
  position: "absolute",
1422
2456
  top: "0",
@@ -1431,6 +2465,7 @@ function createPlayer(opts) {
1431
2465
  });
1432
2466
  viewport.appendChild(scene);
1433
2467
  const sceneContent = document.createElement("div");
2468
+ sceneContent.className = "markdy-scene-content";
1434
2469
  Object.assign(sceneContent.style, {
1435
2470
  position: "absolute",
1436
2471
  top: "0",
@@ -1441,6 +2476,12 @@ function createPlayer(opts) {
1441
2476
  willChange: "transform"
1442
2477
  });
1443
2478
  scene.appendChild(sceneContent);
2479
+ const actorLayer = document.createElement("div");
2480
+ actorLayer.className = "markdy-scene-actor-layer";
2481
+ const safeFrame = computeSafeFrame(ast);
2482
+ actorLayer.dataset.markdySafeFrame = `${safeFrame.x},${safeFrame.y},${safeFrame.scale}`;
2483
+ actorLayer.style.transform = `translate(${safeFrame.x}px, ${safeFrame.y}px) scale(${safeFrame.scale})`;
2484
+ sceneContent.appendChild(actorLayer);
1444
2485
  function scaleScene() {
1445
2486
  const s = viewport.clientWidth / ast.meta.width;
1446
2487
  scene.style.transform = `scale(${s})`;
@@ -1451,11 +2492,11 @@ function createPlayer(opts) {
1451
2492
  const actorEls = /* @__PURE__ */ new Map();
1452
2493
  for (const [name, def] of Object.entries(ast.actors)) {
1453
2494
  const el = createActorEl(name, def, ast.assets, assetOverrides);
1454
- sceneContent.appendChild(el);
2495
+ actorLayer.appendChild(el);
1455
2496
  actorEls.set(name, el);
1456
2497
  }
1457
2498
  const faceSwaps = [];
1458
- const allAnims = buildAnimations(ast, actorEls, sceneContent, assetOverrides, faceSwaps);
2499
+ const allAnims = buildAnimations(ast, actorEls, actorLayer, sceneContent, assetOverrides, faceSwaps);
1459
2500
  faceSwaps.sort((a, b) => a.timeMs - b.timeMs);
1460
2501
  for (const anim of allAnims) {
1461
2502
  anim.pause();