@markdy/renderer-dom 1.0.10 → 1.0.11
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 +3 -2
- package/dist/chunk-VIZHA7GI.js +99 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +294 -148
- package/dist/svg-exporter-7FW6RIP6.js +10 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -77,8 +77,9 @@ diagram.destroy(); // clean up DOM + cancel animations
|
|
|
77
77
|
| `sceneBoundaryProgress` | `boolean \| string` | `true` | Show boundary progress bar, or pass a custom color/gradient string |
|
|
78
78
|
| `progressColor` | `string` | `plan.meta.progressColor ?? "rainbow"` | Custom progress bar color (e.g. `"#3b82f6"`) or gradient (e.g. `"#ec4899, #8b5cf6"`) |
|
|
79
79
|
| `playbackRate` | `number` | `plan.meta.playbackRate ?? 1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
|
|
80
|
-
| `interactiveViewport` | `boolean` | `controls \|\|
|
|
81
|
-
| `controls` | `boolean` | `
|
|
80
|
+
| `interactiveViewport` | `boolean` | `controls \|\| player.interaction` | Enable wheel zoom and drag pan on the rendered viewport |
|
|
81
|
+
| `controls` | `boolean` | `player.controls` | Show the footer toolbar (play, prev/next beat, restart, seek, speed, fit, reset view, SVG, share); also enables viewport interaction |
|
|
82
|
+
| `shareUrl` | `string` | Markdy playground | Base URL used by the Share control when building `#code=` links |
|
|
82
83
|
| `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
|
|
83
84
|
| `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
|
|
84
85
|
| `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/export/svg-exporter.ts
|
|
2
|
+
function copyRenderedStyles(source, clone) {
|
|
3
|
+
if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
|
|
4
|
+
const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
5
|
+
const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
6
|
+
for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
|
|
7
|
+
const computed = window.getComputedStyle(sourceElements[index]);
|
|
8
|
+
const target = cloneElements[index].style;
|
|
9
|
+
for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
|
|
10
|
+
const property = computed.item(propertyIndex);
|
|
11
|
+
target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function normalizeExportViewport(scene) {
|
|
16
|
+
scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
|
|
17
|
+
viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
|
|
18
|
+
viewportTransform.style.transformOrigin = "0 0";
|
|
19
|
+
viewportTransform.style.willChange = "auto";
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function getDiagramSceneElement(containerEl) {
|
|
23
|
+
const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
|
|
24
|
+
if (!sceneEl) throw new Error("No Markdy scene element found in container");
|
|
25
|
+
return sceneEl;
|
|
26
|
+
}
|
|
27
|
+
function prepareHtmlSceneForExport(sceneEl, options = {}) {
|
|
28
|
+
const clonedScene = sceneEl.cloneNode(true);
|
|
29
|
+
copyRenderedStyles(sceneEl, clonedScene);
|
|
30
|
+
normalizeExportViewport(clonedScene);
|
|
31
|
+
const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
|
|
32
|
+
const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
|
|
33
|
+
let width = parseFloat(widthStr);
|
|
34
|
+
let height = parseFloat(heightStr);
|
|
35
|
+
if (isNaN(width)) width = 800;
|
|
36
|
+
if (isNaN(height)) height = 400;
|
|
37
|
+
const scale = options.scale || 1;
|
|
38
|
+
const scaledWidth = width * scale;
|
|
39
|
+
const scaledHeight = height * scale;
|
|
40
|
+
clonedScene.style.transform = `scale(${scale})`;
|
|
41
|
+
clonedScene.style.transformOrigin = "0 0";
|
|
42
|
+
clonedScene.style.position = "relative";
|
|
43
|
+
clonedScene.style.left = "0px";
|
|
44
|
+
clonedScene.style.top = "0px";
|
|
45
|
+
clonedScene.style.margin = "0";
|
|
46
|
+
clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
47
|
+
if (options.transparentBackground) {
|
|
48
|
+
clonedScene.style.background = "transparent";
|
|
49
|
+
}
|
|
50
|
+
return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
|
|
51
|
+
}
|
|
52
|
+
function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
53
|
+
const sceneEl = getDiagramSceneElement(containerEl);
|
|
54
|
+
if (sceneEl.tagName?.toLowerCase() === "svg") {
|
|
55
|
+
const clonedSvg = sceneEl.cloneNode(true);
|
|
56
|
+
if (!clonedSvg.getAttribute("xmlns")) {
|
|
57
|
+
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
58
|
+
}
|
|
59
|
+
const serializer2 = new XMLSerializer();
|
|
60
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
61
|
+
${serializer2.serializeToString(clonedSvg)}`;
|
|
62
|
+
}
|
|
63
|
+
const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
|
|
64
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
65
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
66
|
+
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
67
|
+
svg.setAttribute("width", String(scaledWidth));
|
|
68
|
+
svg.setAttribute("height", String(scaledHeight));
|
|
69
|
+
svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
|
|
70
|
+
if (options.includeThemeStyles !== false) {
|
|
71
|
+
const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
72
|
+
let combinedStyles = `
|
|
73
|
+
foreignObject { width: 100%; height: 100%; }
|
|
74
|
+
.markdy-node { transition: opacity 0.3s ease; }
|
|
75
|
+
`;
|
|
76
|
+
if (typeof document !== "undefined") {
|
|
77
|
+
const styles = document.querySelectorAll("style[id^='markdy-']");
|
|
78
|
+
for (let i = 0; i < styles.length; i++) {
|
|
79
|
+
combinedStyles += styles[i].textContent + "\n";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
styleEl.textContent = combinedStyles;
|
|
83
|
+
svg.appendChild(styleEl);
|
|
84
|
+
}
|
|
85
|
+
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
86
|
+
foreignObject.setAttribute("width", "100%");
|
|
87
|
+
foreignObject.setAttribute("height", "100%");
|
|
88
|
+
foreignObject.appendChild(clonedScene);
|
|
89
|
+
svg.appendChild(foreignObject);
|
|
90
|
+
const serializer = new XMLSerializer();
|
|
91
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
92
|
+
` + serializer.serializeToString(svg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
getDiagramSceneElement,
|
|
97
|
+
prepareHtmlSceneForExport,
|
|
98
|
+
exportDiagramAsVectorSvg
|
|
99
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ interface DiagramOptions {
|
|
|
28
28
|
playbackRate?: number;
|
|
29
29
|
/** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
|
|
30
30
|
interactiveViewport?: boolean;
|
|
31
|
+
/** Base URL for the Share control. Defaults to the Markdy playground. */
|
|
32
|
+
shareUrl?: string;
|
|
31
33
|
/** Show built-in playback and viewport controls. Also enables viewport interaction. Defaults to false. */
|
|
32
34
|
controls?: boolean;
|
|
33
35
|
onWarning?: (warning: Diagnostic) => void;
|
|
@@ -46,6 +48,8 @@ interface Diagram {
|
|
|
46
48
|
isPlaying(): boolean;
|
|
47
49
|
beats(): BeatRange[];
|
|
48
50
|
seekToBeat(name: string): void;
|
|
51
|
+
nextBeat(): void;
|
|
52
|
+
prevBeat(): void;
|
|
49
53
|
destroy(): void;
|
|
50
54
|
}
|
|
51
55
|
declare function createDiagram(opts: DiagramOptions): Diagram;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exportDiagramAsVectorSvg,
|
|
3
|
+
getDiagramSceneElement,
|
|
4
|
+
prepareHtmlSceneForExport
|
|
5
|
+
} from "./chunk-VIZHA7GI.js";
|
|
6
|
+
|
|
1
7
|
// src/diagram.ts
|
|
2
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
compressMarkdyToUrlHash,
|
|
10
|
+
parseAndCompile,
|
|
11
|
+
resolvePlayer
|
|
12
|
+
} from "@markdy/core";
|
|
3
13
|
|
|
4
14
|
// src/geometry/rect.ts
|
|
5
15
|
function boxRect(box) {
|
|
@@ -2571,11 +2581,11 @@ function applyThemeToScene(scene, theme) {
|
|
|
2571
2581
|
|
|
2572
2582
|
// src/diagram.ts
|
|
2573
2583
|
var NORMAL_PLAYBACK_RATE = 4 / 5;
|
|
2574
|
-
var DEFAULT_PLAYBACK_RATE = 1;
|
|
2575
2584
|
var MIN_VIEWPORT_ZOOM = 0.5;
|
|
2576
2585
|
var MAX_VIEWPORT_ZOOM = 3;
|
|
2577
2586
|
var VIEWPORT_ZOOM_STEP = 15e-4;
|
|
2578
2587
|
var DRAG_CLICK_THRESHOLD_PX = 4;
|
|
2588
|
+
var BEAT_NAV_EPSILON_S = 0.05;
|
|
2579
2589
|
var MARKDY_PLAYGROUND_URL = "https://markdy.com/playground/";
|
|
2580
2590
|
function encodeCodeForPlaygroundHash(code) {
|
|
2581
2591
|
return encodeURIComponent(btoa(encodeURIComponent(code)));
|
|
@@ -2628,6 +2638,7 @@ function createDiagram(opts) {
|
|
|
2628
2638
|
playbackRate: initialPlaybackRate,
|
|
2629
2639
|
controls: explicitControls,
|
|
2630
2640
|
interactiveViewport: explicitInteractiveViewport,
|
|
2641
|
+
shareUrl,
|
|
2631
2642
|
autoplay: explicitAutoplay,
|
|
2632
2643
|
loop: explicitLoop,
|
|
2633
2644
|
copyright: explicitCopyright,
|
|
@@ -2640,15 +2651,46 @@ function createDiagram(opts) {
|
|
|
2640
2651
|
for (const w of ast.diagnostics) {
|
|
2641
2652
|
if (w.severity === "warning") onWarning(w);
|
|
2642
2653
|
}
|
|
2643
|
-
const
|
|
2644
|
-
const
|
|
2645
|
-
const
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2654
|
+
const hostProgress = sceneBoundaryProgress === false || sceneBoundaryProgress === void 0 && progressBar === false ? "none" : void 0;
|
|
2655
|
+
const hostProgressColor = progressColor ?? progressBarColor ?? (typeof sceneBoundaryProgress === "string" && sceneBoundaryProgress !== "true" && sceneBoundaryProgress !== "false" ? sceneBoundaryProgress : typeof progressBar === "string" && progressBar !== "true" && progressBar !== "false" ? progressBar : void 0);
|
|
2656
|
+
const player = resolvePlayer(plan.meta.player, {
|
|
2657
|
+
autoplay: explicitAutoplay,
|
|
2658
|
+
loop: explicitLoop,
|
|
2659
|
+
playbackRate: initialPlaybackRate,
|
|
2660
|
+
copyright: explicitCopyright,
|
|
2661
|
+
controls: explicitControls,
|
|
2662
|
+
interactiveViewport: explicitInteractiveViewport,
|
|
2663
|
+
progress: hostProgress,
|
|
2664
|
+
progressColor: hostProgressColor
|
|
2665
|
+
});
|
|
2666
|
+
const { autoplay, loop } = player.playback;
|
|
2667
|
+
const {
|
|
2668
|
+
enabled: interactiveViewport,
|
|
2669
|
+
zoom: allowZoom,
|
|
2670
|
+
pan: allowPan,
|
|
2671
|
+
clickToPlay,
|
|
2672
|
+
doubleClickToReset,
|
|
2673
|
+
keyboard: keyboardShortcuts
|
|
2674
|
+
} = player.interaction;
|
|
2675
|
+
const {
|
|
2676
|
+
enabled: showControls,
|
|
2677
|
+
play: playButton,
|
|
2678
|
+
restart: restartButton,
|
|
2679
|
+
prevBeat: prevBeatButton,
|
|
2680
|
+
nextBeat: nextBeatButton,
|
|
2681
|
+
seek: seekBar,
|
|
2682
|
+
speed: speedControls,
|
|
2683
|
+
speeds: speedOptions,
|
|
2684
|
+
fit: fitViewButton,
|
|
2685
|
+
resetView: resetViewButton,
|
|
2686
|
+
svg: svgButton,
|
|
2687
|
+
share: shareButton
|
|
2688
|
+
} = player.controls;
|
|
2689
|
+
const copyright = player.chrome.badge;
|
|
2690
|
+
const progressMode = player.chrome.progress;
|
|
2691
|
+
const showProgress = progressMode !== "none";
|
|
2692
|
+
let playbackRate = player.playback.rate;
|
|
2693
|
+
const rawColor = player.chrome.progressColor;
|
|
2652
2694
|
const customColor = rawColor && rawColor.trim() !== "rainbow" ? rawColor.trim() : null;
|
|
2653
2695
|
const DEFAULT_RAINBOW = "hsl(0,90%,60%), hsl(45,90%,55%), hsl(90,80%,50%), hsl(180,80%,50%), hsl(270,80%,55%), hsl(330,90%,60%)";
|
|
2654
2696
|
const totalDurationMs = plan.duration * 1e3;
|
|
@@ -2667,11 +2709,11 @@ function createDiagram(opts) {
|
|
|
2667
2709
|
});
|
|
2668
2710
|
container.appendChild(viewport);
|
|
2669
2711
|
let progressEl = null;
|
|
2670
|
-
if (
|
|
2712
|
+
if (showProgress) {
|
|
2671
2713
|
progressEl = document.createElement("div");
|
|
2672
2714
|
Object.assign(progressEl.style, {
|
|
2673
2715
|
position: "absolute",
|
|
2674
|
-
inset: "0",
|
|
2716
|
+
...progressMode === "bar" ? { left: "0", right: "0", bottom: "0", height: "3px" } : { inset: "0" },
|
|
2675
2717
|
zIndex: "9999",
|
|
2676
2718
|
pointerEvents: "none",
|
|
2677
2719
|
borderRadius: "inherit"
|
|
@@ -2682,6 +2724,12 @@ function createDiagram(opts) {
|
|
|
2682
2724
|
const tlAngleNorm = (tlAngle % 360 + 360) % 360;
|
|
2683
2725
|
function updateProgressBar(pct) {
|
|
2684
2726
|
if (!progressEl) return;
|
|
2727
|
+
if (progressMode === "bar") {
|
|
2728
|
+
progressEl.style.background = customColor ?? "#2563eb";
|
|
2729
|
+
progressEl.style.transformOrigin = "left center";
|
|
2730
|
+
progressEl.style.transform = `scaleX(${pct})`;
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2685
2733
|
const deg = pct * 360;
|
|
2686
2734
|
const colorStops = customColor ? customColor.includes(",") ? customColor : `${customColor} 0deg, ${customColor}` : DEFAULT_RAINBOW;
|
|
2687
2735
|
progressEl.style.background = `conic-gradient(from ${tlAngleNorm}deg, ${colorStops} ${deg}deg, transparent ${deg}deg)`;
|
|
@@ -2955,15 +3003,40 @@ function createDiagram(opts) {
|
|
|
2955
3003
|
let suppressNextClick = false;
|
|
2956
3004
|
let controlsPlayButton = null;
|
|
2957
3005
|
let controlsRateButtons = [];
|
|
3006
|
+
let controlsSeekBar = null;
|
|
3007
|
+
let controlsFitButton = null;
|
|
3008
|
+
let fitViewActive = false;
|
|
2958
3009
|
function applyViewportTransform() {
|
|
2959
3010
|
viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
|
|
2960
3011
|
}
|
|
2961
3012
|
function resetViewportTransform() {
|
|
3013
|
+
releaseFitView();
|
|
2962
3014
|
viewportScale = 1;
|
|
2963
3015
|
viewportPanX = 0;
|
|
2964
3016
|
viewportPanY = 0;
|
|
2965
3017
|
applyViewportTransform();
|
|
2966
3018
|
}
|
|
3019
|
+
function releaseFitView() {
|
|
3020
|
+
if (!fitViewActive) return;
|
|
3021
|
+
fitViewActive = false;
|
|
3022
|
+
cameraLayer.style.removeProperty("transform");
|
|
3023
|
+
}
|
|
3024
|
+
function toggleFitView() {
|
|
3025
|
+
if (fitViewActive) {
|
|
3026
|
+
resetViewportTransform();
|
|
3027
|
+
syncControls();
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
const bounds = computeContentBounds();
|
|
3031
|
+
const scale = Math.min(plan.meta.width / bounds.width, plan.meta.height / bounds.height);
|
|
3032
|
+
viewportScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
3033
|
+
viewportPanX = -bounds.minX * viewportScale + (plan.meta.width - bounds.width * viewportScale) / 2;
|
|
3034
|
+
viewportPanY = -bounds.minY * viewportScale + (plan.meta.height - bounds.height * viewportScale) / 2;
|
|
3035
|
+
applyViewportTransform();
|
|
3036
|
+
fitViewActive = true;
|
|
3037
|
+
cameraLayer.style.setProperty("transform", "none", "important");
|
|
3038
|
+
syncControls();
|
|
3039
|
+
}
|
|
2967
3040
|
function handleViewportWheel(event) {
|
|
2968
3041
|
event.preventDefault();
|
|
2969
3042
|
const rect = viewport.getBoundingClientRect();
|
|
@@ -2979,6 +3052,7 @@ function createDiagram(opts) {
|
|
|
2979
3052
|
applyViewportTransform();
|
|
2980
3053
|
}
|
|
2981
3054
|
function handleViewportPointerDown(event) {
|
|
3055
|
+
if (!allowPan) return;
|
|
2982
3056
|
if (event.button !== 0 || activePointerId !== null) return;
|
|
2983
3057
|
activePointerId = event.pointerId;
|
|
2984
3058
|
dragStartX = event.clientX;
|
|
@@ -3031,6 +3105,13 @@ function createDiagram(opts) {
|
|
|
3031
3105
|
button.style.color = active ? "#ffffff" : "#475569";
|
|
3032
3106
|
button.style.borderColor = active ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
|
|
3033
3107
|
}
|
|
3108
|
+
if (controlsSeekBar) controlsSeekBar.value = String(sceneMs / 1e3);
|
|
3109
|
+
if (controlsFitButton) {
|
|
3110
|
+
controlsFitButton.setAttribute("aria-pressed", fitViewActive ? "true" : "false");
|
|
3111
|
+
controlsFitButton.style.background = fitViewActive ? "#1e293b" : "rgba(248, 250, 252, 0.92)";
|
|
3112
|
+
controlsFitButton.style.color = fitViewActive ? "#ffffff" : "#475569";
|
|
3113
|
+
controlsFitButton.style.borderColor = fitViewActive ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
|
|
3114
|
+
}
|
|
3034
3115
|
}
|
|
3035
3116
|
function emitPlayStateChange(playing) {
|
|
3036
3117
|
onPlayStateChange?.(playing);
|
|
@@ -3105,8 +3186,23 @@ function createDiagram(opts) {
|
|
|
3105
3186
|
const beat = plan.beats.find((b) => b.name === name);
|
|
3106
3187
|
if (beat) diagram.seek(beat.start);
|
|
3107
3188
|
},
|
|
3189
|
+
nextBeat() {
|
|
3190
|
+
const next = plan.beats.find((beat) => beat.start > sceneMs / 1e3 + BEAT_NAV_EPSILON_S);
|
|
3191
|
+
if (next) diagram.seek(next.start);
|
|
3192
|
+
},
|
|
3193
|
+
prevBeat() {
|
|
3194
|
+
const current = sceneMs / 1e3;
|
|
3195
|
+
let target = 0;
|
|
3196
|
+
for (const beat of plan.beats) {
|
|
3197
|
+
if (beat.start < current - BEAT_NAV_EPSILON_S) target = beat.start;
|
|
3198
|
+
}
|
|
3199
|
+
diagram.seek(target);
|
|
3200
|
+
},
|
|
3108
3201
|
destroy() {
|
|
3109
3202
|
diagram.pause();
|
|
3203
|
+
if (keyboardShortcuts && typeof window !== "undefined") {
|
|
3204
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
3205
|
+
}
|
|
3110
3206
|
for (const anim of allAnims) anim.cancel();
|
|
3111
3207
|
resizeObserver?.disconnect();
|
|
3112
3208
|
if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
|
|
@@ -3143,6 +3239,159 @@ function createDiagram(opts) {
|
|
|
3143
3239
|
});
|
|
3144
3240
|
return button;
|
|
3145
3241
|
}
|
|
3242
|
+
function mountPlayControl(toolbar) {
|
|
3243
|
+
if (!playButton) return;
|
|
3244
|
+
controlsPlayButton = makeControlButton("Play", "Play diagram");
|
|
3245
|
+
controlsPlayButton.className = "markdy-control-play";
|
|
3246
|
+
controlsPlayButton.addEventListener("click", togglePlayback);
|
|
3247
|
+
toolbar.appendChild(controlsPlayButton);
|
|
3248
|
+
}
|
|
3249
|
+
function mountBeatNavControls(toolbar, position) {
|
|
3250
|
+
const wanted = position === "prev" ? prevBeatButton : nextBeatButton;
|
|
3251
|
+
if (!wanted || plan.beats.length < 2) return;
|
|
3252
|
+
const label = position === "prev" ? "Prev" : "Next";
|
|
3253
|
+
const button = makeControlButton(label, `${label === "Prev" ? "Previous" : "Next"} beat`);
|
|
3254
|
+
button.className = `markdy-control-${position}-beat`;
|
|
3255
|
+
button.addEventListener("click", () => position === "prev" ? diagram.prevBeat() : diagram.nextBeat());
|
|
3256
|
+
toolbar.appendChild(button);
|
|
3257
|
+
}
|
|
3258
|
+
function mountRestartControl(toolbar) {
|
|
3259
|
+
if (!restartButton) return;
|
|
3260
|
+
const button = makeControlButton("Restart", "Restart diagram");
|
|
3261
|
+
button.className = "markdy-control-restart";
|
|
3262
|
+
button.addEventListener("click", () => {
|
|
3263
|
+
diagram.seek(0);
|
|
3264
|
+
diagram.play();
|
|
3265
|
+
});
|
|
3266
|
+
toolbar.appendChild(button);
|
|
3267
|
+
}
|
|
3268
|
+
function mountSeekControl(toolbar) {
|
|
3269
|
+
if (!seekBar) return;
|
|
3270
|
+
controlsSeekBar = document.createElement("input");
|
|
3271
|
+
controlsSeekBar.className = "markdy-control-seek";
|
|
3272
|
+
controlsSeekBar.type = "range";
|
|
3273
|
+
controlsSeekBar.min = "0";
|
|
3274
|
+
controlsSeekBar.max = String(durationSeconds);
|
|
3275
|
+
controlsSeekBar.step = "0.01";
|
|
3276
|
+
controlsSeekBar.value = String(sceneMs / 1e3);
|
|
3277
|
+
controlsSeekBar.setAttribute("aria-label", "Seek diagram timeline");
|
|
3278
|
+
controlsSeekBar.addEventListener("input", () => diagram.seek(Number(controlsSeekBar?.value ?? 0)));
|
|
3279
|
+
toolbar.appendChild(controlsSeekBar);
|
|
3280
|
+
}
|
|
3281
|
+
function mountSpeedControls(toolbar) {
|
|
3282
|
+
if (!speedControls) return;
|
|
3283
|
+
controlsRateButtons = speedOptions.map((rate) => {
|
|
3284
|
+
const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
|
|
3285
|
+
button.className = "markdy-control-rate";
|
|
3286
|
+
button.dataset.rate = String(rate);
|
|
3287
|
+
button.setAttribute("aria-pressed", "false");
|
|
3288
|
+
button.addEventListener("click", () => diagram.setPlaybackRate(rate));
|
|
3289
|
+
toolbar.appendChild(button);
|
|
3290
|
+
return button;
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
function flashControlLabel(button, message) {
|
|
3294
|
+
const original = button.textContent ?? "";
|
|
3295
|
+
button.textContent = message;
|
|
3296
|
+
setTimeout(() => {
|
|
3297
|
+
button.textContent = original;
|
|
3298
|
+
}, 1400);
|
|
3299
|
+
}
|
|
3300
|
+
function downloadFile(filename, contents, type) {
|
|
3301
|
+
const blob = new Blob([contents], { type });
|
|
3302
|
+
const url = URL.createObjectURL(blob);
|
|
3303
|
+
const link = document.createElement("a");
|
|
3304
|
+
link.href = url;
|
|
3305
|
+
link.download = filename;
|
|
3306
|
+
link.style.display = "none";
|
|
3307
|
+
document.body.appendChild(link);
|
|
3308
|
+
link.click();
|
|
3309
|
+
document.body.removeChild(link);
|
|
3310
|
+
URL.revokeObjectURL(url);
|
|
3311
|
+
}
|
|
3312
|
+
function mountSvgControl(toolbar) {
|
|
3313
|
+
if (!svgButton) return;
|
|
3314
|
+
const button = makeControlButton("SVG", "Export diagram as SVG");
|
|
3315
|
+
button.className = "markdy-control-svg";
|
|
3316
|
+
button.addEventListener("click", async () => {
|
|
3317
|
+
const resumeAt = sceneMs;
|
|
3318
|
+
const wasPlaying = isPlaying;
|
|
3319
|
+
try {
|
|
3320
|
+
diagram.pause();
|
|
3321
|
+
diagram.seek(durationSeconds);
|
|
3322
|
+
const { exportDiagramAsVectorSvg: exportDiagramAsVectorSvg2 } = await import("./svg-exporter-7FW6RIP6.js");
|
|
3323
|
+
const svg = exportDiagramAsVectorSvg2(container);
|
|
3324
|
+
const name = (plan.title || "markdy-diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3325
|
+
downloadFile(`${name || "markdy-diagram"}.svg`, svg, "image/svg+xml");
|
|
3326
|
+
} catch (error) {
|
|
3327
|
+
onWarning({ severity: "warning", message: `SVG export failed: ${String(error)}`, line: 0 });
|
|
3328
|
+
flashControlLabel(button, "Failed");
|
|
3329
|
+
} finally {
|
|
3330
|
+
diagram.seek(resumeAt / 1e3);
|
|
3331
|
+
if (wasPlaying) diagram.play();
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
toolbar.appendChild(button);
|
|
3335
|
+
}
|
|
3336
|
+
function mountShareControl(toolbar) {
|
|
3337
|
+
if (!shareButton) return;
|
|
3338
|
+
const button = makeControlButton("Share", "Copy a share link for this diagram");
|
|
3339
|
+
button.className = "markdy-control-share";
|
|
3340
|
+
button.addEventListener("click", async () => {
|
|
3341
|
+
try {
|
|
3342
|
+
const hash = await compressMarkdyToUrlHash(code);
|
|
3343
|
+
const base = shareUrl ?? MARKDY_PLAYGROUND_URL;
|
|
3344
|
+
await navigator.clipboard.writeText(`${base}#code=${hash}`);
|
|
3345
|
+
flashControlLabel(button, "Copied");
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
onWarning({ severity: "warning", message: `Share link failed: ${String(error)}`, line: 0 });
|
|
3348
|
+
flashControlLabel(button, "Failed");
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
toolbar.appendChild(button);
|
|
3352
|
+
}
|
|
3353
|
+
function mountFitControl(toolbar) {
|
|
3354
|
+
if (!fitViewButton) return;
|
|
3355
|
+
controlsFitButton = makeControlButton("Fit", "Fit all items in view and ignore camera zoom");
|
|
3356
|
+
controlsFitButton.className = "markdy-control-fit";
|
|
3357
|
+
controlsFitButton.setAttribute("aria-pressed", "false");
|
|
3358
|
+
controlsFitButton.addEventListener("click", toggleFitView);
|
|
3359
|
+
toolbar.appendChild(controlsFitButton);
|
|
3360
|
+
}
|
|
3361
|
+
function mountResetViewControl(toolbar) {
|
|
3362
|
+
if (!resetViewButton) return;
|
|
3363
|
+
const button = makeControlButton("Reset", "Reset diagram view");
|
|
3364
|
+
button.className = "markdy-control-reset-view";
|
|
3365
|
+
button.addEventListener("click", resetViewportTransform);
|
|
3366
|
+
toolbar.appendChild(button);
|
|
3367
|
+
}
|
|
3368
|
+
function handleKeyDown(event) {
|
|
3369
|
+
const target = event.target;
|
|
3370
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target?.isContentEditable) {
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
switch (event.key) {
|
|
3374
|
+
case "ArrowRight":
|
|
3375
|
+
case "PageDown":
|
|
3376
|
+
event.preventDefault();
|
|
3377
|
+
diagram.nextBeat();
|
|
3378
|
+
break;
|
|
3379
|
+
case "ArrowLeft":
|
|
3380
|
+
case "PageUp":
|
|
3381
|
+
event.preventDefault();
|
|
3382
|
+
diagram.prevBeat();
|
|
3383
|
+
break;
|
|
3384
|
+
case " ":
|
|
3385
|
+
event.preventDefault();
|
|
3386
|
+
togglePlayback();
|
|
3387
|
+
break;
|
|
3388
|
+
case "Home":
|
|
3389
|
+
event.preventDefault();
|
|
3390
|
+
diagram.seek(0);
|
|
3391
|
+
break;
|
|
3392
|
+
default:
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3146
3395
|
function mountControls() {
|
|
3147
3396
|
const toolbar = document.createElement("div");
|
|
3148
3397
|
toolbar.className = "markdy-controls";
|
|
@@ -3168,53 +3417,44 @@ function createDiagram(opts) {
|
|
|
3168
3417
|
for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
|
|
3169
3418
|
toolbar.addEventListener(eventName, (event) => event.stopPropagation());
|
|
3170
3419
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
toolbar
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
toolbar.appendChild(restartButton);
|
|
3182
|
-
controlsRateButtons = [0.5, 1, 2].map((rate) => {
|
|
3183
|
-
const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
|
|
3184
|
-
button.className = "markdy-control-rate";
|
|
3185
|
-
button.dataset.rate = String(rate);
|
|
3186
|
-
button.setAttribute("aria-pressed", "false");
|
|
3187
|
-
button.addEventListener("click", () => diagram.setPlaybackRate(rate));
|
|
3188
|
-
toolbar.appendChild(button);
|
|
3189
|
-
return button;
|
|
3190
|
-
});
|
|
3191
|
-
if (interactiveViewport) {
|
|
3192
|
-
const resetButton = makeControlButton("Reset", "Reset diagram view");
|
|
3193
|
-
resetButton.className = "markdy-control-reset-view";
|
|
3194
|
-
resetButton.addEventListener("click", resetViewportTransform);
|
|
3195
|
-
toolbar.appendChild(resetButton);
|
|
3196
|
-
}
|
|
3420
|
+
mountPlayControl(toolbar);
|
|
3421
|
+
mountBeatNavControls(toolbar, "prev");
|
|
3422
|
+
mountBeatNavControls(toolbar, "next");
|
|
3423
|
+
mountRestartControl(toolbar);
|
|
3424
|
+
mountSeekControl(toolbar);
|
|
3425
|
+
mountSpeedControls(toolbar);
|
|
3426
|
+
mountFitControl(toolbar);
|
|
3427
|
+
mountResetViewControl(toolbar);
|
|
3428
|
+
mountSvgControl(toolbar);
|
|
3429
|
+
mountShareControl(toolbar);
|
|
3197
3430
|
ensureFooter().insertBefore(toolbar, badge ?? null);
|
|
3198
3431
|
syncControls();
|
|
3199
3432
|
}
|
|
3200
3433
|
viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
|
|
3201
3434
|
if (interactiveViewport) {
|
|
3202
3435
|
viewport.style.touchAction = "none";
|
|
3203
|
-
viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3436
|
+
if (allowZoom) viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
|
|
3437
|
+
if (allowPan) {
|
|
3438
|
+
viewport.addEventListener("pointerdown", handleViewportPointerDown);
|
|
3439
|
+
viewport.addEventListener("pointermove", handleViewportPointerMove);
|
|
3440
|
+
viewport.addEventListener("pointerup", handleViewportPointerEnd);
|
|
3441
|
+
viewport.addEventListener("pointercancel", handleViewportPointerEnd);
|
|
3442
|
+
}
|
|
3443
|
+
if (doubleClickToReset) viewport.addEventListener("dblclick", handleViewportDoubleClick);
|
|
3444
|
+
}
|
|
3445
|
+
if (showControls) mountControls();
|
|
3446
|
+
if (clickToPlay) {
|
|
3447
|
+
viewport.addEventListener("click", () => {
|
|
3448
|
+
if (suppressNextClick) {
|
|
3449
|
+
suppressNextClick = false;
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
togglePlayback();
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
if (keyboardShortcuts && typeof window !== "undefined") {
|
|
3456
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
3457
|
+
}
|
|
3218
3458
|
if (autoplay) diagram.play();
|
|
3219
3459
|
return diagram;
|
|
3220
3460
|
}
|
|
@@ -3410,100 +3650,6 @@ function encodeGifSequence(frames, options = {}) {
|
|
|
3410
3650
|
return new Uint8Array(buffer);
|
|
3411
3651
|
}
|
|
3412
3652
|
|
|
3413
|
-
// src/export/svg-exporter.ts
|
|
3414
|
-
function copyRenderedStyles(source, clone) {
|
|
3415
|
-
if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
|
|
3416
|
-
const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
3417
|
-
const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
3418
|
-
for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
|
|
3419
|
-
const computed = window.getComputedStyle(sourceElements[index]);
|
|
3420
|
-
const target = cloneElements[index].style;
|
|
3421
|
-
for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
|
|
3422
|
-
const property = computed.item(propertyIndex);
|
|
3423
|
-
target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
function normalizeExportViewport(scene) {
|
|
3428
|
-
scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
|
|
3429
|
-
viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
|
|
3430
|
-
viewportTransform.style.transformOrigin = "0 0";
|
|
3431
|
-
viewportTransform.style.willChange = "auto";
|
|
3432
|
-
});
|
|
3433
|
-
}
|
|
3434
|
-
function getDiagramSceneElement(containerEl) {
|
|
3435
|
-
const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
|
|
3436
|
-
if (!sceneEl) throw new Error("No Markdy scene element found in container");
|
|
3437
|
-
return sceneEl;
|
|
3438
|
-
}
|
|
3439
|
-
function prepareHtmlSceneForExport(sceneEl, options = {}) {
|
|
3440
|
-
const clonedScene = sceneEl.cloneNode(true);
|
|
3441
|
-
copyRenderedStyles(sceneEl, clonedScene);
|
|
3442
|
-
normalizeExportViewport(clonedScene);
|
|
3443
|
-
const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
|
|
3444
|
-
const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
|
|
3445
|
-
let width = parseFloat(widthStr);
|
|
3446
|
-
let height = parseFloat(heightStr);
|
|
3447
|
-
if (isNaN(width)) width = 800;
|
|
3448
|
-
if (isNaN(height)) height = 400;
|
|
3449
|
-
const scale = options.scale || 1;
|
|
3450
|
-
const scaledWidth = width * scale;
|
|
3451
|
-
const scaledHeight = height * scale;
|
|
3452
|
-
clonedScene.style.transform = `scale(${scale})`;
|
|
3453
|
-
clonedScene.style.transformOrigin = "0 0";
|
|
3454
|
-
clonedScene.style.position = "relative";
|
|
3455
|
-
clonedScene.style.left = "0px";
|
|
3456
|
-
clonedScene.style.top = "0px";
|
|
3457
|
-
clonedScene.style.margin = "0";
|
|
3458
|
-
clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
3459
|
-
if (options.transparentBackground) {
|
|
3460
|
-
clonedScene.style.background = "transparent";
|
|
3461
|
-
}
|
|
3462
|
-
return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
|
|
3463
|
-
}
|
|
3464
|
-
function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
3465
|
-
const sceneEl = getDiagramSceneElement(containerEl);
|
|
3466
|
-
if (sceneEl.tagName?.toLowerCase() === "svg") {
|
|
3467
|
-
const clonedSvg = sceneEl.cloneNode(true);
|
|
3468
|
-
if (!clonedSvg.getAttribute("xmlns")) {
|
|
3469
|
-
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
3470
|
-
}
|
|
3471
|
-
const serializer2 = new XMLSerializer();
|
|
3472
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3473
|
-
${serializer2.serializeToString(clonedSvg)}`;
|
|
3474
|
-
}
|
|
3475
|
-
const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
|
|
3476
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3477
|
-
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
3478
|
-
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
3479
|
-
svg.setAttribute("width", String(scaledWidth));
|
|
3480
|
-
svg.setAttribute("height", String(scaledHeight));
|
|
3481
|
-
svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
|
|
3482
|
-
if (options.includeThemeStyles !== false) {
|
|
3483
|
-
const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
3484
|
-
let combinedStyles = `
|
|
3485
|
-
foreignObject { width: 100%; height: 100%; }
|
|
3486
|
-
.markdy-node { transition: opacity 0.3s ease; }
|
|
3487
|
-
`;
|
|
3488
|
-
if (typeof document !== "undefined") {
|
|
3489
|
-
const styles = document.querySelectorAll("style[id^='markdy-']");
|
|
3490
|
-
for (let i = 0; i < styles.length; i++) {
|
|
3491
|
-
combinedStyles += styles[i].textContent + "\n";
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
styleEl.textContent = combinedStyles;
|
|
3495
|
-
svg.appendChild(styleEl);
|
|
3496
|
-
}
|
|
3497
|
-
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
3498
|
-
foreignObject.setAttribute("width", "100%");
|
|
3499
|
-
foreignObject.setAttribute("height", "100%");
|
|
3500
|
-
foreignObject.appendChild(clonedScene);
|
|
3501
|
-
svg.appendChild(foreignObject);
|
|
3502
|
-
const serializer = new XMLSerializer();
|
|
3503
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3504
|
-
` + serializer.serializeToString(svg);
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
3653
|
// src/export/inline-resources.ts
|
|
3508
3654
|
var TRANSPARENT_PIXEL_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
3509
3655
|
var RESOURCE_FETCH_TIMEOUT_MS = 3e3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"html2canvas": "^1.4.1",
|
|
48
|
-
"@markdy/core": "1.0.
|
|
48
|
+
"@markdy/core": "1.0.11"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"jsdom": "^29.1.1",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"typescript": "^5.9.3",
|
|
54
54
|
"vitest": "^4.1.7",
|
|
55
|
-
"@markdy/stdlib-systems": "1.0.
|
|
55
|
+
"@markdy/stdlib-systems": "1.0.11"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|