@markdy/renderer-dom 0.8.15 → 0.8.17
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 +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +142 -25
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
|
|
|
13
13
|
- **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
|
|
14
14
|
- **Playback-rate controls** — set normalized timeline speed to slow down or speed up diagrams without rebuilding animations
|
|
15
15
|
- **Interactive viewport** — opt into wheel zoom and drag pan while click-to-pause stays available
|
|
16
|
+
- **Embed controls** — opt into a compact playback toolbar with restart, speed, and view reset controls
|
|
16
17
|
- **Semantic themes** — `paper`, `editorial`, `nebula`, `midnight`, `blueprint`, and `graphite`, with per-role node and edge colors
|
|
17
18
|
- **Single dependency** — only `@markdy/core`
|
|
18
19
|
|
|
@@ -76,6 +77,7 @@ diagram.destroy(); // clean up DOM + cancel animations
|
|
|
76
77
|
| `sceneBoundaryProgress` | `boolean` | `progressBar ?? true` | Preferred flag for the rainbow scene-boundary progress bar |
|
|
77
78
|
| `playbackRate` | `number` | `1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
|
|
78
79
|
| `interactiveViewport` | `boolean` | `false` | Enable wheel zoom and drag pan on the rendered viewport |
|
|
80
|
+
| `controls` | `boolean` | `false` | Show a compact toolbar with play/pause, restart, speed, and view reset controls when viewport interaction is enabled |
|
|
79
81
|
| `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
|
|
80
82
|
| `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
|
|
81
83
|
| `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ interface DiagramOptions {
|
|
|
24
24
|
playbackRate?: number;
|
|
25
25
|
/** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
|
|
26
26
|
interactiveViewport?: boolean;
|
|
27
|
+
/** Show built-in playback and viewport controls. Defaults to false. */
|
|
28
|
+
controls?: boolean;
|
|
27
29
|
onWarning?: (warning: Diagnostic) => void;
|
|
28
30
|
onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
|
|
29
31
|
onPlayStateChange?: (playing: boolean) => void;
|
package/dist/index.js
CHANGED
|
@@ -1864,6 +1864,7 @@ function createDiagram(opts) {
|
|
|
1864
1864
|
sceneBoundaryProgress,
|
|
1865
1865
|
playbackRate: initialPlaybackRate = DEFAULT_PLAYBACK_RATE,
|
|
1866
1866
|
interactiveViewport = false,
|
|
1867
|
+
controls = false,
|
|
1867
1868
|
onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
|
|
1868
1869
|
onTimeUpdate,
|
|
1869
1870
|
onPlayStateChange
|
|
@@ -1930,15 +1931,6 @@ function createDiagram(opts) {
|
|
|
1930
1931
|
}
|
|
1931
1932
|
ensureSceneStyles(document);
|
|
1932
1933
|
ensureNodeStyles(document);
|
|
1933
|
-
const viewportTransform = document.createElement("div");
|
|
1934
|
-
viewportTransform.className = "markdy-viewport-transform";
|
|
1935
|
-
Object.assign(viewportTransform.style, {
|
|
1936
|
-
position: "absolute",
|
|
1937
|
-
inset: "0",
|
|
1938
|
-
transformOrigin: "0 0",
|
|
1939
|
-
willChange: interactiveViewport ? "transform" : "auto"
|
|
1940
|
-
});
|
|
1941
|
-
viewport.appendChild(viewportTransform);
|
|
1942
1934
|
const scene = document.createElement("div");
|
|
1943
1935
|
scene.className = "markdy-scene-root";
|
|
1944
1936
|
Object.assign(scene.style, {
|
|
@@ -1952,11 +1944,20 @@ function createDiagram(opts) {
|
|
|
1952
1944
|
transformOrigin: "0 0"
|
|
1953
1945
|
});
|
|
1954
1946
|
applyThemeToScene(scene, plan.theme);
|
|
1955
|
-
|
|
1947
|
+
viewport.appendChild(scene);
|
|
1948
|
+
const viewportTransform = document.createElement("div");
|
|
1949
|
+
viewportTransform.className = "markdy-viewport-transform";
|
|
1950
|
+
Object.assign(viewportTransform.style, {
|
|
1951
|
+
position: "absolute",
|
|
1952
|
+
inset: "0",
|
|
1953
|
+
transformOrigin: "0 0",
|
|
1954
|
+
willChange: interactiveViewport ? "transform" : "auto"
|
|
1955
|
+
});
|
|
1956
|
+
scene.appendChild(viewportTransform);
|
|
1956
1957
|
const sceneContent = document.createElement("div");
|
|
1957
1958
|
sceneContent.className = "markdy-scene-content";
|
|
1958
1959
|
Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
|
|
1959
|
-
|
|
1960
|
+
viewportTransform.appendChild(sceneContent);
|
|
1960
1961
|
const titleEl = createTitleEl(plan.title);
|
|
1961
1962
|
sceneContent.appendChild(titleEl);
|
|
1962
1963
|
const cameraLayer = document.createElement("div");
|
|
@@ -2014,9 +2015,11 @@ function createDiagram(opts) {
|
|
|
2014
2015
|
nodeLayer.appendChild(el);
|
|
2015
2016
|
nodeEls.set(node.id, el);
|
|
2016
2017
|
}
|
|
2018
|
+
let fitScale = 1;
|
|
2017
2019
|
function scaleScene() {
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
+
const width = viewport.clientWidth || plan.meta.width;
|
|
2021
|
+
fitScale = width / plan.meta.width;
|
|
2022
|
+
scene.style.transform = `scale(${fitScale})`;
|
|
2020
2023
|
}
|
|
2021
2024
|
scaleScene();
|
|
2022
2025
|
const resizeObserver = new ResizeObserver(scaleScene);
|
|
@@ -2072,14 +2075,22 @@ function createDiagram(opts) {
|
|
|
2072
2075
|
let dragLastY = 0;
|
|
2073
2076
|
let dragMoved = false;
|
|
2074
2077
|
let suppressNextClick = false;
|
|
2078
|
+
let controlsPlayButton = null;
|
|
2079
|
+
let controlsRateButtons = [];
|
|
2075
2080
|
function applyViewportTransform() {
|
|
2076
2081
|
viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
|
|
2077
2082
|
}
|
|
2083
|
+
function resetViewportTransform() {
|
|
2084
|
+
viewportScale = 1;
|
|
2085
|
+
viewportPanX = 0;
|
|
2086
|
+
viewportPanY = 0;
|
|
2087
|
+
applyViewportTransform();
|
|
2088
|
+
}
|
|
2078
2089
|
function handleViewportWheel(event) {
|
|
2079
2090
|
event.preventDefault();
|
|
2080
2091
|
const rect = viewport.getBoundingClientRect();
|
|
2081
|
-
const pointerX = event.clientX - rect.left;
|
|
2082
|
-
const pointerY = event.clientY - rect.top;
|
|
2092
|
+
const pointerX = (event.clientX - rect.left) / fitScale;
|
|
2093
|
+
const pointerY = (event.clientY - rect.top) / fitScale;
|
|
2083
2094
|
const nextScale = Math.min(MAX_VIEWPORT_ZOOM, Math.max(MIN_VIEWPORT_ZOOM, viewportScale * Math.exp(-event.deltaY * VIEWPORT_ZOOM_STEP)));
|
|
2084
2095
|
if (nextScale === viewportScale) return;
|
|
2085
2096
|
const sceneX = (pointerX - viewportPanX) / viewportScale;
|
|
@@ -2102,8 +2113,8 @@ function createDiagram(opts) {
|
|
|
2102
2113
|
}
|
|
2103
2114
|
function handleViewportPointerMove(event) {
|
|
2104
2115
|
if (event.pointerId !== activePointerId) return;
|
|
2105
|
-
const deltaX = event.clientX - dragLastX;
|
|
2106
|
-
const deltaY = event.clientY - dragLastY;
|
|
2116
|
+
const deltaX = (event.clientX - dragLastX) / fitScale;
|
|
2117
|
+
const deltaY = (event.clientY - dragLastY) / fitScale;
|
|
2107
2118
|
const totalX = event.clientX - dragStartX;
|
|
2108
2119
|
const totalY = event.clientY - dragStartY;
|
|
2109
2120
|
if (!dragMoved && Math.hypot(totalX, totalY) >= DRAG_CLICK_THRESHOLD_PX) dragMoved = true;
|
|
@@ -2121,6 +2132,28 @@ function createDiagram(opts) {
|
|
|
2121
2132
|
dragMoved = false;
|
|
2122
2133
|
viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
|
|
2123
2134
|
}
|
|
2135
|
+
function handleViewportDoubleClick(event) {
|
|
2136
|
+
event.preventDefault();
|
|
2137
|
+
suppressNextClick = false;
|
|
2138
|
+
resetViewportTransform();
|
|
2139
|
+
}
|
|
2140
|
+
function syncControls() {
|
|
2141
|
+
if (controlsPlayButton) {
|
|
2142
|
+
controlsPlayButton.textContent = isPlaying ? "Pause" : "Play";
|
|
2143
|
+
controlsPlayButton.setAttribute("aria-label", isPlaying ? "Pause diagram" : "Play diagram");
|
|
2144
|
+
}
|
|
2145
|
+
for (const button of controlsRateButtons) {
|
|
2146
|
+
const rate = Number(button.dataset.rate ?? "1");
|
|
2147
|
+
const active = Math.abs(rate - playbackRate) < 1e-3;
|
|
2148
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
2149
|
+
button.style.background = active ? "rgba(15, 23, 42, 0.92)" : "rgba(255, 255, 255, 0.92)";
|
|
2150
|
+
button.style.color = active ? "#ffffff" : "#111827";
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
function emitPlayStateChange(playing) {
|
|
2154
|
+
onPlayStateChange?.(playing);
|
|
2155
|
+
syncControls();
|
|
2156
|
+
}
|
|
2124
2157
|
function applyCurrentTime() {
|
|
2125
2158
|
for (const anim of allAnims) anim.currentTime = sceneMs;
|
|
2126
2159
|
onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
|
|
@@ -2134,7 +2167,7 @@ function createDiagram(opts) {
|
|
|
2134
2167
|
sceneMs = totalDurationMs;
|
|
2135
2168
|
applyCurrentTime();
|
|
2136
2169
|
isPlaying = false;
|
|
2137
|
-
|
|
2170
|
+
emitPlayStateChange(false);
|
|
2138
2171
|
lastRafTs = null;
|
|
2139
2172
|
rafId = null;
|
|
2140
2173
|
return;
|
|
@@ -2148,14 +2181,14 @@ function createDiagram(opts) {
|
|
|
2148
2181
|
play() {
|
|
2149
2182
|
if (isPlaying) return;
|
|
2150
2183
|
isPlaying = true;
|
|
2151
|
-
|
|
2184
|
+
emitPlayStateChange(true);
|
|
2152
2185
|
lastRafTs = null;
|
|
2153
2186
|
rafId = requestAnimationFrame(rafTick);
|
|
2154
2187
|
},
|
|
2155
2188
|
pause() {
|
|
2156
2189
|
if (!isPlaying) return;
|
|
2157
2190
|
isPlaying = false;
|
|
2158
|
-
|
|
2191
|
+
emitPlayStateChange(false);
|
|
2159
2192
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
2160
2193
|
rafId = null;
|
|
2161
2194
|
lastRafTs = null;
|
|
@@ -2168,6 +2201,7 @@ function createDiagram(opts) {
|
|
|
2168
2201
|
setPlaybackRate(rate) {
|
|
2169
2202
|
if (!Number.isFinite(rate) || rate <= 0) return;
|
|
2170
2203
|
playbackRate = rate;
|
|
2204
|
+
syncControls();
|
|
2171
2205
|
},
|
|
2172
2206
|
playbackRate() {
|
|
2173
2207
|
return playbackRate;
|
|
@@ -2197,6 +2231,91 @@ function createDiagram(opts) {
|
|
|
2197
2231
|
if (viewport.parentNode === container) container.removeChild(viewport);
|
|
2198
2232
|
}
|
|
2199
2233
|
};
|
|
2234
|
+
function togglePlayback() {
|
|
2235
|
+
if (isPlaying) diagram.pause();
|
|
2236
|
+
else {
|
|
2237
|
+
if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
|
|
2238
|
+
diagram.play();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function makeControlButton(label, ariaLabel) {
|
|
2242
|
+
const button = document.createElement("button");
|
|
2243
|
+
button.type = "button";
|
|
2244
|
+
button.textContent = label;
|
|
2245
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
2246
|
+
button.title = ariaLabel;
|
|
2247
|
+
Object.assign(button.style, {
|
|
2248
|
+
appearance: "none",
|
|
2249
|
+
border: "1px solid rgba(15, 23, 42, 0.16)",
|
|
2250
|
+
borderRadius: "6px",
|
|
2251
|
+
background: "rgba(255, 255, 255, 0.92)",
|
|
2252
|
+
color: "#111827",
|
|
2253
|
+
cursor: "pointer",
|
|
2254
|
+
font: "600 11px/1.2 system-ui, sans-serif",
|
|
2255
|
+
padding: "6px 8px",
|
|
2256
|
+
minWidth: "34px",
|
|
2257
|
+
whiteSpace: "nowrap",
|
|
2258
|
+
boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08)"
|
|
2259
|
+
});
|
|
2260
|
+
return button;
|
|
2261
|
+
}
|
|
2262
|
+
function mountControls() {
|
|
2263
|
+
const toolbar = document.createElement("div");
|
|
2264
|
+
toolbar.className = "markdy-controls";
|
|
2265
|
+
toolbar.setAttribute("role", "toolbar");
|
|
2266
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2267
|
+
Object.assign(toolbar.style, {
|
|
2268
|
+
position: "absolute",
|
|
2269
|
+
left: "50%",
|
|
2270
|
+
bottom: "10px",
|
|
2271
|
+
zIndex: "10000",
|
|
2272
|
+
display: "flex",
|
|
2273
|
+
alignItems: "center",
|
|
2274
|
+
justifyContent: "center",
|
|
2275
|
+
flexWrap: "wrap",
|
|
2276
|
+
gap: "6px",
|
|
2277
|
+
maxWidth: "calc(100% - 20px)",
|
|
2278
|
+
padding: "6px",
|
|
2279
|
+
border: "1px solid rgba(15, 23, 42, 0.12)",
|
|
2280
|
+
borderRadius: "8px",
|
|
2281
|
+
background: "rgba(255, 255, 255, 0.78)",
|
|
2282
|
+
boxShadow: "0 10px 28px rgba(15, 23, 42, 0.16)",
|
|
2283
|
+
backdropFilter: "blur(10px)",
|
|
2284
|
+
transform: "translateX(-50%)",
|
|
2285
|
+
pointerEvents: "auto"
|
|
2286
|
+
});
|
|
2287
|
+
for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
|
|
2288
|
+
toolbar.addEventListener(eventName, (event) => event.stopPropagation());
|
|
2289
|
+
}
|
|
2290
|
+
controlsPlayButton = makeControlButton("Play", "Play diagram");
|
|
2291
|
+
controlsPlayButton.className = "markdy-control-play";
|
|
2292
|
+
controlsPlayButton.addEventListener("click", togglePlayback);
|
|
2293
|
+
toolbar.appendChild(controlsPlayButton);
|
|
2294
|
+
const restartButton = makeControlButton("Restart", "Restart diagram");
|
|
2295
|
+
restartButton.className = "markdy-control-restart";
|
|
2296
|
+
restartButton.addEventListener("click", () => {
|
|
2297
|
+
diagram.seek(0);
|
|
2298
|
+
diagram.play();
|
|
2299
|
+
});
|
|
2300
|
+
toolbar.appendChild(restartButton);
|
|
2301
|
+
controlsRateButtons = [0.5, 1, 2].map((rate) => {
|
|
2302
|
+
const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
|
|
2303
|
+
button.className = "markdy-control-rate";
|
|
2304
|
+
button.dataset.rate = String(rate);
|
|
2305
|
+
button.setAttribute("aria-pressed", "false");
|
|
2306
|
+
button.addEventListener("click", () => diagram.setPlaybackRate(rate));
|
|
2307
|
+
toolbar.appendChild(button);
|
|
2308
|
+
return button;
|
|
2309
|
+
});
|
|
2310
|
+
if (interactiveViewport) {
|
|
2311
|
+
const resetButton = makeControlButton("Reset", "Reset diagram view");
|
|
2312
|
+
resetButton.className = "markdy-control-reset-view";
|
|
2313
|
+
resetButton.addEventListener("click", resetViewportTransform);
|
|
2314
|
+
toolbar.appendChild(resetButton);
|
|
2315
|
+
}
|
|
2316
|
+
viewport.appendChild(toolbar);
|
|
2317
|
+
syncControls();
|
|
2318
|
+
}
|
|
2200
2319
|
viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
|
|
2201
2320
|
if (interactiveViewport) {
|
|
2202
2321
|
viewport.style.touchAction = "none";
|
|
@@ -2205,17 +2324,15 @@ function createDiagram(opts) {
|
|
|
2205
2324
|
viewport.addEventListener("pointermove", handleViewportPointerMove);
|
|
2206
2325
|
viewport.addEventListener("pointerup", handleViewportPointerEnd);
|
|
2207
2326
|
viewport.addEventListener("pointercancel", handleViewportPointerEnd);
|
|
2327
|
+
viewport.addEventListener("dblclick", handleViewportDoubleClick);
|
|
2208
2328
|
}
|
|
2329
|
+
if (controls) mountControls();
|
|
2209
2330
|
viewport.addEventListener("click", () => {
|
|
2210
2331
|
if (suppressNextClick) {
|
|
2211
2332
|
suppressNextClick = false;
|
|
2212
2333
|
return;
|
|
2213
2334
|
}
|
|
2214
|
-
|
|
2215
|
-
else {
|
|
2216
|
-
if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
|
|
2217
|
-
diagram.play();
|
|
2218
|
-
}
|
|
2335
|
+
togglePlayback();
|
|
2219
2336
|
});
|
|
2220
2337
|
if (autoplay) diagram.play();
|
|
2221
2338
|
return diagram;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
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",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@markdy/core": "0.8.
|
|
47
|
+
"@markdy/core": "0.8.17"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"jsdom": "^29.1.1",
|
|
51
51
|
"tsup": "^8.5.1",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
53
|
"vitest": "^4.1.7",
|
|
54
|
-
"@markdy/stdlib-systems": "0.8.
|
|
54
|
+
"@markdy/stdlib-systems": "0.8.17"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup",
|