From 37215531c2484acb06dc39f35dec5af6a85b910c Mon Sep 17 00:00:00 2001 From: makaradam Date: Sat, 2 May 2026 10:24:04 +0200 Subject: [PATCH 1/4] feat: add custom zoom slider with continuous scale control (#513) Adds a Radix UI slider below the zoom preset buttons allowing any scale between 1.0x and 5.0x. When the slider value matches a preset exactly, that preset button also shows as active. - Add `customScale?: number` to `ZoomRegion` and `getZoomScale()` helper that returns customScale when set, falling back to ZOOM_DEPTH_SCALES[depth] - Overlay indicator, playback renderer, and frame exporter all use getZoomScale() so preview, playback, and export are consistent - Fix focus clamping in zoomRegionUtils and frameRenderer to use actual scale instead of depth-based preset scale, preventing zoom drift with custom values - Fix drag boundary in VideoPlayback to use clampFocusToScale with the actual scale so the full canvas is clickable at high custom zoom levels - Timeline item label shows custom scale value when set - Slider styled dark with green thumb/fill when a custom (non-preset) value is active Co-Authored-By: Claude Sonnet 4.6 --- src/components/video-editor/SettingsPanel.tsx | 78 ++++++++++++++++++- src/components/video-editor/VideoEditor.tsx | 26 +++++++ src/components/video-editor/VideoPlayback.tsx | 13 +--- src/components/video-editor/timeline/Item.tsx | 6 +- .../video-editor/timeline/TimelineEditor.tsx | 3 + src/components/video-editor/types.ts | 10 +++ .../videoPlayback/overlayUtils.ts | 11 +-- .../videoPlayback/zoomRegionUtils.ts | 10 +-- src/i18n/locales/en/settings.json | 1 + src/lib/exporter/frameRenderer.ts | 17 +--- 10 files changed, 138 insertions(+), 37 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 377cbbe..36fa255 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1,3 +1,4 @@ +import * as SliderPrimitive from "@radix-ui/react-slider"; import { Bug, ChevronDown, @@ -65,8 +66,11 @@ import type { import { DEFAULT_WEBCAM_SIZE_PRESET, MAX_PLAYBACK_SPEED, + MAX_ZOOM_SCALE, + MIN_ZOOM_SCALE, ROTATION_3D_PRESET_ORDER, SPEED_OPTIONS, + ZOOM_DEPTH_SCALES, } from "./types"; function CustomSpeedInput({ @@ -170,6 +174,9 @@ interface SettingsPanelProps { onWallpaperChange: (path: string) => void; selectedZoomDepth?: ZoomDepth | null; onZoomDepthChange?: (depth: ZoomDepth) => void; + selectedZoomCustomScale?: number | null; + onZoomCustomScaleChange?: (scale: number) => void; + onZoomCustomScaleCommit?: () => void; selectedZoomFocusMode?: ZoomFocusMode | null; onZoomFocusModeChange?: (mode: ZoomFocusMode) => void; hasCursorTelemetry?: boolean; @@ -263,6 +270,9 @@ export function SettingsPanel({ onWallpaperChange, selectedZoomDepth, onZoomDepthChange, + selectedZoomCustomScale, + onZoomCustomScaleChange, + onZoomCustomScaleCommit, selectedZoomFocusMode, onZoomFocusModeChange, hasCursorTelemetry = false, @@ -593,7 +603,9 @@ export function SettingsPanel({
{zoomEnabled && selectedZoomDepth && ( - {ZOOM_DEPTH_OPTIONS.find((o) => o.depth === selectedZoomDepth)?.label} + {selectedZoomCustomScale != null + ? `${selectedZoomCustomScale.toFixed(2)}×` + : ZOOM_DEPTH_OPTIONS.find((o) => o.depth === selectedZoomDepth)?.label} )} @@ -601,7 +613,10 @@ export function SettingsPanel({
{ZOOM_DEPTH_OPTIONS.map((option) => { - const isActive = selectedZoomDepth === option.depth; + const effectiveScale = + selectedZoomCustomScale ?? + (selectedZoomDepth != null ? ZOOM_DEPTH_SCALES[selectedZoomDepth] : null); + const isActive = effectiveScale === ZOOM_DEPTH_SCALES[option.depth]; return (