feat: add first-class text layers

This commit is contained in:
syntaxbullet
2026-07-11 12:38:34 +02:00
parent 606426c885
commit 37aa719047
33 changed files with 315 additions and 41 deletions

44
core/text-layer.ts Normal file
View File

@@ -0,0 +1,44 @@
import type { BaseLayer } from "./base-layer";
import type { Size } from "./geometry";
export const builtInTextFonts = ["Arial", "Georgia", "Courier New", "Trebuchet MS"] as const;
export type TextFontFamily = typeof builtInTextFonts[number];
export type TextAlignment = "left" | "center" | "right";
export type TextFontStyle = "normal" | "italic";
export type TextFontWeight = 400 | 700;
export type TextStyle = {
fontFamily: TextFontFamily;
fontSize: number;
fontWeight: TextFontWeight;
fontStyle: TextFontStyle;
color: string;
alignment: TextAlignment;
lineHeight: number;
};
export type TextLayer = BaseLayer & {
type: "text";
content: string;
style: TextStyle;
};
/** Deterministic layout box shared by selection, renderer and export. */
export function measureTextLayer(layer: Pick<TextLayer, "content" | "style">): Size {
const lines = layer.content.split("\n");
const weightFactor = layer.style.fontWeight === 700 ? 1.04 : 1;
const italicFactor = layer.style.fontStyle === "italic" ? 1.03 : 1;
const familyFactor = layer.style.fontFamily === "Courier New" ? 0.62 : layer.style.fontFamily === "Georgia" ? 0.56 : 0.54;
const longest = Math.max(1, ...lines.map((line) => [...line].length));
return {
w: Math.max(1, longest * layer.style.fontSize * familyFactor * weightFactor * italicFactor),
h: Math.max(1, lines.length * layer.style.fontSize * layer.style.lineHeight),
};
}
export function isValidTextStyle(style: TextStyle): boolean {
return builtInTextFonts.includes(style.fontFamily) && Number.isFinite(style.fontSize) && style.fontSize >= 1 && style.fontSize <= 1000
&& (style.fontWeight === 400 || style.fontWeight === 700) && (style.fontStyle === "normal" || style.fontStyle === "italic")
&& /^#[0-9a-f]{6}$/i.test(style.color) && (style.alignment === "left" || style.alignment === "center" || style.alignment === "right")
&& Number.isFinite(style.lineHeight) && style.lineHeight >= 0.5 && style.lineHeight <= 5;
}