From 8db3bead13441a5882d8689dfc6a9f09c82375d9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Mar 2026 16:47:22 +0100 Subject: feat: masks, properties refactor, shaders, storage migrations, and more --- apps/web/src/lib/text/layout.ts | 29 +++++-- apps/web/src/lib/text/measure-element.ts | 132 +++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/lib/text/measure-element.ts (limited to 'apps/web/src/lib/text') diff --git a/apps/web/src/lib/text/layout.ts b/apps/web/src/lib/text/layout.ts index 3eadcec2..32d0d80e 100644 --- a/apps/web/src/lib/text/layout.ts +++ b/apps/web/src/lib/text/layout.ts @@ -1,5 +1,5 @@ -import { DEFAULT_TEXT_BACKGROUND } from "@/constants/text-constants"; -import type { TextBackground, TextElement } from "@/types/timeline"; +import type { TextBackground, TextElement } from "@/lib/timeline"; +import { DEFAULTS } from "@/lib/timeline/defaults"; type TextRect = { left: number; @@ -14,6 +14,23 @@ export interface TextBlockMeasurement { maxWidth: number; } +type CanvasContext = + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D; + +export function setCanvasLetterSpacing({ + ctx, + letterSpacingPx, +}: { + ctx: CanvasContext; + letterSpacingPx: number; +}): void { + if ("letterSpacing" in ctx) { + (ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = + `${letterSpacingPx}px`; + } +} + export function getMetricAscent({ metrics, fallbackFontSize, @@ -118,11 +135,11 @@ export function getTextBackgroundRect({ const textRect = getTextRect({ textAlign, block }); const paddingX = - (background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) * fontSizeRatio; + (background.paddingX ?? DEFAULTS.text.background.paddingX) * fontSizeRatio; const paddingY = - (background.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY) * fontSizeRatio; - const offsetX = background.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX; - const offsetY = background.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY; + (background.paddingY ?? DEFAULTS.text.background.paddingY) * fontSizeRatio; + const offsetX = background.offsetX ?? DEFAULTS.text.background.offsetX; + const offsetY = background.offsetY ?? DEFAULTS.text.background.offsetY; return { left: textRect.left - paddingX + offsetX, diff --git a/apps/web/src/lib/text/measure-element.ts b/apps/web/src/lib/text/measure-element.ts new file mode 100644 index 00000000..44b2b1ff --- /dev/null +++ b/apps/web/src/lib/text/measure-element.ts @@ -0,0 +1,132 @@ +import { + CORNER_RADIUS_MIN, + FONT_SIZE_SCALE_REFERENCE, +} from "@/constants/text-constants"; +import { resolveNumberAtTime } from "@/lib/animation"; +import { DEFAULTS } from "@/lib/timeline/defaults"; +import type { TextBackground, TextElement } from "@/lib/timeline"; +import { + measureTextBlock, + setCanvasLetterSpacing, + getTextVisualRect, + type TextBlockMeasurement, +} from "./layout"; + +export interface ResolvedTextBackground extends TextBackground { + paddingX: number; + paddingY: number; + offsetX: number; + offsetY: number; + cornerRadius: number; +} + +export interface MeasuredTextElement { + scaledFontSize: number; + fontString: string; + letterSpacing: number; + lineHeightPx: number; + lines: string[]; + lineMetrics: TextMetrics[]; + block: TextBlockMeasurement; + fontSizeRatio: number; + resolvedBackground: ResolvedTextBackground; + visualRect: { left: number; top: number; width: number; height: number }; +} + +/** + * Shared text measurement used by both the renderer and preview bounds. + * Accepts the canvas context to measure on so callers can reuse an existing + * context (e.g. the renderer's) rather than creating a throwaway canvas. + * The context state is preserved via save/restore. + */ +export function measureTextElement({ + element, + canvasHeight, + localTime, + ctx, +}: { + element: TextElement; + canvasHeight: number; + localTime: number; + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; +}): MeasuredTextElement { + const scaledFontSize = + element.fontSize * (canvasHeight / FONT_SIZE_SCALE_REFERENCE); + const fontWeight = element.fontWeight === "bold" ? "bold" : "normal"; + const fontStyle = element.fontStyle === "italic" ? "italic" : "normal"; + const fontFamily = `"${element.fontFamily.replace(/"/g, '\\"')}"`; + const fontString = `${fontStyle} ${fontWeight} ${scaledFontSize}px ${fontFamily}, sans-serif`; + const letterSpacing = element.letterSpacing ?? 0; + const lineHeightPx = + scaledFontSize * (element.lineHeight ?? DEFAULTS.text.lineHeight); + const fontSizeRatio = element.fontSize / DEFAULTS.text.element.fontSize; + const lines = element.content.split("\n"); + + ctx.save(); + ctx.font = fontString; + ctx.textBaseline = "middle"; + setCanvasLetterSpacing({ ctx, letterSpacingPx: letterSpacing }); + const lineMetrics = lines.map((line) => ctx.measureText(line)); + ctx.restore(); + + const block = measureTextBlock({ + lineMetrics, + lineHeightPx, + fallbackFontSize: scaledFontSize, + }); + + const bg = element.background; + const resolvedBackground: ResolvedTextBackground = { + ...bg, + paddingX: resolveNumberAtTime({ + baseValue: bg.paddingX ?? DEFAULTS.text.background.paddingX, + animations: element.animations, + propertyPath: "background.paddingX", + localTime, + }), + paddingY: resolveNumberAtTime({ + baseValue: bg.paddingY ?? DEFAULTS.text.background.paddingY, + animations: element.animations, + propertyPath: "background.paddingY", + localTime, + }), + offsetX: resolveNumberAtTime({ + baseValue: bg.offsetX ?? DEFAULTS.text.background.offsetX, + animations: element.animations, + propertyPath: "background.offsetX", + localTime, + }), + offsetY: resolveNumberAtTime({ + baseValue: bg.offsetY ?? DEFAULTS.text.background.offsetY, + animations: element.animations, + propertyPath: "background.offsetY", + localTime, + }), + cornerRadius: resolveNumberAtTime({ + baseValue: bg.cornerRadius ?? CORNER_RADIUS_MIN, + animations: element.animations, + propertyPath: "background.cornerRadius", + localTime, + }), + }; + + const visualRect = getTextVisualRect({ + textAlign: element.textAlign, + block, + background: resolvedBackground, + fontSizeRatio, + }); + + return { + scaledFontSize, + fontString, + letterSpacing, + lineHeightPx, + lines, + lineMetrics, + block, + fontSizeRatio, + resolvedBackground, + visualRect, + }; +} -- cgit v1.3.1