diff --git a/src/components/video-editor/WebcamCropControl.tsx b/src/components/video-editor/WebcamCropControl.tsx index 3f50c130..cef6a84a 100644 --- a/src/components/video-editor/WebcamCropControl.tsx +++ b/src/components/video-editor/WebcamCropControl.tsx @@ -5,6 +5,14 @@ import { normalizeWebcamCropRegion } from "./webcamOverlay"; type CropHandle = "move" | "nw" | "ne" | "sw" | "se"; +const HANDLE_LABELS: Record = { + move: "Move webcam crop", + nw: "Resize webcam crop from top left", + ne: "Resize webcam crop from top right", + sw: "Resize webcam crop from bottom left", + se: "Resize webcam crop from bottom right", +}; + interface WebcamCropControlProps { cropRegion: CropRegion; mirrored?: boolean; @@ -19,6 +27,8 @@ interface DragState { } const MIN_CROP_SIZE = 0.08; +const KEYBOARD_STEP = 0.01; +const KEYBOARD_FAST_STEP = 0.05; const RESIZE_HANDLES: Array<{ handle: Exclude; @@ -135,6 +145,9 @@ export function WebcamCropControl({ const cropTop = crop.y * 100; const cropWidth = crop.width * 100; const cropHeight = crop.height * 100; + const commitVisualCrop = (nextVisualCrop: CropRegion) => { + onCropChange(mirrored ? flipCropHorizontally(nextVisualCrop) : nextVisualCrop); + }; const getPointerPosition = (event: React.PointerEvent) => { const rect = containerRef.current?.getBoundingClientRect(); @@ -188,7 +201,7 @@ export function WebcamCropControl({ pointer.x - dragState.startX, pointer.y - dragState.startY, ); - onCropChange(mirrored ? flipCropHorizontally(nextVisualCrop) : nextVisualCrop); + commitVisualCrop(nextVisualCrop); }; const endDrag = (event: React.PointerEvent) => { @@ -203,6 +216,33 @@ export function WebcamCropControl({ setActiveHandle(null); }; + const getKeyboardDelta = (event: React.KeyboardEvent) => { + const step = event.shiftKey ? KEYBOARD_FAST_STEP : KEYBOARD_STEP; + if (event.key === "ArrowLeft") { + return { x: -step, y: 0 }; + } + if (event.key === "ArrowRight") { + return { x: step, y: 0 }; + } + if (event.key === "ArrowUp") { + return { x: 0, y: -step }; + } + if (event.key === "ArrowDown") { + return { x: 0, y: step }; + } + return null; + }; + + const handleKeyboardAdjust = (event: React.KeyboardEvent, handle: CropHandle) => { + const delta = getKeyboardDelta(event); + if (!delta) { + return; + } + event.preventDefault(); + event.stopPropagation(); + commitVisualCrop(resizeCrop(crop, handle, delta.x, delta.y)); + }; + return (
handlePointerDown(event, "move")} + onFocus={() => setActiveHandle("move")} + onBlur={() => setActiveHandle(null)} + onKeyDown={(event) => handleKeyboardAdjust(event, "move")} >
@@ -259,13 +304,23 @@ export function WebcamCropControl({ {RESIZE_HANDLES.map((handle) => (
handlePointerDown(event, handle.handle)} + onFocus={() => setActiveHandle(handle.handle)} + onBlur={() => setActiveHandle(null)} + onKeyDown={(event) => handleKeyboardAdjust(event, handle.handle)} /> ))}