@markdy/renderer-dom 0.8.28 → 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 -6
- package/dist/index.js +265 -69
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -97,12 +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
|
-
|
|
106
100
|
interface PngExportOptions extends SvgExportOptions {
|
|
107
101
|
pixelRatio?: number;
|
|
108
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");
|
|
@@ -2939,50 +2963,258 @@ ${serializer2.serializeToString(clonedSvg)}`;
|
|
|
2939
2963
|
` + serializer.serializeToString(svg);
|
|
2940
2964
|
}
|
|
2941
2965
|
|
|
2942
|
-
// src/export/
|
|
2943
|
-
var
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
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
|
+
}
|
|
2974
|
+
async function toDataUri(url) {
|
|
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;
|
|
2996
|
+
}
|
|
2997
|
+
async function inlineCssUrls(cssValue) {
|
|
2998
|
+
const urlPattern = /url\(["']?([^"')]+)["']?\)/g;
|
|
2999
|
+
const matches = [];
|
|
3000
|
+
let m;
|
|
3001
|
+
while ((m = urlPattern.exec(cssValue)) !== null) {
|
|
3002
|
+
const src = m[1];
|
|
3003
|
+
if (shouldInlineUrl(src)) {
|
|
3004
|
+
matches.push({ full: m[0], src });
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
let result = cssValue;
|
|
3008
|
+
await Promise.all(
|
|
3009
|
+
matches.map(async ({ full, src }) => {
|
|
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
|
+
}
|
|
3017
|
+
})
|
|
3018
|
+
);
|
|
3019
|
+
return result;
|
|
3020
|
+
}
|
|
3021
|
+
async function inlineExternalResources(root) {
|
|
3022
|
+
const tasks = [];
|
|
3023
|
+
root.querySelectorAll("img").forEach((img) => {
|
|
3024
|
+
const src = img.getAttribute("src");
|
|
3025
|
+
if (src && shouldInlineUrl(src)) {
|
|
3026
|
+
tasks.push(
|
|
3027
|
+
Promise.resolve().then(() => toDataUri(new URL(src, document.baseURI).href)).then((dataUri) => {
|
|
3028
|
+
img.setAttribute("src", dataUri || TRANSPARENT_PIXEL_DATA_URI);
|
|
3029
|
+
})
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
const allEls = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
3034
|
+
allEls.forEach((el) => {
|
|
3035
|
+
const style = el.getAttribute("style");
|
|
3036
|
+
if (style && style.includes("url(")) {
|
|
3037
|
+
tasks.push(
|
|
3038
|
+
inlineCssUrls(style).then((inlined) => {
|
|
3039
|
+
if (inlined !== style) el.setAttribute("style", inlined);
|
|
3040
|
+
})
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
await Promise.all(tasks);
|
|
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
|
+
}
|
|
3055
|
+
|
|
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"
|
|
2953
3115
|
});
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
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) {
|
|
2962
3161
|
URL.revokeObjectURL(url);
|
|
3162
|
+
throw new Error("Could not get 2D canvas context for PNG export");
|
|
2963
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);
|
|
2964
3191
|
}
|
|
2965
3192
|
async function exportDiagramAsGif(container, timeline, options = {}) {
|
|
2966
|
-
const
|
|
3193
|
+
const requestedFps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
|
|
2967
3194
|
const duration = timeline.duration();
|
|
3195
|
+
const fps = Math.min(requestedFps, MAX_GIF_FRAMES / Math.max(duration, 1e-3));
|
|
2968
3196
|
const frameCount = Math.max(1, Math.ceil(duration * fps));
|
|
2969
3197
|
const delayMs = Math.max(20, Math.round(1e3 / fps));
|
|
3198
|
+
const pixelRatio = options.pixelRatio ?? DEFAULT_GIF_PIXEL_RATIO;
|
|
2970
3199
|
const priorTime = timeline.currentTime();
|
|
2971
3200
|
const wasPlaying = timeline.isPlaying();
|
|
2972
3201
|
timeline.pause();
|
|
2973
3202
|
try {
|
|
2974
3203
|
const frames = [];
|
|
3204
|
+
timeline.seek(duration);
|
|
3205
|
+
await nextFrame();
|
|
3206
|
+
frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
|
|
2975
3207
|
for (let frame = 0; frame < frameCount; frame++) {
|
|
2976
3208
|
timeline.seek(Math.min(duration, frame / fps));
|
|
2977
3209
|
await nextFrame();
|
|
2978
3210
|
frames.push({
|
|
2979
|
-
imageData: await rasterizeFrame(container,
|
|
3211
|
+
imageData: await rasterizeFrame(container, pixelRatio, options),
|
|
2980
3212
|
delayMs
|
|
2981
3213
|
});
|
|
2982
3214
|
}
|
|
2983
3215
|
timeline.seek(duration);
|
|
2984
3216
|
await nextFrame();
|
|
2985
|
-
frames.push({ imageData: await rasterizeFrame(container,
|
|
3217
|
+
frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
|
|
2986
3218
|
const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
|
|
2987
3219
|
const bytes = new Uint8Array(encoded.byteLength);
|
|
2988
3220
|
bytes.set(encoded);
|
|
@@ -2993,42 +3225,6 @@ async function exportDiagramAsGif(container, timeline, options = {}) {
|
|
|
2993
3225
|
}
|
|
2994
3226
|
}
|
|
2995
3227
|
|
|
2996
|
-
// src/export/png-exporter.ts
|
|
2997
|
-
async function exportDiagramAsPng(containerEl, options = {}) {
|
|
2998
|
-
const svgXml = exportDiagramAsVectorSvg(containerEl, options);
|
|
2999
|
-
const pixelRatio = options.pixelRatio || 2;
|
|
3000
|
-
const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
|
|
3001
|
-
const url = URL.createObjectURL(blob);
|
|
3002
|
-
const img = new Image();
|
|
3003
|
-
await new Promise((resolve, reject) => {
|
|
3004
|
-
img.onload = () => resolve();
|
|
3005
|
-
img.onerror = () => {
|
|
3006
|
-
URL.revokeObjectURL(url);
|
|
3007
|
-
reject(new Error("Failed to rasterize SVG into Image for PNG export"));
|
|
3008
|
-
};
|
|
3009
|
-
img.src = url;
|
|
3010
|
-
});
|
|
3011
|
-
const width = img.naturalWidth || 800;
|
|
3012
|
-
const height = img.naturalHeight || 400;
|
|
3013
|
-
const canvas = document.createElement("canvas");
|
|
3014
|
-
canvas.width = width * pixelRatio;
|
|
3015
|
-
canvas.height = height * pixelRatio;
|
|
3016
|
-
const ctx = canvas.getContext("2d");
|
|
3017
|
-
if (!ctx) {
|
|
3018
|
-
URL.revokeObjectURL(url);
|
|
3019
|
-
throw new Error("Could not get 2D canvas context for PNG export");
|
|
3020
|
-
}
|
|
3021
|
-
ctx.scale(pixelRatio, pixelRatio);
|
|
3022
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
3023
|
-
URL.revokeObjectURL(url);
|
|
3024
|
-
return new Promise((resolve, reject) => {
|
|
3025
|
-
canvas.toBlob((b) => {
|
|
3026
|
-
if (b) resolve(b);
|
|
3027
|
-
else reject(new Error("Canvas toBlob failed for PNG export"));
|
|
3028
|
-
}, "image/png");
|
|
3029
|
-
});
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
3228
|
// src/presentation-controller.ts
|
|
3033
3229
|
var DiagramPresentationController = class {
|
|
3034
3230
|
diagram;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "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": "0.
|
|
55
|
+
"@markdy/stdlib-systems": "1.0.1"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|