@markdy/renderer-dom 0.7.10 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +328 -2
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -367,6 +367,66 @@ function createActorEl(name, def, assetDefs, assetOverrides) {
367
367
  el = createFigureEl(def);
368
368
  break;
369
369
  }
370
+ case "service":
371
+ case "client":
372
+ case "db":
373
+ case "queue": {
374
+ const card = document.createElement("div");
375
+ const label = document.createElement("div");
376
+ label.textContent = def.args[0] ?? "";
377
+ Object.assign(label.style, {
378
+ fontFamily: "system-ui, -apple-system, sans-serif",
379
+ fontSize: `${def.size ?? 16}px`,
380
+ fontWeight: "600",
381
+ color: "#e2e8f0",
382
+ textAlign: "center",
383
+ lineHeight: "1.2",
384
+ pointerEvents: "none",
385
+ whiteSpace: "nowrap",
386
+ overflow: "hidden",
387
+ textOverflow: "ellipsis",
388
+ maxWidth: "100%"
389
+ });
390
+ Object.assign(card.style, {
391
+ width: "180px",
392
+ height: "84px",
393
+ boxSizing: "border-box",
394
+ border: "1px solid #475569",
395
+ background: "#0f172a",
396
+ display: "grid",
397
+ placeItems: "center",
398
+ padding: "10px 12px"
399
+ });
400
+ if (def.type === "service") {
401
+ card.style.borderRadius = "12px";
402
+ card.style.background = "linear-gradient(180deg, #1e293b 0%, #0f172a 100%)";
403
+ } else if (def.type === "client") {
404
+ const bar = document.createElement("div");
405
+ Object.assign(bar.style, {
406
+ position: "absolute",
407
+ top: "8px",
408
+ left: "8px",
409
+ right: "8px",
410
+ height: "8px",
411
+ borderRadius: "6px",
412
+ background: "#334155"
413
+ });
414
+ card.style.position = "relative";
415
+ card.style.borderRadius = "10px";
416
+ card.style.paddingTop = "20px";
417
+ card.appendChild(bar);
418
+ } else if (def.type === "db") {
419
+ card.style.borderRadius = "50% / 14%";
420
+ card.style.background = "linear-gradient(180deg, #334155 0%, #0f172a 70%)";
421
+ card.style.boxShadow = "inset 0 10px 0 rgba(226, 232, 240, 0.12)";
422
+ } else if (def.type === "queue") {
423
+ card.style.borderRadius = "8px";
424
+ card.style.boxShadow = "-6px -6px 0 rgba(30, 41, 59, 0.9), -12px -12px 0 rgba(15, 23, 42, 0.9)";
425
+ }
426
+ card.appendChild(label);
427
+ el = card;
428
+ break;
429
+ }
370
430
  default: {
371
431
  const div = document.createElement("div");
372
432
  div.style.width = "100px";
@@ -437,7 +497,22 @@ function buildAnimations(ast, actorEls, scene, assetOverrides, faceSwaps) {
437
497
  const el = actorEls.get(ev.actor);
438
498
  const s = states.get(ev.actor);
439
499
  if (!el || !s) continue;
440
- buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims, txFor(ev.actor));
500
+ buildAction(
501
+ ev,
502
+ el,
503
+ s,
504
+ baseOpts,
505
+ delayMs,
506
+ durMs,
507
+ ast,
508
+ states,
509
+ actorEls,
510
+ scene,
511
+ assetOverrides,
512
+ faceSwaps,
513
+ anims,
514
+ txFor(ev.actor)
515
+ );
441
516
  }
442
517
  return anims;
443
518
  }
@@ -541,8 +616,259 @@ function buildCameraAction(ev, scene, ast, baseOpts, anims, cameraState) {
541
616
  break;
542
617
  }
543
618
  }
