From 8db3bead13441a5882d8689dfc6a9f09c82375d9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Mar 2026 16:47:22 +0100 Subject: feat: masks, properties refactor, shaders, storage migrations, and more --- .../editor/panels/preview/mask-handles.tsx | 238 +++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 apps/web/src/components/editor/panels/preview/mask-handles.tsx (limited to 'apps/web/src/components/editor/panels/preview/mask-handles.tsx') 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 ( +
+ {def.overlayShape === "line" && linePoints && ( + + handlePointerDown({ event, handleId: "position" }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + )} + {def.overlayShape === "box" && rectangleOutlineProps && ( + def.buildOverlayPath ? ( + <> + + + handlePointerDown({ event, handleId: "position" }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + + ) : ( + + 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 ( + + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "feather") { + return ( + + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "right" || handle.id === "left") { + return ( + + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + if (handle.id === "bottom" || handle.id === "top") { + return ( + + 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 ( + + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + } + + return ( + + handlePointerDown({ event, handleId: handle.id }) + } + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + /> + ); + })} +
+ ); +} -- cgit v1.3.1