summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/text
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/text
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/text')
-rw-r--r--apps/web/src/lib/text/background.ts2
-rw-r--r--apps/web/src/lib/text/layout.ts226
-rw-r--r--apps/web/src/lib/text/measure-element.ts133
-rw-r--r--apps/web/src/lib/text/primitives.ts241
-rw-r--r--apps/web/src/lib/text/typography.ts9
5 files changed, 0 insertions, 611 deletions
diff --git a/apps/web/src/lib/text/background.ts b/apps/web/src/lib/text/background.ts
deleted file mode 100644
index be5ab6f5..00000000
--- a/apps/web/src/lib/text/background.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const CORNER_RADIUS_MIN = 0;
-export const CORNER_RADIUS_MAX = 100;
diff --git a/apps/web/src/lib/text/layout.ts b/apps/web/src/lib/text/layout.ts
deleted file mode 100644
index a3badfdc..00000000
--- a/apps/web/src/lib/text/layout.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import type { TextBackground, TextElement } from "@/lib/timeline";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-
-type TextRect = {
- left: number;
- top: number;
- width: number;
- height: number;
-};
-
-export interface TextBlockMeasurement {
- visualCenterOffset: number;
- height: number;
- maxWidth: number;
-}
-
-export type TextCanvasContext =
- | CanvasRenderingContext2D
- | OffscreenCanvasRenderingContext2D;
-
-const TEXT_DECORATION_THICKNESS_RATIO = 0.07;
-const STRIKETHROUGH_VERTICAL_RATIO = 0.35;
-
-export function setCanvasLetterSpacing({
- ctx,
- letterSpacingPx,
-}: {
- ctx: TextCanvasContext;
- letterSpacingPx: number;
-}): void {
- if ("letterSpacing" in ctx) {
- (ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing =
- `${letterSpacingPx}px`;
- }
-}
-
-export function getMetricAscent({
- metrics,
- fallbackFontSize,
-}: {
- metrics: TextMetrics;
- fallbackFontSize: number;
-}): number {
- return metrics.actualBoundingBoxAscent ?? fallbackFontSize * 0.8;
-}
-
-export function getMetricDescent({
- metrics,
- fallbackFontSize,
-}: {
- metrics: TextMetrics;
- fallbackFontSize: number;
-}): number {
- return metrics.actualBoundingBoxDescent ?? fallbackFontSize * 0.2;
-}
-
-export function measureTextBlock({
- lineMetrics,
- lineHeightPx,
-}: {
- lineMetrics: TextMetrics[];
- lineHeightPx: number;
-}): TextBlockMeasurement {
- let maxWidth = 0;
-
- for (const metrics of lineMetrics) {
- maxWidth = Math.max(maxWidth, metrics.width);
- }
-
- const lineCount = lineMetrics.length;
- const height = lineCount * lineHeightPx;
- const visualCenterOffset = ((lineCount - 1) * lineHeightPx) / 2;
-
- return { visualCenterOffset, height, maxWidth };
-}
-
-function getTextRect({
- textAlign,
- block,
-}: {
- textAlign: TextElement["textAlign"];
- block: TextBlockMeasurement;
-}): TextRect {
- const textAlignToLeft: Record<typeof textAlign, number> = {
- left: 0,
- right: -block.maxWidth,
- center: -block.maxWidth / 2,
- };
- const left = textAlignToLeft[textAlign];
-
- return {
- left,
- top: -block.height / 2,
- width: block.maxWidth,
- height: block.height,
- };
-}
-
-function isTextBackgroundVisible({
- background,
-}: {
- background: TextBackground;
-}): boolean {
- return (
- background.enabled &&
- Boolean(background.color) &&
- background.color !== "transparent"
- );
-}
-
-export function getTextBackgroundRect({
- textAlign,
- block,
- background,
- fontSizeRatio = 1,
-}: {
- textAlign: TextElement["textAlign"];
- block: TextBlockMeasurement;
- background: TextBackground;
- fontSizeRatio?: number;
-}): TextRect | null {
- if (!isTextBackgroundVisible({ background })) {
- return null;
- }
-
- const textRect = getTextRect({ textAlign, block });
- const paddingX =
- (background.paddingX ?? DEFAULTS.text.background.paddingX) * fontSizeRatio;
- const paddingY =
- (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,
- top: textRect.top - paddingY + offsetY,
- width: textRect.width + paddingX * 2,
- height: textRect.height + paddingY * 2,
- };
-}
-
-export function getTextVisualRect({
- textAlign,
- block,
- background,
- fontSizeRatio = 1,
-}: {
- textAlign: TextElement["textAlign"];
- block: TextBlockMeasurement;
- background: TextBackground;
- fontSizeRatio?: number;
-}): TextRect {
- const textRect = getTextRect({ textAlign, block });
- const backgroundRect = getTextBackgroundRect({
- textAlign,
- block,
- background,
- fontSizeRatio,
- });
-
- if (!backgroundRect) {
- return textRect;
- }
-
- const left = Math.min(textRect.left, backgroundRect.left);
- const top = Math.min(textRect.top, backgroundRect.top);
- const right = Math.max(
- textRect.left + textRect.width,
- backgroundRect.left + backgroundRect.width,
- );
- const bottom = Math.max(
- textRect.top + textRect.height,
- backgroundRect.top + backgroundRect.height,
- );
-
- return {
- left,
- top,
- width: right - left,
- height: bottom - top,
- };
-}
-
-export function drawTextDecoration({
- ctx,
- textDecoration,
- lineWidth,
- lineY,
- metrics,
- scaledFontSize,
- textAlign,
-}: {
- ctx: TextCanvasContext;
- textDecoration: string;
- lineWidth: number;
- lineY: number;
- metrics: TextMetrics;
- scaledFontSize: number;
- textAlign: CanvasTextAlign;
-}): void {
- if (textDecoration === "none" || !textDecoration) return;
-
- const thickness = Math.max(
- 1,
- scaledFontSize * TEXT_DECORATION_THICKNESS_RATIO,
- );
- const ascent = getMetricAscent({ metrics, fallbackFontSize: scaledFontSize });
- const descent = getMetricDescent({
- metrics,
- fallbackFontSize: scaledFontSize,
- });
-
- let xStart = -lineWidth / 2;
- if (textAlign === "left") xStart = 0;
- if (textAlign === "right") xStart = -lineWidth;
-
- if (textDecoration === "underline") {
- const underlineY = lineY + descent + thickness;
- ctx.fillRect(xStart, underlineY, lineWidth, thickness);
- }
-
- if (textDecoration === "line-through") {
- const strikeY = lineY - (ascent - descent) * STRIKETHROUGH_VERTICAL_RATIO;
- ctx.fillRect(xStart, strikeY, lineWidth, thickness);
- }
-}
diff --git a/apps/web/src/lib/text/measure-element.ts b/apps/web/src/lib/text/measure-element.ts
deleted file mode 100644
index 0b107587..00000000
--- a/apps/web/src/lib/text/measure-element.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { CORNER_RADIUS_MIN } from "@/lib/text/background";
-import { resolveNumberAtTime } from "@/lib/animation";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import type { TextBackground, TextElement } from "@/lib/timeline";
-import {
- getTextVisualRect,
-} from "./layout";
-import {
- measureTextLayout,
- type MeasuredTextLayout,
-} from "./primitives";
-
-export interface ResolvedTextBackground extends TextBackground {
- paddingX: number;
- paddingY: number;
- offsetX: number;
- offsetY: number;
- cornerRadius: number;
-}
-
-export interface MeasuredTextElement extends MeasuredTextLayout {
- resolvedBackground: ResolvedTextBackground;
- visualRect: { left: number; top: number; width: number; height: number };
-}
-
-let textMeasurementContext:
- | CanvasRenderingContext2D
- | OffscreenCanvasRenderingContext2D
- | null = null;
-
-export function getTextMeasurementContext():
- | CanvasRenderingContext2D
- | OffscreenCanvasRenderingContext2D {
- if (textMeasurementContext) {
- return textMeasurementContext;
- }
-
- if (typeof OffscreenCanvas !== "undefined") {
- const canvas = new OffscreenCanvas(1, 1);
- const context = canvas.getContext("2d");
- if (context) {
- textMeasurementContext = context;
- return context;
- }
- }
-
- if (typeof document !== "undefined") {
- const canvas = document.createElement("canvas");
- const context = canvas.getContext("2d");
- if (context) {
- textMeasurementContext = context;
- return context;
- }
- }
-
- throw new Error("Failed to create text measurement context");
-}
-
-export function measureTextElement({
- element,
- canvasHeight,
- localTime,
- ctx,
-}: {
- element: TextElement;
- canvasHeight: number;
- localTime: number;
- ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
-}): MeasuredTextElement {
- const measuredLayout = measureTextLayout({
- text: {
- content: element.content,
- fontSize: element.fontSize,
- fontFamily: element.fontFamily,
- fontWeight: element.fontWeight,
- fontStyle: element.fontStyle,
- textAlign: element.textAlign,
- textDecoration: element.textDecoration,
- letterSpacing: element.letterSpacing,
- lineHeight: element.lineHeight,
- },
- canvasHeight,
- ctx,
- });
-
- 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: measuredLayout.block,
- background: resolvedBackground,
- fontSizeRatio: measuredLayout.fontSizeRatio,
- });
-
- return {
- ...measuredLayout,
- resolvedBackground,
- visualRect,
- };
-}
diff --git a/apps/web/src/lib/text/primitives.ts b/apps/web/src/lib/text/primitives.ts
deleted file mode 100644
index 20674c3c..00000000
--- a/apps/web/src/lib/text/primitives.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-import type { TextCanvasContext, TextBlockMeasurement } from "@/lib/text/layout";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { clamp } from "@/utils/math";
-import { CORNER_RADIUS_MAX, CORNER_RADIUS_MIN } from "./background";
-import {
- drawTextDecoration,
- getTextBackgroundRect,
- measureTextBlock,
- setCanvasLetterSpacing,
-} from "./layout";
-import { FONT_SIZE_SCALE_REFERENCE } from "./typography";
-
-export type TextAlign = "left" | "center" | "right";
-export type TextFontWeight = "normal" | "bold";
-export type TextFontStyle = "normal" | "italic";
-export type TextDecoration = "none" | "underline" | "line-through";
-
-export interface TextLayoutParams {
- content: string;
- fontSize: number;
- fontFamily: string;
- fontWeight: TextFontWeight;
- fontStyle: TextFontStyle;
- textAlign: TextAlign;
- textDecoration?: TextDecoration;
- letterSpacing?: number;
- lineHeight?: number;
-}
-
-export interface ResolvedTextLayout {
- scaledFontSize: number;
- fontString: string;
- letterSpacing: number;
- lineHeightPx: number;
- fontSizeRatio: number;
- textAlign: TextAlign;
- textDecoration: TextDecoration;
-}
-
-export interface MeasuredTextLayout extends ResolvedTextLayout {
- lines: string[];
- lineMetrics: TextMetrics[];
- block: TextBlockMeasurement;
-}
-
-export interface ResolvedTextBackgroundLike {
- enabled: boolean;
- color: string;
- paddingX: number;
- paddingY: number;
- offsetX: number;
- offsetY: number;
- cornerRadius: number;
-}
-
-export function quoteFontFamily({ fontFamily }: { fontFamily: string }): string {
- return `"${fontFamily.replace(/"/g, '\\"')}"`;
-}
-
-export function buildTextFontString({
- fontFamily,
- fontWeight,
- fontStyle,
- scaledFontSize,
-}: {
- fontFamily: string;
- fontWeight: TextFontWeight;
- fontStyle: TextFontStyle;
- scaledFontSize: number;
-}): string {
- return `${fontStyle} ${fontWeight} ${scaledFontSize}px ${quoteFontFamily({ fontFamily })}, sans-serif`;
-}
-
-export function resolveTextLayout({
- text,
- canvasHeight,
-}: {
- text: TextLayoutParams;
- canvasHeight: number;
-}): ResolvedTextLayout {
- const scaledFontSize =
- text.fontSize * (canvasHeight / FONT_SIZE_SCALE_REFERENCE);
- const fontWeight = text.fontWeight === "bold" ? "bold" : "normal";
- const fontStyle = text.fontStyle === "italic" ? "italic" : "normal";
- const letterSpacing = text.letterSpacing ?? DEFAULTS.text.letterSpacing;
- const lineHeightPx =
- scaledFontSize * (text.lineHeight ?? DEFAULTS.text.lineHeight);
- const fontSizeRatio = text.fontSize / DEFAULTS.text.element.fontSize;
-
- return {
- scaledFontSize,
- fontString: buildTextFontString({
- fontFamily: text.fontFamily,
- fontWeight,
- fontStyle,
- scaledFontSize,
- }),
- letterSpacing,
- lineHeightPx,
- fontSizeRatio,
- textAlign: text.textAlign,
- textDecoration: text.textDecoration ?? "none",
- };
-}
-
-export function measureTextLayout({
- text,
- canvasHeight,
- ctx,
-}: {
- text: TextLayoutParams;
- canvasHeight: number;
- ctx: TextCanvasContext;
-}): MeasuredTextLayout {
- const resolvedLayout = resolveTextLayout({ text, canvasHeight });
- const lines = text.content.split("\n");
-
- ctx.save();
- ctx.font = resolvedLayout.fontString;
- ctx.textBaseline = "middle";
- setCanvasLetterSpacing({
- ctx,
- letterSpacingPx: resolvedLayout.letterSpacing,
- });
- const lineMetrics = lines.map((line) => ctx.measureText(line));
- ctx.restore();
-
- const block = measureTextBlock({
- lineMetrics,
- lineHeightPx: resolvedLayout.lineHeightPx,
- });
-
- return {
- ...resolvedLayout,
- lines,
- lineMetrics,
- block,
- };
-}
-
-export function drawMeasuredTextLayout({
- ctx,
- layout,
- textColor,
- background,
- backgroundColor,
- textBaseline = "middle",
-}: {
- ctx: TextCanvasContext;
- layout: MeasuredTextLayout;
- textColor: string;
- background?: ResolvedTextBackgroundLike | null;
- backgroundColor?: string;
- textBaseline?: CanvasTextBaseline;
-}): void {
- ctx.font = layout.fontString;
- ctx.textAlign = layout.textAlign;
- ctx.textBaseline = textBaseline;
- ctx.fillStyle = textColor;
- setCanvasLetterSpacing({ ctx, letterSpacingPx: layout.letterSpacing });
-
- if (
- background?.enabled &&
- backgroundColor &&
- backgroundColor !== "transparent" &&
- layout.lines.length > 0
- ) {
- const backgroundRect = getTextBackgroundRect({
- textAlign: layout.textAlign,
- block: layout.block,
- background: {
- ...background,
- color: backgroundColor,
- },
- fontSizeRatio: layout.fontSizeRatio,
- });
- if (backgroundRect) {
- const p =
- clamp({
- value: background.cornerRadius,
- min: CORNER_RADIUS_MIN,
- max: CORNER_RADIUS_MAX,
- }) / 100;
- const radius =
- (Math.min(backgroundRect.width, backgroundRect.height) / 2) * p;
- ctx.fillStyle = backgroundColor;
- ctx.beginPath();
- ctx.roundRect(
- backgroundRect.left,
- backgroundRect.top,
- backgroundRect.width,
- backgroundRect.height,
- radius,
- );
- ctx.fill();
- ctx.fillStyle = textColor;
- }
- }
-
- for (let index = 0; index < layout.lines.length; index++) {
- const lineY = index * layout.lineHeightPx - layout.block.visualCenterOffset;
- ctx.fillText(layout.lines[index], 0, lineY);
- drawTextDecoration({
- ctx,
- textDecoration: layout.textDecoration,
- lineWidth: layout.lineMetrics[index].width,
- lineY,
- metrics: layout.lineMetrics[index],
- scaledFontSize: layout.scaledFontSize,
- textAlign: layout.textAlign,
- });
- }
-}
-
-export function strokeMeasuredTextLayout({
- ctx,
- layout,
- strokeColor,
- strokeWidth,
- textBaseline = "middle",
-}: {
- ctx: TextCanvasContext;
- layout: MeasuredTextLayout;
- strokeColor: string;
- strokeWidth: number;
- textBaseline?: CanvasTextBaseline;
-}): void {
- ctx.font = layout.fontString;
- ctx.textAlign = layout.textAlign;
- ctx.textBaseline = textBaseline;
- ctx.strokeStyle = strokeColor;
- ctx.lineWidth = strokeWidth;
- ctx.lineJoin = "round";
- ctx.lineCap = "round";
- setCanvasLetterSpacing({ ctx, letterSpacingPx: layout.letterSpacing });
-
- for (let index = 0; index < layout.lines.length; index++) {
- const lineY = index * layout.lineHeightPx - layout.block.visualCenterOffset;
- ctx.strokeText(layout.lines[index], 0, lineY);
- }
-}
diff --git a/apps/web/src/lib/text/typography.ts b/apps/web/src/lib/text/typography.ts
deleted file mode 100644
index 837236ab..00000000
--- a/apps/web/src/lib/text/typography.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const MIN_FONT_SIZE = 5;
-export const MAX_FONT_SIZE = 300;
-export const DEFAULT_TEXT_COLOR = "#000000";
-
-/**
- * higher value: smaller font size
- * lower value: larger font size
- */
-export const FONT_SIZE_SCALE_REFERENCE = 90;