Skip to content

Commit a51e05f

Browse files
committed
Allow advanced padding to position video vertically
1 parent 8c7d23d commit a51e05f

6 files changed

Lines changed: 145 additions & 32 deletions

File tree

src/components/video-editor/SettingsPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import type {
7373
ZoomTransitionEasing,
7474
} from "./types";
7575
import {
76+
ADVANCED_VERTICAL_PADDING_MAX,
7677
DEFAULT_AUTO_CAPTION_SETTINGS,
7778
DEFAULT_CROP_REGION,
7879
DEFAULT_CURSOR_CLICK_BOUNCE,
@@ -2413,7 +2414,7 @@ export function SettingsPanel({
24132414
value={padding.top}
24142415
defaultValue={DEFAULT_PADDING.top}
24152416
min={0}
2416-
max={100}
2417+
max={ADVANCED_VERTICAL_PADDING_MAX}
24172418
step={1}
24182419
onChange={(v) => handlePaddingSideChange("top", v)}
24192420
formatValue={(v) => `${v}%`}
@@ -2424,7 +2425,7 @@ export function SettingsPanel({
24242425
value={padding.bottom}
24252426
defaultValue={DEFAULT_PADDING.bottom}
24262427
min={0}
2427-
max={100}
2428+
max={ADVANCED_VERTICAL_PADDING_MAX}
24282429
step={1}
24292430
onChange={(v) => handlePaddingSideChange("bottom", v)}
24302431
formatValue={(v) => `${v}%`}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { normalizeProjectEditor } from "./projectPersistence";
4+
import { ADVANCED_VERTICAL_PADDING_MAX } from "./types";
5+
6+
describe("normalizeProjectEditor", () => {
7+
it("preserves the extended advanced vertical padding range", () => {
8+
const editor = normalizeProjectEditor({
9+
padding: {
10+
top: 240,
11+
bottom: ADVANCED_VERTICAL_PADDING_MAX,
12+
left: 22,
13+
right: 22,
14+
linked: false,
15+
},
16+
});
17+
18+
expect(editor.padding).toMatchObject({
19+
top: 240,
20+
bottom: ADVANCED_VERTICAL_PADDING_MAX,
21+
left: 22,
22+
right: 22,
23+
linked: false,
24+
});
25+
});
26+
27+
it("keeps linked padding clamped to the original range", () => {
28+
const editor = normalizeProjectEditor({
29+
padding: {
30+
top: ADVANCED_VERTICAL_PADDING_MAX,
31+
bottom: ADVANCED_VERTICAL_PADDING_MAX,
32+
left: ADVANCED_VERTICAL_PADDING_MAX,
33+
right: ADVANCED_VERTICAL_PADDING_MAX,
34+
linked: true,
35+
},
36+
});
37+
38+
expect(editor.padding).toMatchObject({
39+
top: 100,
40+
bottom: 100,
41+
left: 100,
42+
right: 100,
43+
linked: true,
44+
});
45+
});
46+
});

src/components/video-editor/projectPersistence.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers";
2222
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
2323
import { CURSOR_MOTION_PRESETS, resolveCursorMotionPresetId } from "./cursorMotionPresets";
2424
import {
25+
ADVANCED_VERTICAL_PADDING_MAX,
2526
type AnnotationRegion,
2627
type AudioRegion,
2728
type AutoCaptionAnimation,
@@ -950,14 +951,17 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
950951
const p = editor.padding;
951952
if (p && typeof p === "object") {
952953
const linked = typeof p.linked === "boolean" ? p.linked : true;
953-
const top = isFiniteNumber(p.top) ? clamp(p.top, 0, 100) : DEFAULT_PADDING.top;
954+
const verticalMax = linked ? 100 : ADVANCED_VERTICAL_PADDING_MAX;
955+
const top = isFiniteNumber(p.top)
956+
? clamp(p.top, 0, verticalMax)
957+
: DEFAULT_PADDING.top;
954958
if (linked) {
955959
return { top, bottom: top, left: top, right: top, linked: true };
956960
}
957961
return {
958962
top,
959963
bottom: isFiniteNumber(p.bottom)
960-
? clamp(p.bottom, 0, 100)
964+
? clamp(p.bottom, 0, verticalMax)
961965
: DEFAULT_PADDING.bottom,
962966
left: isFiniteNumber(p.left) ? clamp(p.left, 0, 100) : DEFAULT_PADDING.left,
963967
right: isFiniteNumber(p.right) ? clamp(p.right, 0, 100) : DEFAULT_PADDING.right,

src/components/video-editor/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ export const DEFAULT_CROP_REGION: CropRegion = {
497497
height: 1,
498498
};
499499

500+
export const ADVANCED_VERTICAL_PADDING_MAX = 250;
501+
500502
export interface Padding {
501503
top: number;
502504
bottom: number;

src/components/video-editor/videoPlayback/layoutUtils.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,59 @@
11
import { describe, expect, it } from "vitest";
2-
import { scalePreviewBorderRadius } from "./layoutUtils";
2+
3+
import { ADVANCED_VERTICAL_PADDING_MAX } from "../types";
4+
import { computePaddedLayout, scalePreviewBorderRadius } from "./layoutUtils";
5+
6+
const BASE_LAYOUT_PARAMS = {
7+
width: 1000,
8+
height: 1000,
9+
cropRegion: { x: 0, y: 0, width: 1, height: 1 },
10+
videoWidth: 1000,
11+
videoHeight: 1000,
12+
};
13+
14+
describe("computePaddedLayout", () => {
15+
it("allows advanced bottom padding to pin the video to the top edge", () => {
16+
const layout = computePaddedLayout({
17+
...BASE_LAYOUT_PARAMS,
18+
padding: {
19+
top: 0,
20+
bottom: ADVANCED_VERTICAL_PADDING_MAX,
21+
left: 0,
22+
right: 0,
23+
linked: false,
24+
},
25+
});
26+
27+
expect(layout.centerOffsetY).toBeCloseTo(0);
28+
});
29+
30+
it("allows advanced top padding to pin the video to the bottom edge", () => {
31+
const layout = computePaddedLayout({
32+
...BASE_LAYOUT_PARAMS,
33+
padding: {
34+
top: ADVANCED_VERTICAL_PADDING_MAX,
35+
bottom: 0,
36+
left: 0,
37+
right: 0,
38+
linked: false,
39+
},
40+
});
41+
42+
expect(layout.centerOffsetY + layout.croppedDisplayHeight).toBeCloseTo(
43+
BASE_LAYOUT_PARAMS.height,
44+
);
45+
});
46+
47+
it("preserves linked padding centering behavior", () => {
48+
const layout = computePaddedLayout({
49+
...BASE_LAYOUT_PARAMS,
50+
padding: { top: 20, bottom: 20, left: 20, right: 20, linked: true },
51+
});
52+
53+
expect(layout.centerOffsetY).toBeCloseTo(40);
54+
expect(layout.centerOffsetY + layout.croppedDisplayHeight).toBeCloseTo(960);
55+
});
56+
});
357

458
describe("scalePreviewBorderRadius", () => {
559
it("matches export scaling against the logical preview size", () => {
@@ -13,4 +67,4 @@ describe("scalePreviewBorderRadius", () => {
1367
expect(scalePreviewBorderRadius(960, 0, 16)).toBe(0);
1468
expect(scalePreviewBorderRadius(960, 540, -8)).toBe(0);
1569
});
16-
});
70+
});

src/components/video-editor/videoPlayback/layoutUtils.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import { Application, Graphics, Sprite } from "pixi.js";
22
import { drawSquircleOnGraphics } from "@/lib/geometry/squircle";
3-
import type { CropRegion, Padding } from "../types";
3+
import { ADVANCED_VERTICAL_PADDING_MAX, type CropRegion, type Padding } from "../types";
44

55
export const PADDING_SCALE_FACTOR = 0.2;
66
export const BASE_PREVIEW_WIDTH = 1920;
77
export const BASE_PREVIEW_HEIGHT = 1080;
88

9-
export function scalePreviewBorderRadius(
10-
width: number,
11-
height: number,
12-
borderRadius = 0,
13-
): number {
9+
export function scalePreviewBorderRadius(width: number, height: number, borderRadius = 0): number {
1410
if (width <= 0 || height <= 0) {
1511
return 0;
1612
}
@@ -23,12 +19,7 @@ export function isZeroPadding(padding: Padding | number): boolean {
2319
if (typeof padding === "number") {
2420
return padding === 0;
2521
}
26-
return (
27-
padding.top === 0 &&
28-
padding.bottom === 0 &&
29-
padding.left === 0 &&
30-
padding.right === 0
31-
);
22+
return padding.top === 0 && padding.bottom === 0 && padding.left === 0 && padding.right === 0;
3223
}
3324

3425
export interface PaddedLayoutResult {
@@ -64,13 +55,21 @@ export function computePaddedLayout(params: {
6455
? { top: padding, bottom: padding, left: padding, right: padding }
6556
: padding;
6657

67-
// Padding is a percentage (0-100)
68-
// Clamp to ensure we don't have overlapping padding that exceeds 100% of a dimension
69-
const clampPercent = (v: number) => Math.min(100, Math.max(0, v));
70-
const leftPadFrac = (clampPercent(p.left) / 100) * PADDING_SCALE_FACTOR;
71-
const rightPadFrac = (clampPercent(p.right) / 100) * PADDING_SCALE_FACTOR;
72-
const topPadFrac = (clampPercent(p.top) / 100) * PADDING_SCALE_FACTOR;
73-
const bottomPadFrac = (clampPercent(p.bottom) / 100) * PADDING_SCALE_FACTOR;
58+
// Padding is a percentage. Linked padding keeps the original 0-100 scaling
59+
// behavior; advanced vertical padding gets extra range for positioning.
60+
const isAdvancedPadding = typeof padding !== "number" && padding.linked === false;
61+
const clampPercent = (v: number, max = 100) => Math.min(max, Math.max(0, v));
62+
const leftPercent = clampPercent(p.left);
63+
const rightPercent = clampPercent(p.right);
64+
const topPercent = clampPercent(p.top, isAdvancedPadding ? ADVANCED_VERTICAL_PADDING_MAX : 100);
65+
const bottomPercent = clampPercent(
66+
p.bottom,
67+
isAdvancedPadding ? ADVANCED_VERTICAL_PADDING_MAX : 100,
68+
);
69+
const leftPadFrac = (leftPercent / 100) * PADDING_SCALE_FACTOR;
70+
const rightPadFrac = (rightPercent / 100) * PADDING_SCALE_FACTOR;
71+
const topPadFrac = (Math.min(topPercent, 100) / 100) * PADDING_SCALE_FACTOR;
72+
const bottomPadFrac = (Math.min(bottomPercent, 100) / 100) * PADDING_SCALE_FACTOR;
7473

7574
const availableFracW = Math.max(0, 1.0 - leftPadFrac - rightPadFrac);
7675
const availableFracH = Math.max(0, 1.0 - topPadFrac - bottomPadFrac);
@@ -103,17 +102,24 @@ export function computePaddedLayout(params: {
103102
const fullFrameDisplayH = fullFrameVideoH * scale;
104103

105104
const availableCenterX = leftPadFrac * width + maxDisplayWidth / 2;
106-
const availableCenterY = topPadFrac * height + maxDisplayHeight / 2;
105+
const availableCenterY = isAdvancedPadding
106+
? (() => {
107+
const verticalTravel = Math.max(0, height - fullFrameDisplayH);
108+
const centeredOffsetY = verticalTravel / 2;
109+
const directionalOffsetY =
110+
centeredOffsetY +
111+
((topPercent - bottomPercent) / ADVANCED_VERTICAL_PADDING_MAX) *
112+
centeredOffsetY;
113+
const frameOffsetY = Math.min(verticalTravel, Math.max(0, directionalOffsetY));
114+
return frameOffsetY + fullFrameDisplayH / 2;
115+
})()
116+
: topPadFrac * height + maxDisplayHeight / 2;
107117

108118
const frameCenterX = availableCenterX - fullFrameDisplayW / 2;
109119
const frameCenterY = availableCenterY - fullFrameDisplayH / 2;
110120

111-
const centerOffsetX = insets
112-
? frameCenterX + insets.left * fullFrameDisplayW
113-
: frameCenterX;
114-
const centerOffsetY = insets
115-
? frameCenterY + insets.top * fullFrameDisplayH
116-
: frameCenterY;
121+
const centerOffsetX = insets ? frameCenterX + insets.left * fullFrameDisplayW : frameCenterX;
122+
const centerOffsetY = insets ? frameCenterY + insets.top * fullFrameDisplayH : frameCenterY;
117123

118124
const spriteX = centerOffsetX - crop.x * fullVideoDisplayWidth;
119125
const spriteY = centerOffsetY - crop.y * fullVideoDisplayHeight;

0 commit comments

Comments
 (0)