diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-27 00:55:15 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:56:48 +0200 |
| commit | 4d127a038bf768eded68aae3cfa5c1b12241bdac (patch) | |
| tree | 16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/hooks/use-mask-handles.ts | |
| parent | 82817cb50cb0487350573d6129130aec705f924b (diff) | |
| download | opencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz | |
chore: normalize line endings
Diffstat (limited to 'apps/web/src/hooks/use-mask-handles.ts')
| -rw-r--r-- | apps/web/src/hooks/use-mask-handles.ts | 516 |
1 files changed, 258 insertions, 258 deletions
diff --git a/apps/web/src/hooks/use-mask-handles.ts b/apps/web/src/hooks/use-mask-handles.ts index c3e83304..63fdd674 100644 --- a/apps/web/src/hooks/use-mask-handles.ts +++ b/apps/web/src/hooks/use-mask-handles.ts @@ -1,258 +1,258 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; -import { useShiftKey } from "@/hooks/use-shift-key"; -import { masksRegistry } from "@/lib/masks"; -import { - getMaskHandlePositions, - getLineMaskLinePoints, -} from "@/lib/masks/handle-positions"; -import { snapMaskInteraction } from "@/lib/masks/snap"; -import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; -import { - SNAP_THRESHOLD_SCREEN_PIXELS, - type SnapLine, -} from "@/lib/preview/preview-snap"; -import type { ParamValues } from "@/lib/params"; -import type { - BaseMaskParams, - MaskHandlePosition, - MaskLinePoints, -} from "@/lib/masks/types"; -import type { MaskableElement } from "@/lib/timeline"; -import { registerCanceller } from "@/lib/cancel-interaction"; - -interface DragState { - trackId: string; - elementId: string; - handleId: string; - startCanvasX: number; - startCanvasY: number; - startParams: BaseMaskParams & ParamValues; -} - -export function useMaskHandles({ - onSnapLinesChange, -}: { - onSnapLinesChange?: (lines: SnapLine[]) => void; -}) { - const editor = useEditor(); - const isShiftHeldRef = useShiftKey(); - const viewport = usePreviewViewport(); - const [activeHandleId, setActiveHandleId] = useState<string | null>(null); - const dragStateRef = useRef<DragState | null>(null); - const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>( - null, - ); - - const tracks = useEditor((e) => e.timeline.getRenderTracks()); - const currentTime = useEditor((e) => e.playback.getCurrentTime()); - const mediaAssets = useEditor((e) => e.media.getAssets()); - const canvasSize = useEditor( - (e) => e.project.getActive().settings.canvasSize, - ); - const selectedElements = useEditor((e) => e.selection.getSelectedElements()); - - const elementsWithBounds = getVisibleElementsWithBounds({ - tracks, - currentTime, - canvasSize, - mediaAssets, - }); - - const selectedWithMask = - selectedElements.length === 1 - ? (() => { - const sel = selectedElements[0]; - const entry = elementsWithBounds.find( - (item) => - item.trackId === sel.trackId && item.elementId === sel.elementId, - ); - if (!entry) return null; - const element = entry.element as MaskableElement; - if (!element.masks?.length) return null; - return { ...entry, element, mask: element.masks[0] }; - })() - : null; - - const handlePositions: MaskHandlePosition[] = selectedWithMask - ? (() => { - const def = masksRegistry.get(selectedWithMask.mask.type); - const { x: scaleX, y: scaleY } = viewport.getDisplayScale(); - const displayScale = (scaleX + scaleY) / 2; - return getMaskHandlePositions({ - overlayShape: def.overlayShape, - features: def.features, - params: selectedWithMask.mask.params, - bounds: selectedWithMask.bounds, - displayScale, - }); - })() - : []; - - const linePoints: MaskLinePoints | null = - selectedWithMask?.mask.type === "split" - ? getLineMaskLinePoints({ - centerX: selectedWithMask.mask.params.centerX, - centerY: selectedWithMask.mask.params.centerY, - rotation: selectedWithMask.mask.params.rotation, - bounds: selectedWithMask.bounds, - }) - : null; - - const clearMaskHandleState = useCallback(() => { - dragStateRef.current = null; - setActiveHandleId(null); - onSnapLinesChange?.([]); - }, [onSnapLinesChange]); - - const releaseCapturedPointer = useCallback(() => { - const capture = captureRef.current; - if (!capture) return; - - if (capture.element.hasPointerCapture(capture.pointerId)) { - capture.element.releasePointerCapture(capture.pointerId); - } - - captureRef.current = null; - }, []); - - useEffect(() => { - if (!activeHandleId) return; - - return registerCanceller({ - fn: () => { - editor.timeline.discardPreview(); - clearMaskHandleState(); - releaseCapturedPointer(); - }, - }); - }, [ - activeHandleId, - clearMaskHandleState, - editor.timeline, - releaseCapturedPointer, - ]); - - const handlePointerDown = useCallback( - ({ event, handleId }: { event: React.PointerEvent; handleId: string }) => { - if (!selectedWithMask) return; - event.stopPropagation(); - - const pos = viewport.screenToCanvas({ - clientX: event.clientX, - clientY: event.clientY, - }); - if (!pos) return; - - dragStateRef.current = { - trackId: selectedWithMask.trackId, - elementId: selectedWithMask.elementId, - handleId, - startCanvasX: pos.x, - startCanvasY: pos.y, - startParams: { ...selectedWithMask.mask.params }, - }; - setActiveHandleId(handleId); - const captureTarget = event.currentTarget as HTMLElement; - captureTarget.setPointerCapture(event.pointerId); - captureRef.current = { - element: captureTarget, - pointerId: event.pointerId, - }; - }, - [selectedWithMask, viewport], - ); - - const handlePointerMove = useCallback( - ({ event }: { event: React.PointerEvent }) => { - const drag = dragStateRef.current; - if (!drag || !selectedWithMask) return; - - const pos = viewport.screenToCanvas({ - clientX: event.clientX, - clientY: event.clientY, - }); - if (!pos) return; - - const deltaX = pos.x - drag.startCanvasX; - const deltaY = pos.y - drag.startCanvasY; - const def = masksRegistry.get(selectedWithMask.mask.type); - - const rawParams = def.computeParamUpdate({ - handleId: drag.handleId, - startParams: drag.startParams, - deltaX, - deltaY, - startCanvasX: drag.startCanvasX, - startCanvasY: drag.startCanvasY, - bounds: selectedWithMask.bounds, - canvasSize, - }); - const proposedParams = { ...drag.startParams, ...rawParams }; - - const snapThreshold = viewport.screenPixelsToLogicalThreshold({ - screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, - }); - const { params: nextParams, activeLines } = isShiftHeldRef.current - ? { params: proposedParams, activeLines: [] as SnapLine[] } - : snapMaskInteraction({ - handleId: drag.handleId, - startParams: drag.startParams, - proposedParams, - bounds: selectedWithMask.bounds, - canvasSize, - snapThreshold, - }); - - onSnapLinesChange?.(activeLines); - - const updatedMask = { - ...selectedWithMask.mask, - params: nextParams, - }; - editor.timeline.previewElements({ - updates: [ - { - trackId: drag.trackId, - elementId: drag.elementId, - updates: { - masks: [ - updatedMask, - ...(selectedWithMask.element.masks?.slice(1) ?? []), - ], - } as Partial<MaskableElement>, - }, - ], - }); - }, - [ - selectedWithMask, - canvasSize, - editor, - isShiftHeldRef, - onSnapLinesChange, - viewport, - ], - ); - - const handlePointerUp = useCallback(() => { - if (dragStateRef.current) { - editor.timeline.commitPreview(); - clearMaskHandleState(); - } - releaseCapturedPointer(); - }, - [clearMaskHandleState, editor, releaseCapturedPointer], - ); - - return { - selectedWithMask, - handlePositions, - linePoints, - activeHandleId, - handlePointerDown, - handlePointerMove, - handlePointerUp, - }; -} +import { useCallback, useEffect, useRef, useState } from "react";
+import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport";
+import { useEditor } from "@/hooks/use-editor";
+import { useShiftKey } from "@/hooks/use-shift-key";
+import { masksRegistry } from "@/lib/masks";
+import {
+ getMaskHandlePositions,
+ getLineMaskLinePoints,
+} from "@/lib/masks/handle-positions";
+import { snapMaskInteraction } from "@/lib/masks/snap";
+import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds";
+import {
+ SNAP_THRESHOLD_SCREEN_PIXELS,
+ type SnapLine,
+} from "@/lib/preview/preview-snap";
+import type { ParamValues } from "@/lib/params";
+import type {
+ BaseMaskParams,
+ MaskHandlePosition,
+ MaskLinePoints,
+} from "@/lib/masks/types";
+import type { MaskableElement } from "@/lib/timeline";
+import { registerCanceller } from "@/lib/cancel-interaction";
+
+interface DragState {
+ trackId: string;
+ elementId: string;
+ handleId: string;
+ startCanvasX: number;
+ startCanvasY: number;
+ startParams: BaseMaskParams & ParamValues;
+}
+
+export function useMaskHandles({
+ onSnapLinesChange,
+}: {
+ onSnapLinesChange?: (lines: SnapLine[]) => void;
+}) {
+ const editor = useEditor();
+ const isShiftHeldRef = useShiftKey();
+ const viewport = usePreviewViewport();
+ const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
+ const dragStateRef = useRef<DragState | null>(null);
+ const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>(
+ null,
+ );
+
+ const tracks = useEditor((e) => e.timeline.getRenderTracks());
+ const currentTime = useEditor((e) => e.playback.getCurrentTime());
+ const mediaAssets = useEditor((e) => e.media.getAssets());
+ const canvasSize = useEditor(
+ (e) => e.project.getActive().settings.canvasSize,
+ );
+ const selectedElements = useEditor((e) => e.selection.getSelectedElements());
+
+ const elementsWithBounds = getVisibleElementsWithBounds({
+ tracks,
+ currentTime,
+ canvasSize,
+ mediaAssets,
+ });
+
+ const selectedWithMask =
+ selectedElements.length === 1
+ ? (() => {
+ const sel = selectedElements[0];
+ const entry = elementsWithBounds.find(
+ (item) =>
+ item.trackId === sel.trackId && item.elementId === sel.elementId,
+ );
+ if (!entry) return null;
+ const element = entry.element as MaskableElement;
+ if (!element.masks?.length) return null;
+ return { ...entry, element, mask: element.masks[0] };
+ })()
+ : null;
+
+ const handlePositions: MaskHandlePosition[] = selectedWithMask
+ ? (() => {
+ const def = masksRegistry.get(selectedWithMask.mask.type);
+ const { x: scaleX, y: scaleY } = viewport.getDisplayScale();
+ const displayScale = (scaleX + scaleY) / 2;
+ return getMaskHandlePositions({
+ overlayShape: def.overlayShape,
+ features: def.features,
+ params: selectedWithMask.mask.params,
+ bounds: selectedWithMask.bounds,
+ displayScale,
+ });
+ })()
+ : [];
+
+ const linePoints: MaskLinePoints | null =
+ selectedWithMask?.mask.type === "split"
+ ? getLineMaskLinePoints({
+ centerX: selectedWithMask.mask.params.centerX,
+ centerY: selectedWithMask.mask.params.centerY,
+ rotation: selectedWithMask.mask.params.rotation,
+ bounds: selectedWithMask.bounds,
+ })
+ : null;
+
+ const clearMaskHandleState = useCallback(() => {
+ dragStateRef.current = null;
+ setActiveHandleId(null);
+ onSnapLinesChange?.([]);
+ }, [onSnapLinesChange]);
+
+ const releaseCapturedPointer = useCallback(() => {
+ const capture = captureRef.current;
+ if (!capture) return;
+
+ if (capture.element.hasPointerCapture(capture.pointerId)) {
+ capture.element.releasePointerCapture(capture.pointerId);
+ }
+
+ captureRef.current = null;
+ }, []);
+
+ useEffect(() => {
+ if (!activeHandleId) return;
+
+ return registerCanceller({
+ fn: () => {
+ editor.timeline.discardPreview();
+ clearMaskHandleState();
+ releaseCapturedPointer();
+ },
+ });
+ }, [
+ activeHandleId,
+ clearMaskHandleState,
+ editor.timeline,
+ releaseCapturedPointer,
+ ]);
+
+ const handlePointerDown = useCallback(
+ ({ event, handleId }: { event: React.PointerEvent; handleId: string }) => {
+ if (!selectedWithMask) return;
+ event.stopPropagation();
+
+ const pos = viewport.screenToCanvas({
+ clientX: event.clientX,
+ clientY: event.clientY,
+ });
+ if (!pos) return;
+
+ dragStateRef.current = {
+ trackId: selectedWithMask.trackId,
+ elementId: selectedWithMask.elementId,
+ handleId,
+ startCanvasX: pos.x,
+ startCanvasY: pos.y,
+ startParams: { ...selectedWithMask.mask.params },
+ };
+ setActiveHandleId(handleId);
+ const captureTarget = event.currentTarget as HTMLElement;
+ captureTarget.setPointerCapture(event.pointerId);
+ captureRef.current = {
+ element: captureTarget,
+ pointerId: event.pointerId,
+ };
+ },
+ [selectedWithMask, viewport],
+ );
+
+ const handlePointerMove = useCallback(
+ ({ event }: { event: React.PointerEvent }) => {
+ const drag = dragStateRef.current;
+ if (!drag || !selectedWithMask) return;
+
+ const pos = viewport.screenToCanvas({
+ clientX: event.clientX,
+ clientY: event.clientY,
+ });
+ if (!pos) return;
+
+ const deltaX = pos.x - drag.startCanvasX;
+ const deltaY = pos.y - drag.startCanvasY;
+ const def = masksRegistry.get(selectedWithMask.mask.type);
+
+ const rawParams = def.computeParamUpdate({
+ handleId: drag.handleId,
+ startParams: drag.startParams,
+ deltaX,
+ deltaY,
+ startCanvasX: drag.startCanvasX,
+ startCanvasY: drag.startCanvasY,
+ bounds: selectedWithMask.bounds,
+ canvasSize,
+ });
+ const proposedParams = { ...drag.startParams, ...rawParams };
+
+ const snapThreshold = viewport.screenPixelsToLogicalThreshold({
+ screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
+ });
+ const { params: nextParams, activeLines } = isShiftHeldRef.current
+ ? { params: proposedParams, activeLines: [] as SnapLine[] }
+ : snapMaskInteraction({
+ handleId: drag.handleId,
+ startParams: drag.startParams,
+ proposedParams,
+ bounds: selectedWithMask.bounds,
+ canvasSize,
+ snapThreshold,
+ });
+
+ onSnapLinesChange?.(activeLines);
+
+ const updatedMask = {
+ ...selectedWithMask.mask,
+ params: nextParams,
+ };
+ editor.timeline.previewElements({
+ updates: [
+ {
+ trackId: drag.trackId,
+ elementId: drag.elementId,
+ updates: {
+ masks: [
+ updatedMask,
+ ...(selectedWithMask.element.masks?.slice(1) ?? []),
+ ],
+ } as Partial<MaskableElement>,
+ },
+ ],
+ });
+ },
+ [
+ selectedWithMask,
+ canvasSize,
+ editor,
+ isShiftHeldRef,
+ onSnapLinesChange,
+ viewport,
+ ],
+ );
+
+ const handlePointerUp = useCallback(() => {
+ if (dragStateRef.current) {
+ editor.timeline.commitPreview();
+ clearMaskHandleState();
+ }
+ releaseCapturedPointer();
+ },
+ [clearMaskHandleState, editor, releaseCapturedPointer],
+ );
+
+ return {
+ selectedWithMask,
+ handlePositions,
+ linePoints,
+ activeHandleId,
+ handlePointerDown,
+ handlePointerMove,
+ handlePointerUp,
+ };
+}
|
