Skip to content

Commit

Permalink
feat: support nested elements in dom-to-canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
fand committed Jun 8, 2024
1 parent 0f68613 commit 232075e
Showing 1 changed file with 53 additions and 15 deletions.
68 changes: 53 additions & 15 deletions packages/react-vfx/src/dom-to-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,33 +21,40 @@ const convertHtmlToXml = (html: string): string => {

// Clone DOM node.
function cloneNode<T extends Node>(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<HTMLCanvasElement> {
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");
Expand All @@ -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);
}
}

0 comments on commit 232075e

Please sign in to comment.