fix(export): apply layer masks in png export
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { Artboard } from "@core/artboard";
|
import type { Artboard } from "@core/artboard";
|
||||||
import type { Asset } from "@core/asset";
|
import type { Asset } from "@core/asset";
|
||||||
|
import type { Rect } from "@core/geometry";
|
||||||
import type { Layer } from "@core/layer";
|
import type { Layer } from "@core/layer";
|
||||||
|
|
||||||
export async function downloadArtboardPng(artboard: Artboard, assets: readonly Asset[]) {
|
export async function downloadArtboardPng(artboard: Artboard, assets: readonly Asset[]) {
|
||||||
@@ -19,7 +20,7 @@ export async function downloadArtboardPng(artboard: Artboard, assets: readonly A
|
|||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(-artboard.bounds.x, -artboard.bounds.y);
|
context.translate(-artboard.bounds.x, -artboard.bounds.y);
|
||||||
for (const layer of artboard.layers) await drawLayer(context, layer, assets);
|
for (const layer of artboard.layers) await drawLayer(context, layer, artboard.layers, assets, artboard.bounds);
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
const url = canvas.toDataURL("image/png");
|
const url = canvas.toDataURL("image/png");
|
||||||
@@ -29,14 +30,28 @@ export async function downloadArtboardPng(artboard: Artboard, assets: readonly A
|
|||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function drawLayer(context: CanvasRenderingContext2D, layer: Layer, assets: readonly Asset[]) {
|
async function drawLayer(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
layer: Layer,
|
||||||
|
layerTree: readonly Layer[],
|
||||||
|
assets: readonly Asset[],
|
||||||
|
artboardBounds: Rect,
|
||||||
|
ignoreOwnMask = false,
|
||||||
|
) {
|
||||||
if (!layer.visible) return;
|
if (!layer.visible) return;
|
||||||
|
|
||||||
|
if (!ignoreOwnMask && layer.clippingMask) {
|
||||||
|
const maskLayer = findLayer(layerTree, layer.clippingMask.maskLayerId);
|
||||||
|
if (!maskLayer) return;
|
||||||
|
await drawMaskedLayer(context, layer, maskLayer, layerTree, assets, artboardBounds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.globalAlpha *= layer.opacity;
|
context.globalAlpha *= layer.opacity;
|
||||||
|
|
||||||
if (layer.type === "group") {
|
if (layer.type === "group") {
|
||||||
for (const child of layer.children) await drawLayer(context, child, assets);
|
for (const child of layer.children) await drawLayer(context, child, layerTree, assets, artboardBounds);
|
||||||
context.restore();
|
context.restore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,6 +73,59 @@ async function drawLayer(context: CanvasRenderingContext2D, layer: Layer, assets
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function drawMaskedLayer(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
layer: Layer,
|
||||||
|
maskLayer: Layer,
|
||||||
|
layerTree: readonly Layer[],
|
||||||
|
assets: readonly Asset[],
|
||||||
|
artboardBounds: Rect,
|
||||||
|
) {
|
||||||
|
const layerCanvas = createArtboardCanvas(artboardBounds);
|
||||||
|
const layerContext = translatedContext(layerCanvas, artboardBounds);
|
||||||
|
await drawLayer(layerContext, layer, layerTree, assets, artboardBounds, true);
|
||||||
|
layerContext.restore();
|
||||||
|
|
||||||
|
const maskCanvas = createArtboardCanvas(artboardBounds);
|
||||||
|
const maskContext = translatedContext(maskCanvas, artboardBounds);
|
||||||
|
await drawLayer(maskContext, maskLayer, layerTree, assets, artboardBounds, true);
|
||||||
|
maskContext.restore();
|
||||||
|
|
||||||
|
const compositeContext = layerCanvas.getContext("2d");
|
||||||
|
if (!compositeContext) return;
|
||||||
|
compositeContext.globalCompositeOperation = "destination-in";
|
||||||
|
compositeContext.drawImage(maskCanvas, 0, 0);
|
||||||
|
compositeContext.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
|
context.drawImage(layerCanvas, artboardBounds.x, artboardBounds.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArtboardCanvas(bounds: Rect) {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.round(bounds.w));
|
||||||
|
canvas.height = Math.max(1, Math.round(bounds.h));
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function translatedContext(canvas: HTMLCanvasElement, bounds: Rect) {
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) throw new Error("Canvas 2D is not available");
|
||||||
|
context.save();
|
||||||
|
context.translate(-bounds.x, -bounds.y);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined {
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.id === layerId) return layer;
|
||||||
|
if (layer.type === "group") {
|
||||||
|
const child = findLayer(layer.children, layerId);
|
||||||
|
if (child) return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function loadImage(source: string) {
|
function loadImage(source: string) {
|
||||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|||||||
Reference in New Issue
Block a user