summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/preview/element-bounds.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/preview/element-bounds.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/preview/element-bounds.ts')
-rw-r--r--apps/web/src/lib/preview/element-bounds.ts308
1 files changed, 0 insertions, 308 deletions
diff --git a/apps/web/src/lib/preview/element-bounds.ts b/apps/web/src/lib/preview/element-bounds.ts
deleted file mode 100644
index 39595c33..00000000
--- a/apps/web/src/lib/preview/element-bounds.ts
+++ /dev/null
@@ -1,308 +0,0 @@
-import type { SceneTracks, TimelineElement } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
-import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size";
-import { DEFAULT_GRAPHIC_SOURCE_SIZE } from "@/lib/graphics";
-import { measureTextElement } from "@/lib/text/measure-element";
-import {
- getElementLocalTime,
- resolveTransformAtTime,
-} from "@/lib/animation";
-
-export interface ElementBounds {
- cx: number;
- cy: number;
- width: number;
- height: number;
- rotation: number;
-}
-
-export interface ElementWithBounds {
- trackId: string;
- elementId: string;
- element: TimelineElement;
- bounds: ElementBounds;
-}
-
-function getVisualElementBounds({
- canvasWidth,
- canvasHeight,
- sourceWidth,
- sourceHeight,
- transform,
-}: {
- canvasWidth: number;
- canvasHeight: number;
- sourceWidth: number;
- sourceHeight: number;
- transform: {
- scaleX: number;
- scaleY: number;
- position: { x: number; y: number };
- rotate: number;
- };
-}): ElementBounds {
- const containScale = Math.min(
- canvasWidth / sourceWidth,
- canvasHeight / sourceHeight,
- );
- const scaledWidth = sourceWidth * containScale * transform.scaleX;
- const scaledHeight = sourceHeight * containScale * transform.scaleY;
- const cx = canvasWidth / 2 + transform.position.x;
- const cy = canvasHeight / 2 + transform.position.y;
-
- return {
- cx,
- cy,
- width: scaledWidth,
- height: scaledHeight,
- rotation: transform.rotate,
- };
-}
-
-function getTransformedRectBounds({
- canvasWidth,
- canvasHeight,
- rect,
- transform,
-}: {
- canvasWidth: number;
- canvasHeight: number;
- rect: { left: number; top: number; width: number; height: number };
- transform: {
- scaleX: number;
- scaleY: number;
- position: { x: number; y: number };
- rotate: number;
- };
-}): ElementBounds {
- const localCenterX = rect.left + rect.width / 2;
- const localCenterY = rect.top + rect.height / 2;
- const scaledCenterX = localCenterX * transform.scaleX;
- const scaledCenterY = localCenterY * transform.scaleY;
- const rotationRad = (transform.rotate * Math.PI) / 180;
- const cos = Math.cos(rotationRad);
- const sin = Math.sin(rotationRad);
- return {
- cx:
- canvasWidth / 2 +
- transform.position.x +
- scaledCenterX * cos -
- scaledCenterY * sin,
- cy:
- canvasHeight / 2 +
- transform.position.y +
- scaledCenterX * sin +
- scaledCenterY * cos,
- width: rect.width * transform.scaleX,
- height: rect.height * transform.scaleY,
- rotation: transform.rotate,
- };
-}
-
-/**
- * Bounds policy: bounds reflect base content geometry (text glyphs + background,
- * sticker/image/video content area) and base transform. Post-effect spill (blur,
- * glow) and mask-clipped regions are intentionally excluded — handles manipulate
- * the canonical element geometry, not visual effect output.
- */
-function getElementBounds({
- element,
- canvasSize,
- mediaAsset,
- localTime,
-}: {
- element: TimelineElement;
- canvasSize: { width: number; height: number };
- mediaAsset?: MediaAsset | null;
- localTime: number;
-}): ElementBounds | 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({
- canvasWidth,
- canvasHeight,
- sourceWidth,
- sourceHeight,
- transform,
- });
- }
-
- if (element.type === "sticker") {
- const transform = resolveTransformAtTime({
- baseTransform: element.transform,
- animations: element.animations,
- localTime,
- });
- return getVisualElementBounds({
- canvasWidth,
- canvasHeight,
- sourceWidth: element.intrinsicWidth ?? STICKER_INTRINSIC_SIZE_FALLBACK,
- sourceHeight: element.intrinsicHeight ?? STICKER_INTRINSIC_SIZE_FALLBACK,
- transform,
- });
- }
-
- if (element.type === "graphic") {
- const transform = resolveTransformAtTime({
- baseTransform: element.transform,
- animations: element.animations,
- localTime,
- });
- return getVisualElementBounds({
- canvasWidth,
- canvasHeight,
- sourceWidth: DEFAULT_GRAPHIC_SOURCE_SIZE,
- sourceHeight: DEFAULT_GRAPHIC_SOURCE_SIZE,
- transform,
- });
- }
-
- if (element.type === "text") {
- const transform = resolveTransformAtTime({
- baseTransform: element.transform,
- animations: element.animations,
- localTime,
- });
-
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- if (!ctx) return null;
-
- const measured = measureTextElement({
- element,
- canvasHeight,
- localTime,
- ctx,
- });
-
- return getTransformedRectBounds({
- canvasWidth,
- canvasHeight,
- rect: measured.visualRect,
- transform,
- });
- }
-
- return null;
-}
-
-export const ROTATION_HANDLE_OFFSET = 24;
-
-export type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
-export type Edge = "right" | "left" | "bottom";
-
-export function getCornerPosition({
- bounds,
- corner,
-}: {
- bounds: ElementBounds;
- corner: Corner;
-}): { x: number; y: number } {
- const halfW = bounds.width / 2;
- const halfH = bounds.height / 2;
- const angleRad = (bounds.rotation * Math.PI) / 180;
- const cos = Math.cos(angleRad);
- const sin = Math.sin(angleRad);
- const localX =
- corner === "top-left" || corner === "bottom-left" ? -halfW : halfW;
- const localY =
- corner === "top-left" || corner === "top-right" ? -halfH : halfH;
- return {
- x: bounds.cx + (localX * cos - localY * sin),
- y: bounds.cy + (localX * sin + localY * cos),
- };
-}
-
-export function getEdgeHandlePosition({
- bounds,
- edge,
-}: {
- bounds: ElementBounds;
- edge: Edge;
-}): { x: number; y: number } {
- const halfWidth = bounds.width / 2;
- const halfHeight = bounds.height / 2;
- const angleRad = (bounds.rotation * Math.PI) / 180;
- const cos = Math.cos(angleRad);
- const sin = Math.sin(angleRad);
- const localX = edge === "right" ? halfWidth : edge === "left" ? -halfWidth : 0;
- const localY = edge === "bottom" ? halfHeight : 0;
- return {
- x: bounds.cx + (localX * cos - localY * sin),
- y: bounds.cy + (localX * sin + localY * cos),
- };
-}
-
-export function getVisibleElementsWithBounds({
- tracks,
- currentTime,
- canvasSize,
- mediaAssets,
-}: {
- tracks: SceneTracks;
- currentTime: number;
- canvasSize: { width: number; height: number };
- mediaAssets: MediaAsset[];
-}): ElementWithBounds[] {
- const mediaMap = new Map(mediaAssets.map((m) => [m.id, m]));
- const orderedTracks = [
- ...tracks.overlay.filter((track) => !("hidden" in track && track.hidden)),
- ...(!tracks.main.hidden ? [tracks.main] : []),
- ].reverse();
-
- const result: ElementWithBounds[] = [];
-
- for (const track of orderedTracks) {
- const elements = track.elements
- .filter((element) => !("hidden" in element && element.hidden))
- .filter(
- (element) =>
- currentTime >= element.startTime &&
- currentTime < element.startTime + element.duration,
- )
- .slice()
- .sort((a, b) => {
- if (a.startTime !== b.startTime) return a.startTime - b.startTime;
- return a.id.localeCompare(b.id);
- });
-
- 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)
- : undefined;
- const bounds = getElementBounds({
- element,
- canvasSize,
- mediaAsset,
- localTime,
- });
- if (bounds) {
- result.push({
- trackId: track.id,
- elementId: element.id,
- element,
- bounds,
- });
- }
- }
- }
-
- return result;
-}