diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/components/editor/panels/preview/mask-handles.tsx | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/components/editor/panels/preview/mask-handles.tsx')
| -rw-r--r-- | apps/web/src/components/editor/panels/preview/mask-handles.tsx | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/apps/web/src/components/editor/panels/preview/mask-handles.tsx b/apps/web/src/components/editor/panels/preview/mask-handles.tsx new file mode 100644 index 00000000..0c11f27a --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/mask-handles.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; +import { useMaskHandles } from "@/hooks/use-mask-handles"; +import { masksRegistry } from "@/lib/masks"; +import type { SnapLine } from "@/lib/preview/preview-snap"; +import type { ParamValues } from "@/lib/params"; +import type { RectangleMaskParams } from "@/lib/masks/types"; +import { + CornerHandle, + EdgeHandle, + IconHandle, + LineOverlay, + BoundingBoxOutline, + ShapeOutline, +} from "./handle-primitives"; +import { Rotate01Icon, FeatherIcon } from "@hugeicons/core-free-icons"; + +function hasRectangleOutlineParams( + params: ParamValues, +): params is RectangleMaskParams { + return ( + typeof params.centerX === "number" && + typeof params.centerY === "number" && + typeof params.width === "number" && + typeof params.height === "number" + ); +} + +export function MaskHandles({ + onSnapLinesChange, +}: { + onSnapLinesChange?: (lines: SnapLine[]) => void; +}) { + const viewport = usePreviewViewport(); + const { + selectedWithMask, + handlePositions, + linePoints, + handlePointerDown, + handlePointerMove, + handlePointerUp, + } = useMaskHandles({ onSnapLinesChange }); + + if (!selectedWithMask) return null; + + const toOverlay = ({ + canvasX, + canvasY, + }: { + canvasX: number; + canvasY: number; + }) => + viewport.canvasToOverlay({ + canvasX, + canvasY, + }); + + const def = masksRegistry.get(selectedWithMask.mask.type); + const { bounds } = selectedWithMask; + const maskRotation = selectedWithMask.mask.params.rotation; + + const { x: scaleX, y: scaleY } = viewport.getDisplayScale(); + + const rectangleOutlineProps = hasRectangleOutlineParams( + selectedWithMask.mask.params, + ) + ? { + center: toOverlay({ + canvasX: + bounds.cx + selectedWithMask.mask.params.centerX * bounds.width, + canvasY: + bounds.cy + selectedWithMask.mask.params.centerY * bounds.height, + }), + outlineWidth: + selectedWithMask.mask.params.width * bounds.width * scaleX, + outlineHeight: + selectedWithMask.mask.params.height * bounds.height * scaleY, + rotation: maskRotation, + } + : null; + + const onPointerMove = (event: React.PointerEvent) => + handlePointerMove({ event }); + const onPointerUp = () => handlePointerUp(); + + return ( + <div + className="pointer-events-none absolute inset-0 overflow-hidden" + aria-hidden + > + {def.overlayShape === "line" && linePoints && ( + <LineOverlay + start={toOverlay({ + canvasX: linePoints.start.x, + canvasY: linePoints.start.y, + })} + end={toOverlay({ + canvasX: linePoints.end.x, + canvasY: linePoints.end.y, + })} + onPointerDown={(event) => + handlePointerDown({ event, handleId: "position" }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + )} + {def.overlayShape === "box" && rectangleOutlineProps && ( + def.buildOverlayPath ? ( + <> + <BoundingBoxOutline {...rectangleOutlineProps} dashed /> + <ShapeOutline + {...rectangleOutlineProps} + pathData={def.buildOverlayPath({ + width: rectangleOutlineProps.outlineWidth, + height: rectangleOutlineProps.outlineHeight, + })} + onPointerDown={(event) => + handlePointerDown({ event, handleId: "position" }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + </> + ) : ( + <BoundingBoxOutline + {...rectangleOutlineProps} + cursor="cursor-move" + onPointerDown={(event) => + handlePointerDown({ event, handleId: "position" }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ) + )} + {handlePositions.map((handle) => { + const screen = toOverlay({ canvasX: handle.x, canvasY: handle.y }); + + if (handle.id === "rotation") { + return ( + <IconHandle + key={handle.id} + icon={Rotate01Icon} + screen={screen} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "feather") { + return ( + <IconHandle + key={handle.id} + icon={FeatherIcon} + screen={screen} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "right" || handle.id === "left") { + return ( + <EdgeHandle + key={handle.id} + edge="right" + screen={screen} + rotation={maskRotation} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "bottom" || handle.id === "top") { + return ( + <EdgeHandle + key={handle.id} + edge="bottom" + screen={screen} + rotation={maskRotation} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if ( + handle.id === "top-left" || + handle.id === "top-right" || + handle.id === "bottom-left" || + handle.id === "bottom-right" || + handle.id === "scale" + ) { + return ( + <CornerHandle + key={handle.id} + screen={screen} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + return ( + <CornerHandle + key={handle.id} + cursor={handle.cursor} + screen={screen} + onPointerDown={(event) => + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + })} + </div> + ); +} |
