import { pngItemFromNote, pngLayout, type PngItem } from "@/lib/png-layout"; const INK = "#1a1a1a"; const PAPER = "#fbf6ee"; const SPACE_STROKE = "#c4b8a8"; function loadSvg(svg: SVGElement): Promise { const clone = svg.cloneNode(true) as SVGElement; if (!clone.getAttribute("xmlns")) clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); const xml = new XMLSerializer().serializeToString(clone); const url = URL.createObjectURL(new Blob([xml], { type: "image/svg+xml;charset=utf-8" })); return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { URL.revokeObjectURL(url); resolve(image); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Could not draw a chart.")); }; image.src = url; }); } export async function renderLinePng(exportRoot: HTMLElement, title: string): Promise { const slots = [...exportRoot.querySelectorAll("[data-export-slot]")]; const items: PngItem[] = []; const images: Array = []; for (const slot of slots) { const kind = slot.dataset.kind; if (kind !== "note" && kind !== "rest" && kind !== "space") continue; items.push( pngItemFromNote({ kind, pitch: slot.dataset.pitch ?? "", quarters: Number(slot.dataset.quarters), label: "", }), ); const svg = slot.querySelector("svg"); images.push(svg ? await loadSvg(svg) : null); } if (items.length === 0) throw new Error("Add a note, rest, or space before saving a PNG."); const layout = pngLayout(items, title); const scale = 2; const canvas = document.createElement("canvas"); canvas.width = Math.ceil(layout.width * scale); canvas.height = Math.ceil(layout.height * scale); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not start a canvas."); ctx.scale(scale, scale); ctx.fillStyle = PAPER; ctx.fillRect(0, 0, layout.width, layout.height); ctx.fillStyle = "#6a4328"; ctx.font = "600 12px Georgia, serif"; ctx.fillText("Soprano in C · Baroque · 4/4", layout.pad, layout.pad + 14); ctx.fillStyle = INK; ctx.font = "600 22px Georgia, serif"; ctx.fillText(title, layout.pad, layout.pad + 40); const glyphTop = layout.pad + 56; let x = layout.pad; items.forEach((item, index) => { const slotWidth = layout.slots[index]?.width ?? item.glyphWidth; if (item.kind === "space") { ctx.save(); ctx.strokeStyle = SPACE_STROKE; ctx.setLineDash([4, 4]); ctx.strokeRect(x + 6, glyphTop + 8, Math.max(8, item.glyphWidth - 12), layout.glyphHeight - 16); ctx.restore(); } else if (images[index]) { ctx.drawImage(images[index], x, glyphTop, item.glyphWidth, item.glyphHeight); } const center = x + item.glyphWidth / 2; ctx.textAlign = "center"; ctx.fillStyle = INK; ctx.font = "600 13px sans-serif"; ctx.fillText(item.caption, center, glyphTop + layout.glyphHeight + 18); ctx.textAlign = "start"; x += slotWidth + layout.gap; }); const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png")); if (!blob) throw new Error("Could not encode the PNG."); return blob; } export function downloadBlob(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1500); }