mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
193 lines
4.1 KiB
TypeScript
193 lines
4.1 KiB
TypeScript
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;
|
|
}
|
|
|
|
type CanvasContext =
|
|
| CanvasRenderingContext2D
|
|
| OffscreenCanvasRenderingContext2D;
|
|
|
|
export function setCanvasLetterSpacing({
|
|
ctx,
|
|
letterSpacingPx,
|
|
}: {
|
|
ctx: CanvasContext;
|
|
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,
|
|
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 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,
|
|
};
|
|
}
|