summaryrefslogtreecommitdiff
path: root/apps/web/src/masks/builtin/definitions/heart.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/masks/builtin/definitions/heart.ts')
-rw-r--r--apps/web/src/masks/builtin/definitions/heart.ts148
1 files changed, 148 insertions, 0 deletions
diff --git a/apps/web/src/masks/builtin/definitions/heart.ts b/apps/web/src/masks/builtin/definitions/heart.ts
new file mode 100644
index 00000000..d1af5eb2
--- /dev/null
+++ b/apps/web/src/masks/builtin/definitions/heart.ts
@@ -0,0 +1,148 @@
+import type { MaskDefinition } from "@/masks/types";
+import {
+ BOX_LIKE_MASK_PARAMS,
+ buildBoxMaskInteraction,
+ 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.475 });
+ const rightControl1 = toPoint({
+ localX: halfWidth,
+ localY: -halfHeight * 1.225,
+ });
+ const rightControl2 = toPoint({
+ localX: halfWidth,
+ localY: -halfHeight * 0.125,
+ });
+ const bottom = toPoint({ localX: 0, localY: halfHeight * 0.725 });
+ const leftControl1 = toPoint({
+ localX: -halfWidth,
+ localY: -halfHeight * 0.125,
+ });
+ const leftControl2 = toPoint({
+ localX: -halfWidth,
+ localY: -halfHeight * 1.225,
+ });
+
+ 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<"heart"> = {
+ type: "heart",
+ name: "Heart",
+ features: {
+ hasPosition: true,
+ hasRotation: true,
+ sizeMode: "width-height",
+ },
+ params: BOX_LIKE_MASK_PARAMS,
+ interaction: buildBoxMaskInteraction({
+ sizeMode: "width-height",
+ 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.475}`,
+ `C ${cx + halfWidth},${cy - halfHeight * 1.225} ${cx + halfWidth},${cy - halfHeight * 0.125} ${cx},${cy + halfHeight * 0.725}`,
+ `C ${cx - halfWidth},${cy - halfHeight * 0.125} ${cx - halfWidth},${cy - halfHeight * 1.225} ${cx},${cy - halfHeight * 0.475}`,
+ "Z",
+ ].join(" ");
+ },
+ }),
+ buildDefault(context) {
+ return {
+ type: "heart",
+ params: getDefaultSquareMaskParams(context),
+ };
+ },
+ computeParamUpdate: computeBoxMaskParamUpdate,
+ renderer: {
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
+ getBoxLikeGeometry({ params, width, height });
+ return buildHeartPath({
+ centerX,
+ centerY,
+ halfWidth: maskWidth / 2,
+ halfHeight: maskHeight / 2,
+ rotationRad,
+ });
+ },
+ },
+ stroke: {
+ kind: "strokeFromPath",
+ buildStrokePath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ 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,
+ });
+ },
+ },
+ },
+};