@markdy/renderer-dom 1.0.0 → 1.0.1
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/dist/index.d.ts +0 -11
- package/dist/index.js +223 -93
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -97,17 +97,6 @@ interface GifDiagramExportOptions extends SvgExportOptions {
|
|
|
97
97
|
}
|
|
98
98
|
declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* packages/renderer-dom/src/export/png-exporter.ts
|
|
102
|
-
* High-DPI raster PNG export with 2x retina scaling.
|
|
103
|
-
* Zero external dependencies.
|
|
104
|
-
*
|
|
105
|
-
* Fix: Inline all external resources (images, fonts) as base64 data URIs
|
|
106
|
-
* before drawing the SVG to canvas. A foreignObject-wrapped SVG taints the
|
|
107
|
-
* canvas whenever it references any external URL, so every resource must be
|
|
108
|
-
* inlined first.
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
100
|
interface PngExportOptions extends SvgExportOptions {
|
|
112
101
|
pixelRatio?: number;
|
|
113
102
|
}
|
package/dist/index.js
CHANGED
|
@@ -2038,6 +2038,10 @@ var MIN_VIEWPORT_ZOOM = 0.5;
|
|
|
2038
2038
|
var MAX_VIEWPORT_ZOOM = 3;
|
|
2039
2039
|
var VIEWPORT_ZOOM_STEP = 15e-4;
|
|
2040
2040
|
var DRAG_CLICK_THRESHOLD_PX = 4;
|
|
2041
|
+
var MARKDY_PLAYGROUND_URL = "https://markdy.com/playground/";
|
|
2042
|
+
function encodeCodeForPlaygroundHash(code) {
|
|
2043
|
+
return encodeURIComponent(btoa(encodeURIComponent(code)));
|
|
2044
|
+
}
|
|
2041
2045
|
function createBeatCaptionLayer(doc, beats) {
|
|
2042
2046
|
const layer = doc.createElement("div");
|
|
2043
2047
|
layer.className = "markdy-beat-caption-layer";
|
|
@@ -2171,7 +2175,7 @@ function createDiagram(opts) {
|
|
|
2171
2175
|
let badge = null;
|
|
2172
2176
|
if (copyright) {
|
|
2173
2177
|
badge = document.createElement("a");
|
|
2174
|
-
badge.href =
|
|
2178
|
+
badge.href = `${MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
|
|
2175
2179
|
badge.target = "_blank";
|
|
2176
2180
|
badge.rel = "noopener noreferrer";
|
|
2177
2181
|
badge.textContent = "Powered by Markdy";
|
|
@@ -2862,6 +2866,9 @@ function encodeGifSequence(frames, options = {}) {
|
|
|
2862
2866
|
return new Uint8Array(buffer);
|
|
2863
2867
|
}
|
|
2864
2868
|
|
|
2869
|
+
// src/export/png-exporter.ts
|
|
2870
|
+
import html2canvas from "html2canvas";
|
|
2871
|
+
|
|
2865
2872
|
// src/export/svg-exporter.ts
|
|
2866
2873
|
function copyRenderedStyles(source, clone) {
|
|
2867
2874
|
if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
|
|
@@ -2876,20 +2883,22 @@ function copyRenderedStyles(source, clone) {
|
|
|
2876
2883
|
}
|
|
2877
2884
|
}
|
|
2878
2885
|
}
|
|
2879
|
-
function
|
|
2886
|
+
function normalizeExportViewport(scene) {
|
|
2887
|
+
scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
|
|
2888
|
+
viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
|
|
2889
|
+
viewportTransform.style.transformOrigin = "0 0";
|
|
2890
|
+
viewportTransform.style.willChange = "auto";
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function getDiagramSceneElement(containerEl) {
|
|
2880
2894
|
const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
|
|
2881
2895
|
if (!sceneEl) throw new Error("No Markdy scene element found in container");
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
2886
|
-
}
|
|
2887
|
-
const serializer2 = new XMLSerializer();
|
|
2888
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2889
|
-
${serializer2.serializeToString(clonedSvg)}`;
|
|
2890
|
-
}
|
|
2896
|
+
return sceneEl;
|
|
2897
|
+
}
|
|
2898
|
+
function prepareHtmlSceneForExport(sceneEl, options = {}) {
|
|
2891
2899
|
const clonedScene = sceneEl.cloneNode(true);
|
|
2892
2900
|
copyRenderedStyles(sceneEl, clonedScene);
|
|
2901
|
+
normalizeExportViewport(clonedScene);
|
|
2893
2902
|
const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
|
|
2894
2903
|
const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
|
|
2895
2904
|
let width = parseFloat(widthStr);
|
|
@@ -2904,10 +2913,25 @@ ${serializer2.serializeToString(clonedSvg)}`;
|
|
|
2904
2913
|
clonedScene.style.position = "relative";
|
|
2905
2914
|
clonedScene.style.left = "0px";
|
|
2906
2915
|
clonedScene.style.top = "0px";
|
|
2916
|
+
clonedScene.style.margin = "0";
|
|
2907
2917
|
clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2908
2918
|
if (options.transparentBackground) {
|
|
2909
2919
|
clonedScene.style.background = "transparent";
|
|
2910
2920
|
}
|
|
2921
|
+
return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
|
|
2922
|
+
}
|
|
2923
|
+
function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
2924
|
+
const sceneEl = getDiagramSceneElement(containerEl);
|
|
2925
|
+
if (sceneEl.tagName?.toLowerCase() === "svg") {
|
|
2926
|
+
const clonedSvg = sceneEl.cloneNode(true);
|
|
2927
|
+
if (!clonedSvg.getAttribute("xmlns")) {
|
|
2928
|
+
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
2929
|
+
}
|
|
2930
|
+
const serializer2 = new XMLSerializer();
|
|
2931
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2932
|
+
${serializer2.serializeToString(clonedSvg)}`;
|
|
2933
|
+
}
|
|
2934
|
+
const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
|
|
2911
2935
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2912
2936
|
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
2913
2937
|
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
@@ -2940,19 +2964,35 @@ ${serializer2.serializeToString(clonedSvg)}`;
|
|
|
2940
2964
|
}
|
|
2941
2965
|
|
|
2942
2966
|
// src/export/inline-resources.ts
|
|
2967
|
+
var TRANSPARENT_PIXEL_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
2968
|
+
var RESOURCE_FETCH_TIMEOUT_MS = 3e3;
|
|
2969
|
+
var dataUriCache = /* @__PURE__ */ new Map();
|
|
2970
|
+
function shouldInlineUrl(url) {
|
|
2971
|
+
const trimmed = url.trim();
|
|
2972
|
+
return !!trimmed && !trimmed.startsWith("data:") && !trimmed.startsWith("blob:") && !trimmed.startsWith("#");
|
|
2973
|
+
}
|
|
2943
2974
|
async function toDataUri(url) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
const
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2975
|
+
if (dataUriCache.has(url)) return dataUriCache.get(url);
|
|
2976
|
+
const request = (async () => {
|
|
2977
|
+
const controller = new AbortController();
|
|
2978
|
+
const timeout = setTimeout(() => controller.abort(), RESOURCE_FETCH_TIMEOUT_MS);
|
|
2979
|
+
try {
|
|
2980
|
+
const resp = await fetch(url, { mode: "cors", credentials: "same-origin", signal: controller.signal });
|
|
2981
|
+
if (!resp.ok) return null;
|
|
2982
|
+
const arrayBuffer = await resp.arrayBuffer();
|
|
2983
|
+
const mimeType = resp.headers.get("Content-Type") || "application/octet-stream";
|
|
2984
|
+
const base64 = btoa(
|
|
2985
|
+
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
|
|
2986
|
+
);
|
|
2987
|
+
return `data:${mimeType};base64,${base64}`;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return null;
|
|
2990
|
+
} finally {
|
|
2991
|
+
clearTimeout(timeout);
|
|
2992
|
+
}
|
|
2993
|
+
})();
|
|
2994
|
+
dataUriCache.set(url, request);
|
|
2995
|
+
return request;
|
|
2956
2996
|
}
|
|
2957
2997
|
async function inlineCssUrls(cssValue) {
|
|
2958
2998
|
const urlPattern = /url\(["']?([^"')]+)["']?\)/g;
|
|
@@ -2960,16 +3000,20 @@ async function inlineCssUrls(cssValue) {
|
|
|
2960
3000
|
let m;
|
|
2961
3001
|
while ((m = urlPattern.exec(cssValue)) !== null) {
|
|
2962
3002
|
const src = m[1];
|
|
2963
|
-
if (
|
|
3003
|
+
if (shouldInlineUrl(src)) {
|
|
2964
3004
|
matches.push({ full: m[0], src });
|
|
2965
3005
|
}
|
|
2966
3006
|
}
|
|
2967
3007
|
let result = cssValue;
|
|
2968
3008
|
await Promise.all(
|
|
2969
3009
|
matches.map(async ({ full, src }) => {
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3010
|
+
try {
|
|
3011
|
+
const absoluteSrc = new URL(src, document.baseURI).href;
|
|
3012
|
+
const dataUri = await toDataUri(absoluteSrc);
|
|
3013
|
+
result = result.split(full).join(dataUri ? `url("${dataUri}")` : "none");
|
|
3014
|
+
} catch {
|
|
3015
|
+
result = result.split(full).join("none");
|
|
3016
|
+
}
|
|
2973
3017
|
})
|
|
2974
3018
|
);
|
|
2975
3019
|
return result;
|
|
@@ -2978,11 +3022,10 @@ async function inlineExternalResources(root) {
|
|
|
2978
3022
|
const tasks = [];
|
|
2979
3023
|
root.querySelectorAll("img").forEach((img) => {
|
|
2980
3024
|
const src = img.getAttribute("src");
|
|
2981
|
-
if (src &&
|
|
2982
|
-
const absoluteSrc = new URL(src, document.baseURI).href;
|
|
3025
|
+
if (src && shouldInlineUrl(src)) {
|
|
2983
3026
|
tasks.push(
|
|
2984
|
-
toDataUri(
|
|
2985
|
-
|
|
3027
|
+
Promise.resolve().then(() => toDataUri(new URL(src, document.baseURI).href)).then((dataUri) => {
|
|
3028
|
+
img.setAttribute("src", dataUri || TRANSPARENT_PIXEL_DATA_URI);
|
|
2986
3029
|
})
|
|
2987
3030
|
);
|
|
2988
3031
|
}
|
|
@@ -3000,53 +3043,178 @@ async function inlineExternalResources(root) {
|
|
|
3000
3043
|
});
|
|
3001
3044
|
await Promise.all(tasks);
|
|
3002
3045
|
}
|
|
3046
|
+
async function inlineSerializedSvgResources(svgXml) {
|
|
3047
|
+
const doc = new DOMParser().parseFromString(svgXml, "image/svg+xml");
|
|
3048
|
+
if (doc.querySelector("parsererror")) return svgXml;
|
|
3049
|
+
await inlineExternalResources(doc.documentElement);
|
|
3050
|
+
const serializer = new XMLSerializer();
|
|
3051
|
+
const serializedSvg = serializer.serializeToString(doc.documentElement);
|
|
3052
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3053
|
+
${serializedSvg}`;
|
|
3054
|
+
}
|
|
3003
3055
|
|
|
3004
|
-
// src/export/
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3056
|
+
// src/export/png-exporter.ts
|
|
3057
|
+
function cssColorComponentToByte(value) {
|
|
3058
|
+
const trimmed = value.trim();
|
|
3059
|
+
const numeric = trimmed.endsWith("%") ? Number.parseFloat(trimmed) / 100 : Number.parseFloat(trimmed);
|
|
3060
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
3061
|
+
return Math.round(Math.min(1, Math.max(0, numeric)) * 255);
|
|
3062
|
+
}
|
|
3063
|
+
function cssAlphaToNumber(value) {
|
|
3064
|
+
if (!value) return 1;
|
|
3065
|
+
const trimmed = value.trim();
|
|
3066
|
+
const numeric = trimmed.endsWith("%") ? Number.parseFloat(trimmed) / 100 : Number.parseFloat(trimmed);
|
|
3067
|
+
if (!Number.isFinite(numeric)) return 1;
|
|
3068
|
+
return Math.min(1, Math.max(0, numeric));
|
|
3069
|
+
}
|
|
3070
|
+
function normalizeCssColorFunctions(cssValue) {
|
|
3071
|
+
return cssValue.replace(/color\(\s*[-a-z0-9]+\s+([^)]*)\)/gi, (_match, rawComponents) => {
|
|
3072
|
+
const [rawChannels, rawAlpha] = String(rawComponents).split("/");
|
|
3073
|
+
const channels = rawChannels.trim().split(/\s+/);
|
|
3074
|
+
if (channels.length < 3) return "rgba(0, 0, 0, 0)";
|
|
3075
|
+
const red = cssColorComponentToByte(channels[0]);
|
|
3076
|
+
const green = cssColorComponentToByte(channels[1]);
|
|
3077
|
+
const blue = cssColorComponentToByte(channels[2]);
|
|
3078
|
+
const alpha = cssAlphaToNumber(rawAlpha);
|
|
3079
|
+
return alpha >= 1 ? `rgb(${red}, ${green}, ${blue})` : `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
function sanitizeHtml2CanvasStyles(root) {
|
|
3083
|
+
[root, ...Array.from(root.querySelectorAll("*"))].forEach((element) => {
|
|
3084
|
+
const style = element.getAttribute("style");
|
|
3085
|
+
if (style?.includes("color(")) {
|
|
3086
|
+
element.setAttribute("style", normalizeCssColorFunctions(style));
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
async function exportDiagramAsPng(containerEl, options = {}) {
|
|
3091
|
+
const canvas = await rasterizeDiagramToCanvas(containerEl, options, options.pixelRatio || 2);
|
|
3092
|
+
return new Promise((resolve, reject) => {
|
|
3093
|
+
canvas.toBlob((b) => {
|
|
3094
|
+
if (b) resolve(b);
|
|
3095
|
+
else reject(new Error("Canvas toBlob failed for PNG export"));
|
|
3096
|
+
}, "image/png");
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
async function rasterizeDiagramToCanvas(containerEl, options = {}, pixelRatio = 1) {
|
|
3100
|
+
const sceneEl = getDiagramSceneElement(containerEl);
|
|
3101
|
+
if (sceneEl.tagName?.toLowerCase() !== "svg") {
|
|
3102
|
+
const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
|
|
3103
|
+
await inlineExternalResources(clonedScene);
|
|
3104
|
+
sanitizeHtml2CanvasStyles(clonedScene);
|
|
3105
|
+
const host = document.createElement("div");
|
|
3106
|
+
Object.assign(host.style, {
|
|
3107
|
+
position: "fixed",
|
|
3108
|
+
left: "-100000px",
|
|
3109
|
+
top: "0",
|
|
3110
|
+
width: `${scaledWidth}px`,
|
|
3111
|
+
height: `${scaledHeight}px`,
|
|
3112
|
+
overflow: "hidden",
|
|
3113
|
+
pointerEvents: "none",
|
|
3114
|
+
zIndex: "-1"
|
|
3017
3115
|
});
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3116
|
+
host.setAttribute("aria-hidden", "true");
|
|
3117
|
+
host.appendChild(clonedScene);
|
|
3118
|
+
document.body.appendChild(host);
|
|
3119
|
+
try {
|
|
3120
|
+
await document.fonts?.ready;
|
|
3121
|
+
return await html2canvas(clonedScene, {
|
|
3122
|
+
allowTaint: false,
|
|
3123
|
+
backgroundColor: options.transparentBackground ? null : void 0,
|
|
3124
|
+
height: scaledHeight,
|
|
3125
|
+
imageTimeout: 3e3,
|
|
3126
|
+
logging: false,
|
|
3127
|
+
onclone: (clonedDocument, clonedElement) => {
|
|
3128
|
+
clonedDocument.querySelectorAll("style, link[rel='stylesheet']").forEach((styleNode) => styleNode.remove());
|
|
3129
|
+
sanitizeHtml2CanvasStyles(clonedElement);
|
|
3130
|
+
},
|
|
3131
|
+
scale: pixelRatio,
|
|
3132
|
+
useCORS: true,
|
|
3133
|
+
width: scaledWidth
|
|
3134
|
+
});
|
|
3135
|
+
} finally {
|
|
3136
|
+
host.remove();
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
const pixelRatioForSvg = pixelRatio || 1;
|
|
3140
|
+
const clonedContainer = containerEl.cloneNode(true);
|
|
3141
|
+
await inlineExternalResources(clonedContainer);
|
|
3142
|
+
const svgXml = await inlineSerializedSvgResources(exportDiagramAsVectorSvg(clonedContainer, options));
|
|
3143
|
+
const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
|
|
3144
|
+
const url = URL.createObjectURL(blob);
|
|
3145
|
+
const img = new Image();
|
|
3146
|
+
await new Promise((resolve, reject) => {
|
|
3147
|
+
img.onload = () => resolve();
|
|
3148
|
+
img.onerror = () => {
|
|
3149
|
+
URL.revokeObjectURL(url);
|
|
3150
|
+
reject(new Error("Failed to rasterize SVG into Image for PNG export"));
|
|
3151
|
+
};
|
|
3152
|
+
img.src = url;
|
|
3153
|
+
});
|
|
3154
|
+
const width = img.naturalWidth || 800;
|
|
3155
|
+
const height = img.naturalHeight || 400;
|
|
3156
|
+
const canvas = document.createElement("canvas");
|
|
3157
|
+
canvas.width = width * pixelRatioForSvg;
|
|
3158
|
+
canvas.height = height * pixelRatioForSvg;
|
|
3159
|
+
const ctx = canvas.getContext("2d");
|
|
3160
|
+
if (!ctx) {
|
|
3026
3161
|
URL.revokeObjectURL(url);
|
|
3162
|
+
throw new Error("Could not get 2D canvas context for PNG export");
|
|
3027
3163
|
}
|
|
3164
|
+
ctx.scale(pixelRatioForSvg, pixelRatioForSvg);
|
|
3165
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
3166
|
+
URL.revokeObjectURL(url);
|
|
3167
|
+
return canvas;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/export/gif-exporter.ts
|
|
3171
|
+
var nextFrame = () => new Promise((resolve) => {
|
|
3172
|
+
let settled = false;
|
|
3173
|
+
const finish = () => {
|
|
3174
|
+
if (settled) return;
|
|
3175
|
+
settled = true;
|
|
3176
|
+
resolve();
|
|
3177
|
+
};
|
|
3178
|
+
const fallback = setTimeout(finish, 80);
|
|
3179
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
3180
|
+
clearTimeout(fallback);
|
|
3181
|
+
finish();
|
|
3182
|
+
}));
|
|
3183
|
+
});
|
|
3184
|
+
var DEFAULT_GIF_PIXEL_RATIO = 0.3;
|
|
3185
|
+
var MAX_GIF_FRAMES = 24;
|
|
3186
|
+
async function rasterizeFrame(container, pixelRatio, options) {
|
|
3187
|
+
const canvas = await rasterizeDiagramToCanvas(container, options, pixelRatio);
|
|
3188
|
+
const context = canvas.getContext("2d");
|
|
3189
|
+
if (!context) throw new Error("Could not create GIF canvas context");
|
|
3190
|
+
return context.getImageData(0, 0, canvas.width, canvas.height);
|
|
3028
3191
|
}
|
|
3029
3192
|
async function exportDiagramAsGif(container, timeline, options = {}) {
|
|
3030
|
-
const
|
|
3193
|
+
const requestedFps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
|
|
3031
3194
|
const duration = timeline.duration();
|
|
3195
|
+
const fps = Math.min(requestedFps, MAX_GIF_FRAMES / Math.max(duration, 1e-3));
|
|
3032
3196
|
const frameCount = Math.max(1, Math.ceil(duration * fps));
|
|
3033
3197
|
const delayMs = Math.max(20, Math.round(1e3 / fps));
|
|
3198
|
+
const pixelRatio = options.pixelRatio ?? DEFAULT_GIF_PIXEL_RATIO;
|
|
3034
3199
|
const priorTime = timeline.currentTime();
|
|
3035
3200
|
const wasPlaying = timeline.isPlaying();
|
|
3036
3201
|
timeline.pause();
|
|
3037
3202
|
try {
|
|
3038
3203
|
const frames = [];
|
|
3204
|
+
timeline.seek(duration);
|
|
3205
|
+
await nextFrame();
|
|
3206
|
+
frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
|
|
3039
3207
|
for (let frame = 0; frame < frameCount; frame++) {
|
|
3040
3208
|
timeline.seek(Math.min(duration, frame / fps));
|
|
3041
3209
|
await nextFrame();
|
|
3042
3210
|
frames.push({
|
|
3043
|
-
imageData: await rasterizeFrame(container,
|
|
3211
|
+
imageData: await rasterizeFrame(container, pixelRatio, options),
|
|
3044
3212
|
delayMs
|
|
3045
3213
|
});
|
|
3046
3214
|
}
|
|
3047
3215
|
timeline.seek(duration);
|
|
3048
3216
|
await nextFrame();
|
|
3049
|
-
frames.push({ imageData: await rasterizeFrame(container,
|
|
3217
|
+
frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
|
|
3050
3218
|
const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
|
|
3051
3219
|
const bytes = new Uint8Array(encoded.byteLength);
|
|
3052
3220
|
bytes.set(encoded);
|
|
@@ -3057,44 +3225,6 @@ async function exportDiagramAsGif(container, timeline, options = {}) {
|
|
|
3057
3225
|
}
|
|
3058
3226
|
}
|
|
3059
3227
|
|
|
3060
|
-
// src/export/png-exporter.ts
|
|
3061
|
-
async function exportDiagramAsPng(containerEl, options = {}) {
|
|
3062
|
-
const pixelRatio = options.pixelRatio || 2;
|
|
3063
|
-
const clonedContainer = containerEl.cloneNode(true);
|
|
3064
|
-
await inlineExternalResources(clonedContainer);
|
|
3065
|
-
const svgXml = exportDiagramAsVectorSvg(clonedContainer, options);
|
|
3066
|
-
const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
|
|
3067
|
-
const url = URL.createObjectURL(blob);
|
|
3068
|
-
const img = new Image();
|
|
3069
|
-
await new Promise((resolve, reject) => {
|
|
3070
|
-
img.onload = () => resolve();
|
|
3071
|
-
img.onerror = () => {
|
|
3072
|
-
URL.revokeObjectURL(url);
|
|
3073
|
-
reject(new Error("Failed to rasterize SVG into Image for PNG export"));
|
|
3074
|
-
};
|
|
3075
|
-
img.src = url;
|
|
3076
|
-
});
|
|
3077
|
-
const width = img.naturalWidth || 800;
|
|
3078
|
-
const height = img.naturalHeight || 400;
|
|
3079
|
-
const canvas = document.createElement("canvas");
|
|
3080
|
-
canvas.width = width * pixelRatio;
|
|
3081
|
-
canvas.height = height * pixelRatio;
|
|
3082
|
-
const ctx = canvas.getContext("2d");
|
|
3083
|
-
if (!ctx) {
|
|
3084
|
-
URL.revokeObjectURL(url);
|
|
3085
|
-
throw new Error("Could not get 2D canvas context for PNG export");
|
|
3086
|
-
}
|
|
3087
|
-
ctx.scale(pixelRatio, pixelRatio);
|
|
3088
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
3089
|
-
URL.revokeObjectURL(url);
|
|
3090
|
-
return new Promise((resolve, reject) => {
|
|
3091
|
-
canvas.toBlob((b) => {
|
|
3092
|
-
if (b) resolve(b);
|
|
3093
|
-
else reject(new Error("Canvas toBlob failed for PNG export"));
|
|
3094
|
-
}, "image/png");
|
|
3095
|
-
});
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
3228
|
// src/presentation-controller.ts
|
|
3099
3229
|
var DiagramPresentationController = class {
|
|
3100
3230
|
diagram;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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,15 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"
|
|
47
|
+
"html2canvas": "^1.4.1",
|
|
48
|
+
"@markdy/core": "1.0.1"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"jsdom": "^29.1.1",
|
|
51
52
|
"tsup": "^8.5.1",
|
|
52
53
|
"typescript": "^5.9.3",
|
|
53
54
|
"vitest": "^4.1.7",
|
|
54
|
-
"@markdy/stdlib-systems": "1.0.
|
|
55
|
+
"@markdy/stdlib-systems": "1.0.1"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|