summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/preview/element-bounds.ts53
-rw-r--r--apps/web/src/lib/text/layout.ts167
-rw-r--r--apps/web/src/lib/timeline/element-utils.ts9
3 files changed, 207 insertions, 22 deletions
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,