feat: masks, properties refactor, shaders, storage migrations, and more

This commit is contained in:
Maze Winther
2026-03-29 15:48:22 +02:00
parent 39ea298a9c
commit 8db3bead13
690 changed files with 35618 additions and 7337 deletions
+23 -6
View File
@@ -1,5 +1,5 @@
import { DEFAULT_TEXT_BACKGROUND } from "@/constants/text-constants";
import type { TextBackground, TextElement } from "@/types/timeline";
import type { TextBackground, TextElement } from "@/lib/timeline";
import { DEFAULTS } from "@/lib/timeline/defaults";
type TextRect = {
left: number;
@@ -14,6 +14,23 @@ export interface TextBlockMeasurement {
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,
@@ -118,11 +135,11 @@ export function getTextBackgroundRect({
const textRect = getTextRect({ textAlign, block });
const paddingX =
(background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) * fontSizeRatio;
(background.paddingX ?? DEFAULTS.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;
(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,
+132
View File
@@ -0,0 +1,132 @@
import {
CORNER_RADIUS_MIN,
FONT_SIZE_SCALE_REFERENCE,
} from "@/constants/text-constants";
import { resolveNumberAtTime } from "@/lib/animation";
import { DEFAULTS } from "@/lib/timeline/defaults";
import type { TextBackground, TextElement } from "@/lib/timeline";
import {
measureTextBlock,
setCanvasLetterSpacing,
getTextVisualRect,
type TextBlockMeasurement,
} from "./layout";
export interface ResolvedTextBackground extends TextBackground {
paddingX: number;
paddingY: number;
offsetX: number;
offsetY: number;
cornerRadius: number;
}
export interface MeasuredTextElement {
scaledFontSize: number;
fontString: string;
letterSpacing: number;
lineHeightPx: number;
lines: string[];
lineMetrics: TextMetrics[];
block: TextBlockMeasurement;
fontSizeRatio: number;
resolvedBackground: ResolvedTextBackground;
visualRect: { left: number; top: number; width: number; height: number };
}
/**
* Shared text measurement used by both the renderer and preview bounds.
* Accepts the canvas context to measure on so callers can reuse an existing
* context (e.g. the renderer's) rather than creating a throwaway canvas.
* The context state is preserved via save/restore.
*/
export function measureTextElement({
element,
canvasHeight,
localTime,
ctx,
}: {
element: TextElement;
canvasHeight: number;
localTime: number;
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
}): MeasuredTextElement {
const scaledFontSize =
element.fontSize * (canvasHeight / FONT_SIZE_SCALE_REFERENCE);
const fontWeight = element.fontWeight === "bold" ? "bold" : "normal";
const fontStyle = element.fontStyle === "italic" ? "italic" : "normal";
const fontFamily = `"${element.fontFamily.replace(/"/g, '\\"')}"`;
const fontString = `${fontStyle} ${fontWeight} ${scaledFontSize}px ${fontFamily}, sans-serif`;
const letterSpacing = element.letterSpacing ?? 0;
const lineHeightPx =
scaledFontSize * (element.lineHeight ?? DEFAULTS.text.lineHeight);
const fontSizeRatio = element.fontSize / DEFAULTS.text.element.fontSize;
const lines = element.content.split("\n");
ctx.save();
ctx.font = fontString;
ctx.textBaseline = "middle";
setCanvasLetterSpacing({ ctx, letterSpacingPx: letterSpacing });
const lineMetrics = lines.map((line) => ctx.measureText(line));
ctx.restore();
const block = measureTextBlock({
lineMetrics,
lineHeightPx,
fallbackFontSize: scaledFontSize,
});
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,
background: resolvedBackground,
fontSizeRatio,
});
return {
scaledFontSize,
fontString,
letterSpacing,
lineHeightPx,
lines,
lineMetrics,
block,
fontSizeRatio,
resolvedBackground,
visualRect,
};
}