73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import type { Artboard } from "@core/artboard";
|
|
import type { Asset } from "@core/asset";
|
|
import type { Layer } from "@core/layer";
|
|
|
|
export async function downloadArtboardPng(artboard: Artboard, assets: readonly Asset[]) {
|
|
const width = Math.max(1, Math.round(artboard.bounds.w));
|
|
const height = Math.max(1, Math.round(artboard.bounds.h));
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
const context = canvas.getContext("2d");
|
|
if (!context) throw new Error("Canvas 2D is not available");
|
|
|
|
if (artboard.backgroundColor !== "transparent") {
|
|
context.fillStyle = artboard.backgroundColor;
|
|
context.fillRect(0, 0, width, height);
|
|
}
|
|
|
|
context.save();
|
|
context.translate(-artboard.bounds.x, -artboard.bounds.y);
|
|
for (const layer of artboard.layers) await drawLayer(context, layer, assets);
|
|
context.restore();
|
|
|
|
const url = canvas.toDataURL("image/png");
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `${safeFilename(artboard.name)}.png`;
|
|
link.click();
|
|
}
|
|
|
|
async function drawLayer(context: CanvasRenderingContext2D, layer: Layer, assets: readonly Asset[]) {
|
|
if (!layer.visible) return;
|
|
|
|
context.save();
|
|
context.globalAlpha *= layer.opacity;
|
|
|
|
if (layer.type === "group") {
|
|
for (const child of layer.children) await drawLayer(context, child, assets);
|
|
context.restore();
|
|
return;
|
|
}
|
|
|
|
const asset = assets.find((candidate) => candidate.id === layer.assetId);
|
|
if (!asset) {
|
|
context.restore();
|
|
return;
|
|
}
|
|
|
|
const image = await loadImage(asset.source);
|
|
context.drawImage(
|
|
image,
|
|
layer.transform.position.x,
|
|
layer.transform.position.y,
|
|
asset.intrinsicSize.w * layer.transform.scale.x,
|
|
asset.intrinsicSize.h * layer.transform.scale.y,
|
|
);
|
|
context.restore();
|
|
}
|
|
|
|
function loadImage(source: string) {
|
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
const image = new Image();
|
|
image.onload = () => resolve(image);
|
|
image.onerror = () => reject(new Error("Failed to load image for export"));
|
|
image.src = source;
|
|
});
|
|
}
|
|
|
|
function safeFilename(name: string) {
|
|
return name.trim().replace(/[^a-z0-9-_]+/gi, "-").replace(/^-+|-+$/g, "") || "artboard";
|
|
}
|