@markdy/renderer-dom 0.7.25 → 0.7.26

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 +592 -659
  3. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -459,79 +459,69 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
459
459
  return el;
460
460
  }
461
461
 
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;
462
+ // src/camera.ts
463
+ function freshCameraState() {
464
+ return { x: 0, y: 0, zoom: 1 };
481
465
  }
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;
466
+ function cameraTx(s) {
467
+ return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
468
+ }
469
+ var DEFAULT_SHAKE_INTENSITY = 8;
470
+ function buildCameraAction(ev, scene, ast, baseOpts, anims, state) {
471
+ switch (ev.action) {
472
+ case "pan": {
473
+ const to = ev.params.to;
474
+ if (!to) break;
475
+ const next = {
476
+ ...state,
477
+ x: to[0] - ast.meta.width / 2,
478
+ y: to[1] - ast.meta.height / 2
479
+ };
480
+ anims.push(
481
+ scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
482
+ );
483
+ state.x = next.x;
484
+ state.y = next.y;
485
+ break;
512
486
  }
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
- );
487
+ case "zoom": {
488
+ const next = {
489
+ ...state,
490
+ zoom: typeof ev.params.to === "number" ? ev.params.to : state.zoom
491
+ };
492
+ anims.push(
493
+ scene.animate([{ transform: cameraTx(state) }, { transform: cameraTx(next) }], baseOpts)
494
+ );
495
+ state.zoom = next.zoom;
496
+ break;
497
+ }
498
+ case "shake": {
499
+ const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : DEFAULT_SHAKE_INTENSITY;
500
+ const at = (dx, dy, offset) => ({
501
+ transform: cameraTx({ ...state, x: state.x + dx, y: state.y + dy }),
502
+ offset
503
+ });
504
+ anims.push(
505
+ scene.animate(
506
+ [
507
+ at(0, 0, 0),
508
+ at(-mag, -mag * 0.4, 0.15),
509
+ at(mag, mag * 0.4, 0.35),
510
+ at(-mag * 0.6, mag * 0.3, 0.55),
511
+ at(mag * 0.5, -mag * 0.3, 0.75),
512
+ at(0, 0, 1)
513
+ ],
514
+ { ...baseOpts, easing: "linear" }
515
+ )
516
+ );
517
+ break;
518
+ }
519
+ default:
520
+ break;
532
521
  }
533
- return anims;
534
522
  }
