summaryrefslogtreecommitdiff
path: root/apps/web/src/masks/definitions/custom.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/masks/definitions/custom.ts')
-rw-r--r--apps/web/src/masks/definitions/custom.ts656
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,
- },
- };
-}