diff options
Diffstat (limited to 'apps/web/src/lib/text')
| -rw-r--r-- | apps/web/src/lib/text/background.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/text/layout.ts | 226 | ||||
| -rw-r--r-- | apps/web/src/lib/text/measure-element.ts | 133 | ||||
| -rw-r--r-- | apps/web/src/lib/text/primitives.ts | 241 | ||||
| -rw-r--r-- | apps/web/src/lib/text/typography.ts | 9 |
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; |
