From 232075ebcfac76299d43ec407a4d9951f5b1620d Mon Sep 17 00:00:00 2001 From: fand Date: Sat, 8 Jun 2024 00:17:03 -0700 Subject: [PATCH] feat: support nested elements in dom-to-canvas --- packages/react-vfx/src/dom-to-canvas.ts | 68 +++++++++++++++++++------ 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/packages/react-vfx/src/dom-to-canvas.ts b/packages/react-vfx/src/dom-to-canvas.ts index fa3ff6e..db575d9 100644 --- a/packages/react-vfx/src/dom-to-canvas.ts +++ b/packages/react-vfx/src/dom-to-canvas.ts @@ -11,7 +11,7 @@ const convertHtmlToXml = (html: string): string => { doc.documentElement.appendChild(range.createContextualFragment(html)); doc.documentElement.setAttribute( "xmlns", - doc.documentElement.namespaceURI! + doc.documentElement.namespaceURI!, ); // Get well-formed markup @@ -21,33 +21,40 @@ const convertHtmlToXml = (html: string): string => { // Clone DOM node. function cloneNode(node: T): T { - return node.cloneNode() as T; + return node.cloneNode(true) as T; } // Render element content to canvas and return it. -// ref. export default function getCanvasFromElement( element: HTMLElement, - oldCanvas?: HTMLCanvasElement + oldCanvas?: HTMLCanvasElement, ): Promise { const rect = element.getBoundingClientRect(); + const width = Math.max(rect.width * 1.01, rect.width + 1); // XXX + const height = Math.max(rect.height * 1.01, rect.height + 1); - const canvas = oldCanvas ?? document.createElement("canvas"); - canvas.width = Math.max(rect.width * 1.01, rect.width + 1); // XXX - canvas.height = Math.max(rect.height * 1.01, rect.height + 1); + const ratio = window.devicePixelRatio; + + const canvas = oldCanvas || document.createElement("canvas"); + canvas.width = width * ratio; + canvas.height = height * ratio; + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; // Clone element with styles in text attribute // to apply styles in SVG const newElement = cloneNode(element); const styles = window.getComputedStyle(element); - Array.from(styles).forEach((key) => { - newElement.style.setProperty( - key, - styles.getPropertyValue(key), - styles.getPropertyPriority(key) - ); + syncStylesOfTree(element, newElement); + + // Traverse and update input value + traverseDOM(newElement, (el) => { + if (el.tagName === "INPUT") { + el.setAttribute("value", (el as HTMLInputElement).value); + } else if (el.tagName === "TEXTAREA") { + el.innerHTML = (el as HTMLTextAreaElement).value; + } }); - newElement.innerHTML = element.innerHTML; // Wrap the element for text styling const wrapper = document.createElement("div"); @@ -69,11 +76,42 @@ export default function getCanvasFromElement( if (ctx === null) { return reject(); } + ctx.scale(ratio, ratio); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); resolve(canvas); }; img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }); } + +function traverseDOM( + element: HTMLElement, + callback: (e: HTMLElement) => void, +): void { + callback(element); + + element.childNodes.forEach((child) => { + if (child.nodeType === Node.ELEMENT_NODE) { + traverseDOM(child as HTMLElement, callback); + } + }); +} + +function syncStylesOfTree(el1: HTMLElement, el2: HTMLElement): void { + const styles = window.getComputedStyle(el1); + Array.from(styles).forEach((key) => { + el2.style.setProperty( + key, + styles.getPropertyValue(key), + styles.getPropertyPriority(key), + ); + }); + + for (let i = 0; i < el1.children.length; i++) { + const c1 = el1.children[i] as HTMLElement; + const c2 = el2.children[i] as HTMLElement; + syncStylesOfTree(c1, c2); + } +}