307 lines
6.8 KiB
TypeScript
307 lines
6.8 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,
|
|
type TextAlign,
|
|
type TextDecoration,
|
|
type TextFontStyle,
|
|
type TextFontWeight,
|
|
type TextLayoutParams,
|
|
} 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 text = buildTextLayoutParamsFromElement({ element });
|
|
const measuredLayout = measureTextLayout({
|
|
text,
|
|
canvasHeight,
|
|
ctx,
|
|
});
|
|
|
|
const bg = buildTextBackgroundFromElement({ element });
|
|
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: text.textAlign,
|
|
block: measuredLayout.block,
|
|
background: resolvedBackground,
|
|
fontSizeRatio: measuredLayout.fontSizeRatio,
|
|
});
|
|
|
|
return {
|
|
...measuredLayout,
|
|
resolvedBackground,
|
|
visualRect,
|
|
};
|
|
}
|
|
|
|
export function buildTextLayoutParamsFromElement({
|
|
element,
|
|
}: {
|
|
element: TextElement;
|
|
}): TextLayoutParams {
|
|
return {
|
|
content: readStringParam({
|
|
params: element.params,
|
|
key: "content",
|
|
fallback: "Default text",
|
|
}),
|
|
fontSize: readNumberParam({
|
|
params: element.params,
|
|
key: "fontSize",
|
|
fallback: 15,
|
|
}),
|
|
fontFamily: readStringParam({
|
|
params: element.params,
|
|
key: "fontFamily",
|
|
fallback: "Arial",
|
|
}),
|
|
fontWeight: readFontWeight({
|
|
value: element.params.fontWeight,
|
|
fallback: "normal",
|
|
}),
|
|
fontStyle: readFontStyle({
|
|
value: element.params.fontStyle,
|
|
fallback: "normal",
|
|
}),
|
|
textAlign: readTextAlign({
|
|
value: element.params.textAlign,
|
|
fallback: "center",
|
|
}),
|
|
textDecoration: readTextDecoration({
|
|
value: element.params.textDecoration,
|
|
fallback: "none",
|
|
}),
|
|
letterSpacing: readNumberParam({
|
|
params: element.params,
|
|
key: "letterSpacing",
|
|
fallback: DEFAULTS.text.letterSpacing,
|
|
}),
|
|
lineHeight: readNumberParam({
|
|
params: element.params,
|
|
key: "lineHeight",
|
|
fallback: DEFAULTS.text.lineHeight,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function buildTextBackgroundFromElement({
|
|
element,
|
|
}: {
|
|
element: TextElement;
|
|
}): TextBackground {
|
|
return {
|
|
enabled: readBooleanParam({
|
|
params: element.params,
|
|
key: "background.enabled",
|
|
fallback: DEFAULTS.text.background.enabled,
|
|
}),
|
|
color: readStringParam({
|
|
params: element.params,
|
|
key: "background.color",
|
|
fallback: DEFAULTS.text.background.color,
|
|
}),
|
|
cornerRadius: readNumberParam({
|
|
params: element.params,
|
|
key: "background.cornerRadius",
|
|
fallback: DEFAULTS.text.background.cornerRadius,
|
|
}),
|
|
paddingX: readNumberParam({
|
|
params: element.params,
|
|
key: "background.paddingX",
|
|
fallback: DEFAULTS.text.background.paddingX,
|
|
}),
|
|
paddingY: readNumberParam({
|
|
params: element.params,
|
|
key: "background.paddingY",
|
|
fallback: DEFAULTS.text.background.paddingY,
|
|
}),
|
|
offsetX: readNumberParam({
|
|
params: element.params,
|
|
key: "background.offsetX",
|
|
fallback: DEFAULTS.text.background.offsetX,
|
|
}),
|
|
offsetY: readNumberParam({
|
|
params: element.params,
|
|
key: "background.offsetY",
|
|
fallback: DEFAULTS.text.background.offsetY,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function readStringParam({
|
|
params,
|
|
key,
|
|
fallback,
|
|
}: {
|
|
params: TextElement["params"];
|
|
key: string;
|
|
fallback: string;
|
|
}): string {
|
|
const value = params[key];
|
|
return typeof value === "string" ? value : fallback;
|
|
}
|
|
|
|
function readNumberParam({
|
|
params,
|
|
key,
|
|
fallback,
|
|
}: {
|
|
params: TextElement["params"];
|
|
key: string;
|
|
fallback: number;
|
|
}): number {
|
|
const value = params[key];
|
|
return typeof value === "number" ? value : fallback;
|
|
}
|
|
|
|
function readBooleanParam({
|
|
params,
|
|
key,
|
|
fallback,
|
|
}: {
|
|
params: TextElement["params"];
|
|
key: string;
|
|
fallback: boolean;
|
|
}): boolean {
|
|
const value = params[key];
|
|
return typeof value === "boolean" ? value : fallback;
|
|
}
|
|
|
|
function readTextAlign({
|
|
value,
|
|
fallback,
|
|
}: {
|
|
value: unknown;
|
|
fallback: TextAlign;
|
|
}): TextAlign {
|
|
return value === "left" || value === "center" || value === "right"
|
|
? value
|
|
: fallback;
|
|
}
|
|
|
|
function readFontWeight({
|
|
value,
|
|
fallback,
|
|
}: {
|
|
value: unknown;
|
|
fallback: TextFontWeight;
|
|
}): TextFontWeight {
|
|
return value === "bold" || value === "normal" ? value : fallback;
|
|
}
|
|
|
|
function readFontStyle({
|
|
value,
|
|
fallback,
|
|
}: {
|
|
value: unknown;
|
|
fallback: TextFontStyle;
|
|
}): TextFontStyle {
|
|
return value === "italic" || value === "normal" ? value : fallback;
|
|
}
|
|
|
|
function readTextDecoration({
|
|
value,
|
|
fallback,
|
|
}: {
|
|
value: unknown;
|
|
fallback: TextDecoration;
|
|
}): TextDecoration {
|
|
return value === "none" || value === "underline" || value === "line-through"
|
|
? value
|
|
: fallback;
|
|
}
|