OpenCut/apps/web/src/text/measure-element.ts

134 lines
3.4 KiB
TypeScript

import { CORNER_RADIUS_MIN } from "@/text/background";
import { DEFAULTS } from "@/timeline/defaults";
import type { TextBackground, TextElement } from "@/timeline";
import { resolveNumberAtTime } from "@/animation/values";
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,
};
}