feat(scene): rotate video previews like images
Reuse previewRotationDeg for video scenes across editor, control, presentation and overlays via ContainedVideo layout parity. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { containMediaRect } from './containMediaRect';
|
||||
import { containMediaLayout, containMediaRect } from './containMediaRect';
|
||||
|
||||
void test('containMediaRect: letterboxes 16:9 into square host', () => {
|
||||
const r = containMediaRect({
|
||||
@@ -43,3 +43,22 @@ void test('containMediaRect: zoom grows rect around ox/oy', () => {
|
||||
assert.ok(zoomed!.w > base!.w);
|
||||
assert.ok(zoomed!.x < base!.x);
|
||||
});
|
||||
|
||||
void test('containMediaLayout: 90° swaps fit axes and element stays unrotated size', () => {
|
||||
const layout = containMediaLayout({
|
||||
hostW: 400,
|
||||
hostH: 400,
|
||||
mediaW: 1920,
|
||||
mediaH: 1080,
|
||||
scale: 1,
|
||||
ox: 0.5,
|
||||
oy: 0.5,
|
||||
rotationDeg: 90,
|
||||
});
|
||||
assert.ok(layout);
|
||||
// After 90°, layout AABB is portrait 1080x1920 fitted into square → height fills.
|
||||
assert.ok(Math.abs(layout!.contentRect.h - 400) < 0.01);
|
||||
assert.ok(Math.abs(layout!.contentRect.w - (400 * 1080) / 1920) < 0.01);
|
||||
assert.ok(Math.abs(layout!.elementW - layout!.contentRect.h) < 0.01);
|
||||
assert.ok(Math.abs(layout!.elementH - layout!.contentRect.w) < 0.01);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,63 @@
|
||||
* Pure layout math shared by ContainedVideo / RotatedImage contain-mode.
|
||||
* Kept free of DOM so unit tests can lock overlay alignment for video scenes.
|
||||
*/
|
||||
|
||||
export type MediaRotationDeg = 0 | 90 | 180 | 270;
|
||||
|
||||
export type ContainMediaRect = { x: number; y: number; w: number; h: number };
|
||||
|
||||
export type ContainMediaLayout = {
|
||||
/** Bounding box of the visible media after rotation (overlay coordinate space). */
|
||||
contentRect: ContainMediaRect;
|
||||
/** Unrotated element size; apply CSS rotate(rotationDeg) on the media node. */
|
||||
elementW: number;
|
||||
elementH: number;
|
||||
};
|
||||
|
||||
export function containMediaLayout(args: {
|
||||
hostW: number;
|
||||
hostH: number;
|
||||
mediaW: number;
|
||||
mediaH: number;
|
||||
scale: number;
|
||||
ox: number;
|
||||
oy: number;
|
||||
rotationDeg?: MediaRotationDeg;
|
||||
mode?: 'contain' | 'cover';
|
||||
}): ContainMediaLayout | null {
|
||||
const {
|
||||
hostW,
|
||||
hostH,
|
||||
mediaW,
|
||||
mediaH,
|
||||
scale,
|
||||
ox,
|
||||
oy,
|
||||
rotationDeg = 0,
|
||||
mode = 'contain',
|
||||
} = args;
|
||||
if (hostW <= 1 || hostH <= 1 || mediaW <= 0 || mediaH <= 0) return null;
|
||||
const rotated = rotationDeg === 90 || rotationDeg === 270;
|
||||
const layoutW = rotated ? mediaH : mediaW;
|
||||
const layoutH = rotated ? mediaW : mediaH;
|
||||
const sx = hostW / layoutW;
|
||||
const sy = hostH / layoutH;
|
||||
const fit = mode === 'cover' ? Math.max(sx, sy) : Math.min(sx, sy);
|
||||
const s = fit * Math.max(1, scale);
|
||||
const w = layoutW * s;
|
||||
const h = layoutH * s;
|
||||
return {
|
||||
contentRect: {
|
||||
x: hostW / 2 - ox * w,
|
||||
y: hostH / 2 - oy * h,
|
||||
w,
|
||||
h,
|
||||
},
|
||||
elementW: mediaW * s,
|
||||
elementH: mediaH * s,
|
||||
};
|
||||
}
|
||||
|
||||
export function containMediaRect(args: {
|
||||
hostW: number;
|
||||
hostH: number;
|
||||
@@ -10,17 +67,8 @@ export function containMediaRect(args: {
|
||||
scale: number;
|
||||
ox: number;
|
||||
oy: number;
|
||||
}): { x: number; y: number; w: number; h: number } | null {
|
||||
const { hostW, hostH, mediaW, mediaH, scale, ox, oy } = args;
|
||||
if (hostW <= 1 || hostH <= 1 || mediaW <= 0 || mediaH <= 0) return null;
|
||||
const fit = Math.min(hostW / mediaW, hostH / mediaH);
|
||||
const s = fit * Math.max(1, scale);
|
||||
const w = mediaW * s;
|
||||
const h = mediaH * s;
|
||||
return {
|
||||
x: hostW / 2 - ox * w,
|
||||
y: hostH / 2 - oy * h,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
rotationDeg?: MediaRotationDeg;
|
||||
mode?: 'contain' | 'cover';
|
||||
}): ContainMediaRect | null {
|
||||
return containMediaLayout(args)?.contentRect ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user