diff options
Diffstat (limited to 'apps/web/src/masks/definitions/custom.ts')
| -rw-r--r-- | apps/web/src/masks/definitions/custom.ts | 656 |
1 files changed, 0 insertions, 656 deletions
diff --git a/apps/web/src/masks/definitions/custom.ts b/apps/web/src/masks/definitions/custom.ts deleted file mode 100644 index 08155aae..00000000 --- a/apps/web/src/masks/definitions/custom.ts +++ /dev/null @@ -1,656 +0,0 @@ -import { generateUUID } from "@/utils/id"; -import type { ParamDefinition } from "@/params"; -import { PEN_CURSOR } from "@/preview/components/cursors"; -import type { ElementBounds } from "@/preview/element-bounds"; -import type { - CustomMask, - CustomMaskParams, - MaskDefinition, - MaskHandlePosition, - MaskOverlay, - MaskParamUpdateArgs, -} from "@/masks/types"; -import { - buildCustomMaskPath2D, - buildCustomMaskSvgPath, - customMaskCanvasPointToLocal, - findClosestPointOnCustomMaskSegment, - getCustomMaskCanvasSegments, - getCustomMaskCanvasGeometry, - getCustomMaskLocalBounds, - getCustomMaskSegmentCount, - insertPointIntoCustomMaskSegment, - recenterCustomMaskPath, - type CustomMaskPathPoint, -} from "@/masks/custom-path"; -import { getBoxMaskHandlePositions } from "@/masks/handle-positions"; -import { computeFeatherUpdate } from "@/masks/param-update"; -import { - setMaskLocalCenter, - toGlobalMaskSnapLines, -} from "@/masks/geometry"; -import { - snapPosition, - snapRotation, - snapScale, -} from "@/preview/preview-snap"; - -const PERCENTAGE_DISPLAY = { - displayMultiplier: 100, - step: 1, -} as const; - -const CUSTOM_MASK_PARAMS: ParamDefinition<keyof CustomMaskParams & string>[] = [ - { - key: "centerX", - label: "X", - type: "number", - default: 0, - min: -100, - max: 100, - ...PERCENTAGE_DISPLAY, - }, - { - key: "centerY", - label: "Y", - type: "number", - default: 0, - min: -100, - max: 100, - ...PERCENTAGE_DISPLAY, - }, - { - key: "rotation", - label: "Rotation", - type: "number", - default: 0, - min: 0, - max: 360, - step: 1, - }, - { - key: "scale", - label: "Scale", - type: "number", - default: 1, - min: 1, - max: 500, - ...PERCENTAGE_DISPLAY, - }, -]; - -function getCustomMaskDisplayHandles({ - params, - displayScale, - bounds, -}: { - params: CustomMaskParams; - displayScale: number; - bounds: ElementBounds; -}): { - handles: MaskHandlePosition[]; - overlays: MaskOverlay[]; -} { - const points = params.path; - const geometry = getCustomMaskCanvasGeometry({ - points, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - }); - - const handles: MaskHandlePosition[] = []; - const overlays: MaskOverlay[] = []; - - if (points.length > 0) { - overlays.push({ - id: "path", - type: "canvas-path", - pathData: buildCustomMaskSvgPath({ - points, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - closed: params.closed, - }), - coordinateSpace: "canvas", - }); - } - - if (params.closed) { - const segmentStrokeWidth = 12; - overlays.push( - ...getCustomMaskCanvasSegments({ - points, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - closed: true, - }).map((segment): MaskOverlay => ({ - id: `segment:${segment.index}`, - type: "canvas-path" as const, - pathData: segment.pathData, - coordinateSpace: "canvas" as const, - handleId: { kind: "segment", index: segment.index }, - cursor: PEN_CURSOR, - strokeOpacity: 0, - strokeWidth: segmentStrokeWidth, - })), - ); - } - - const localBounds = getCustomMaskLocalBounds({ points, bounds }); - if (params.closed && localBounds) { - handles.push( - ...getBoxMaskHandlePositions({ - centerX: params.centerX, - centerY: params.centerY, - width: (localBounds.width * params.scale) / bounds.width, - height: (localBounds.height * params.scale) / bounds.height, - rotation: params.rotation, - feather: params.feather, - sizeMode: "uniform", - showScaleHandle: false, - bounds, - displayScale, - }), - ); - } - - geometry.anchors.forEach((point) => { - handles.push({ - id: { kind: "anchor", pointId: point.id }, - x: point.anchor.x, - y: point.anchor.y, - cursor: params.closed ? "move" : "pointer", - kind: "point", - }); - }); - - return { - handles, - overlays, - }; -} - -function updateCustomMaskPoint({ - points, - pointId, - updater, -}: { - points: CustomMaskPathPoint[]; - pointId: string; - updater: (point: CustomMaskPathPoint) => CustomMaskPathPoint; -}) { - return points.map((point) => (point.id === pointId ? updater(point) : point)); -} - -function computeCustomMaskParamUpdate({ - handleId, - startParams, - deltaX, - deltaY, - startCanvasX, - startCanvasY, - bounds, -}: MaskParamUpdateArgs<CustomMaskParams>): Partial<CustomMaskParams> { - if (handleId.kind === "position") { - return { - centerX: startParams.centerX + deltaX / bounds.width, - centerY: startParams.centerY + deltaY / bounds.height, - }; - } - - const pivotX = bounds.cx + startParams.centerX * bounds.width; - const pivotY = bounds.cy + startParams.centerY * bounds.height; - - if (handleId.kind === "rotation") { - const startAngle = - (Math.atan2(startCanvasY - pivotY, startCanvasX - pivotX) * 180) / - Math.PI; - const currentAngle = - (Math.atan2( - startCanvasY + deltaY - pivotY, - startCanvasX + deltaX - pivotX, - ) * - 180) / - Math.PI; - - let deltaAngle = currentAngle - startAngle; - if (deltaAngle > 180) deltaAngle -= 360; - if (deltaAngle < -180) deltaAngle += 360; - - return { - rotation: (((startParams.rotation + deltaAngle) % 360) + 360) % 360, - }; - } - - if (handleId.kind === "feather") { - const angleRad = (startParams.rotation * Math.PI) / 180; - return computeFeatherUpdate({ - startFeather: startParams.feather, - deltaX, - deltaY, - directionX: -Math.sin(angleRad), - directionY: Math.cos(angleRad), - }); - } - - if (handleId.kind === "scale") { - const startDistance = Math.hypot( - startCanvasX - pivotX, - startCanvasY - pivotY, - ); - const currentDistance = Math.hypot( - startCanvasX + deltaX - pivotX, - startCanvasY + deltaY - pivotY, - ); - const scaleFactor = startDistance > 0 ? currentDistance / startDistance : 1; - return { - scale: Math.max(0.01, startParams.scale * scaleFactor), - }; - } - - if (handleId.kind !== "anchor" && handleId.kind !== "tangent") { - return {}; - } - - const points = startParams.path; - const currentPoint = { - x: startCanvasX + deltaX, - y: startCanvasY + deltaY, - }; - const localPoint = customMaskCanvasPointToLocal({ - point: currentPoint, - centerX: startParams.centerX, - centerY: startParams.centerY, - rotation: startParams.rotation, - scale: startParams.scale, - bounds, - }); - - return { - path: updateCustomMaskPoint({ - points, - pointId: handleId.pointId, - updater: (point) => { - if (handleId.kind === "anchor") { - return { - ...point, - x: localPoint.x, - y: localPoint.y, - }; - } - - if (handleId.side === "in") { - return { - ...point, - inX: localPoint.x - point.x, - inY: localPoint.y - point.y, - }; - } - - return { - ...point, - outX: localPoint.x - point.x, - outY: localPoint.y - point.y, - }; - }, - }), - }; -} - -export const customMaskDefinition: MaskDefinition<"custom"> = { - type: "custom", - name: "Custom", - features: { - hasPosition: true, - hasRotation: true, - sizeMode: "uniform", - }, - params: CUSTOM_MASK_PARAMS, - interaction: { - getInteraction({ - params, - bounds, - displayScale, - scaleX: _scaleX, - scaleY: _scaleY, - }) { - return getCustomMaskDisplayHandles({ params, bounds, displayScale }); - }, - snap({ - handleId, - proposedParams, - startParams, - bounds, - canvasSize, - snapThreshold, - }) { - const points = startParams.path; - const localBounds = getCustomMaskLocalBounds({ points, bounds }); - if (!startParams.closed || !localBounds) { - return { - params: proposedParams, - activeLines: [], - }; - } - - const position = { - x: proposedParams.centerX * bounds.width, - y: proposedParams.centerY * bounds.height, - }; - - if (handleId.kind === "position") { - const { snappedPosition, activeLines } = snapPosition({ - proposedPosition: position, - canvasSize: bounds, - elementSize: { - width: localBounds.width * proposedParams.scale, - height: localBounds.height * proposedParams.scale, - }, - rotation: proposedParams.rotation, - snapThreshold, - }); - - return { - params: { - ...proposedParams, - ...setMaskLocalCenter({ - center: snappedPosition, - bounds, - }), - }, - activeLines: toGlobalMaskSnapLines({ - lines: activeLines, - bounds, - canvasSize, - }), - }; - } - - if (handleId.kind === "rotation") { - const { snappedRotation } = snapRotation({ - proposedRotation: proposedParams.rotation, - }); - return { - params: { - ...proposedParams, - rotation: snappedRotation, - }, - activeLines: [], - }; - } - - if (handleId.kind === "scale") { - const { snappedScale, activeLines } = snapScale({ - proposedScale: proposedParams.scale, - position, - baseWidth: localBounds.width, - baseHeight: localBounds.height, - rotation: proposedParams.rotation, - canvasSize: bounds, - snapThreshold, - preferredEdges: { - right: true, - bottom: true, - }, - }); - - return { - params: { - ...proposedParams, - scale: Math.max(0.01, snappedScale), - }, - activeLines: toGlobalMaskSnapLines({ - lines: activeLines, - bounds, - canvasSize, - }), - }; - } - - return { - params: proposedParams, - activeLines: [], - }; - }, - }, - buildDefault(): Omit<CustomMask, "id"> { - return { - type: "custom", - params: { - feather: 0, - inverted: false, - strokeColor: "#ffffff", - strokeWidth: 0, - strokeAlign: "center", - path: [], - closed: false, - centerX: 0, - centerY: 0, - rotation: 0, - scale: 1, - }, - }; - }, - computeParamUpdate: computeCustomMaskParamUpdate, - isActive(params) { - return params.closed; - }, - renderer: { - body: { - kind: "fillPath", - buildPath({ resolvedParams, width, height }) { - const params = resolvedParams; - const points = params.path; - if (!params.closed) { - return new Path2D(); - } - - return buildCustomMaskPath2D({ - points, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds: { - cx: width / 2, - cy: height / 2, - width, - height, - rotation: 0, - }, - closed: true, - }); - }, - }, - stroke: { - kind: "renderStroke", - renderStroke({ resolvedParams, ctx, width, height }) { - const params = resolvedParams; - if (!params.closed) { - return; - } - - const points = params.path; - const path = buildCustomMaskPath2D({ - points, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds: { - cx: width / 2, - cy: height / 2, - width, - height, - rotation: 0, - }, - closed: true, - }); - - ctx.save(); - ctx.strokeStyle = params.strokeColor; - ctx.lineWidth = params.strokeWidth; - ctx.lineJoin = "round"; - ctx.lineCap = "round"; - ctx.stroke(path); - - if (params.strokeAlign === "inside") { - ctx.globalCompositeOperation = "destination-in"; - ctx.fillStyle = "#ffffff"; - ctx.fill(path); - } - - if (params.strokeAlign === "outside") { - ctx.globalCompositeOperation = "destination-out"; - ctx.fillStyle = "#ffffff"; - ctx.fill(path); - } - ctx.restore(); - }, - }, - }, -}; - -export function appendPointToCustomMask({ - params, - canvasPoint, - bounds, -}: { - params: CustomMaskParams; - canvasPoint: { x: number; y: number }; - bounds: ElementBounds; -}): CustomMaskParams { - const points = params.path; - - if (points.length === 0) { - return { - ...params, - centerX: - bounds.width === 0 ? 0 : (canvasPoint.x - bounds.cx) / bounds.width, - centerY: - bounds.height === 0 ? 0 : (canvasPoint.y - bounds.cy) / bounds.height, - rotation: 0, - scale: 1, - path: [ - { - id: generateUUID(), - x: 0, - y: 0, - inX: 0, - inY: 0, - outX: 0, - outY: 0, - }, - ], - }; - } - - const localPoint = customMaskCanvasPointToLocal({ - point: canvasPoint, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - }); - const nextPoints = [ - ...points, - { - id: generateUUID(), - x: localPoint.x, - y: localPoint.y, - inX: 0, - inY: 0, - outX: 0, - outY: 0, - }, - ]; - const recentered = recenterCustomMaskPath({ - points: nextPoints, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - }); - - return { - ...params, - centerX: recentered.centerX, - centerY: recentered.centerY, - path: recentered.points, - }; -} - -export function insertPointOnCustomMaskSegment({ - params, - segmentIndex, - canvasPoint, - bounds, - pointId = generateUUID(), -}: { - params: CustomMaskParams; - segmentIndex: number; - canvasPoint: { x: number; y: number }; - bounds: ElementBounds; - pointId?: string; -}): { params: CustomMaskParams; pointId: string } | null { - const points = params.path; - if (getCustomMaskSegmentCount({ points, closed: params.closed }) === 0) { - return null; - } - - const closestPoint = findClosestPointOnCustomMaskSegment({ - points, - segmentIndex, - canvasPoint, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - closed: params.closed, - }); - if (!closestPoint) { - return null; - } - - const nextPoints = insertPointIntoCustomMaskSegment({ - points, - segmentIndex, - pointId, - t: closestPoint.t, - closed: params.closed, - }); - if (nextPoints.length === points.length) { - return null; - } - - const recentered = recenterCustomMaskPath({ - points: nextPoints, - centerX: params.centerX, - centerY: params.centerY, - rotation: params.rotation, - scale: params.scale, - bounds, - }); - - return { - pointId, - params: { - ...params, - centerX: recentered.centerX, - centerY: recentered.centerY, - path: recentered.points, - }, - }; -} |
