feat: add first-class text layers
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { resolveLayerDrawImage } from "./exportArtboardPng";
|
||||
import { resolveLayerDrawImage, resolveTextLayerDraw } from "./exportArtboardPng";
|
||||
|
||||
describe("artboard PNG export", () => {
|
||||
test("draws only cropped source pixels at their retained document position", () => {
|
||||
@@ -14,4 +14,11 @@ describe("artboard PNG export", () => {
|
||||
destination: { x: 120, y: 65, w: 80, h: 60 },
|
||||
});
|
||||
});
|
||||
|
||||
test("uses deterministic text data centered for renderer rotation parity", () => {
|
||||
const draw = resolveTextLayerDraw({ id: "text", type: "text", name: "Text", visible: true, locked: false, opacity: 1, transform: { position: { x: 10, y: 20 }, scale: { x: 2, y: 3 }, rotation: 1 }, content: "AB", style: { fontFamily: "Arial", fontSize: 10, fontWeight: 400, fontStyle: "normal", color: "#ffffff", alignment: "center", lineHeight: 1.2 } });
|
||||
expect(draw.size).toEqual({ w: 10.8, h: 12 });
|
||||
expect(draw.center).toEqual({ x: 20.8, y: 38 });
|
||||
expect(draw.textX).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Rect } from "@core/geometry";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { Size } from "@core/geometry";
|
||||
import { getLayerMask } from "@core/layer-mask-utils";
|
||||
import { measureTextLayer } from "@core/text-layer";
|
||||
|
||||
export async function downloadArtboardPng(artboard: Artboard, assets: readonly Asset[]) {
|
||||
const width = Math.max(1, Math.round(artboard.bounds.w));
|
||||
@@ -71,6 +72,12 @@ async function drawLayer(
|
||||
return;
|
||||
}
|
||||
|
||||
if (layer.type === "text") {
|
||||
drawTextLayer(context, layer);
|
||||
context.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = assets.find((candidate) => candidate.id === layer.assetId);
|
||||
if (!asset) {
|
||||
context.restore();
|
||||
@@ -84,6 +91,29 @@ async function drawLayer(
|
||||
context.restore();
|
||||
}
|
||||
|
||||
export function drawTextLayer(context: CanvasRenderingContext2D, layer: Extract<Layer, { type: "text" }>) {
|
||||
const draw = resolveTextLayerDraw(layer);
|
||||
const { size } = draw;
|
||||
context.translate(draw.center.x, draw.center.y);
|
||||
context.rotate(layer.transform.rotation);
|
||||
context.scale(layer.transform.scale.x, layer.transform.scale.y);
|
||||
context.fillStyle = layer.style.color;
|
||||
context.font = `${layer.style.fontStyle} ${layer.style.fontWeight} ${layer.style.fontSize}px ${JSON.stringify(layer.style.fontFamily)}, Arial, sans-serif`;
|
||||
context.textAlign = layer.style.alignment;
|
||||
context.textBaseline = "alphabetic";
|
||||
const x = draw.textX;
|
||||
layer.content.split("\n").forEach((line, index) => context.fillText(line, x, (index + 0.82) * layer.style.fontSize * layer.style.lineHeight - size.h / 2));
|
||||
}
|
||||
|
||||
export function resolveTextLayerDraw(layer: Extract<Layer, { type: "text" }>) {
|
||||
const size = measureTextLayer(layer);
|
||||
return {
|
||||
size,
|
||||
center: { x: layer.transform.position.x + size.w * layer.transform.scale.x / 2, y: layer.transform.position.y + size.h * layer.transform.scale.y / 2 },
|
||||
textX: (layer.style.alignment === "left" ? 0 : layer.style.alignment === "center" ? size.w / 2 : size.w) - size.w / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveLayerDrawImage(layer: Extract<Layer, { type: "image" | "raster" }>, intrinsicSize: Size) {
|
||||
const source = layer.sourceRect ?? { x: 0, y: 0, ...intrinsicSize };
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user