544
- function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, scene, assetOverrides, faceSwaps, anims, txFn) {
619
+ var FLOW_STROKE_BY_ACTION = {
620
+ request: "#38bdf8",
621
+ response: "#a78bfa",
622
+ emit: "#f59e0b"
623
+ };
624
+ function actorSizeByType(type) {
625
+ switch (type) {
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
+ }
640
+ }
641
+ function actorCenter(state, actorType) {
642
+ const { width, height } = actorSizeByType(actorType);
643
+ return {
644
+ x: state.x + width / 2,
645
+ y: state.y + height / 2
646
+ };
647
+ }
648
+ function actorRect(state, actorType) {
649
+ 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
+ };
656
+ }
657
+ function inflateRect(rect, pad) {
658
+ return {
659
+ x1: rect.x1 - pad,
660
+ y1: rect.y1 - pad,
661
+ x2: rect.x2 + pad,
662
+ y2: rect.y2 + pad
663
+ };
664
+ }
665
+ function segmentIntersectsRect(a, b, rect) {
666
+ const minX = Math.min(a.x, b.x);
667
+ const maxX = Math.max(a.x, b.x);
668
+ const minY = Math.min(a.y, b.y);
669
+ const maxY = Math.max(a.y, b.y);
670
+ const horizontal = Math.abs(a.y - b.y) < 1e-3;
671
+ const vertical = Math.abs(a.x - b.x) < 1e-3;
672
+ if (horizontal) {
673
+ const yHit = a.y > rect.y1 && a.y < rect.y2;
674
+ const xOverlap = maxX > rect.x1 && minX < rect.x2;
675
+ return yHit && xOverlap;
676
+ }
677
+ if (vertical) {
678
+ const xHit = a.x > rect.x1 && a.x < rect.x2;
679
+ const yOverlap = maxY > rect.y1 && minY < rect.y2;
680
+ return xHit && yOverlap;
681
+ }
682
+ return false;
683
+ }
684
+ function countPathIntersections(points, obstacles) {
685
+ let hits = 0;
686
+ for (let i = 0; i < points.length - 1; i++) {
687
+ for (const obstacle of obstacles) {
688
+ if (segmentIntersectsRect(points[i], points[i + 1], obstacle)) hits++;
689
+ }
690
+ }
691
+ return hits;
692
+ }
693
+ function toPathD(points) {
694
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${round1(p.x)} ${round1(p.y)}`).join(" ");
695
+ }
696
+ function round1(n) {
697
+ return Math.round(n * 10) / 10;
698
+ }
699
+ function polylineLength(points) {
700
+ let total = 0;
701
+ for (let i = 0; i < points.length - 1; i++) {
702
+ total += Math.hypot(points[i + 1].x - points[i].x, points[i + 1].y - points[i].y);
703
+ }
704
+ return Math.max(1, total);
705
+ }
706
+ function pointAtDistance(points, dist) {
707
+ let remain = dist;
708
+ for (let i = 0; i < points.length - 1; i++) {
709
+ const a = points[i];
710
+ const b = points[i + 1];
711
+ const seg = Math.hypot(b.x - a.x, b.y - a.y);
712
+ if (remain <= seg || i === points.length - 2) {
713
+ const t = seg <= 0 ? 0 : remain / seg;
714
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
715
+ }
716
+ remain -= seg;
717
+ }
718
+ return points[0];
719
+ }
720
+ function routeFlowPath(sourceName, targetName, sourceState, targetState, states, ast, lane) {
721
+ const sourceRect = actorRect(sourceState, ast.actors[sourceName]?.type ?? "box");
722
+ const targetRect = actorRect(targetState, ast.actors[targetName]?.type ?? "box");
723
+ const laneShift = lane * 18;
724
+ const sourceCenter = actorCenter(sourceState, ast.actors[sourceName]?.type ?? "box");
725
+ const targetCenter = actorCenter(targetState, ast.actors[targetName]?.type ?? "box");
726
+ const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
727
+ 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
+ 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 };
729
+ const obstacles = [];
730
+ for (const [name, state] of states.entries()) {
731
+ if (name === sourceName || name === targetName) continue;
732
+ const type = ast.actors[name]?.type ?? "box";
733
+ obstacles.push(inflateRect(actorRect(state, type), 8));
734
+ }
735
+ const candidates = [];
736
+ if (Math.abs(source.y - target.y) < 1e-3 || Math.abs(source.x - target.x) < 1e-3) {
737
+ candidates.push([source, target]);
738
+ }
739
+ const midX = round1((source.x + target.x) / 2 + laneShift);
740
+ const midY = round1((source.y + target.y) / 2 + laneShift);
741
+ candidates.push(
742
+ [source, { x: midX, y: source.y }, { x: midX, y: target.y }, target],
743
+ [source, { x: source.x, y: midY }, { x: target.x, y: midY }, target]
744
+ );
745
+ const minObstacleY = obstacles.length ? Math.min(...obstacles.map((o) => o.y1)) : Math.min(source.y, target.y);
746
+ const maxObstacleY = obstacles.length ? Math.max(...obstacles.map((o) => o.y2)) : Math.max(source.y, target.y);
747
+ const topLane = Math.max(16, minObstacleY - 24 - lane * 12);
748
+ const bottomLane = Math.min(ast.meta.height - 16, maxObstacleY + 24 + lane * 12);
749
+ candidates.push(
750
+ [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
751
+ [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
752
+ );
753
+ let best = candidates[0];
754
+ let bestHits = Number.POSITIVE_INFINITY;
755
+ for (const candidate of candidates) {
756
+ const hits = countPathIntersections(candidate, obstacles);
757
+ if (hits < bestHits) {
758
+ best = candidate;
759
+ bestHits = hits;
760
+ if (hits === 0) break;
761
+ }
762
+ }
763
+ return best;
764
+ }
765
+ function ensureEdgeLayer(scene) {
766
+ const existing = scene.querySelector("svg[data-markdy-edge-layer='1']");
767
+ if (existing) return existing;
768
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
769
+ svg.setAttribute("data-markdy-edge-layer", "1");
770
+ Object.assign(svg.style, {
771
+ position: "absolute",
772
+ inset: "0",
773
+ width: "100%",
774
+ height: "100%",
775
+ overflow: "visible",
776
+ pointerEvents: "none",
777
+ zIndex: "95"
778
+ });
779
+ scene.appendChild(svg);
780
+ return svg;
781
+ }
782
+ function renderFlowEdge(ev, sourceName, targetName, sourceState, targetState, states, ast, lane, scene, baseOpts, anims) {
783
+ const styleToken = String(ev.params.style ?? "");
784
+ const isDashed = styleToken === "dashed" || styleToken === "fire_and_forget" || ev.action === "response";
785
+ const stroke = FLOW_STROKE_BY_ACTION[ev.action] ?? "#38bdf8";
786
+ const points = routeFlowPath(
787
+ sourceName,
788
+ targetName,
789
+ sourceState,
790
+ targetState,
791
+ states,
792
+ ast,
793
+ lane
794
+ );
795
+ const length = polylineLength(points);
796
+ const midPoint = pointAtDistance(points, length * 0.5);
797
+ const pathD = toPathD(points);
798
+ const svg = ensureEdgeLayer(scene);
799
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
800
+ group.setAttribute("data-markdy-flow-edge", "1");
801
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
802
+ path.setAttribute("d", pathD);
803
+ path.setAttribute("fill", "none");
804
+ path.setAttribute("stroke", stroke);
805
+ path.setAttribute("stroke-width", "2.5");
806
+ path.setAttribute("data-markdy-flow-action", ev.action);
807
+ path.style.strokeDasharray = isDashed ? "8 6" : `${length}`;
808
+ path.style.strokeDashoffset = `${length}`;
809
+ group.appendChild(path);
810
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
811
+ marker.setAttribute("r", "3");
812
+ marker.setAttribute("fill", stroke);
813
+ marker.style.offsetPath = `path('${pathD}')`;
814
+ marker.style.offsetDistance = "0%";
815
+ marker.style.opacity = "0";
816
+ group.appendChild(marker);
817
+ const labelRaw = String(ev.params.label ?? "");
818
+ if (labelRaw) {
819
+ const label = labelRaw.length > 28 ? `${labelRaw.slice(0, 27)}\u2026` : labelRaw;
820
+ const labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
821
+ labelEl.setAttribute("x", `${round1(midPoint.x)}`);
822
+ labelEl.setAttribute("y", `${round1(midPoint.y - 8)}`);
823
+ labelEl.setAttribute("text-anchor", "middle");
824
+ labelEl.setAttribute("font-size", "12");
825
+ labelEl.setAttribute("fill", "#cbd5e1");
826
+ labelEl.textContent = label;
827
+ labelEl.setAttribute("data-full-label", labelRaw);
828
+ group.appendChild(labelEl);
829
+ }
830
+ svg.appendChild(group);
831
+ anims.push(path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], baseOpts));
832
+ anims.push(
833
+ marker.animate(
834
+ [{ offsetDistance: "0%", opacity: 1 }, { offsetDistance: "100%", opacity: 1 }],
835
+ baseOpts
836
+ )
837
+ );
838
+ const fadeOutDelay = Number(baseOpts.delay ?? 0) + Number(baseOpts.duration ?? 0);
839
+ anims.push(
840
+ group.animate([{ opacity: 1 }, { opacity: 0 }], {
841
+ delay: fadeOutDelay,
842
+ duration: 140,
843
+ fill: "forwards"
844
+ })
845
+ );
846
+ }
847
+ function buildAction(ev, el, s, baseOpts, delayMs, durMs, ast, states, _actorEls, scene, assetOverrides, faceSwaps, anims, txFn) {
545
848
  switch (ev.action) {
849
+ case "request":
850
+ case "response":
851
+ case "emit": {
852
+ const targetActorName = String(ev.params.to ?? "");
853
+ if (!targetActorName) break;
854
+ const targetState = states.get(targetActorName);
855
+ if (!targetState) break;
856
+ const lane = ev.line % 5 - 2;
857
+ renderFlowEdge(
858
+ ev,
859
+ ev.actor,
860
+ targetActorName,
861
+ s,
862
+ targetState,
863
+ states,
864
+ ast,
865
+ lane,
866
+ scene,
867
+ baseOpts,
868
+ anims
869
+ );
870
+ break;
871
+ }
546
872
  // ── move ────────────────────────────────────────────────────────────────
547
873
  case "move": {
548
874
  const toArr = ev.params.to;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "Web Animations API renderer for MarkdyScript scenes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/core": "0.7.10"
46
+ "@markdy/core": "0.7.12"
47
47
  },
48
48
  "devDependencies": {
49
49
  "jsdom": "^29.1.1",