diff options
Diffstat (limited to 'apps/web/src/lib/preview/element-bounds.ts')
| -rw-r--r-- | apps/web/src/lib/preview/element-bounds.ts | 93 |
1 files changed, 64 insertions, 29 deletions
diff --git a/apps/web/src/lib/preview/element-bounds.ts b/apps/web/src/lib/preview/element-bounds.ts index ec26c14d..5b1f301d 100644 --- a/apps/web/src/lib/preview/element-bounds.ts +++ b/apps/web/src/lib/preview/element-bounds.ts @@ -2,9 +2,12 @@ import type { TimelineTrack, TimelineElement } from "@/types/timeline"; import type { MediaAsset } from "@/types/assets"; import { isMainTrack } from "@/lib/timeline"; import { + DEFAULT_TEXT_ELEMENT, DEFAULT_LINE_HEIGHT, FONT_SIZE_SCALE_REFERENCE, } from "@/constants/text-constants"; +import { getTextVisualRect, measureTextBlock } from "@/lib/text/layout"; +import { getElementLocalTime, resolveTransformAtTime } from "@/lib/animation"; export interface ElementBounds { cx: number; @@ -60,17 +63,24 @@ export function getElementBounds({ element, canvasSize, mediaAsset, + localTime, }: { element: TimelineElement; canvasSize: { width: number; height: number }; mediaAsset?: MediaAsset | null; + localTime: number; }): ElementBounds | null { - if (element.type === "audio") return null; + if (element.type === "audio" || element.type === "effect") return null; if ("hidden" in element && element.hidden) return null; const { width: canvasWidth, height: canvasHeight } = canvasSize; if (element.type === "video" || element.type === "image") { + const transform = resolveTransformAtTime({ + baseTransform: element.transform, + animations: element.animations, + localTime, + }); const sourceWidth = mediaAsset?.width ?? canvasWidth; const sourceHeight = mediaAsset?.height ?? canvasHeight; return getVisualElementBounds({ @@ -78,21 +88,31 @@ export function getElementBounds({ canvasHeight, sourceWidth, sourceHeight, - transform: element.transform, + transform, }); } if (element.type === "sticker") { + const transform = resolveTransformAtTime({ + baseTransform: element.transform, + animations: element.animations, + localTime, + }); return getVisualElementBounds({ canvasWidth, canvasHeight, sourceWidth: 200, sourceHeight: 200, - transform: element.transform, + transform, }); } if (element.type === "text") { + const transform = resolveTransformAtTime({ + baseTransform: element.transform, + animations: element.animations, + localTime, + }); const scaledFontSize = element.fontSize * (canvasHeight / FONT_SIZE_SCALE_REFERENCE); const letterSpacing = element.letterSpacing ?? 0; @@ -121,37 +141,46 @@ export function getElementBounds({ const lines = element.content.split("\n"); const lineMetrics = lines.map((line) => ctx.measureText(line)); - - 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 - (metrics.actualBoundingBoxAscent ?? scaledFontSize * 0.8), - ); - bottom = Math.max( - bottom, - y + (metrics.actualBoundingBoxDescent ?? scaledFontSize * 0.2), - ); - maxWidth = Math.max(maxWidth, metrics.width); - } - - measuredWidth = maxWidth; - measuredHeight = bottom - top; + const block = measureTextBlock({ + lineMetrics, + lineHeightPx, + fallbackFontSize: scaledFontSize, + }); + const fontSizeRatio = element.fontSize / DEFAULT_TEXT_ELEMENT.fontSize; + const visualRect = getTextVisualRect({ + textAlign: element.textAlign, + block, + background: element.background, + fontSizeRatio, + }); + measuredWidth = visualRect.width; + measuredHeight = visualRect.height; + const localCenterX = visualRect.left + visualRect.width / 2; + const localCenterY = visualRect.top + visualRect.height / 2; + const scaledCenterX = localCenterX * transform.scale; + const scaledCenterY = localCenterY * transform.scale; + const rotationRad = (transform.rotate * Math.PI) / 180; + const cos = Math.cos(rotationRad); + const sin = Math.sin(rotationRad); + const rotatedCenterX = scaledCenterX * cos - scaledCenterY * sin; + const rotatedCenterY = scaledCenterX * sin + scaledCenterY * cos; + return { + cx: canvasWidth / 2 + transform.position.x + rotatedCenterX, + cy: canvasHeight / 2 + transform.position.y + rotatedCenterY, + width: measuredWidth * transform.scale, + height: measuredHeight * transform.scale, + rotation: transform.rotate, + }; } - const width = measuredWidth * element.transform.scale; - const height = measuredHeight * element.transform.scale; + const width = measuredWidth * transform.scale; + const height = measuredHeight * transform.scale; return { - cx: canvasWidth / 2 + element.transform.position.x, - cy: canvasHeight / 2 + element.transform.position.y, + cx: canvasWidth / 2 + transform.position.x, + cy: canvasHeight / 2 + transform.position.y, width, height, - rotation: element.transform.rotate, + rotation: transform.rotate, }; } @@ -195,6 +224,11 @@ export function getVisibleElementsWithBounds({ }); for (const element of elements) { + const localTime = getElementLocalTime({ + timelineTime: currentTime, + elementStartTime: element.startTime, + elementDuration: element.duration, + }); const mediaAsset = element.type === "video" || element.type === "image" ? mediaMap.get(element.mediaId) @@ -203,6 +237,7 @@ export function getVisibleElementsWithBounds({ element, canvasSize, mediaAsset, + localTime, }); if (bounds) { result.push({ |
