AppImage release / appimage (push) Successful in 50s
Dragging no longer selects text. The line box resizes, rests and spaces can be added, and Save PNG writes the row. Each chart keeps the foot at the top, and the treble staff shows the pitch with a notehead, stem, flags, and dots. Version 0.2.1. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { pngItemFromNote, pngLayout, type PngItem } from "@/lib/png-layout";
|
|
|
|
const INK = "#1a1a1a";
|
|
const PAPER = "#fbf6ee";
|
|
const SPACE_STROKE = "#c4b8a8";
|
|
|
|
function loadSvg(svg: SVGElement): Promise<HTMLImageElement> {
|
|
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<Blob> {
|
|
const slots = [...exportRoot.querySelectorAll<HTMLElement>("[data-export-slot]")];
|
|
const items: PngItem[] = [];
|
|
const images: Array<HTMLImageElement | null> = [];
|
|
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<Blob | null>((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);
|
|
}
|