mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: cinematic bars, diamond, heart, and star masks
Made-with: Cursor
This commit is contained in:
@@ -450,6 +450,44 @@ function MaskParamsFields({
|
|||||||
</SectionField>
|
</SectionField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{definition.features.sizeMode === "height-only" &&
|
||||||
|
"height" in mask.params && (
|
||||||
|
<SectionField label="Height">
|
||||||
|
<MaskNumberField
|
||||||
|
icon="H"
|
||||||
|
param={getNumberParamDefinition({
|
||||||
|
definition,
|
||||||
|
key: "height",
|
||||||
|
})}
|
||||||
|
value={getMaskNumber({
|
||||||
|
params: mask.params,
|
||||||
|
key: "height",
|
||||||
|
})}
|
||||||
|
onPreview={previewNumberParam("height")}
|
||||||
|
onCommit={onCommit}
|
||||||
|
/>
|
||||||
|
</SectionField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{definition.features.sizeMode === "width-only" &&
|
||||||
|
"width" in mask.params && (
|
||||||
|
<SectionField label="Width">
|
||||||
|
<MaskNumberField
|
||||||
|
icon="W"
|
||||||
|
param={getNumberParamDefinition({
|
||||||
|
definition,
|
||||||
|
key: "width",
|
||||||
|
})}
|
||||||
|
value={getMaskNumber({
|
||||||
|
params: mask.params,
|
||||||
|
key: "width",
|
||||||
|
})}
|
||||||
|
onPreview={previewNumberParam("width")}
|
||||||
|
onCommit={onCommit}
|
||||||
|
/>
|
||||||
|
</SectionField>
|
||||||
|
)}
|
||||||
|
|
||||||
{definition.features.sizeMode === "uniform" && "scale" in mask.params && (
|
{definition.features.sizeMode === "uniform" && "scale" in mask.params && (
|
||||||
<SectionField label="Scale">
|
<SectionField label="Scale">
|
||||||
<MaskNumberField
|
<MaskNumberField
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import type {
|
||||||
|
MaskDefaultContext,
|
||||||
|
MaskDefinition,
|
||||||
|
RectangleMaskParams,
|
||||||
|
} from "@/lib/masks/types";
|
||||||
|
import {
|
||||||
|
BOX_LIKE_MASK_PARAMS,
|
||||||
|
computeBoxMaskParamUpdate,
|
||||||
|
getDefaultBaseMaskParams,
|
||||||
|
getStrokeOffset,
|
||||||
|
rotatePoint,
|
||||||
|
} from "./box-like";
|
||||||
|
|
||||||
|
function getDefaultCinematicBarsMaskParams({
|
||||||
|
elementSize,
|
||||||
|
}: MaskDefaultContext): RectangleMaskParams {
|
||||||
|
const absWidth = Math.abs(elementSize?.width ?? 0);
|
||||||
|
const absHeight = Math.abs(elementSize?.height ?? 0);
|
||||||
|
const diagonal =
|
||||||
|
absWidth > 0 && absHeight > 0
|
||||||
|
? Math.sqrt(absWidth ** 2 + absHeight ** 2)
|
||||||
|
: 0;
|
||||||
|
const fullSpanWidth =
|
||||||
|
absWidth > 0 ? diagonal / absWidth : Math.SQRT2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getDefaultBaseMaskParams(),
|
||||||
|
centerX: 0,
|
||||||
|
centerY: 0,
|
||||||
|
width: Math.max(fullSpanWidth, 1),
|
||||||
|
height: 0.6,
|
||||||
|
rotation: 0,
|
||||||
|
scale: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBandPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth,
|
||||||
|
halfHeight,
|
||||||
|
rotationRad,
|
||||||
|
}: {
|
||||||
|
centerX: number;
|
||||||
|
centerY: number;
|
||||||
|
halfWidth: number;
|
||||||
|
halfHeight: number;
|
||||||
|
rotationRad: number;
|
||||||
|
}): Path2D {
|
||||||
|
const corners = [
|
||||||
|
{ x: centerX - halfWidth, y: centerY - halfHeight },
|
||||||
|
{ x: centerX + halfWidth, y: centerY - halfHeight },
|
||||||
|
{ x: centerX + halfWidth, y: centerY + halfHeight },
|
||||||
|
{ x: centerX - halfWidth, y: centerY + halfHeight },
|
||||||
|
].map((point) =>
|
||||||
|
rotatePoint({
|
||||||
|
...point,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotationRad,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = new Path2D();
|
||||||
|
path.moveTo(corners[0].x, corners[0].y);
|
||||||
|
for (const corner of corners.slice(1)) {
|
||||||
|
path.lineTo(corner.x, corner.y);
|
||||||
|
}
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cinematicBarsMaskDefinition: MaskDefinition<RectangleMaskParams> = {
|
||||||
|
type: "cinematic-bars",
|
||||||
|
name: "Cinematic Bars",
|
||||||
|
overlayShape: "box",
|
||||||
|
buildOverlayPath({ width, height }) {
|
||||||
|
return `M 0,0 H ${width} V ${height} H 0 Z`;
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
hasPosition: true,
|
||||||
|
hasRotation: true,
|
||||||
|
sizeMode: "height-only",
|
||||||
|
},
|
||||||
|
params: BOX_LIKE_MASK_PARAMS,
|
||||||
|
buildDefault(context) {
|
||||||
|
return {
|
||||||
|
type: "cinematic-bars",
|
||||||
|
params: getDefaultCinematicBarsMaskParams(context),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computeParamUpdate: computeBoxMaskParamUpdate,
|
||||||
|
renderer: {
|
||||||
|
buildPath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const centerX = width / 2 + params.centerX * width;
|
||||||
|
const centerY = height / 2 + params.centerY * height;
|
||||||
|
const maskWidth = Math.max(params.width * width, width);
|
||||||
|
const maskHeight = Math.max(params.height, 0.01) * height;
|
||||||
|
const rotationRad = (params.rotation * Math.PI) / 180;
|
||||||
|
|
||||||
|
return buildBandPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: maskWidth / 2,
|
||||||
|
halfHeight: maskHeight / 2,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildStrokePath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const centerX = width / 2 + params.centerX * width;
|
||||||
|
const centerY = height / 2 + params.centerY * height;
|
||||||
|
const rotationRad = (params.rotation * Math.PI) / 180;
|
||||||
|
const offset = getStrokeOffset({
|
||||||
|
strokeAlign: params.strokeAlign,
|
||||||
|
strokeWidth: params.strokeWidth,
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildBandPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: Math.max((Math.max(params.width * width, width) / 2) + offset, 1),
|
||||||
|
halfHeight: Math.max(
|
||||||
|
(Math.max(params.height, 0.01) * height) / 2 + offset,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
|
||||||
|
import {
|
||||||
|
BOX_LIKE_MASK_PARAMS,
|
||||||
|
computeBoxMaskParamUpdate,
|
||||||
|
getBoxLikeGeometry,
|
||||||
|
getDefaultSquareMaskParams,
|
||||||
|
getStrokeOffset,
|
||||||
|
rotatePoint,
|
||||||
|
} from "./box-like";
|
||||||
|
|
||||||
|
function buildDiamondPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth,
|
||||||
|
halfHeight,
|
||||||
|
rotationRad,
|
||||||
|
}: {
|
||||||
|
centerX: number;
|
||||||
|
centerY: number;
|
||||||
|
halfWidth: number;
|
||||||
|
halfHeight: number;
|
||||||
|
rotationRad: number;
|
||||||
|
}): Path2D {
|
||||||
|
const points = [
|
||||||
|
{ x: centerX, y: centerY - halfHeight },
|
||||||
|
{ x: centerX + halfWidth, y: centerY },
|
||||||
|
{ x: centerX, y: centerY + halfHeight },
|
||||||
|
{ x: centerX - halfWidth, y: centerY },
|
||||||
|
].map((point) =>
|
||||||
|
rotatePoint({
|
||||||
|
...point,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotationRad,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = new Path2D();
|
||||||
|
path.moveTo(points[0].x, points[0].y);
|
||||||
|
for (const point of points.slice(1)) {
|
||||||
|
path.lineTo(point.x, point.y);
|
||||||
|
}
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diamondMaskDefinition: MaskDefinition<RectangleMaskParams> = {
|
||||||
|
type: "diamond",
|
||||||
|
name: "Diamond",
|
||||||
|
overlayShape: "box",
|
||||||
|
buildOverlayPath({ width, height }) {
|
||||||
|
return `M ${width / 2},0 L ${width},${height / 2} L ${width / 2},${height} L 0,${height / 2} Z`;
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
hasPosition: true,
|
||||||
|
hasRotation: true,
|
||||||
|
sizeMode: "width-height",
|
||||||
|
},
|
||||||
|
params: BOX_LIKE_MASK_PARAMS,
|
||||||
|
buildDefault(context) {
|
||||||
|
return {
|
||||||
|
type: "diamond",
|
||||||
|
params: getDefaultSquareMaskParams(context),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computeParamUpdate: computeBoxMaskParamUpdate,
|
||||||
|
renderer: {
|
||||||
|
buildPath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
return buildDiamondPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: maskWidth / 2,
|
||||||
|
halfHeight: maskHeight / 2,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildStrokePath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
const offset = getStrokeOffset({
|
||||||
|
strokeAlign: params.strokeAlign,
|
||||||
|
strokeWidth: params.strokeWidth,
|
||||||
|
});
|
||||||
|
return buildDiamondPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: Math.max(maskWidth / 2 + offset, 1),
|
||||||
|
halfHeight: Math.max(maskHeight / 2 + offset, 1),
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
|
||||||
|
import {
|
||||||
|
BOX_LIKE_MASK_PARAMS,
|
||||||
|
computeBoxMaskParamUpdate,
|
||||||
|
getBoxLikeGeometry,
|
||||||
|
getDefaultSquareMaskParams,
|
||||||
|
getStrokeOffset,
|
||||||
|
rotatePoint,
|
||||||
|
} from "./box-like";
|
||||||
|
|
||||||
|
function buildHeartPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth,
|
||||||
|
halfHeight,
|
||||||
|
rotationRad,
|
||||||
|
}: {
|
||||||
|
centerX: number;
|
||||||
|
centerY: number;
|
||||||
|
halfWidth: number;
|
||||||
|
halfHeight: number;
|
||||||
|
rotationRad: number;
|
||||||
|
}): Path2D {
|
||||||
|
const toPoint = ({
|
||||||
|
localX,
|
||||||
|
localY,
|
||||||
|
}: {
|
||||||
|
localX: number;
|
||||||
|
localY: number;
|
||||||
|
}) =>
|
||||||
|
rotatePoint({
|
||||||
|
x: centerX + localX,
|
||||||
|
y: centerY + localY,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = toPoint({ localX: 0, localY: -halfHeight * 0.2 });
|
||||||
|
const rightControl1 = toPoint({
|
||||||
|
localX: halfWidth,
|
||||||
|
localY: -halfHeight * 0.95,
|
||||||
|
});
|
||||||
|
const rightControl2 = toPoint({
|
||||||
|
localX: halfWidth,
|
||||||
|
localY: halfHeight * 0.15,
|
||||||
|
});
|
||||||
|
const bottom = toPoint({ localX: 0, localY: halfHeight });
|
||||||
|
const leftControl1 = toPoint({
|
||||||
|
localX: -halfWidth,
|
||||||
|
localY: halfHeight * 0.15,
|
||||||
|
});
|
||||||
|
const leftControl2 = toPoint({
|
||||||
|
localX: -halfWidth,
|
||||||
|
localY: -halfHeight * 0.95,
|
||||||
|
});
|
||||||
|
|
||||||
|
const path = new Path2D();
|
||||||
|
path.moveTo(start.x, start.y);
|
||||||
|
path.bezierCurveTo(
|
||||||
|
rightControl1.x,
|
||||||
|
rightControl1.y,
|
||||||
|
rightControl2.x,
|
||||||
|
rightControl2.y,
|
||||||
|
bottom.x,
|
||||||
|
bottom.y,
|
||||||
|
);
|
||||||
|
path.bezierCurveTo(
|
||||||
|
leftControl1.x,
|
||||||
|
leftControl1.y,
|
||||||
|
leftControl2.x,
|
||||||
|
leftControl2.y,
|
||||||
|
start.x,
|
||||||
|
start.y,
|
||||||
|
);
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const heartMaskDefinition: MaskDefinition<RectangleMaskParams> = {
|
||||||
|
type: "heart",
|
||||||
|
name: "Heart",
|
||||||
|
overlayShape: "box",
|
||||||
|
buildOverlayPath({ width, height }) {
|
||||||
|
const cx = width / 2;
|
||||||
|
const cy = height / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
return [
|
||||||
|
`M ${cx},${cy - halfHeight * 0.2}`,
|
||||||
|
`C ${cx + halfWidth},${cy - halfHeight * 0.95} ${cx + halfWidth},${cy + halfHeight * 0.15} ${cx},${cy + halfHeight}`,
|
||||||
|
`C ${cx - halfWidth},${cy + halfHeight * 0.15} ${cx - halfWidth},${cy - halfHeight * 0.95} ${cx},${cy - halfHeight * 0.2}`,
|
||||||
|
"Z",
|
||||||
|
].join(" ");
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
hasPosition: true,
|
||||||
|
hasRotation: true,
|
||||||
|
sizeMode: "width-height",
|
||||||
|
},
|
||||||
|
params: BOX_LIKE_MASK_PARAMS,
|
||||||
|
buildDefault(context) {
|
||||||
|
return {
|
||||||
|
type: "heart",
|
||||||
|
params: getDefaultSquareMaskParams(context),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computeParamUpdate: computeBoxMaskParamUpdate,
|
||||||
|
renderer: {
|
||||||
|
buildPath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
return buildHeartPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: maskWidth / 2,
|
||||||
|
halfHeight: maskHeight / 2,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildStrokePath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
const offset = getStrokeOffset({
|
||||||
|
strokeAlign: params.strokeAlign,
|
||||||
|
strokeWidth: params.strokeWidth,
|
||||||
|
});
|
||||||
|
return buildHeartPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: Math.max(maskWidth / 2 + offset, 1),
|
||||||
|
halfHeight: Math.max(maskHeight / 2 + offset, 1),
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import type { BaseMaskParams, MaskDefinition } from "@/lib/masks/types";
|
import type { BaseMaskParams, MaskDefinition } from "@/lib/masks/types";
|
||||||
import { masksRegistry, type MaskIconProps } from "../registry";
|
import { masksRegistry, type MaskIconProps } from "../registry";
|
||||||
|
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
|
||||||
|
import { diamondMaskDefinition } from "./diamond";
|
||||||
import { ellipseMaskDefinition } from "./ellipse";
|
import { ellipseMaskDefinition } from "./ellipse";
|
||||||
|
import { heartMaskDefinition } from "./heart";
|
||||||
import { rectangleMaskDefinition } from "./rectangle";
|
import { rectangleMaskDefinition } from "./rectangle";
|
||||||
import { splitMaskDefinition } from "./split";
|
import { splitMaskDefinition } from "./split";
|
||||||
|
import { starMaskDefinition } from "./star";
|
||||||
import {
|
import {
|
||||||
|
MinusSignIcon,
|
||||||
PanelRightDashedIcon,
|
PanelRightDashedIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
|
FavouriteIcon,
|
||||||
|
DiamondIcon,
|
||||||
|
StarsIcon,
|
||||||
} from "@hugeicons/core-free-icons";
|
} from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
function registerDefaultMask<TParams extends BaseMaskParams>({
|
function registerDefaultMask<TParams extends BaseMaskParams>({
|
||||||
@@ -28,6 +36,10 @@ export function registerDefaultMasks(): void {
|
|||||||
definition: splitMaskDefinition,
|
definition: splitMaskDefinition,
|
||||||
icon: { icon: PanelRightDashedIcon, strokeWidth: 1 },
|
icon: { icon: PanelRightDashedIcon, strokeWidth: 1 },
|
||||||
});
|
});
|
||||||
|
registerDefaultMask({
|
||||||
|
definition: cinematicBarsMaskDefinition,
|
||||||
|
icon: { icon: MinusSignIcon },
|
||||||
|
});
|
||||||
registerDefaultMask({
|
registerDefaultMask({
|
||||||
definition: rectangleMaskDefinition,
|
definition: rectangleMaskDefinition,
|
||||||
icon: { icon: SquareIcon },
|
icon: { icon: SquareIcon },
|
||||||
@@ -36,4 +48,16 @@ export function registerDefaultMasks(): void {
|
|||||||
definition: ellipseMaskDefinition,
|
definition: ellipseMaskDefinition,
|
||||||
icon: { icon: CircleIcon },
|
icon: { icon: CircleIcon },
|
||||||
});
|
});
|
||||||
|
registerDefaultMask({
|
||||||
|
definition: heartMaskDefinition,
|
||||||
|
icon: { icon: FavouriteIcon },
|
||||||
|
});
|
||||||
|
registerDefaultMask({
|
||||||
|
definition: diamondMaskDefinition,
|
||||||
|
icon: { icon: DiamondIcon },
|
||||||
|
});
|
||||||
|
registerDefaultMask({
|
||||||
|
definition: starMaskDefinition,
|
||||||
|
icon: { icon: StarsIcon },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import type { MaskDefinition, RectangleMaskParams } from "@/lib/masks/types";
|
||||||
|
import {
|
||||||
|
BOX_LIKE_MASK_PARAMS,
|
||||||
|
computeBoxMaskParamUpdate,
|
||||||
|
getBoxLikeGeometry,
|
||||||
|
getDefaultSquareMaskParams,
|
||||||
|
getStrokeOffset,
|
||||||
|
rotatePoint,
|
||||||
|
} from "./box-like";
|
||||||
|
|
||||||
|
const STAR_INNER_RADIUS_RATIO = 0.45;
|
||||||
|
const STAR_VERTEX_COUNT = 10;
|
||||||
|
|
||||||
|
function buildStarPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth,
|
||||||
|
halfHeight,
|
||||||
|
rotationRad,
|
||||||
|
}: {
|
||||||
|
centerX: number;
|
||||||
|
centerY: number;
|
||||||
|
halfWidth: number;
|
||||||
|
halfHeight: number;
|
||||||
|
rotationRad: number;
|
||||||
|
}): Path2D {
|
||||||
|
const path = new Path2D();
|
||||||
|
|
||||||
|
for (let index = 0; index < STAR_VERTEX_COUNT; index++) {
|
||||||
|
const isOuterVertex = index % 2 === 0;
|
||||||
|
const radiusX = isOuterVertex
|
||||||
|
? halfWidth
|
||||||
|
: halfWidth * STAR_INNER_RADIUS_RATIO;
|
||||||
|
const radiusY = isOuterVertex
|
||||||
|
? halfHeight
|
||||||
|
: halfHeight * STAR_INNER_RADIUS_RATIO;
|
||||||
|
const angle = (index * Math.PI) / 5 - Math.PI / 2;
|
||||||
|
const point = rotatePoint({
|
||||||
|
x: centerX + radiusX * Math.cos(angle),
|
||||||
|
y: centerY + radiusY * Math.sin(angle),
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
path.moveTo(point.x, point.y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(point.x, point.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlayStarPath({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}): string {
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
const segments: string[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < STAR_VERTEX_COUNT; index++) {
|
||||||
|
const isOuterVertex = index % 2 === 0;
|
||||||
|
const radiusX = isOuterVertex
|
||||||
|
? halfWidth
|
||||||
|
: halfWidth * STAR_INNER_RADIUS_RATIO;
|
||||||
|
const radiusY = isOuterVertex
|
||||||
|
? halfHeight
|
||||||
|
: halfHeight * STAR_INNER_RADIUS_RATIO;
|
||||||
|
const angle = (index * Math.PI) / 5 - Math.PI / 2;
|
||||||
|
const x = centerX + radiusX * Math.cos(angle);
|
||||||
|
const y = centerY + radiusY * Math.sin(angle);
|
||||||
|
segments.push(`${index === 0 ? "M" : "L"} ${x},${y}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${segments.join(" ")} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const starMaskDefinition: MaskDefinition<RectangleMaskParams> = {
|
||||||
|
type: "star",
|
||||||
|
name: "Star",
|
||||||
|
overlayShape: "box",
|
||||||
|
buildOverlayPath({ width, height }) {
|
||||||
|
return buildOverlayStarPath({ width, height });
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
hasPosition: true,
|
||||||
|
hasRotation: true,
|
||||||
|
sizeMode: "width-height",
|
||||||
|
},
|
||||||
|
params: BOX_LIKE_MASK_PARAMS,
|
||||||
|
buildDefault(context) {
|
||||||
|
return {
|
||||||
|
type: "star",
|
||||||
|
params: getDefaultSquareMaskParams(context),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computeParamUpdate: computeBoxMaskParamUpdate,
|
||||||
|
renderer: {
|
||||||
|
buildPath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
return buildStarPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: maskWidth / 2,
|
||||||
|
halfHeight: maskHeight / 2,
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildStrokePath({ resolvedParams, width, height }) {
|
||||||
|
const params = resolvedParams as RectangleMaskParams;
|
||||||
|
const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
|
||||||
|
getBoxLikeGeometry({ params, width, height });
|
||||||
|
const offset = getStrokeOffset({
|
||||||
|
strokeAlign: params.strokeAlign,
|
||||||
|
strokeWidth: params.strokeWidth,
|
||||||
|
});
|
||||||
|
return buildStarPath({
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
halfWidth: Math.max(maskWidth / 2 + offset, 1),
|
||||||
|
halfHeight: Math.max(maskHeight / 2 + offset, 1),
|
||||||
|
rotationRad,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import type { ElementBounds } from "@/lib/preview/element-bounds";
|
import type { ElementBounds } from "@/lib/preview/element-bounds";
|
||||||
import type { ParamDefinition, ParamValues } from "@/lib/params";
|
import type { ParamDefinition, ParamValues } from "@/lib/params";
|
||||||
|
|
||||||
export type MaskType = "split" | "rectangle" | "ellipse";
|
export type MaskType =
|
||||||
|
| "split"
|
||||||
|
| "cinematic-bars"
|
||||||
|
| "rectangle"
|
||||||
|
| "ellipse"
|
||||||
|
| "heart"
|
||||||
|
| "diamond"
|
||||||
|
| "star";
|
||||||
|
|
||||||
export interface BaseMaskParams extends ParamValues {
|
export interface BaseMaskParams extends ParamValues {
|
||||||
feather: number;
|
feather: number;
|
||||||
@@ -32,6 +39,12 @@ export interface SplitMask {
|
|||||||
params: SplitMaskParams;
|
params: SplitMaskParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CinematicBarsMask {
|
||||||
|
id: string;
|
||||||
|
type: "cinematic-bars";
|
||||||
|
params: RectangleMaskParams;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RectangleMask {
|
export interface RectangleMask {
|
||||||
id: string;
|
id: string;
|
||||||
type: "rectangle";
|
type: "rectangle";
|
||||||
@@ -44,7 +57,32 @@ export interface EllipseMask {
|
|||||||
params: RectangleMaskParams;
|
params: RectangleMaskParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Mask = SplitMask | RectangleMask | EllipseMask;
|
export interface HeartMask {
|
||||||
|
id: string;
|
||||||
|
type: "heart";
|
||||||
|
params: RectangleMaskParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiamondMask {
|
||||||
|
id: string;
|
||||||
|
type: "diamond";
|
||||||
|
params: RectangleMaskParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StarMask {
|
||||||
|
id: string;
|
||||||
|
type: "star";
|
||||||
|
params: RectangleMaskParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mask =
|
||||||
|
| SplitMask
|
||||||
|
| CinematicBarsMask
|
||||||
|
| RectangleMask
|
||||||
|
| EllipseMask
|
||||||
|
| HeartMask
|
||||||
|
| DiamondMask
|
||||||
|
| StarMask;
|
||||||
|
|
||||||
export interface MaskRenderer {
|
export interface MaskRenderer {
|
||||||
buildPath(params: {
|
buildPath(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user