From f75b5d3fd28349ee69f03be34c1316122992c283 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Feb 2026 01:37:56 +0100 Subject: fix a ton of issues + improvement --- apps/web/src/lib/preview/element-bounds.ts | 53 +++++---- apps/web/src/lib/text/layout.ts | 167 +++++++++++++++++++++++++++++ apps/web/src/lib/timeline/element-utils.ts | 9 +- 3 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 apps/web/src/lib/text/layout.ts (limited to 'apps/web/src/lib') diff --git a/apps/web/src/lib/preview/element-bounds.ts b/apps/web/src/lib/preview/element-bounds.ts index ec26c14d..9e58ec52 100644 --- a/apps/web/src/lib/preview/element-bounds.ts +++ b/apps/web/src/lib/preview/element-bounds.ts @@ -2,9 +2,11 @@ import type { TimelineTrack, TimelineElement } from "@/types/timeline"; import type { MediaAsset } from "@/types/assets"; import { isMainTrack } from "@/lib/timeline"; import { + DEFAULT_TEXT_ELEMENT, DEFAULT_LINE_HEIGHT, FONT_SIZE_SCALE_REFERENCE, } from "@/constants/text-constants"; +import { getTextVisualRect, measureTextBlock } from "@/lib/text/layout"; export interface ElementBounds { cx: number; @@ -121,27 +123,36 @@ export function getElementBounds({ const lines = element.content.split("\n"); const lineMetrics = lines.map((line) => ctx.measureText(line)); - - let top = Number.POSITIVE_INFINITY; - let bottom = Number.NEGATIVE_INFINITY; - let maxWidth = 0; - - for (let i = 0; i < lineMetrics.length; i++) { - const metrics = lineMetrics[i]; - const y = i * lineHeightPx; - top = Math.min( - top, - y - (metrics.actualBoundingBoxAscent ?? scaledFontSize * 0.8), - ); - bottom = Math.max( - bottom, - y + (metrics.actualBoundingBoxDescent ?? scaledFontSize * 0.2), - ); - maxWidth = Math.max(maxWidth, metrics.width); - } - - measuredWidth = maxWidth; - measuredHeight = bottom - top; + const block = measureTextBlock({ + lineMetrics, + lineHeightPx, + fallbackFontSize: scaledFontSize, + }); + const fontSizeRatio = element.fontSize / DEFAULT_TEXT_ELEMENT.fontSize; + const visualRect = getTextVisualRect({ + textAlign: element.textAlign, + block, + background: element.background, + fontSizeRatio, + }); + measuredWidth = visualRect.width; + measuredHeight = visualRect.height; + const localCenterX = visualRect.left + visualRect.width / 2; + const localCenterY = visualRect.top + visualRect.height / 2; + const scaledCenterX = localCenterX * element.transform.scale; + const scaledCenterY = localCenterY * element.transform.scale; + const rotationRad = (element.transform.rotate * Math.PI) / 180; + const cos = Math.cos(rotationRad); + const sin = Math.sin(rotationRad); + const rotatedCenterX = scaledCenterX * cos - scaledCenterY * sin; + const rotatedCenterY = scaledCenterX * sin + scaledCenterY * cos; + return { + cx: canvasWidth / 2 + element.transform.position.x + rotatedCenterX, + cy: canvasHeight / 2 + element.transform.position.y + rotatedCenterY, + width: measuredWidth * element.transform.scale, + height: measuredHeight * element.transform.scale, + rotation: element.transform.rotate, + }; } const width = measuredWidth * element.transform.scale; diff --git a/apps/web/src/lib/text/layout.ts b/apps/web/src/lib/text/layout.ts new file mode 100644 index 00000000..8c38e9aa --- /dev/null +++ b/apps/web/src/lib/text/layout.ts @@ -0,0 +1,167 @@ +import { DEFAULT_TEXT_BACKGROUND } from "@/constants/text-constants"; +import type { TextElement } from "@/types/timeline"; + +type TextRect = { + left: number; + top: number; + width: number; + height: number; +}; + +export interface TextBlockMeasurement { + visualCenterOffset: number; + height: number; + maxWidth: number; +} + +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, + fallbackFontSize, +}: { + lineMetrics: TextMetrics[]; + lineHeightPx: number; + fallbackFontSize: number; +}): TextBlockMeasurement { + let top = Number.POSITIVE_INFINITY; + let bottom = Number.NEGATIVE_INFINITY; + let maxWidth = 0; + + for (let index = 0; index < lineMetrics.length; index++) { + const metrics = lineMetrics[index]; + const lineY = index * lineHeightPx; + top = Math.min( + top, + lineY - getMetricAscent({ metrics, fallbackFontSize }), + ); + bottom = Math.max( + bottom, + lineY + getMetricDescent({ metrics, fallbackFontSize }), + ); + maxWidth = Math.max(maxWidth, metrics.width); + } + + const height = bottom - top; + const visualCenterOffset = (top + bottom) / 2; + + return { visualCenterOffset, height, maxWidth }; +} + +function getTextRect({ + textAlign, + block, +}: { + textAlign: TextElement["textAlign"]; + block: TextBlockMeasurement; +}): TextRect { + const left = + textAlign === "left" ? 0 : textAlign === "right" ? -block.maxWidth : -block.maxWidth / 2; + + return { + left, + top: -block.height / 2, + width: block.maxWidth, + height: block.height, + }; +} + +function isTextBackgroundVisible({ + background, +}: { + background: TextElement["background"]; +}): boolean { + return Boolean(background.color) && background.color !== "transparent"; +} + +export function getTextBackgroundRect({ + textAlign, + block, + background, + fontSizeRatio = 1, +}: { + textAlign: TextElement["textAlign"]; + block: TextBlockMeasurement; + background: TextElement["background"]; + fontSizeRatio?: number; +}): TextRect | null { + if (!isTextBackgroundVisible({ background })) { + return null; + } + + const textRect = getTextRect({ textAlign, block }); + const paddingX = + (background.paddingX ?? DEFAULT_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; + + 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: TextElement["background"]; + 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, + }; +} diff --git a/apps/web/src/lib/timeline/element-utils.ts b/apps/web/src/lib/timeline/element-utils.ts index fa74b14e..c51667bf 100644 --- a/apps/web/src/lib/timeline/element-utils.ts +++ b/apps/web/src/lib/timeline/element-utils.ts @@ -154,7 +154,14 @@ export function buildTextElement({ : DEFAULT_TEXT_ELEMENT.fontSize, fontFamily: t.fontFamily ?? DEFAULT_TEXT_ELEMENT.fontFamily, color: t.color ?? DEFAULT_TEXT_ELEMENT.color, - backgroundColor: t.backgroundColor ?? DEFAULT_TEXT_ELEMENT.backgroundColor, + background: { + color: t.background?.color ?? DEFAULT_TEXT_ELEMENT.background.color, + cornerRadius: t.background?.cornerRadius, + paddingX: t.background?.paddingX, + paddingY: t.background?.paddingY, + offsetX: t.background?.offsetX, + offsetY: t.background?.offsetY, + }, textAlign: t.textAlign ?? DEFAULT_TEXT_ELEMENT.textAlign, fontWeight: t.fontWeight ?? DEFAULT_TEXT_ELEMENT.fontWeight, fontStyle: t.fontStyle ?? DEFAULT_TEXT_ELEMENT.fontStyle, -- cgit v1.3.1