@markdy/renderer-dom 0.7.24 → 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.
- package/README.md +38 -5
- package/dist/index.d.ts +12 -1
- package/dist/index.js +631 -657
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -21,10 +21,20 @@ var EASE_MAP = {
|
|
|
21
21
|
linear: "linear",
|
|
22
22
|
in: "ease-in",
|
|
23
23
|
out: "ease-out",
|
|
24
|
-
inout: "ease-in-out"
|
|
24
|
+
inout: "ease-in-out",
|
|
25
|
+
// Named cubic-bezier presets for authors who want a more polished feel
|
|
26
|
+
// than the four raw CSS keywords above without hand-writing a curve.
|
|
27
|
+
smooth: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
28
|
+
snappy: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
29
|
+
overshoot: "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
30
|
+
sharp: "cubic-bezier(0.4, 0, 1, 1)"
|
|
25
31
|
};
|
|
32
|
+
var CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*,\s*-?\d*\.?\d+\s*\)$/;
|
|
26
33
|
function toEasing(val) {
|
|
27
|
-
|
|
34
|
+
const str = String(val ?? "");
|
|
35
|
+
if (EASE_MAP[str]) return EASE_MAP[str];
|
|
36
|
+
if (CUBIC_BEZIER_RE.test(str)) return str;
|
|
37
|
+
return "linear";
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
// src/figure.ts
|
|
@@ -449,73 +459,69 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
|
|
|
449
459
|
return el;
|
|
450
460
|
}
|
|
451
461
|
|
|
452
|
-
// src/
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
let hex = bg.trim().replace(/^#/, "");
|
|
456
|
-
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
457
|
-
if (hex.length === 6) {
|
|
458
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
459
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
460
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
461
|
-
return 0.299 * r + 0.587 * g + 0.114 * b <= 140;
|
|
462
|
-
}
|
|
463
|
-
const dark = { black: true, "#000": true, "#000000": true };
|
|
464
|
-
return dark[bg.toLowerCase()] ?? false;
|
|
462
|
+
// src/camera.ts
|
|
463
|
+
function freshCameraState() {
|
|
464
|
+
return { x: 0, y: 0, zoom: 1 };
|
|
465
465
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const easing = toEasing(ev.params.ease);
|
|
487
|
-
const baseOpts = {
|
|
488
|
-
delay: delayMs,
|
|
489
|
-
duration: durMs,
|
|
490
|
-
fill: "forwards",
|
|
491
|
-
easing
|
|
492
|
-
};
|
|
493
|
-
if (ev.actor === "camera") {
|
|
494
|
-
buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState);
|
|
495
|
-
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;
|
|
496
486
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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;
|
|
516
521
|
}
|
|
517
|
-
return anims;
|
|
518
522
|
}
|
|
523
|
+
|
|
524
|
+
// src/stage.ts
|
|
519
525
|
function offscreenState(s, direction, sceneWidth, sceneHeight) {
|
|
520
526
|
const out = { ...s };
|
|
521
527
|
switch (direction) {
|
|
@@ -545,114 +551,252 @@ function preInitInlineStyles(ast, actorEls, states, events, txFor) {
|
|
|
545
551
|
const s = states.get(name);
|
|
546
552
|
if (!el || !s) continue;
|
|
547
553
|
const firstEv = firstEventByActor.get(name);
|
|
548
|
-
|
|
549
|
-
if (firstEv
|
|
554
|
+
if (!firstEv) continue;
|
|
555
|
+
if (firstEv.action === "enter") {
|
|
550
556
|
const from = String(firstEv.params.from ?? "left");
|
|
551
|
-
|
|
552
|
-
el.style.transform = txFn(offscreen);
|
|
557
|
+
el.style.transform = txFor(name)(offscreenState(s, from, ast.meta.width, ast.meta.height));
|
|
553
558
|
}
|
|
554
|
-
if (firstEv
|
|
559
|
+
if (firstEv.action === "fade_in" && (def.opacity === void 0 || def.opacity > 0)) {
|
|
555
560
|
el.style.opacity = "0";
|
|
556
561
|
}
|
|
557
562
|
}
|
|
558
563
|
}
|
|
559
|
-
|
|
560
|
-
|
|
564
|
+
|
|
565
|
+
// src/actions/figure.ts
|
|
566
|
+
function sideOf(ctx) {
|
|
567
|
+
return String(ctx.ev.params.side ?? "right") === "left" ? "left" : "right";
|
|
561
568
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
569
|
+
function partEl(ctx, part) {
|
|
570
|
+
const selector = PART_SEL[part];
|
|
571
|
+
return selector ? ctx.el.querySelector(selector) : null;
|
|
564
572
|
}
|
|
565
|
-
function
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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 });
|
|
614
773
|
}
|
|
615
|
-
default:
|
|
616
|
-
break;
|
|
617
774
|
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
request: "#38bdf8",
|
|
621
|
-
response: "#a78bfa",
|
|
622
|
-
emit: "#f59e0b"
|
|
775
|
+
keyframes.push({ transform: tx2(state), offset: 1 });
|
|
776
|
+
anims.push(el.animate(keyframes, { ...baseOpts, easing: "ease-out" }));
|
|
623
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 }
|
|
788
|
+
};
|
|
789
|
+
var DEFAULT_ACTOR_SIZE = { width: 140, height: 42 };
|
|
624
790
|
function actorSizeByType(type) {
|
|
625
|
-
|
|
626
|
-
case "service":
|
|
627
|
-
case "client":
|
|
628
|
-
case "db":
|
|
629
|
-
case "queue":
|
|
630
|
-
return { width: 180, height: 84 };
|
|
631
|
-
case "box":
|
|
632
|
-
return { width: 100, height: 100 };
|
|
633
|
-
case "caption":
|
|
634
|
-
return { width: 260, height: 56 };
|
|
635
|
-
case "figure":
|
|
636
|
-
return { width: 120, height: 170 };
|
|
637
|
-
default:
|
|
638
|
-
return { width: 140, height: 42 };
|
|
639
|
-
}
|
|
791
|
+
return ACTOR_SIZES[type] ?? DEFAULT_ACTOR_SIZE;
|
|
640
792
|
}
|
|
641
793
|
function actorCenter(state, actorType) {
|
|
642
794
|
const { width, height } = actorSizeByType(actorType);
|
|
643
|
-
return {
|
|
644
|
-
x: state.x + width / 2,
|
|
645
|
-
y: state.y + height / 2
|
|
646
|
-
};
|
|
795
|
+
return { x: state.x + width / 2, y: state.y + height / 2 };
|
|
647
796
|
}
|
|
648
797
|
function actorRect(state, actorType) {
|
|
649
798
|
const { width, height } = actorSizeByType(actorType);
|
|
650
|
-
return {
|
|
651
|
-
x1: state.x,
|
|
652
|
-
y1: state.y,
|
|
653
|
-
x2: state.x + width,
|
|
654
|
-
y2: state.y + height
|
|
655
|
-
};
|
|
799
|
+
return { x1: state.x, y1: state.y, x2: state.x + width, y2: state.y + height };
|
|
656
800
|
}
|
|
657
801
|
function inflateRect(rect, pad) {
|
|
658
802
|
return {
|
|
@@ -690,12 +834,14 @@ function countPathIntersections(points, obstacles) {
|
|
|
690
834
|
}
|
|
691
835
|
return hits;
|
|
692
836
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
837
|
+
|
|
838
|
+
// src/geometry/path.ts
|
|
696
839
|
function round1(n) {
|
|
697
840
|
return Math.round(n * 10) / 10;
|
|
698
841
|
}
|
|
842
|
+
function toPathD(points) {
|
|
843
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
|
|
844
|
+
}
|
|
699
845
|
function polylineLength(points) {
|
|
700
846
|
let total = 0;
|
|
701
847
|
for (let i = 0; i < points.length - 1; i++) {
|
|
@@ -704,13 +850,13 @@ function polylineLength(points) {
|
|
|
704
850
|
return Math.max(1, total);
|
|
705
851
|
}
|
|
706
852
|
function pointAtDistance(points, dist) {
|
|
707
|
-
let remain = dist;
|
|
853
|
+
let remain = Math.max(0, dist);
|
|
708
854
|
for (let i = 0; i < points.length - 1; i++) {
|
|
709
855
|
const a = points[i];
|
|
710
856
|
const b = points[i + 1];
|
|
711
857
|
const seg = Math.hypot(b.x - a.x, b.y - a.y);
|
|
712
858
|
if (remain <= seg || i === points.length - 2) {
|
|
713
|
-
const t = seg <= 0 ? 0 : remain / seg;
|
|
859
|
+
const t = seg <= 0 ? 0 : Math.min(1, remain / seg);
|
|
714
860
|
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
715
861
|
}
|
|
716
862
|
remain -= seg;
|
|
@@ -718,11 +864,13 @@ function pointAtDistance(points, dist) {
|
|
|
718
864
|
return points[0];
|
|
719
865
|
}
|
|
720
866
|
function routeFlowPath(sourceName, targetName, sourceState, targetState, states, ast, lane) {
|
|
721
|
-
const
|
|
722
|
-
const
|
|
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);
|
|
723
871
|
const laneShift = lane * 18;
|
|
724
|
-
const sourceCenter = actorCenter(sourceState,
|
|
725
|
-
const targetCenter = actorCenter(targetState,
|
|
872
|
+
const sourceCenter = actorCenter(sourceState, sourceType);
|
|
873
|
+
const targetCenter = actorCenter(targetState, targetType);
|
|
726
874
|
const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
|
|
727
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 };
|
|
728
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 };
|
|
@@ -762,11 +910,22 @@ function routeFlowPath(sourceName, targetName, sourceState, targetState, states,
|
|
|
762
910
|
}
|
|
763
911
|
return best;
|
|
764
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;
|
|
765
924
|
function ensureEdgeLayer(scene) {
|
|
766
|
-
const existing = scene.querySelector(
|
|
925
|
+
const existing = scene.querySelector(`svg[${EDGE_LAYER_ATTR}='1']`);
|
|
767
926
|
if (existing) return existing;
|
|
768
927
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
769
|
-
svg.setAttribute(
|
|
928
|
+
svg.setAttribute(EDGE_LAYER_ATTR, "1");
|
|
770
929
|
Object.assign(svg.style, {
|
|
771
930
|
position: "absolute",
|
|
772
931
|
inset: "0",
|
|
@@ -779,23 +938,19 @@ function ensureEdgeLayer(scene) {
|
|
|
779
938
|
scene.appendChild(svg);
|
|
780
939
|
return svg;
|
|
781
940
|
}
|
|
782
|
-
function
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
ast,
|
|
793
|
-
lane
|
|
794
|
-
);
|
|
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);
|
|
795
951
|
const length = polylineLength(points);
|
|
796
|
-
const midPoint = pointAtDistance(points, length * 0.5);
|
|
797
952
|
const pathD = toPathD(points);
|
|
798
|
-
const
|
|
953
|
+
const stroke = STROKE_BY_ACTION[ev.action] ?? DEFAULT_STROKE;
|
|
799
954
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
800
955
|
group.setAttribute("data-markdy-flow-edge", "1");
|
|
801
956
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
@@ -804,7 +959,7 @@ function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, st
|
|
|
804
959
|
path.setAttribute("stroke", stroke);
|
|
805
960
|
path.setAttribute("stroke-width", "2.5");
|
|
806
961
|
path.setAttribute("data-markdy-flow-action", ev.action);
|
|
807
|
-
path.style.strokeDasharray = isDashed ? "8 6" : `${length}`;
|
|
962
|
+
path.style.strokeDasharray = isDashed(ctx) ? "8 6" : `${length}`;
|
|
808
963
|
path.style.strokeDashoffset = `${length}`;
|
|
809
964
|
group.appendChild(path);
|
|
810
965
|
const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
@@ -814,491 +969,285 @@ function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, st
|
|
|
814
969
|
marker.style.offsetDistance = "0%";
|
|
815
970
|
marker.style.opacity = "0";
|
|
816
971
|
group.appendChild(marker);
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
819
|
-
const
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
group.appendChild(
|
|
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);
|
|
829
984
|
}
|
|
830
|
-
|
|
831
|
-
anims.push(path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts));
|
|
985
|
+
ensureEdgeLayer(scene).appendChild(group);
|
|
832
986
|
anims.push(
|
|
987
|
+
path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts),
|
|
833
988
|
marker.animate(
|
|
834
|
-
[
|
|
989
|
+
[
|
|
990
|
+
{ offsetDistance: "0%", opacity: 1 },
|
|
991
|
+
{ offsetDistance: "100%", opacity: 1 }
|
|
992
|
+
],
|
|
835
993
|
baseOpts
|
|
836
|
-
)
|
|
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
|
+
})
|
|
837
1000
|
);
|
|
838
|
-
|
|
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);
|
|
839
1088
|
anims.push(
|
|
840
|
-
|
|
841
|
-
delay:
|
|
842
|
-
duration:
|
|
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,
|
|
843
1097
|
fill: "forwards"
|
|
844
1098
|
})
|
|
845
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;
|
|
846
1121
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
s.scale = toScale;
|
|
953
|
-
break;
|
|
954
|
-
}
|
|
955
|
-
// ── rotate ──────────────────────────────────────────────────────────────
|
|
956
|
-
case "rotate": {
|
|
957
|
-
const toDeg = typeof ev.params.to === "number" ? ev.params.to : s.rotate;
|
|
958
|
-
anims.push(
|
|
959
|
-
el.animate(
|
|
960
|
-
[{ transform: txFn(s) }, { transform: txFn({ ...s, rotate: toDeg }) }],
|
|
961
|
-
baseOpts
|
|
962
|
-
)
|
|
963
|
-
);
|
|
964
|
-
s.rotate = toDeg;
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
// ── shake ───────────────────────────────────────────────────────────────
|
|
968
|
-
case "shake": {
|
|
969
|
-
const mag = typeof ev.params.intensity === "number" ? ev.params.intensity : 5;
|
|
970
|
-
anims.push(
|
|
971
|
-
el.animate(
|
|
972
|
-
[
|
|
973
|
-
{ transform: txFn(s), offset: 0 },
|
|
974
|
-
{ transform: txFn({ ...s, x: s.x + mag }), offset: 0.2 },
|
|
975
|
-
{ transform: txFn({ ...s, x: s.x - mag }), offset: 0.4 },
|
|
976
|
-
{ transform: txFn({ ...s, x: s.x + mag }), offset: 0.6 },
|
|
977
|
-
{ transform: txFn({ ...s, x: s.x - mag }), offset: 0.8 },
|
|
978
|
-
{ transform: txFn(s), offset: 1 }
|
|
979
|
-
],
|
|
980
|
-
{ ...baseOpts, easing: "linear" }
|
|
981
|
-
)
|
|
982
|
-
);
|
|
983
|
-
break;
|
|
984
|
-
}
|
|
985
|
-
// ── punch ───────────────────────────────────────────────────────────────
|
|
986
|
-
case "punch": {
|
|
987
|
-
const pSide = String(ev.params.side ?? "right");
|
|
988
|
-
const pArmEl = el.querySelector(
|
|
989
|
-
pSide === "left" ? "[data-fig-arm-l]" : "[data-fig-arm-r]"
|
|
990
|
-
);
|
|
991
|
-
if (!pArmEl) break;
|
|
992
|
-
const pRest = readRotation(pArmEl);
|
|
993
|
-
const pExtend = pSide === "left" ? -75 : 75;
|
|
994
|
-
anims.push(
|
|
995
|
-
pArmEl.animate(
|
|
996
|
-
[
|
|
997
|
-
{ transform: `rotate(${pRest}deg)` },
|
|
998
|
-
{ transform: `rotate(${pExtend}deg)`, offset: 0.35 },
|
|
999
|
-
{ transform: `rotate(${pRest}deg)` }
|
|
1000
|
-
],
|
|
1001
|
-
{ ...baseOpts, easing: "ease-in-out", fill: "forwards" }
|
|
1002
|
-
)
|
|
1003
|
-
);
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
// ── kick ────────────────────────────────────────────────────────────────
|
|
1007
|
-
case "kick": {
|
|
1008
|
-
const kSide = String(ev.params.side ?? "right");
|
|
1009
|
-
const kLegEl = el.querySelector(
|
|
1010
|
-
kSide === "left" ? "[data-fig-leg-l]" : "[data-fig-leg-r]"
|
|
1011
|
-
);
|
|
1012
|
-
if (!kLegEl) break;
|
|
1013
|
-
const kRest = readRotation(kLegEl);
|
|
1014
|
-
const kExtend = kSide === "left" ? -100 : 100;
|
|
1015
|
-
anims.push(
|
|
1016
|
-
kLegEl.animate(
|
|
1017
|
-
[
|
|
1018
|
-
{ transform: `rotate(${kRest}deg)` },
|
|
1019
|
-
{ transform: `rotate(${kExtend}deg)`, offset: 0.38 },
|
|
1020
|
-
{ transform: `rotate(${kRest}deg)` }
|
|
1021
|
-
],
|
|
1022
|
-
{ ...baseOpts, easing: "ease-in-out", fill: "forwards" }
|
|
1023
|
-
)
|
|
1024
|
-
);
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
// ── rotate_part ─────────────────────────────────────────────────────────
|
|
1028
|
-
case "rotate_part": {
|
|
1029
|
-
const rpName = String(ev.params.part ?? "");
|
|
1030
|
-
const rpSel = PART_SEL[rpName];
|
|
1031
|
-
if (!rpSel) break;
|
|
1032
|
-
const rpEl = el.querySelector(rpSel);
|
|
1033
|
-
if (!rpEl) break;
|
|
1034
|
-
const rpFrom = readRotation(rpEl);
|
|
1035
|
-
const rpTo = typeof ev.params.to === "number" ? ev.params.to : rpFrom;
|
|
1036
|
-
anims.push(
|
|
1037
|
-
rpEl.animate(
|
|
1038
|
-
[
|
|
1039
|
-
{ transform: `rotate(${rpFrom}deg)` },
|
|
1040
|
-
{ transform: `rotate(${rpTo}deg)` }
|
|
1041
|
-
],
|
|
1042
|
-
{ ...baseOpts, fill: "forwards" }
|
|
1043
|
-
)
|
|
1044
|
-
);
|
|
1045
|
-
rpEl.style.transform = rpEl.style.transform.replace(
|
|
1046
|
-
/rotate\([^)]*\)/,
|
|
1047
|
-
`rotate(${rpTo}deg)`
|
|
1048
|
-
);
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
// ── face ────────────────────────────────────────────────────────────────
|
|
1052
|
-
case "face": {
|
|
1053
|
-
const fEl = el.querySelector("[data-fig-face]");
|
|
1054
|
-
if (!fEl) break;
|
|
1055
|
-
const emoji = String(ev.params.text ?? ev.params._0 ?? "");
|
|
1056
|
-
if (emoji) faceSwaps.push({ timeMs: ev.time * 1e3, el: fEl, emoji });
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
// ── say ─────────────────────────────────────────────────────────────────
|
|
1060
|
-
case "say": {
|
|
1061
|
-
const text = String(ev.params.text ?? "");
|
|
1062
|
-
const inverseScale = 1 / (s.scale || 1);
|
|
1063
|
-
const sceneDark = isSceneDark(scene);
|
|
1064
|
-
const bubbleBg = sceneDark ? "#1e2530" : "white";
|
|
1065
|
-
const bubbleBorder = sceneDark ? "#475569" : "#222";
|
|
1066
|
-
const bubbleText = sceneDark ? "#e2e8f0" : "#222";
|
|
1067
|
-
const bubbleShadow = sceneDark ? "0 2px 8px rgba(0,0,0,0.35)" : "0 2px 8px rgba(0,0,0,0.12)";
|
|
1068
|
-
const bubble = document.createElement("div");
|
|
1069
|
-
bubble.textContent = text;
|
|
1070
|
-
bubble.style.opacity = "0";
|
|
1071
|
-
Object.assign(bubble.style, {
|
|
1072
|
-
position: "absolute",
|
|
1073
|
-
bottom: "calc(100% + 10px)",
|
|
1074
|
-
left: "50%",
|
|
1075
|
-
transform: `translateX(-50%) scale(${inverseScale})`,
|
|
1076
|
-
transformOrigin: "center bottom",
|
|
1077
|
-
background: bubbleBg,
|
|
1078
|
-
border: `2px solid ${bubbleBorder}`,
|
|
1079
|
-
color: bubbleText,
|
|
1080
|
-
borderRadius: "12px",
|
|
1081
|
-
padding: "6px 14px",
|
|
1082
|
-
fontFamily: "system-ui, sans-serif",
|
|
1083
|
-
fontSize: "15px",
|
|
1084
|
-
lineHeight: "1.3",
|
|
1085
|
-
whiteSpace: "nowrap",
|
|
1086
|
-
maxWidth: "220px",
|
|
1087
|
-
overflow: "hidden",
|
|
1088
|
-
textOverflow: "ellipsis",
|
|
1089
|
-
pointerEvents: "none",
|
|
1090
|
-
zIndex: "10",
|
|
1091
|
-
boxShadow: bubbleShadow
|
|
1092
|
-
});
|
|
1093
|
-
const tail = document.createElement("span");
|
|
1094
|
-
Object.assign(tail.style, {
|
|
1095
|
-
position: "absolute",
|
|
1096
|
-
bottom: "-10px",
|
|
1097
|
-
left: "50%",
|
|
1098
|
-
transform: "translateX(-50%)",
|
|
1099
|
-
width: "0",
|
|
1100
|
-
height: "0",
|
|
1101
|
-
borderLeft: "7px solid transparent",
|
|
1102
|
-
borderRight: "7px solid transparent",
|
|
1103
|
-
borderTop: `10px solid ${bubbleBorder}`
|
|
1104
|
-
});
|
|
1105
|
-
bubble.appendChild(tail);
|
|
1106
|
-
el.style.overflow = "visible";
|
|
1107
|
-
el.appendChild(bubble);
|
|
1108
|
-
const fadeDur = Math.min(200, durMs * 0.15);
|
|
1109
|
-
anims.push(
|
|
1110
|
-
bubble.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
1111
|
-
delay: delayMs,
|
|
1112
|
-
duration: fadeDur,
|
|
1113
|
-
fill: "forwards"
|
|
1114
|
-
}),
|
|
1115
|
-
bubble.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
1116
|
-
delay: delayMs + durMs - fadeDur,
|
|
1117
|
-
duration: fadeDur,
|
|
1118
|
-
fill: "forwards"
|
|
1119
|
-
})
|
|
1120
|
-
);
|
|
1121
|
-
break;
|
|
1122
|
-
}
|
|
1123
|
-
// ── pose ────────────────────────────────────────────────────────────────
|
|
1124
|
-
// Sets multiple body parts to target angles simultaneously.
|
|
1125
|
-
// Usage: @1.0: hero.pose(arm_left=45, arm_right=-45, leg_left=10, dur=0.4)
|
|
1126
|
-
case "pose": {
|
|
1127
|
-
const poseParts = ["arm_left", "arm_right", "leg_left", "leg_right", "head", "body"];
|
|
1128
|
-
for (const partName of poseParts) {
|
|
1129
|
-
if (typeof ev.params[partName] !== "number") continue;
|
|
1130
|
-
const pSel = PART_SEL[partName];
|
|
1131
|
-
if (!pSel) continue;
|
|
1132
|
-
const pEl = el.querySelector(pSel);
|
|
1133
|
-
if (!pEl) continue;
|
|
1134
|
-
const fromDeg = readRotation(pEl);
|
|
1135
|
-
const toDeg = ev.params[partName];
|
|
1136
|
-
anims.push(
|
|
1137
|
-
pEl.animate(
|
|
1138
|
-
[
|
|
1139
|
-
{ transform: `rotate(${fromDeg}deg)` },
|
|
1140
|
-
{ transform: `rotate(${toDeg}deg)` }
|
|
1141
|
-
],
|
|
1142
|
-
{ ...baseOpts, fill: "forwards" }
|
|
1143
|
-
)
|
|
1144
|
-
);
|
|
1145
|
-
pEl.style.transform = pEl.style.transform.replace(
|
|
1146
|
-
/rotate\([^)]*\)/,
|
|
1147
|
-
`rotate(${toDeg}deg)`
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
|
-
break;
|
|
1151
|
-
}
|
|
1152
|
-
// ── wave ────────────────────────────────────────────────────────────────
|
|
1153
|
-
// Built-in wave gesture: raises arm, oscillates, returns.
|
|
1154
|
-
// Usage: @2.0: hero.wave(side=right, dur=0.8)
|
|
1155
|
-
case "wave": {
|
|
1156
|
-
const wSide = String(ev.params.side ?? "right");
|
|
1157
|
-
const wArmEl = el.querySelector(
|
|
1158
|
-
wSide === "left" ? "[data-fig-arm-l]" : "[data-fig-arm-r]"
|
|
1159
|
-
);
|
|
1160
|
-
if (!wArmEl) break;
|
|
1161
|
-
const wRest = readRotation(wArmEl);
|
|
1162
|
-
const wUp = wSide === "left" ? 70 : -70;
|
|
1163
|
-
const wMid1 = wSide === "left" ? 50 : -50;
|
|
1164
|
-
const wMid2 = wSide === "left" ? 70 : -70;
|
|
1165
|
-
anims.push(
|
|
1166
|
-
wArmEl.animate(
|
|
1167
|
-
[
|
|
1168
|
-
{ transform: `rotate(${wRest}deg)`, offset: 0 },
|
|
1169
|
-
{ transform: `rotate(${wUp}deg)`, offset: 0.2 },
|
|
1170
|
-
{ transform: `rotate(${wMid1}deg)`, offset: 0.4 },
|
|
1171
|
-
{ transform: `rotate(${wMid2}deg)`, offset: 0.55 },
|
|
1172
|
-
{ transform: `rotate(${wMid1}deg)`, offset: 0.7 },
|
|
1173
|
-
{ transform: `rotate(${wMid2}deg)`, offset: 0.85 },
|
|
1174
|
-
{ transform: `rotate(${wRest}deg)`, offset: 1 }
|
|
1175
|
-
],
|
|
1176
|
-
{ ...baseOpts, easing: "ease-in-out", fill: "forwards" }
|
|
1177
|
-
)
|
|
1178
|
-
);
|
|
1179
|
-
break;
|
|
1180
|
-
}
|
|
1181
|
-
// ── jump ────────────────────────────────────────────────────────────────
|
|
1182
|
-
// Jumps the actor up with squash/stretch effect.
|
|
1183
|
-
// Usage: @3.0: hero.jump(height=30, dur=0.5)
|
|
1184
|
-
case "jump": {
|
|
1185
|
-
const jHeight = typeof ev.params.height === "number" ? ev.params.height : 30;
|
|
1186
|
-
anims.push(
|
|
1187
|
-
el.animate(
|
|
1188
|
-
[
|
|
1189
|
-
{ transform: txFn(s), offset: 0 },
|
|
1190
|
-
{ transform: txFn({ ...s, scale: s.scale * 0.9 }), offset: 0.1 },
|
|
1191
|
-
{ transform: txFn({ ...s, y: s.y - jHeight, scale: s.scale * 1.1 }), offset: 0.45 },
|
|
1192
|
-
{ transform: txFn({ ...s, y: s.y - jHeight * 0.3, scale: s.scale * 1.05 }), offset: 0.7 },
|
|
1193
|
-
{ transform: txFn({ ...s, scale: s.scale * 0.92 }), offset: 0.88 },
|
|
1194
|
-
{ transform: txFn(s), offset: 1 }
|
|
1195
|
-
],
|
|
1196
|
-
{ ...baseOpts, easing: "ease-in-out" }
|
|
1197
|
-
)
|
|
1198
|
-
);
|
|
1199
|
-
break;
|
|
1200
|
-
}
|
|
1201
|
-
// ── nod ─────────────────────────────────────────────────────────────────
|
|
1202
|
-
// Nods the head down and back up. Figure actors only.
|
|
1203
|
-
// Usage: @2.0: hero.nod(dur=0.4)
|
|
1204
|
-
case "nod": {
|
|
1205
|
-
const nHeadEl = el.querySelector("[data-fig-head]");
|
|
1206
|
-
if (!nHeadEl) break;
|
|
1207
|
-
const nRest = readRotation(nHeadEl);
|
|
1208
|
-
const nDown = 15;
|
|
1209
|
-
anims.push(
|
|
1210
|
-
nHeadEl.animate(
|
|
1211
|
-
[
|
|
1212
|
-
{ transform: `rotate(${nRest}deg)`, offset: 0 },
|
|
1213
|
-
{ transform: `rotate(${nDown}deg)`, offset: 0.35 },
|
|
1214
|
-
{ transform: `rotate(${nRest}deg)`, offset: 0.65 },
|
|
1215
|
-
{ transform: `rotate(${nDown}deg)`, offset: 0.8 },
|
|
1216
|
-
{ transform: `rotate(${nRest}deg)`, offset: 1 }
|
|
1217
|
-
],
|
|
1218
|
-
{ ...baseOpts, easing: "ease-in-out", fill: "forwards" }
|
|
1219
|
-
)
|
|
1220
|
-
);
|
|
1221
|
-
break;
|
|
1222
|
-
}
|
|
1223
|
-
// ── bounce ──────────────────────────────────────────────────────────────
|
|
1224
|
-
// Bounces the actor vertically with diminishing amplitude.
|
|
1225
|
-
// Usage: @1.0: hero.bounce(intensity=15, count=3, dur=0.6)
|
|
1226
|
-
case "bounce": {
|
|
1227
|
-
const bIntensity = typeof ev.params.intensity === "number" ? ev.params.intensity : 15;
|
|
1228
|
-
const bCount = typeof ev.params.count === "number" ? ev.params.count : 3;
|
|
1229
|
-
const keyframes = [{ transform: txFn(s), offset: 0 }];
|
|
1230
|
-
for (let bi = 0; bi < bCount; bi++) {
|
|
1231
|
-
const amp = bIntensity * Math.pow(0.55, bi);
|
|
1232
|
-
const baseOffset = (bi + 0.5) / (bCount + 0.5);
|
|
1233
|
-
const peakOffset = Math.min(baseOffset, 0.98);
|
|
1234
|
-
const valleyOffset = Math.min(baseOffset + 0.25 / (bCount + 0.5), 0.99);
|
|
1235
|
-
keyframes.push({
|
|
1236
|
-
transform: txFn({ ...s, y: s.y - amp }),
|
|
1237
|
-
offset: peakOffset
|
|
1238
|
-
});
|
|
1239
|
-
if (bi < bCount - 1) {
|
|
1240
|
-
keyframes.push({
|
|
1241
|
-
transform: txFn(s),
|
|
1242
|
-
offset: valleyOffset
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
keyframes.push({ transform: txFn(s), offset: 1 });
|
|
1247
|
-
anims.push(
|
|
1248
|
-
el.animate(keyframes, { ...baseOpts, easing: "ease-out" })
|
|
1249
|
-
);
|
|
1250
|
-
break;
|
|
1251
|
-
}
|
|
1252
|
-
// ── throw ───────────────────────────────────────────────────────────────
|
|
1253
|
-
case "throw": {
|
|
1254
|
-
const assetName = String(ev.params.asset ?? "");
|
|
1255
|
-
const targetActorName = String(ev.params.to ?? "");
|
|
1256
|
-
const targetState = states.get(targetActorName);
|
|
1257
|
-
const assetDef = ast.assets[assetName];
|
|
1258
|
-
if (!assetDef || !targetState) break;
|
|
1259
|
-
let projectile;
|
|
1260
|
-
if (assetDef.type === "image") {
|
|
1261
|
-
const img = document.createElement("img");
|
|
1262
|
-
img.src = assetOverrides[assetName] ?? assetDef.value;
|
|
1263
|
-
img.alt = assetName;
|
|
1264
|
-
img.setAttribute("draggable", "false");
|
|
1265
|
-
img.style.width = "32px";
|
|
1266
|
-
img.style.height = "32px";
|
|
1267
|
-
projectile = img;
|
|
1268
|
-
} else {
|
|
1269
|
-
const span = document.createElement("span");
|
|
1270
|
-
span.className = "iconify";
|
|
1271
|
-
span.dataset.icon = assetDef.value;
|
|
1272
|
-
span.style.fontSize = "32px";
|
|
1273
|
-
span.style.lineHeight = "1";
|
|
1274
|
-
span.style.display = "inline-block";
|
|
1275
|
-
projectile = span;
|
|
1276
|
-
}
|
|
1277
|
-
Object.assign(projectile.style, {
|
|
1278
|
-
position: "absolute",
|
|
1279
|
-
left: "0",
|
|
1280
|
-
top: "0",
|
|
1281
|
-
pointerEvents: "none",
|
|
1282
|
-
zIndex: "9",
|
|
1283
|
-
opacity: "0"
|
|
1284
|
-
});
|
|
1285
|
-
scene.appendChild(projectile);
|
|
1286
|
-
const throwAnim = projectile.animate(
|
|
1287
|
-
[
|
|
1288
|
-
{ transform: tx(s), opacity: 1 },
|
|
1289
|
-
{ transform: tx(targetState), opacity: 0 }
|
|
1290
|
-
],
|
|
1291
|
-
{ ...baseOpts, easing: "ease-in" }
|
|
1292
|
-
);
|
|
1293
|
-
throwAnim.addEventListener("finish", () => {
|
|
1294
|
-
if (projectile.parentNode === scene) scene.removeChild(projectile);
|
|
1295
|
-
});
|
|
1296
|
-
anims.push(throwAnim);
|
|
1297
|
-
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;
|
|
1298
1227
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
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);
|
|
1301
1249
|
}
|
|
1250
|
+
return anims;
|
|
1302
1251
|
}
|
|
1303
1252
|
|
|
1304
1253
|
// src/player.ts
|
|
@@ -1328,10 +1277,13 @@ function createPlayer(opts) {
|
|
|
1328
1277
|
loop = true,
|
|
1329
1278
|
copyright = true,
|
|
1330
1279
|
progressBar = true,
|
|
1331
|
-
onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message} (${w.kind})`)
|
|
1280
|
+
onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message} (${w.kind})`),
|
|
1281
|
+
onTimeUpdate,
|
|
1282
|
+
onPlayStateChange
|
|
1332
1283
|
} = opts;
|
|
1333
1284
|
const ast = parse(code, imports ? { imports } : void 0);
|
|
1334
1285
|
const totalDurationMs = (ast.meta.duration ?? 0) * 1e3;
|
|
1286
|
+
const durationSeconds = totalDurationMs / 1e3;
|
|
1335
1287
|
for (const w of ast.warnings) onWarning(w);
|
|
1336
1288
|
const viewport = document.createElement("div");
|
|
1337
1289
|
Object.assign(viewport.style, {
|
|
@@ -1456,6 +1408,7 @@ function createPlayer(opts) {
|
|
|
1456
1408
|
anim.currentTime = sceneMs;
|
|
1457
1409
|
}
|
|
1458
1410
|
applyFaceSwaps();
|
|
1411
|
+
onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
|
|
1459
1412
|
}
|
|
1460
1413
|
function applyFaceSwaps() {
|
|
1461
1414
|
if (faceSwaps.length === 0) return;
|
|
@@ -1489,6 +1442,7 @@ function createPlayer(opts) {
|
|
|
1489
1442
|
sceneMs = totalDurationMs;
|
|
1490
1443
|
applyCurrentTime();
|
|
1491
1444
|
isPlaying = false;
|
|
1445
|
+
onPlayStateChange?.(false);
|
|
1492
1446
|
lastRafTs = null;
|
|
1493
1447
|
rafId = null;
|
|
1494
1448
|
return;
|
|
@@ -1502,12 +1456,14 @@ function createPlayer(opts) {
|
|
|
1502
1456
|
play() {
|
|
1503
1457
|
if (isPlaying) return;
|
|
1504
1458
|
isPlaying = true;
|
|
1459
|
+
onPlayStateChange?.(true);
|
|
1505
1460
|
lastRafTs = null;
|
|
1506
1461
|
rafId = requestAnimationFrame(rafTick);
|
|
1507
1462
|
},
|
|
1508
1463
|
pause() {
|
|
1509
1464
|
if (!isPlaying) return;
|
|
1510
1465
|
isPlaying = false;
|
|
1466
|
+
onPlayStateChange?.(false);
|
|
1511
1467
|
if (rafId !== null) {
|
|
1512
1468
|
cancelAnimationFrame(rafId);
|
|
1513
1469
|
rafId = null;
|
|
@@ -1515,10 +1471,28 @@ function createPlayer(opts) {
|
|
|
1515
1471
|
lastRafTs = null;
|
|
1516
1472
|
},
|
|
1517
1473
|
seek(seconds) {
|
|
1518
|
-
|
|
1474
|
+
const nextMs = seconds * 1e3;
|
|
1475
|
+
sceneMs = totalDurationMs > 0 ? Math.min(Math.max(nextMs, 0), totalDurationMs) : Math.max(nextMs, 0);
|
|
1519
1476
|
applyCurrentTime();
|
|
1520
1477
|
if (totalDurationMs > 0) updateProgressBar(sceneMs / totalDurationMs);
|
|
1521
1478
|
},
|
|
1479
|
+
currentTime() {
|
|
1480
|
+
return sceneMs / 1e3;
|
|
1481
|
+
},
|
|
1482
|
+
duration() {
|
|
1483
|
+
return durationSeconds;
|
|
1484
|
+
},
|
|
1485
|
+
isPlaying() {
|
|
1486
|
+
return isPlaying;
|
|
1487
|
+
},
|
|
1488
|
+
chapters() {
|
|
1489
|
+
return ast.chapters;
|
|
1490
|
+
},
|
|
1491
|
+
seekToChapter(name) {
|
|
1492
|
+
const chapter = ast.chapters.find((c) => c.name === name);
|
|
1493
|
+
if (!chapter) return;
|
|
1494
|
+
player.seek(chapter.startTime);
|
|
1495
|
+
},
|
|
1522
1496
|
destroy() {
|
|
1523
1497
|
player.pause();
|
|
1524
1498
|
for (const anim of allAnims) anim.cancel();
|