From f75b5d3fd28349ee69f03be34c1316122992c283 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Feb 2026 01:37:56 +0100 Subject: fix a ton of issues + improvement --- apps/web/src/services/renderer/nodes/text-node.ts | 112 +++++++-------------- apps/web/src/services/storage/migrations/index.ts | 4 +- .../storage/migrations/transformers/v6-to-v7.ts | 106 +++++++++++++++++++ .../src/services/storage/migrations/v6-to-v7.ts | 16 +++ 4 files changed, 160 insertions(+), 78 deletions(-) create mode 100644 apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts create mode 100644 apps/web/src/services/storage/migrations/v6-to-v7.ts (limited to 'apps/web/src/services') diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts index b7e36d64..76d78b31 100644 --- a/apps/web/src/services/renderer/nodes/text-node.ts +++ b/apps/web/src/services/renderer/nodes/text-node.ts @@ -2,9 +2,16 @@ import type { CanvasRenderer } from "../canvas-renderer"; import { BaseNode } from "./base-node"; import type { TextElement } from "@/types/timeline"; import { + DEFAULT_TEXT_ELEMENT, DEFAULT_LINE_HEIGHT, FONT_SIZE_SCALE_REFERENCE, } from "@/constants/text-constants"; +import { + getMetricAscent, + getMetricDescent, + getTextBackgroundRect, + measureTextBlock, +} from "@/lib/text/layout"; function scaleFontSize({ fontSize, @@ -20,65 +27,6 @@ function quoteFontFamily({ fontFamily }: { fontFamily: string }): string { return `"${fontFamily.replace(/"/g, '\\"')}"`; } -function getMetricAscent({ - metrics, - fallback, -}: { - metrics: TextMetrics; - fallback: number; -}): number { - return metrics.actualBoundingBoxAscent ?? fallback * 0.8; -} - -function getMetricDescent({ - metrics, - fallback, -}: { - metrics: TextMetrics; - fallback: number; -}): number { - return metrics.actualBoundingBoxDescent ?? fallback * 0.2; -} - -interface TextBlockMeasurement { - visualCenterOffset: number; - height: number; - maxWidth: number; -} - -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 i = 0; i < lineMetrics.length; i++) { - const metrics = lineMetrics[i]; - const y = i * lineHeightPx; - top = Math.min( - top, - y - getMetricAscent({ metrics, fallback: fallbackFontSize }), - ); - bottom = Math.max( - bottom, - y + getMetricDescent({ metrics, fallback: fallbackFontSize }), - ); - maxWidth = Math.max(maxWidth, metrics.width); - } - - const height = bottom - top; - const visualCenterOffset = (top + bottom) / 2; - - return { visualCenterOffset, height, maxWidth }; -} - function drawTextDecoration({ ctx, textDecoration, @@ -99,8 +47,8 @@ function drawTextDecoration({ if (textDecoration === "none" || !textDecoration) return; const thickness = Math.max(1, scaledFontSize * 0.07); - const ascent = getMetricAscent({ metrics, fallback: scaledFontSize }); - const descent = getMetricDescent({ metrics, fallback: scaledFontSize }); + const ascent = getMetricAscent({ metrics, fallbackFontSize: scaledFontSize }); + const descent = getMetricDescent({ metrics, fallbackFontSize: scaledFontSize }); let xStart = -lineWidth / 2; if (textAlign === "left") xStart = 0; @@ -171,6 +119,7 @@ export class TextNode extends BaseNode { const lines = this.params.content.split("\n"); const lineHeightPx = scaledFontSize * lineHeight; + const fontSizeRatio = this.params.fontSize / DEFAULT_TEXT_ELEMENT.fontSize; const baseline = this.params.textBaseline ?? "middle"; renderer.context.textBaseline = baseline; @@ -191,22 +140,31 @@ export class TextNode extends BaseNode { ) as GlobalCompositeOperation; renderer.context.globalAlpha = this.params.opacity; - if (this.params.backgroundColor && lineCount > 0) { - const padX = 8; - const padY = 4; - renderer.context.fillStyle = this.params.backgroundColor; - let bgLeft = -block.maxWidth / 2; - if (renderer.context.textAlign === "left") bgLeft = 0; - if (renderer.context.textAlign === "right") bgLeft = -block.maxWidth; - - renderer.context.fillRect( - bgLeft - padX, - -block.height / 2 - padY, - block.maxWidth + padX * 2, - block.height + padY * 2, - ); - - renderer.context.fillStyle = this.params.color; + if ( + this.params.background.color && + this.params.background.color !== "transparent" && + lineCount > 0 + ) { + const { color, cornerRadius = 0 } = this.params.background; + const backgroundRect = getTextBackgroundRect({ + textAlign: this.params.textAlign, + block, + background: this.params.background, + fontSizeRatio, + }); + if (backgroundRect) { + renderer.context.fillStyle = color; + renderer.context.beginPath(); + renderer.context.roundRect( + backgroundRect.left, + backgroundRect.top, + backgroundRect.width, + backgroundRect.height, + cornerRadius, + ); + renderer.context.fill(); + renderer.context.fillStyle = this.params.color; + } } for (let i = 0; i < lineCount; i++) { diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index 00c009c7..fd40f14c 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -5,10 +5,11 @@ import { V2toV3Migration } from "./v2-to-v3"; import { V3toV4Migration } from "./v3-to-v4"; import { V4toV5Migration } from "./v4-to-v5"; import { V5toV6Migration } from "./v5-to-v6"; +import { V6toV7Migration } from "./v6-to-v7"; export { runStorageMigrations } from "./runner"; export type { MigrationProgress } from "./runner"; -export const CURRENT_PROJECT_VERSION = 6; +export const CURRENT_PROJECT_VERSION = 7; export const migrations = [ new V0toV1Migration(), @@ -17,4 +18,5 @@ export const migrations = [ new V3toV4Migration(), new V4toV5Migration(), new V5toV6Migration(), + new V6toV7Migration(), ]; diff --git a/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts b/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts new file mode 100644 index 00000000..6f4848d4 --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts @@ -0,0 +1,106 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV6ToV7({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV7Project({ project })) { + return { project, skipped: true, reason: "already v7" }; + } + + const migratedProject = migrateProjectTextElements({ project }); + + return { + project: { ...migratedProject, version: 7 }, + skipped: false, + }; +} + +function migrateProjectTextElements({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + let hasChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migrated = migrateSceneTextElements({ scene }); + if (migrated !== scene) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return project; + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneTextElements({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + let hasChanges = false; + const migratedTracks = tracksValue.map((track) => { + const migrated = migrateTrackTextElements({ track }); + if (migrated !== track) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return scene; + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackTextElements({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + let hasChanges = false; + const migratedElements = elementsValue.map((element) => { + const migrated = migrateTextElement({ element }); + if (migrated !== element) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return track; + return { ...track, elements: migratedElements }; +} + +function migrateTextElement({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + if (element.type !== "text") return element; + if (isRecord(element.background)) return element; + + const backgroundColor = + typeof element.backgroundColor === "string" + ? element.backgroundColor + : "transparent"; + + const { backgroundColor: _removed, ...rest } = element; + + return { + ...rest, + background: { + color: backgroundColor, + cornerRadius: 0, + paddingX: 8, + paddingY: 4, + offsetX: 0, + offsetY: 0, + }, + }; +} + +function isV7Project({ project }: { project: ProjectRecord }): boolean { + return typeof project.version === "number" && project.version >= 7; +} diff --git a/apps/web/src/services/storage/migrations/v6-to-v7.ts b/apps/web/src/services/storage/migrations/v6-to-v7.ts new file mode 100644 index 00000000..0556b4d5 --- /dev/null +++ b/apps/web/src/services/storage/migrations/v6-to-v7.ts @@ -0,0 +1,16 @@ +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV6ToV7 } from "./transformers/v6-to-v7"; + +export class V6toV7Migration extends StorageMigration { + from = 6; + to = 7; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV6ToV7({ project }); + } +} -- cgit v1.3.1