diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-13 04:40:48 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-13 05:01:51 +0200 |
| commit | 5a2aaf3601b6b050603f51e487a31fb07862a21f (patch) | |
| tree | 1c1acdfb3a6c8210d90af181af62975624999155 /apps/web/src/lib/masks/definitions/heart.ts | |
| parent | 108d3f4ecad4c0ead14941c2067a21546750fedc (diff) | |
| download | opencut-5a2aaf3601b6b050603f51e487a31fb07862a21f.tar.gz | |
feat: cinematic bars, diamond, heart, and star masks
Made-with: Cursor
Diffstat (limited to 'apps/web/src/lib/masks/definitions/heart.ts')
| -rw-r--r-- | apps/web/src/lib/masks/definitions/heart.ts | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/apps/web/src/lib/masks/definitions/heart.ts b/apps/web/src/lib/masks/definitions/heart.ts new file mode 100644 index 00000000..2c53367a --- /dev/null +++ b/apps/web/src/lib/masks/definitions/heart.ts @@ -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, + }); + }, + }, +}; |
