summaryrefslogtreecommitdiff
path: root/apps/web/src/preview/components/mask-handles.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/preview/components/mask-handles.tsx')
-rw-r--r--apps/web/src/preview/components/mask-handles.tsx308
1 files changed, 308 insertions, 0 deletions
diff --git a/apps/web/src/preview/components/mask-handles.tsx b/apps/web/src/preview/components/mask-handles.tsx
new file mode 100644
index 00000000..340cab0a
--- /dev/null
+++ b/apps/web/src/preview/components/mask-handles.tsx
@@ -0,0 +1,308 @@
+"use client";
+
+import { PEN_CURSOR } from "@/preview/components/cursors";
+import { usePreviewViewport } from "@/preview/components/preview-viewport";
+import { useMaskHandles } from "@/masks/use-mask-handles";
+import type { SnapLine } from "@/preview/preview-snap";
+import {
+ CornerHandle,
+ CircleHandle,
+ CanvasPathOutline,
+ EdgeHandle,
+ IconHandle,
+ LineOverlay,
+ BoundingBoxOutline,
+ ShapeOutline,
+} from "./handle-primitives";
+
+const CUSTOM_MASK_ANCHOR_SIZE = 7;
+const CUSTOM_MASK_TANGENT_SIZE = 6;
+import { Rotate01Icon, FeatherIcon } from "@hugeicons/core-free-icons";
+
+export function MaskHandles({
+ onSnapLinesChange,
+}: {
+ onSnapLinesChange?: (lines: SnapLine[]) => void;
+}) {
+ const viewport = usePreviewViewport();
+ const {
+ selectedWithMask,
+ handlePositions,
+ overlays,
+ isCreatingCustomMask,
+ handleCanvasPointerDown,
+ handlePointerDown,
+ handlePointerMove,
+ handlePointerUp,
+ } = useMaskHandles({ onSnapLinesChange });
+
+ if (!selectedWithMask) return null;
+
+ const toOverlay = ({
+ canvasX,
+ canvasY,
+ }: {
+ canvasX: number;
+ canvasY: number;
+ }) =>
+ viewport.canvasToOverlay({
+ canvasX,
+ canvasY,
+ });
+
+ const { x: scaleX, y: scaleY } = viewport.getDisplayScale();
+ const canvasOrigin = toOverlay({ canvasX: 0, canvasY: 0 });
+
+ const onPointerMove = (event: React.PointerEvent) => {
+ if (viewport.handlePanPointerMove({ event })) {
+ return;
+ }
+
+ handlePointerMove({ event });
+ };
+ const onPointerUp = (event: React.PointerEvent) => {
+ if (viewport.handlePanPointerUp({ event })) {
+ return;
+ }
+
+ handlePointerUp();
+ };
+ const handleMaskPointerDown = ({
+ event,
+ handleId,
+ }: {
+ event: React.PointerEvent;
+ handleId: string;
+ }) => {
+ if (viewport.handlePanPointerDown({ event })) {
+ return;
+ }
+
+ handlePointerDown({ event, handleId });
+ };
+ const handleCanvasOverlayPointerDown = (event: React.PointerEvent) => {
+ if (viewport.handlePanPointerDown({ event })) {
+ return;
+ }
+
+ handleCanvasPointerDown({ event });
+ };
+
+ return (
+ <div
+ className="pointer-events-none absolute inset-0 overflow-hidden"
+ aria-hidden
+ >
+ {isCreatingCustomMask ? (
+ <div
+ className="absolute inset-0 pointer-events-auto"
+ style={{ cursor: PEN_CURSOR }}
+ onPointerDown={handleCanvasOverlayPointerDown}
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ onPointerCancel={onPointerUp}
+ />
+ ) : null}
+ {overlays.map((overlay) => {
+ const overlayHandleId = overlay.handleId;
+ const pointerHandlers = overlayHandleId
+ ? {
+ onPointerDown: (event: React.PointerEvent) =>
+ handleMaskPointerDown({ event, handleId: overlayHandleId }),
+ onPointerMove,
+ onPointerUp,
+ }
+ : {};
+
+ if (overlay.type === "line") {
+ return (
+ <LineOverlay
+ key={overlay.id}
+ cursor={overlay.cursor}
+ start={toOverlay({
+ canvasX: overlay.start.x,
+ canvasY: overlay.start.y,
+ })}
+ end={toOverlay({
+ canvasX: overlay.end.x,
+ canvasY: overlay.end.y,
+ })}
+ {...pointerHandlers}
+ />
+ );
+ }
+
+ if (overlay.type === "rect") {
+ return (
+ <BoundingBoxOutline
+ key={overlay.id}
+ center={toOverlay({
+ canvasX: overlay.center.x,
+ canvasY: overlay.center.y,
+ })}
+ outlineWidth={overlay.width * scaleX}
+ outlineHeight={overlay.height * scaleY}
+ rotation={overlay.rotation}
+ cursor={overlay.cursor}
+ dashed={overlay.dashed}
+ {...pointerHandlers}
+ />
+ );
+ }
+
+ if (overlay.type === "shape") {
+ return (
+ <ShapeOutline
+ key={overlay.id}
+ center={toOverlay({
+ canvasX: overlay.center.x,
+ canvasY: overlay.center.y,
+ })}
+ outlineWidth={overlay.width * scaleX}
+ outlineHeight={overlay.height * scaleY}
+ rotation={overlay.rotation}
+ pathData={overlay.pathData}
+ cursor={overlay.cursor}
+ {...pointerHandlers}
+ />
+ );
+ }
+
+ if (overlay.type === "canvas-path") {
+ return (
+ <CanvasPathOutline
+ key={overlay.id}
+ pathData={overlay.pathData}
+ translateX={
+ overlay.coordinateSpace === "canvas" ? canvasOrigin.x : 0
+ }
+ translateY={
+ overlay.coordinateSpace === "canvas" ? canvasOrigin.y : 0
+ }
+ scaleX={overlay.coordinateSpace === "canvas" ? scaleX : 1}
+ scaleY={overlay.coordinateSpace === "canvas" ? scaleY : 1}
+ cursor={overlay.cursor}
+ strokeWidth={overlay.strokeWidth}
+ strokeOpacity={overlay.strokeOpacity}
+ {...pointerHandlers}
+ />
+ );
+ }
+
+ return null;
+ })}
+ {handlePositions.map((handle) => {
+ const screen = toOverlay({ canvasX: handle.x, canvasY: handle.y });
+
+ if (handle.kind === "icon" && handle.icon === "rotate") {
+ return (
+ <IconHandle
+ key={handle.id}
+ icon={Rotate01Icon}
+ screen={screen}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ if (handle.kind === "icon" && handle.icon === "feather") {
+ return (
+ <IconHandle
+ key={handle.id}
+ icon={FeatherIcon}
+ screen={screen}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ if (handle.kind === "edge" && handle.edgeAxis === "horizontal") {
+ return (
+ <EdgeHandle
+ key={handle.id}
+ edge="right"
+ screen={screen}
+ rotation={handle.rotation ?? 0}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ if (handle.kind === "edge" && handle.edgeAxis === "vertical") {
+ return (
+ <EdgeHandle
+ key={handle.id}
+ edge="bottom"
+ screen={screen}
+ rotation={handle.rotation ?? 0}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ if (handle.kind === "point" || handle.kind === "tangent") {
+ return (
+ <CircleHandle
+ key={handle.id}
+ screen={screen}
+ size={
+ handle.kind === "tangent"
+ ? CUSTOM_MASK_TANGENT_SIZE
+ : CUSTOM_MASK_ANCHOR_SIZE
+ }
+ isSelected={handle.isSelected}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ if (handle.kind === "corner") {
+ return (
+ <CornerHandle
+ key={handle.id}
+ screen={screen}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ }
+
+ return (
+ <CornerHandle
+ key={handle.id}
+ cursor={handle.cursor}
+ screen={screen}
+ onPointerDown={(event) =>
+ handleMaskPointerDown({ event, handleId: handle.id })
+ }
+ onPointerMove={onPointerMove}
+ onPointerUp={onPointerUp}
+ />
+ );
+ })}
+ </div>
+ );
+}