523
+
524
+ // src/stage.ts
535
525
  function offscreenState(s, direction, sceneWidth, sceneHeight) {
536
526
  const out = { ...s };
537
527
  switch (direction) {
@@ -561,114 +551,252 @@ function preInitInlineStyles(ast, actorEls, states, events, txFor) {
561
551
  const s = states.get(name);
562
552
  if (!el || !s) continue;
563
553
  const firstEv = firstEventByActor.get(name);
564
- const txFn = txFor(name);
565
- if (firstEv?.action === "enter") {
554
+ if (!firstEv) continue;
555
+ if (firstEv.action === "enter") {
566
556
  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);
557
+ el.style.transform = txFor(name)(offscreenState(s, from, ast.meta.width, ast.meta.height));
569
558
  }
570
- if (firstEv?.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
559
+ if (firstEv.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
571
560
  el.style.opacity = "0";
572
561
  }
573
562
  }
574
563
  }
575
- function freshCameraState() {
576
- return { x: 0, y: 0, zoom: 1 };
564
+
565
+ // src/actions/figure.ts
566
+ function sideOf(ctx) {
567
+ return String(ctx.ev.params.side ?? "right") === "left" ? "left" : "right";
577
568
  }
578
- function cameraTx(s) {
579
- return `translate(${-s.x}px, ${-s.y}px) scale(${s.zoom})`;
569
+ function partEl(ctx, part) {
570
+ const selector = PART_SEL[part];
571
+ return selector ? ctx.el.querySelector(selector) : null;
580
572
  }
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;
573
+ function rotateKeyframe(deg, offset) {
574
+ return offset === void 0 ? { transform: `rotate(${deg}deg)` } : { transform: `rotate(${deg}deg)`, offset };
575
+ }
576
+ function swingLimb(ctx, part, extendDeg, peakOffset) {
577
+ const el = partEl(ctx, part);
578
+ if (!el) return;
579
+ const rest = readRotation(el);
580
+ ctx.anims.push(
581
+ el.animate([rotateKeyframe(rest), rotateKeyframe(extendDeg, peakOffset), rotateKeyframe(rest)], {
582
+ ...ctx.baseOpts,
583
+ easing: "ease-in-out",
584
+ fill: "forwards"
585
+ })
586
+ );
587
+ }
588
+ var punch = (ctx) => {
589
+ const side = sideOf(ctx);
590
+ swingLimb(ctx, side === "left" ? "arm_left" : "arm_right", side === "left" ? -75 : 75, 0.35);
591
+ };
592
+ var kick = (ctx) => {
593
+ const side = sideOf(ctx);
594
+ swingLimb(ctx, side === "left" ? "leg_left" : "leg_right", side === "left" ? -100 : 100, 0.38);
595
+ };
596
+ var wave = (ctx) => {
597
+ const side = sideOf(ctx);
598
+ const el = partEl(ctx, side === "left" ? "arm_left" : "arm_right");
599
+ if (!el) return;
600
+ const rest = readRotation(el);
601
+ const up = side === "left" ? 70 : -70;
602
+ const inward = side === "left" ? 50 : -50;
603
+ ctx.anims.push(
604
+ el.animate(
605
+ [
606
+ rotateKeyframe(rest, 0),
607
+ rotateKeyframe(up, 0.2),
608
+ rotateKeyframe(inward, 0.4),
609
+ rotateKeyframe(up, 0.55),
610
+ rotateKeyframe(inward, 0.7),
611
+ rotateKeyframe(up, 0.85),
612
+ rotateKeyframe(rest, 1)
613
+ ],
614
+ { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
615
+ )
616
+ );
617
+ };
618
+ var nod = (ctx) => {
619
+ const el = partEl(ctx, "head");
620
+ if (!el) return;
621
+ const rest = readRotation(el);
622
+ const down = 15;
623
+ ctx.anims.push(
624
+ el.animate(
625
+ [
626
+ rotateKeyframe(rest, 0),
627
+ rotateKeyframe(down, 0.35),
628
+ rotateKeyframe(rest, 0.65),
629
+ rotateKeyframe(down, 0.8),
630
+ rotateKeyframe(rest, 1)
631
+ ],
632
+ { ...ctx.baseOpts, easing: "ease-in-out", fill: "forwards" }
633
+ )
634
+ );
635
+ };
636
+ function commitRotation(el, deg) {
637
+ el.style.transform = el.style.transform.replace(/rotate\([^)]*\)/, `rotate(${deg}deg)`);
638
+ }
639
+ var rotatePart = (ctx) => {
640
+ const el = partEl(ctx, String(ctx.ev.params.part ?? ""));
641
+ if (!el) return;
642
+ const from = readRotation(el);
643
+ const to = typeof ctx.ev.params.to === "number" ? ctx.ev.params.to : from;
644
+ ctx.anims.push(
645
+ el.animate([rotateKeyframe(from), rotateKeyframe(to)], { ...ctx.baseOpts, fill: "forwards" })
646
+ );
647
+ commitRotation(el, to);
648
+ };
649
+ var POSEABLE_PARTS = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
650
+ var pose = (ctx) => {
651
+ for (const part of POSEABLE_PARTS) {
652
+ const to = ctx.ev.params[part];
653
+ if (typeof to !== "number") continue;
654
+ const el = partEl(ctx, part);
655
+ if (!el) continue;
656
+ ctx.anims.push(
657
+ el.animate([rotateKeyframe(readRotation(el)), rotateKeyframe(to)], {
658
+ ...ctx.baseOpts,
659
+ fill: "forwards"
660
+ })
661
+ );
662
+ commitRotation(el, to);
663
+ }
664
+ };
665
+ var face = (ctx) => {
666
+ const el = ctx.el.querySelector("[data-fig-face]");
667
+ if (!el) return;
668
+ const emoji = String(ctx.ev.params.text ?? ctx.ev.params._0 ?? "");
669
+ if (emoji) ctx.faceSwaps.push({ timeMs: ctx.ev.time * 1e3, el, emoji });
670
+ };
671
+
672
+ // src/actions/transform.ts
673
+ var move = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
674
+ const to = ev.params.to;
675
+ const toX = to?.[0] ?? state.x;
676
+ const toY = to?.[1] ?? state.y;
677
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, x: toX, y: toY }) }], baseOpts));
678
+ state.x = toX;
679
+ state.y = toY;
680
+ };
681
+ var enter = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
682
+ const from = String(ev.params.from ?? "left");
683
+ const fromState = offscreenState(state, from, ast.meta.width, ast.meta.height);
684
+ anims.push(
685
+ el.animate(
686
+ [
687
+ { transform: tx2(fromState), opacity: state.opacity },
688
+ { transform: tx2(state), opacity: 1 }
689
+ ],
690
+ baseOpts
691
+ )
692
+ );
693
+ state.opacity = 1;
694
+ };
695
+ var exit = ({ ev, el, state, baseOpts, ast, anims, tx: tx2 }) => {
696
+ const to = String(ev.params.to ?? "right");
697
+ const toState = offscreenState(state, to, ast.meta.width, ast.meta.height);
698
+ anims.push(
699
+ el.animate(
700
+ [
701
+ { transform: tx2(state), opacity: state.opacity },
702
+ { transform: tx2(toState), opacity: 0 }
703
+ ],
704
+ baseOpts
705
+ )
706
+ );
707
+ state.x = toState.x;
708
+ state.y = toState.y;
709
+ state.opacity = 0;
710
+ };
711
+ var fadeIn = ({ el, state, baseOpts, anims }) => {
712
+ anims.push(el.animate([{ opacity: 0 }, { opacity: 1 }], baseOpts));
713
+ state.opacity = 1;
714
+ };
715
+ var fadeOut = ({ el, state, baseOpts, anims }) => {
716
+ anims.push(el.animate([{ opacity: state.opacity }, { opacity: 0 }], baseOpts));
717
+ state.opacity = 0;
718
+ };
719
+ var scale = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
720
+ const to = typeof ev.params.to === "number" ? ev.params.to : state.scale;
721
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, scale: to }) }], baseOpts));
722
+ state.scale = to;
723
+ };
724
+ var rotate = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
725
+ const to = typeof ev.params.to === "number" ? ev.params.to : state.rotate;
726
+ anims.push(el.animate([{ transform: tx2(state) }, { transform: tx2({ ...state, rotate: to }) }], baseOpts));
727
+ state.rotate = to;
728
+ };
729
+ var shake = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
730
+ const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 5;
731
+ anims.push(
732
+ el.animate(
733
+ [
734
+ { transform: tx2(state), offset: 0 },
735
+ { transform: tx2({ ...state, x: state.x + mag }), offset: 0.2 },
736
+ { transform: tx2({ ...state, x: state.x - mag }), offset: 0.4 },
737
+ { transform: tx2({ ...state, x: state.x + mag }), offset: 0.6 },
738
+ { transform: tx2({ ...state, x: state.x - mag }), offset: 0.8 },
739
+ { transform: tx2(state), offset: 1 }
740
+ ],
741
+ { ...baseOpts, easing: "linear" }
742
+ )
743
+ );
744
+ };
745
+ var jump = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
746
+ const height = typeof ev.params.height === "number" ? ev.params.height : 30;
747
+ anims.push(
748
+ el.animate(
749
+ [
750
+ { transform: tx2(state), offset: 0 },
751
+ { transform: tx2({ ...state, scale: state.scale * 0.9 }), offset: 0.1 },
752
+ { transform: tx2({ ...state, y: state.y - height, scale: state.scale * 1.1 }), offset: 0.45 },
753
+ { transform: tx2({ ...state, y: state.y - height * 0.3, scale: state.scale * 1.05 }), offset: 0.7 },
754
+ { transform: tx2({ ...state, scale: state.scale * 0.92 }), offset: 0.88 },
755
+ { transform: tx2(state), offset: 1 }
756
+ ],
757
+ { ...baseOpts, easing: "ease-in-out" }
758
+ )
759
+ );
760
+ };
761
+ var bounce = ({ ev, el, state, baseOpts, anims, tx: tx2 }) => {
762
+ const intensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
763
+ const count = typeof ev.params.count === "number" ? ev.params.count : 3;
764
+ const keyframes = [{ transform: tx2(state), offset: 0 }];
765
+ for (let i = 0; i < count; i++) {
766
+ const amp = intensity * Math.pow(0.55, i);
767
+ const baseOffset = (i + 0.5) / (count + 0.5);
768
+ const peakOffset = Math.min(baseOffset, 0.98);
769
+ const valleyOffset = Math.min(baseOffset + 0.25 / (count + 0.5), 0.99);
770
+ keyframes.push({ transform: tx2({ ...state, y: state.y - amp }), offset: peakOffset });
771
+ if (i < count - 1) {
772
+ keyframes.push({ transform: tx2(state), offset: valleyOffset });
630
773
  }
631
- default:
632
- break;
633
774
  }
634
- }
635
- var FLOW_STROKE_BY_ACTION = {
636
- request: "#38bdf8",
637
- response: "#a78bfa",
638
- emit: "#f59e0b"
775
+ keyframes.push({ transform: tx2(state), offset: 1 });
776
+ anims.push(el.animate(keyframes, { ...baseOpts, easing: "ease-out" }));
777
+ };
778
+
779
+ // src/geometry/rect.ts
780
+ var ACTOR_SIZES = {
781
+ service: { width: 180, height: 84 },
782
+ client: { width: 180, height: 84 },
783
+ db: { width: 180, height: 84 },
784
+ queue: { width: 180, height: 84 },
785
+ box: { width: 100, height: 100 },
786
+ caption: { width: 260, height: 56 },
787
+ figure: { width: 120, height: 170 }
639
788
  };
