summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/text
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-25 01:37:56 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-25 01:37:56 +0100
commitf75b5d3fd28349ee69f03be34c1316122992c283 (patch)
tree45e233d9b4c83b02be2a7bfe13ea943a676bd9be /apps/web/src/lib/text
parent4d77e3f2bb9884ad9e43761eaf0ede8c1a9cfccc (diff)
downloadopencut-f75b5d3fd28349ee69f03be34c1316122992c283.tar.gz
fix a ton of issues + improvement
Diffstat (limited to 'apps/web/src/lib/text')
-rw-r--r--apps/web/src/lib/text/layout.ts167
1 files changed, 167 insertions, 0 deletions
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,
+ };
+}