789
+ var DEFAULT_ACTOR_SIZE = { width: 140, height: 42 };
640
790
  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
- }
791
+ return ACTOR_SIZES[type] ?? DEFAULT_ACTOR_SIZE;
656
792
  }
657
793
  function actorCenter(state, actorType) {
658
794
  const { width, height } = actorSizeByType(actorType);
659
- return {
660
- x: state.x + width / 2,
661
- y: state.y + height / 2
662
- };
795
+ return { x: state.x + width / 2, y: state.y + height / 2 };
663
796
  }
664
797
  function actorRect(state, actorType) {
665
798
  const { width, height } = actorSizeByType(actorType);
666
- return {
667
- x1: state.x,
668
- y1: state.y,
669
- x2: state.x + width,
670
- y2: state.y + height
671
- };
799
+ return { x1: state.x, y1: state.y, x2: state.x + width, y2: state.y + height };
672
800
  }
673
801
  function inflateRect(rect, pad) {
674
802
  return {
@@ -706,12 +834,14 @@ function countPathIntersections(points, obstacles) {
706
834
  }
707
835
  return hits;
708
836
  }
709
- function toPathD(points) {
710
- return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
711
- }
837
+
838
+ // src/geometry/path.ts
712
839
  function round1(n) {
713
840
  return Math.round(n * 10) / 10;
714
841
  }
842
+ function toPathD(points) {
843
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
844
+ }
715
845
  function polylineLength(points) {
716
846
  let total = 0;
717
847
  for (let i = 0; i < points.length - 1; i++) {
@@ -720,13 +850,13 @@ function polylineLength(points) {
720
850
  return Math.max(1, total);
721
851
  }
722
852
  function pointAtDistance(points, dist) {
723
- let remain = dist;
853
+ let remain = Math.max(0, dist);
724
854
  for (let i = 0; i < points.length - 1; i++) {
725
855
  const a = points[i];
726
856
  const b = points[i + 1];
727
857
  const seg = Math.hypot(b.x - a.x, b.y - a.y);
728
858
  if (remain <= seg || i === points.length - 2) {
729
- const t = seg <= 0 ? 0 : remain / seg;
859
+ const t = seg <= 0 ? 0 : Math.min(1, remain / seg);
730
860
  return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
731
861
  }
732
862
  remain -= seg;
@@ -734,11 +864,13 @@ function pointAtDistance(points, dist) {
734
864
  return points[0];
735
865
  }
736
866
  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");
867
+ const sourceType = ast.actors[sourceName]?.type ?? "box";
868
+ const targetType = ast.actors[targetName]?.type ?? "box";
869
+ const sourceRect = actorRect(sourceState, sourceType);
870
+ const targetRect = actorRect(targetState, targetType);
739
871
  const laneShift = lane * 18;
740
- const sourceCenter = actorCenter(sourceState, ast.actors[sourceName]?.type ?? "box");
741
- const targetCenter = actorCenter(targetState, ast.actors[targetName]?.type ?? "box");
872
+ const sourceCenter = actorCenter(sourceState, sourceType);
873
+ const targetCenter = actorCenter(targetState, targetType);
742
874
  const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
743
875
  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
876
  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 };
@@ -778,11 +910,22 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
778
910
  }
779
911
  return best;
780
912
  }
913
+
914
+ // src/actions/flow.ts
915
+ var EDGE_LAYER_ATTR = "data-markdy-edge-layer";
916
+ var STROKE_BY_ACTION = {
917
+ request: "#38bdf8",
918
+ response: "#a78bfa",
919
+ emit: "#f59e0b"
920
+ };
921
+ var DEFAULT_STROKE = "#38bdf8";
922
+ var LABEL_MAX_CHARS = 28;
923
+ var EDGE_FADE_MS = 140;
781
924
  function ensureEdgeLayer(scene) {
782
- const existing = scene.querySelector("svg[data-markdy-edge-layer='1']");
925
+ const existing = scene.querySelector(`svg[${EDGE_LAYER_ATTR}='1']`);
783
926
  if (existing) return existing;
784
927
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
785
- svg.setAttribute("data-markdy-edge-layer", "1");
928
+ svg.setAttribute(EDGE_LAYER_ATTR, "1");
786
929
  Object.assign(svg.style, {
787
930
  position: "absolute",
788
931
  inset: "0",
@@ -795,23 +938,19 @@ function ensureEdgeLayer(scene) {
795
938
  scene.appendChild(svg);
796
939
  return svg;
797
940
  }
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
- );
941
+ function isDashed(ctx) {
942
+ const style = String(ctx.ev.params.style ?? "");
943
+ return style === "dashed" || style === "fire_and_forget" || ctx.ev.action === "response";
944
+ }
945
+ function buildEdge(ctx, targetName) {
946
+ const { ev, state, states, ast, scene, baseOpts, anims } = ctx;
947
+ const targetState = states.get(targetName);
948
+ if (!targetState) return;
949
+ const lane = ev.line % 5 - 2;
950
+ const points = routeFlowPath(ev.actor, targetName, state, targetState, states, ast, lane);
811
951
  const length = polylineLength(points);
812
- const midPoint = pointAtDistance(points, length * 0.5);
813
952
  const pathD = toPathD(points);
814
- const svg = ensureEdgeLayer(scene);
953
+ const stroke = STROKE_BY_ACTION[ev.action] ?? DEFAULT_STROKE;
815
954
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
816
955
  group.setAttribute("data-markdy-flow-edge", "1");
817
956
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -820,7 +959,7 @@ function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, st
820
959
  path.setAttribute("stroke", stroke);
821
960
  path.setAttribute("stroke-width", "2.5");
822
961
  path.setAttribute("data-markdy-flow-action", ev.action);
823
- path.style.strokeDasharray = isDashed ? "8 6" : `${length}`;
962
+ path.style.strokeDasharray = isDashed(ctx) ? "8 6" : `${length}`;
824
963
  path.style.strokeDashoffset = `${length}`;
825
964
  group.appendChild(path);
826
965
  const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
@@ -830,491 +969,285 @@ function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, st
830
969
  marker.style.offsetDistance = "0%";
831
970
  marker.style.opacity = "0";
832
971
  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);
972
+ const labelText = String(ev.params.label ?? "");
973
+ if (labelText) {
974
+ const midPoint = pointAtDistance(points, length * 0.5);
975
+ const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
976
+ label.setAttribute("x", `${round1(midPoint.x)}`);
977
+ label.setAttribute("y", `${round1(midPoint.y - 8)}`);
978
+ label.setAttribute("text-anchor", "middle");
979
+ label.setAttribute("font-size", "12");
980
+ label.setAttribute("fill", "#cbd5e1");
981
+ label.setAttribute("data-full-label", labelText);
982
+ label.textContent = labelText.length > LABEL_MAX_CHARS ? `${labelText.slice(0, LABEL_MAX_CHARS - 1)}\u2026` : labelText;
983
+ group.appendChild(label);
845
984
  }
846
- svg.appendChild(group);
847
- anims.push(path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts));
985
+ ensureEdgeLayer(scene).appendChild(group);
848
986
  anims.push(
987
+ path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts),
849
988
  marker.animate(
850
- [{ offsetDistance: "0%", opacity: 1 }, { offsetDistance: "100%", opacity: 1 }],
989
+ [
990
+ { offsetDistance: "0%", opacity: 1 },
991
+ { offsetDistance: "100%", opacity: 1 }
992
+ ],
851
993
  baseOpts
852
- )
994
+ ),
995
+ group.animate([{ opacity: 1 }, { opacity: 0 }], {
996
+ delay: Number(baseOpts.delay ?? 0) + Number(baseOpts.duration ?? 0),
997
+ duration: EDGE_FADE_MS,
998
+ fill: "forwards"
999
+ })
853
1000
  );
854
- const fadeOutDelay = Number(baseOpts.delay ?? 0) + Number(baseOpts.duration ?? 0);
1001
+ }
1002
+ var flowEdge = (ctx) => {
1003
+ const targetName = String(ctx.ev.params.to ?? "");
1004
+ if (targetName) buildEdge(ctx, targetName);
1005
+ };
1006
+
1007
+ // src/theme.ts
1008
+ var DARK_LUMINANCE_THRESHOLD = 140;
1009
+ var NAMED_DARK = {
1010
+ black: true,
1011
+ "#000": true,
1012
+ "#000000": true
1013
+ };
1014
+ function isSceneDark(scene) {
1015
+ const bg = scene.style.background || "white";
1016
+ let hex = bg.trim().replace(/^#/, "");
1017
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
1018
+ if (hex.length === 6) {
1019
+ const r = parseInt(hex.slice(0, 2), 16);
1020
+ const g = parseInt(hex.slice(2, 4), 16);
1021
+ const b = parseInt(hex.slice(4, 6), 16);
1022
+ return 0.299 * r + 0.587 * g + 0.114 * b <= DARK_LUMINANCE_THRESHOLD;
1023
+ }
1024
+ return NAMED_DARK[bg.toLowerCase()] ?? false;
1025
+ }
1026
+ function speechBubbleTheme(dark) {
1027
+ return dark ? {
1028
+ background: "#1e2530",
1029
+ border: "#475569",
1030
+ text: "#e2e8f0",
1031
+ shadow: "0 2px 8px rgba(0,0,0,0.35)"
1032
+ } : {
1033
+ background: "white",
1034
+ border: "#222",
1035
+ text: "#222",
1036
+ shadow: "0 2px 8px rgba(0,0,0,0.12)"
1037
+ };
1038
+ }
1039
+
1040
+ // src/actions/speech.ts
1041
+ var MAX_FADE_MS = 200;
1042
+ var FADE_FRACTION = 0.15;
1043
+ var say = ({ ev, el, state, delayMs, durMs, scene, anims }) => {
1044
+ const text = String(ev.params.text ?? "");
1045
+ const theme = speechBubbleTheme(isSceneDark(scene));
1046
+ const inverseScale = 1 / (state.scale || 1);
1047
+ const bubble = document.createElement("div");
1048
+ bubble.textContent = text;
1049
+ Object.assign(bubble.style, {
1050
+ position: "absolute",
1051
+ bottom: "calc(100% + 10px)",
1052
+ left: "50%",
1053
+ transform: `translateX(-50%) scale(${inverseScale})`,
1054
+ transformOrigin: "center bottom",
1055
+ background: theme.background,
1056
+ border: `2px solid ${theme.border}`,
1057
+ color: theme.text,
1058
+ borderRadius: "12px",
1059
+ padding: "6px 14px",
1060
+ fontFamily: "system-ui, sans-serif",
1061
+ fontSize: "15px",
1062
+ lineHeight: "1.3",
1063
+ whiteSpace: "nowrap",
1064
+ maxWidth: "220px",
1065
+ overflow: "hidden",
1066
+ textOverflow: "ellipsis",
1067
+ pointerEvents: "none",
1068
+ zIndex: "10",
1069
+ boxShadow: theme.shadow,
1070
+ opacity: "0"
1071
+ });
1072
+ const tail = document.createElement("span");
1073
+ Object.assign(tail.style, {
1074
+ position: "absolute",
1075
+ bottom: "-10px",
1076
+ left: "50%",
1077
+ transform: "translateX(-50%)",
1078
+ width: "0",
1079
+ height: "0",
1080
+ borderLeft: "7px solid transparent",
1081
+ borderRight: "7px solid transparent",
1082
+ borderTop: `10px solid ${theme.border}`
1083
+ });
1084
+ bubble.appendChild(tail);
1085
+ el.style.overflow = "visible";
1086
+ el.appendChild(bubble);
1087
+ const fadeMs = Math.min(MAX_FADE_MS, durMs * FADE_FRACTION);
855
1088
  anims.push(
856
- group.animate([{ opacity: 1 }, { opacity: 0 }], {
857
- delay: fadeOutDelay,
858
- duration: 140,
1089
+ bubble.animate([{ opacity: 0 }, { opacity: 1 }], {
1090
+ delay: delayMs,
1091
+ duration: fadeMs,
1092
+ fill: "forwards"
1093
+ }),
1094
+ bubble.animate([{ opacity: 1 }, { opacity: 0 }], {
1095
+ delay: delayMs + durMs - fadeMs,
1096
+ duration: fadeMs,
859
1097
  fill: "forwards"
860
1098
  })
861
1099
  );
1100
+ };
1101
+
1102
+ // src/actions/projectile.ts
1103
+ var PROJECTILE_SIZE_PX = 32;
1104
+ function createProjectile(assetName, assetDef, assetOverrides) {
1105
+ if (assetDef.type === "image") {
1106
+ const img = document.createElement("img");
1107
+ img.src = assetOverrides[assetName] ?? assetDef.value;
1108
+ img.alt = assetName;
1109
+ img.setAttribute("draggable", "false");
1110
+ img.style.width = `${PROJECTILE_SIZE_PX}px`;
1111
+ img.style.height = `${PROJECTILE_SIZE_PX}px`;
1112
+ return img;
1113
+ }
1114
+ const span = document.createElement("span");
1115
+ span.className = "iconify";
1116
+ span.dataset.icon = assetDef.value;
1117
+ span.style.fontSize = `${PROJECTILE_SIZE_PX}px`;
1118
+ span.style.lineHeight = "1";
1119
+ span.style.display = "inline-block";
1120
+ return span;
862
1121
  }
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;
1216
- }
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;
1238
- }
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;
1267
- }
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;
1122
+ var throwAsset = ({
1123
+ ev,
1124
+ state,
1125
+ baseOpts,
1126
+ ast,
1127
+ states,
1128
+ scene,
1129
+ assetOverrides,
1130
+ anims
1131
+ }) => {
1132
+ const assetName = String(ev.params.asset ?? "");
1133
+ const targetState = states.get(String(ev.params.to ?? ""));
1134
+ const assetDef = ast.assets[assetName];
1135
+ if (!assetDef || !targetState) return;
1136
+ const projectile = createProjectile(assetName, assetDef, assetOverrides);
1137
+ Object.assign(projectile.style, {
1138
+ position: "absolute",
1139
+ left: "0",
1140
+ top: "0",
1141
+ pointerEvents: "none",
1142
+ zIndex: "9",
1143
+ opacity: "0"
1144
+ });
1145
+ scene.appendChild(projectile);
1146
+ const anim = projectile.animate(
1147
+ [
1148
+ { transform: tx(state), opacity: 1 },
1149
+ { transform: tx(targetState), opacity: 0 }
1150
+ ],
1151
+ { ...baseOpts, easing: "ease-in" }
1152
+ );
1153
+ anim.addEventListener("finish", () => {
1154
+ if (projectile.parentNode === scene) scene.removeChild(projectile);
1155
+ });
1156
+ anims.push(anim);
1157
+ };
1158
+
1159
+ // src/actions/registry.ts
1160
+ var ACTION_HANDLERS = Object.freeze({
1161
+ // Universal — every actor type
1162
+ move,
1163
+ enter,
1164
+ exit,
1165
+ fade_in: fadeIn,
1166
+ fade_out: fadeOut,
1167
+ scale,
1168
+ rotate,
1169
+ shake,
1170
+ jump,
1171
+ bounce,
1172
+ say,
1173
+ throw: throwAsset,
1174
+ // Figure-only the parser rejects these on other actor types
1175
+ punch,
1176
+ kick,
1177
+ wave,
1178
+ nod,
1179
+ rotate_part: rotatePart,
1180
+ pose,
1181
+ face,
1182
+ // System-diagram flow edges (@markdy/stdlib-systems)
1183
+ request: flowEdge,
1184
+ response: flowEdge,
1185
+ emit: flowEdge
1186
+ });
1187
+ function handlerFor(action) {
1188
+ return ACTION_HANDLERS[action];
1189
+ }
1190
+
1191
+ // src/animations.ts
1192
+ var DEFAULT_DURATION_S = 0.5;
1193
+ var DEFAULT_EASE_BY_ACTION = {
1194
+ enter: "out",
1195
+ fade_in: "out",
1196
+ exit: "in",
1197
+ fade_out: "in"
1198
+ };
1199
+ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
1200
+ const anims = [];
1201
+ const states = /* @__PURE__ */ new Map();
1202
+ for (const [name, def] of Object.entries(ast.actors)) {
1203
+ states.set(name, stateFrom(def));
1204
+ }
1205
+ const captionActors = new Set(
1206
+ Object.entries(ast.actors).filter(([, def]) => def.type === "caption").map(([name]) => name)
1207
+ );
1208
+ const txFor = (actorName) => captionActors.has(actorName) ? txCaption : tx;
1209
+ const events = [...ast.events].sort((a, b) => a.time - b.time);
1210
+ preInitInlineStyles(ast, actorEls, states, events, txFor);
1211
+ const cameraState = freshCameraState();
1212
+ for (const ev of events) {
1213
+ const delayMs = ev.time * 1e3;
1214
+ const durMs = Math.max(
1215
+ 1,
1216
+ (typeof ev.params.dur === "number" ? ev.params.dur : DEFAULT_DURATION_S) * 1e3
1217
+ );
1218
+ const baseOpts = {
1219
+ delay: delayMs,
1220
+ duration: durMs,
1221
+ fill: "forwards",
1222
+ easing: toEasing(ev.params.ease ?? DEFAULT_EASE_BY_ACTION[ev.action])
1223
+ };
1224
+ if (ev.actor === "camera") {
1225
+ buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState);
1226
+ continue;
1314
1227
  }
1315
- default:
1316
- break;
1228
+ const el = actorEls.get(ev.actor);
1229
+ const state = states.get(ev.actor);
1230
+ const handler = handlerFor(ev.action);
1231
+ if (!el || !state || !handler) continue;
1232
+ const ctx = {
1233
+ ev,
1234
+ el,
1235
+ state,
1236
+ baseOpts,
1237
+ delayMs,
1238
+ durMs,
1239
+ ast,
1240
+ states,
1241
+ actorEls,
1242
+ scene,
1243
+ assetOverrides,
1244
+ faceSwaps,
1245
+ anims,
1246
+ tx: txFor(ev.actor)
1247
+ };
1248
+ handler(ctx);
1317
1249
  }
1250
+ return anims;
1318
1251
  }
1319
1252
 
1320
1253
  // src/player.ts