summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-29 18:27:21 +0200
committerMaze Winther <mazewinther@gmail.com>2026-05-02 03:27:23 +0200
commitae1292a14bf5c29544a5f7a49e65b994410f87de (patch)
tree5fd3b7fcef3d87d6fcdbe38bf34b99d32038b41f /apps/web/src
parent6ea5a156b6b7f609354a4563cfb2d00ec7f41811 (diff)
downloadopencut-ae1292a14bf5c29544a5f7a49e65b994410f87de.tar.gz
refactor: make mask renderer dispatch explicit
Made-with: Cursor
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/masks/definitions/cinematic-bars.ts77
-rw-r--r--apps/web/src/masks/definitions/custom.ts126
-rw-r--r--apps/web/src/masks/definitions/diamond.ts62
-rw-r--r--apps/web/src/masks/definitions/ellipse.ts78
-rw-r--r--apps/web/src/masks/definitions/heart.ts62
-rw-r--r--apps/web/src/masks/definitions/index.ts11
-rw-r--r--apps/web/src/masks/definitions/rectangle.ts62
-rw-r--r--apps/web/src/masks/definitions/split.ts223
-rw-r--r--apps/web/src/masks/definitions/star.ts62
-rw-r--r--apps/web/src/masks/definitions/text.ts110
-rw-r--r--apps/web/src/masks/index.ts68
-rw-r--r--apps/web/src/masks/registry.ts68
-rw-r--r--apps/web/src/masks/types.ts90
-rw-r--r--apps/web/src/services/renderer/compositor/frame-descriptor.ts120
14 files changed, 660 insertions, 559 deletions
diff --git a/apps/web/src/masks/definitions/cinematic-bars.ts b/apps/web/src/masks/definitions/cinematic-bars.ts
index 36b31328..68717c02 100644
--- a/apps/web/src/masks/definitions/cinematic-bars.ts
+++ b/apps/web/src/masks/definitions/cinematic-bars.ts
@@ -71,7 +71,7 @@ function buildBandPath({
return path;
}
-export const cinematicBarsMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const cinematicBarsMaskDefinition: MaskDefinition<"cinematic-bars"> = {
type: "cinematic-bars",
name: "Cinematic Bars",
features: {
@@ -94,42 +94,51 @@ export const cinematicBarsMaskDefinition: MaskDefinition<RectangleMaskParams> =
},
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;
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ 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,
- });
+ 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,
- });
+ stroke: {
+ kind: "strokeFromPath",
+ buildStrokePath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ 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,
- });
+ 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,
+ });
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/custom.ts b/apps/web/src/masks/definitions/custom.ts
index cd6e2055..6e0f7af6 100644
--- a/apps/web/src/masks/definitions/custom.ts
+++ b/apps/web/src/masks/definitions/custom.ts
@@ -308,7 +308,7 @@ function computeCustomMaskParamUpdate({
};
}
-export const customMaskDefinition: MaskDefinition<CustomMaskParams> = {
+export const customMaskDefinition: MaskDefinition<"custom"> = {
type: "custom",
name: "Custom",
features: {
@@ -447,71 +447,77 @@ export const customMaskDefinition: MaskDefinition<CustomMaskParams> = {
return params.closed;
},
renderer: {
- buildPath({ resolvedParams, width, height }) {
- const params = resolvedParams as CustomMaskParams;
- const points = params.path;
- if (!params.closed) {
- return new Path2D();
- }
+ 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,
- });
+ 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,
+ });
+ },
},
- renderStroke({ resolvedParams, ctx, width, height }) {
- const params = resolvedParams as CustomMaskParams;
- if (!params.closed) {
- return;
- }
+ 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,
- });
+ 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);
+ 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 === "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();
+ if (params.strokeAlign === "outside") {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.fillStyle = "#ffffff";
+ ctx.fill(path);
+ }
+ ctx.restore();
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/diamond.ts b/apps/web/src/masks/definitions/diamond.ts
index 037aee70..fe96dbe9 100644
--- a/apps/web/src/masks/definitions/diamond.ts
+++ b/apps/web/src/masks/definitions/diamond.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
+import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
@@ -45,7 +45,7 @@ function buildDiamondPath({
return path;
}
-export const diamondMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const diamondMaskDefinition: MaskDefinition<"diamond"> = {
type: "diamond",
name: "Diamond",
features: {
@@ -68,33 +68,39 @@ export const diamondMaskDefinition: MaskDefinition<RectangleMaskParams> = {
},
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,
- });
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ 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,
- });
+ 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 buildDiamondPath({
+ centerX,
+ centerY,
+ halfWidth: Math.max(maskWidth / 2 + offset, 1),
+ halfHeight: Math.max(maskHeight / 2 + offset, 1),
+ rotationRad,
+ });
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/ellipse.ts b/apps/web/src/masks/definitions/ellipse.ts
index d0567817..f57d7444 100644
--- a/apps/web/src/masks/definitions/ellipse.ts
+++ b/apps/web/src/masks/definitions/ellipse.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
+import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
@@ -8,7 +8,7 @@ import {
getStrokeOffset,
} from "./box-like";
-export const ellipseMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const ellipseMaskDefinition: MaskDefinition<"ellipse"> = {
type: "ellipse",
name: "Ellipse",
features: {
@@ -35,41 +35,47 @@ export const ellipseMaskDefinition: MaskDefinition<RectangleMaskParams> = {
},
computeParamUpdate: computeBoxMaskParamUpdate,
renderer: {
- buildPath({ resolvedParams, width, height }) {
- const params = resolvedParams as RectangleMaskParams;
- const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
- getBoxLikeGeometry({ params, width, height });
- const path = new Path2D();
- path.ellipse(
- centerX,
- centerY,
- maskWidth / 2,
- maskHeight / 2,
- rotationRad,
- 0,
- Math.PI * 2,
- );
- return path;
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
+ getBoxLikeGeometry({ params, width, height });
+ const path = new Path2D();
+ path.ellipse(
+ centerX,
+ centerY,
+ maskWidth / 2,
+ maskHeight / 2,
+ rotationRad,
+ 0,
+ Math.PI * 2,
+ );
+ return path;
+ },
},
- 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,
- });
- const path = new Path2D();
- path.ellipse(
- centerX,
- centerY,
- Math.max(1, maskWidth / 2 + offset),
- Math.max(1, maskHeight / 2 + offset),
- rotationRad,
- 0,
- Math.PI * 2,
- );
- return path;
+ 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,
+ });
+ const path = new Path2D();
+ path.ellipse(
+ centerX,
+ centerY,
+ Math.max(1, maskWidth / 2 + offset),
+ Math.max(1, maskHeight / 2 + offset),
+ rotationRad,
+ 0,
+ Math.PI * 2,
+ );
+ return path;
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/heart.ts b/apps/web/src/masks/definitions/heart.ts
index d15c09e6..ec546f18 100644
--- a/apps/web/src/masks/definitions/heart.ts
+++ b/apps/web/src/masks/definitions/heart.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
+import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
@@ -78,7 +78,7 @@ function buildHeartPath({
return path;
}
-export const heartMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const heartMaskDefinition: MaskDefinition<"heart"> = {
type: "heart",
name: "Heart",
features: {
@@ -110,33 +110,39 @@ export const heartMaskDefinition: MaskDefinition<RectangleMaskParams> = {
},
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,
- });
+ 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,
+ });
+ },
},
- 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,
- });
+ 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,
+ });
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/index.ts b/apps/web/src/masks/definitions/index.ts
index 9cee1020..a5aec404 100644
--- a/apps/web/src/masks/definitions/index.ts
+++ b/apps/web/src/masks/definitions/index.ts
@@ -1,5 +1,8 @@
-import type { BaseMaskParams, MaskDefinition } from "@/masks/types";
-import { masksRegistry, type MaskIconProps } from "../registry";
+import {
+ masksRegistry,
+ type MaskDefinitionForRegistration,
+ type MaskIconProps,
+} from "../registry";
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
import { customMaskDefinition } from "./custom";
import { diamondMaskDefinition } from "./diamond";
@@ -20,11 +23,11 @@ import {
TextFontIcon,
} from "@hugeicons/core-free-icons";
-function registerDefaultMask<TParams extends BaseMaskParams>({
+function registerDefaultMask({
definition,
icon,
}: {
- definition: MaskDefinition<TParams>;
+ definition: MaskDefinitionForRegistration;
icon: MaskIconProps;
}) {
if (masksRegistry.has(definition.type)) {
diff --git a/apps/web/src/masks/definitions/rectangle.ts b/apps/web/src/masks/definitions/rectangle.ts
index 7bf1a7fd..db7a4578 100644
--- a/apps/web/src/masks/definitions/rectangle.ts
+++ b/apps/web/src/masks/definitions/rectangle.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
+import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
@@ -45,7 +45,7 @@ function buildRectanglePath({
return path;
}
-export const rectangleMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const rectangleMaskDefinition: MaskDefinition<"rectangle"> = {
type: "rectangle",
name: "Rectangle",
features: {
@@ -65,33 +65,39 @@ export const rectangleMaskDefinition: MaskDefinition<RectangleMaskParams> = {
},
computeParamUpdate: computeBoxMaskParamUpdate,
renderer: {
- buildPath({ resolvedParams, width, height }) {
- const params = resolvedParams as RectangleMaskParams;
- const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
- getBoxLikeGeometry({ params, width, height });
- return buildRectanglePath({
- centerX,
- centerY,
- halfWidth: maskWidth / 2,
- halfHeight: maskHeight / 2,
- rotationRad,
- });
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ const { centerX, centerY, maskWidth, maskHeight, rotationRad } =
+ getBoxLikeGeometry({ params, width, height });
+ return buildRectanglePath({
+ 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 buildRectanglePath({
- centerX,
- centerY,
- halfWidth: Math.max(1, maskWidth / 2 + offset),
- halfHeight: Math.max(1, maskHeight / 2 + offset),
- 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 buildRectanglePath({
+ centerX,
+ centerY,
+ halfWidth: Math.max(1, maskWidth / 2 + offset),
+ halfHeight: Math.max(1, maskHeight / 2 + offset),
+ rotationRad,
+ });
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/split.ts b/apps/web/src/masks/definitions/split.ts
index ddd4e452..de67b876 100644
--- a/apps/web/src/masks/definitions/split.ts
+++ b/apps/web/src/masks/definitions/split.ts
@@ -71,11 +71,11 @@ export function getSplitMaskStrokeSegment({
width,
height,
}: {
- resolvedParams: unknown;
+ resolvedParams: SplitMaskParams;
width: number;
height: number;
}): [{ x: number; y: number }, { x: number; y: number }] | null {
- const { centerX, centerY, rotation } = resolvedParams as SplitMaskParams;
+ const { centerX, centerY, rotation } = resolvedParams;
const { normalX, normalY, lineX, lineY } = splitLineGeometry({
centerX,
centerY,
@@ -180,7 +180,7 @@ function computeSplitMaskParamUpdate({
return {};
}
-export const splitMaskDefinition: MaskDefinition<SplitMaskParams> = {
+export const splitMaskDefinition: MaskDefinition<"split"> = {
type: "split",
name: "Split",
features: {
@@ -267,124 +267,131 @@ export const splitMaskDefinition: MaskDefinition<SplitMaskParams> = {
},
],
renderer: {
- renderMaskHandlesFeather: true,
- renderMask({ resolvedParams, ctx, width, height, feather }) {
- const { centerX, centerY, rotation } = resolvedParams as SplitMaskParams;
- const { normalX, normalY, lineX, lineY } = splitLineGeometry({
- centerX,
- centerY,
- rotation,
- width,
- height,
- });
+ body: {
+ kind: "drawWithFeather",
+ drawWithFeather({ resolvedParams, ctx, width, height, feather }) {
+ const { centerX, centerY, rotation } = resolvedParams;
+ const { normalX, normalY, lineX, lineY } = splitLineGeometry({
+ centerX,
+ centerY,
+ rotation,
+ width,
+ height,
+ });
- // Analytical gradient avoids JFA's two-sided distance artifact near canvas edges.
- const featherHalf = feather / 2;
- const gradient = ctx.createLinearGradient(
- lineX - normalX * featherHalf,
- lineY - normalY * featherHalf,
- lineX + normalX * featherHalf,
- lineY + normalY * featherHalf,
- );
- gradient.addColorStop(0, "rgba(255,255,255,0)");
- gradient.addColorStop(1, "white");
+ // Analytical gradient avoids JFA's two-sided distance artifact near canvas edges.
+ const featherHalf = feather / 2;
+ const gradient = ctx.createLinearGradient(
+ lineX - normalX * featherHalf,
+ lineY - normalY * featherHalf,
+ lineX + normalX * featherHalf,
+ lineY + normalY * featherHalf,
+ );
+ gradient.addColorStop(0, "rgba(255,255,255,0)");
+ gradient.addColorStop(1, "white");
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, width, height);
- },
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, height);
+ },
- buildPath({ resolvedParams, width, height }) {
- const { centerX, centerY, rotation } = resolvedParams as SplitMaskParams;
- const { normalX, normalY, lineX, lineY } = splitLineGeometry({
- centerX,
- centerY,
- rotation,
- width,
- height,
- });
+ opaqueFastPath: {
+ buildPath({ resolvedParams, width, height }) {
+ const { centerX, centerY, rotation } = resolvedParams;
+ const { normalX, normalY, lineX, lineY } = splitLineGeometry({
+ centerX,
+ centerY,
+ rotation,
+ width,
+ height,
+ });
- const edges: [number, number, number, number][] = [
- [0, 0, width, 0],
- [width, 0, width, height],
- [width, height, 0, height],
- [0, height, 0, 0],
- ];
+ const edges: [number, number, number, number][] = [
+ [0, 0, width, 0],
+ [width, 0, width, height],
+ [width, height, 0, height],
+ [0, height, 0, 0],
+ ];
- const isInsideHalfPlane = ({
- x,
- y,
- }: {
- x: number;
- y: number;
- }) => halfPlaneSign({ lineX, lineY, normalX, normalY, x, y }) >= 0;
+ const isInsideHalfPlane = ({
+ x,
+ y,
+ }: {
+ x: number;
+ y: number;
+ }) => halfPlaneSign({ lineX, lineY, normalX, normalY, x, y }) >= 0;
- const vertices: [number, number][] = [];
- for (const [x1, y1, x2, y2] of edges) {
- const isVertex1Inside = isInsideHalfPlane({ x: x1, y: y1 });
- const isVertex2Inside = isInsideHalfPlane({ x: x2, y: y2 });
+ const vertices: [number, number][] = [];
+ for (const [x1, y1, x2, y2] of edges) {
+ const isVertex1Inside = isInsideHalfPlane({ x: x1, y: y1 });
+ const isVertex2Inside = isInsideHalfPlane({ x: x2, y: y2 });
- if (isVertex1Inside && isVertex2Inside) {
- vertices.push([x2, y2]);
- } else if (isVertex1Inside && !isVertex2Inside) {
- const hit = lineEdgeIntersection({
- lineX,
- lineY,
- normalX,
- normalY,
- x1,
- y1,
- x2,
- y2,
- });
- if (hit) vertices.push([hit.x, hit.y]);
- } else if (!isVertex1Inside && isVertex2Inside) {
- const hit = lineEdgeIntersection({
- lineX,
- lineY,
- normalX,
- normalY,
- x1,
- y1,
- x2,
- y2,
- });
- if (hit) {
- vertices.push([hit.x, hit.y]);
- vertices.push([x2, y2]);
+ if (isVertex1Inside && isVertex2Inside) {
+ vertices.push([x2, y2]);
+ } else if (isVertex1Inside && !isVertex2Inside) {
+ const hit = lineEdgeIntersection({
+ lineX,
+ lineY,
+ normalX,
+ normalY,
+ x1,
+ y1,
+ x2,
+ y2,
+ });
+ if (hit) vertices.push([hit.x, hit.y]);
+ } else if (!isVertex1Inside && isVertex2Inside) {
+ const hit = lineEdgeIntersection({
+ lineX,
+ lineY,
+ normalX,
+ normalY,
+ x1,
+ y1,
+ x2,
+ y2,
+ });
+ if (hit) {
+ vertices.push([hit.x, hit.y]);
+ vertices.push([x2, y2]);
+ }
+ }
}
- }
- }
- if (
- vertices.length < 3 ||
- polygonArea({ vertices }) < MIN_POLYGON_AREA_PX
- ) {
- return new Path2D();
- }
+ if (
+ vertices.length < 3 ||
+ polygonArea({ vertices }) < MIN_POLYGON_AREA_PX
+ ) {
+ return new Path2D();
+ }
- const path = new Path2D();
- path.moveTo(vertices[0][0], vertices[0][1]);
- for (let i = 1; i < vertices.length; i++) {
- path.lineTo(vertices[i][0], vertices[i][1]);
- }
- path.closePath();
- return path;
+ const path = new Path2D();
+ path.moveTo(vertices[0][0], vertices[0][1]);
+ for (let i = 1; i < vertices.length; i++) {
+ path.lineTo(vertices[i][0], vertices[i][1]);
+ }
+ path.closePath();
+ return path;
+ },
+ },
},
- buildStrokePath({ resolvedParams, width, height }) {
- const segment = getSplitMaskStrokeSegment({
- resolvedParams,
- width,
- height,
- });
- const path = new Path2D();
+ stroke: {
+ kind: "strokeFromPath",
+ buildStrokePath({ resolvedParams, width, height }) {
+ const segment = getSplitMaskStrokeSegment({
+ resolvedParams,
+ width,
+ height,
+ });
+ const path = new Path2D();
- if (!segment) {
- return path;
- }
+ if (!segment) {
+ return path;
+ }
- path.moveTo(segment[0].x, segment[0].y);
- path.lineTo(segment[1].x, segment[1].y);
- return path;
+ path.moveTo(segment[0].x, segment[0].y);
+ path.lineTo(segment[1].x, segment[1].y);
+ return path;
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/star.ts b/apps/web/src/masks/definitions/star.ts
index a7f8819c..2a1a9b7e 100644
--- a/apps/web/src/masks/definitions/star.ts
+++ b/apps/web/src/masks/definitions/star.ts
@@ -1,4 +1,4 @@
-import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
+import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
@@ -85,7 +85,7 @@ function buildOverlayStarPath({
return `${segments.join(" ")} Z`;
}
-export const starMaskDefinition: MaskDefinition<RectangleMaskParams> = {
+export const starMaskDefinition: MaskDefinition<"star"> = {
type: "star",
name: "Star",
features: {
@@ -108,33 +108,39 @@ export const starMaskDefinition: MaskDefinition<RectangleMaskParams> = {
},
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,
- });
+ body: {
+ kind: "fillPath",
+ buildPath({ resolvedParams, width, height }) {
+ const params = resolvedParams;
+ 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,
- });
+ 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 buildStarPath({
+ centerX,
+ centerY,
+ halfWidth: Math.max(maskWidth / 2 + offset, 1),
+ halfHeight: Math.max(maskHeight / 2 + offset, 1),
+ rotationRad,
+ });
+ },
},
},
};
diff --git a/apps/web/src/masks/definitions/text.ts b/apps/web/src/masks/definitions/text.ts
index c65564d1..aa786e78 100644
--- a/apps/web/src/masks/definitions/text.ts
+++ b/apps/web/src/masks/definitions/text.ts
@@ -205,7 +205,7 @@ function computeTextMaskParamUpdate({
return {};
}
-export const textMaskDefinition: MaskDefinition<TextMaskParams> = {
+export const textMaskDefinition: MaskDefinition<"text"> = {
type: "text",
name: "Text",
features: {
@@ -370,69 +370,75 @@ export const textMaskDefinition: MaskDefinition<TextMaskParams> = {
return params.content.trim().length > 0;
},
renderer: {
- renderMask({ resolvedParams, ctx, width, height }) {
- const params = resolvedParams as TextMaskParams;
- const { layout } = measureTextMask({ params, height });
+ body: {
+ kind: "drawOpaque",
+ drawOpaque({ resolvedParams, ctx, width, height }) {
+ const params = resolvedParams;
+ const { layout } = measureTextMask({ params, height });
- ctx.save();
- ctx.translate(
- width / 2 + params.centerX * width,
- height / 2 + params.centerY * height,
- );
- ctx.scale(params.scale, params.scale);
- if (params.rotation) {
- ctx.rotate((params.rotation * Math.PI) / 180);
- }
- drawMeasuredTextLayout({
- ctx,
- layout,
- textColor: "#ffffff",
- background: null,
- });
- ctx.restore();
- },
- renderStroke({ resolvedParams, ctx, width, height }) {
- const params = resolvedParams as TextMaskParams;
- const { layout } = measureTextMask({ params, height });
-
- ctx.save();
- ctx.translate(
- width / 2 + params.centerX * width,
- height / 2 + params.centerY * height,
- );
- ctx.scale(params.scale, params.scale);
- if (params.rotation) {
- ctx.rotate((params.rotation * Math.PI) / 180);
- }
-
- strokeMeasuredTextLayout({
- ctx,
- layout,
- strokeColor: params.strokeColor,
- strokeWidth: params.strokeWidth,
- });
-
- if (params.strokeAlign === "inside") {
- ctx.globalCompositeOperation = "destination-in";
+ ctx.save();
+ ctx.translate(
+ width / 2 + params.centerX * width,
+ height / 2 + params.centerY * height,
+ );
+ ctx.scale(params.scale, params.scale);
+ if (params.rotation) {
+ ctx.rotate((params.rotation * Math.PI) / 180);
+ }
drawMeasuredTextLayout({
ctx,
layout,
textColor: "#ffffff",
background: null,
});
- }
+ ctx.restore();
+ },
+ },
+ stroke: {
+ kind: "renderStroke",
+ renderStroke({ resolvedParams, ctx, width, height }) {
+ const params = resolvedParams;
+ const { layout } = measureTextMask({ params, height });
- if (params.strokeAlign === "outside") {
- ctx.globalCompositeOperation = "destination-out";
- drawMeasuredTextLayout({
+ ctx.save();
+ ctx.translate(
+ width / 2 + params.centerX * width,
+ height / 2 + params.centerY * height,
+ );
+ ctx.scale(params.scale, params.scale);
+ if (params.rotation) {
+ ctx.rotate((params.rotation * Math.PI) / 180);
+ }
+
+ strokeMeasuredTextLayout({
ctx,
layout,
- textColor: "#ffffff",
- background: null,
+ strokeColor: params.strokeColor,
+ strokeWidth: params.strokeWidth,
});
- }
- ctx.restore();
+ if (params.strokeAlign === "inside") {
+ ctx.globalCompositeOperation = "destination-in";
+ drawMeasuredTextLayout({
+ ctx,
+ layout,
+ textColor: "#ffffff",
+ background: null,
+ });
+ }
+
+ if (params.strokeAlign === "outside") {
+ ctx.globalCompositeOperation = "destination-out";
+ drawMeasuredTextLayout({
+ ctx,
+ layout,
+ textColor: "#ffffff",
+ background: null,
+ });
+ }
+
+ ctx.restore();
+ },
},
},
};
diff --git a/apps/web/src/masks/index.ts b/apps/web/src/masks/index.ts
index 3544e239..df7649e1 100644
--- a/apps/web/src/masks/index.ts
+++ b/apps/web/src/masks/index.ts
@@ -1,18 +1,50 @@
-import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
-import { masksRegistry } from "./registry";
-import { generateUUID } from "@/utils/id";
-
-export { masksRegistry } from "./registry";
-export { registerDefaultMasks } from "./definitions";
-
-export function buildDefaultMaskInstance({
- maskType,
- elementSize,
-}: {
- maskType: MaskType;
- elementSize?: { width: number; height: number };
-}): Mask {
- const definition = masksRegistry.get(maskType);
- const context: MaskDefaultContext = { elementSize };
- return { ...definition.buildDefault(context), id: generateUUID() } as Mask;
-}
+import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
+import { masksRegistry } from "./registry";
+import { generateUUID } from "@/utils/id";
+
+export { masksRegistry } from "./registry";
+export { registerDefaultMasks } from "./definitions";
+
+type MaskWithoutId = Mask extends infer TMask
+ ? TMask extends Mask
+ ? Omit<TMask, "id">
+ : never
+ : never;
+
+function withMaskId({ mask, id }: { mask: MaskWithoutId; id: string }): Mask {
+ switch (mask.type) {
+ case "split":
+ return { ...mask, id };
+ case "cinematic-bars":
+ return { ...mask, id };
+ case "rectangle":
+ return { ...mask, id };
+ case "ellipse":
+ return { ...mask, id };
+ case "heart":
+ return { ...mask, id };
+ case "diamond":
+ return { ...mask, id };
+ case "star":
+ return { ...mask, id };
+ case "text":
+ return { ...mask, id };
+ case "custom":
+ return { ...mask, id };
+ }
+}
+
+export function buildDefaultMaskInstance({
+ maskType,
+ elementSize,
+}: {
+ maskType: MaskType;
+ elementSize?: { width: number; height: number };
+}): Mask {
+ const definition = masksRegistry.get(maskType);
+ const context: MaskDefaultContext = { elementSize };
+ return withMaskId({
+ mask: definition.buildDefault(context),
+ id: generateUUID(),
+ });
+}
diff --git a/apps/web/src/masks/registry.ts b/apps/web/src/masks/registry.ts
index 0bcf1cf0..64ab2f60 100644
--- a/apps/web/src/masks/registry.ts
+++ b/apps/web/src/masks/registry.ts
@@ -2,11 +2,11 @@ import { MAX_FEATHER } from "@/masks/feather";
import type { ParamDefinition } from "@/params";
import type {
BaseMaskParams,
+ Mask,
MaskDefaultContext,
MaskDefinition,
- MaskInteractionResult,
- MaskSnapArgs,
- MaskSnapResult,
+ MaskParamUpdateArgs,
+ MaskRenderer,
MaskType,
} from "@/masks/types";
import type { HugeiconsIconProps } from "@hugeicons/react";
@@ -17,6 +17,16 @@ export type MaskIconProps = {
strokeWidth?: number;
};
+type RegisteredMaskWithoutId = Mask extends infer TMask
+ ? TMask extends Mask
+ ? Omit<TMask, "id">
+ : never
+ : never;
+
+export type MaskDefinitionForRegistration = {
+ [TType in MaskType]: MaskDefinition<TType>;
+}[MaskType];
+
const BASE_MASK_PARAM_DEFINITIONS: ParamDefinition<
keyof BaseMaskParams & string
>[] = [
@@ -50,28 +60,15 @@ const BASE_MASK_PARAM_DEFINITIONS: ParamDefinition<
export interface RegisteredMaskDefinition {
type: MaskType;
name: string;
- features: MaskDefinition<BaseMaskParams>["features"];
+ features: MaskDefinition["features"];
params: ParamDefinition<string>[];
- renderer: MaskDefinition<BaseMaskParams>["renderer"];
- interaction: {
- getInteraction(args: {
- params: BaseMaskParams;
- bounds: Parameters<
- MaskDefinition<BaseMaskParams>["interaction"]["getInteraction"]
- >[0]["bounds"];
- displayScale: number;
- scaleX: number;
- scaleY: number;
- }): MaskInteractionResult;
- snap?(args: MaskSnapArgs<BaseMaskParams>): MaskSnapResult<BaseMaskParams>;
- };
- isActive?: (params: BaseMaskParams) => boolean;
- buildDefault(
- context: MaskDefaultContext,
- ): ReturnType<MaskDefinition<BaseMaskParams>["buildDefault"]>;
+ renderer: MaskRenderer<BaseMaskParams>;
+ interaction: MaskDefinition["interaction"];
+ isActive?(params: BaseMaskParams): boolean;
+ buildDefault(context: MaskDefaultContext): RegisteredMaskWithoutId;
computeParamUpdate(
- args: Parameters<MaskDefinition<BaseMaskParams>["computeParamUpdate"]>[0],
- ): ReturnType<MaskDefinition<BaseMaskParams>["computeParamUpdate"]>;
+ args: MaskParamUpdateArgs<BaseMaskParams>,
+ ): ReturnType<MaskDefinition["computeParamUpdate"]>;
icon: MaskIconProps;
}
@@ -83,11 +80,11 @@ export class MasksRegistry extends DefinitionRegistry<
super("mask");
}
- registerMask<TParams extends BaseMaskParams>({
+ registerMask({
definition,
icon,
}: {
- definition: MaskDefinition<TParams>;
+ definition: MaskDefinitionForRegistration;
icon: MaskIconProps;
}): void {
const withBaseParams: RegisteredMaskDefinition = {
@@ -96,23 +93,10 @@ export class MasksRegistry extends DefinitionRegistry<
features: definition.features,
params: [...definition.params, ...BASE_MASK_PARAM_DEFINITIONS],
renderer: definition.renderer,
- interaction: {
- getInteraction(args) {
- return definition.interaction.getInteraction(args as never);
- },
- snap: definition.interaction.snap
- ? (args) => definition.interaction.snap?.(args as never) as never
- : undefined,
- },
- isActive: definition.isActive
- ? (params) => definition.isActive?.(params as TParams) ?? true
- : undefined,
- buildDefault(context) {
- return definition.buildDefault(context);
- },
- computeParamUpdate(args) {
- return definition.computeParamUpdate(args as never);
- },
+ interaction: definition.interaction,
+ isActive: definition.isActive,
+ buildDefault: definition.buildDefault,
+ computeParamUpdate: definition.computeParamUpdate,
icon,
};
this.register({
diff --git a/apps/web/src/masks/types.ts b/apps/web/src/masks/types.ts
index 4f6c90aa..d35ca889 100644
--- a/apps/web/src/masks/types.ts
+++ b/apps/web/src/masks/types.ts
@@ -131,32 +131,50 @@ export type Mask =
| TextMask
| CustomMask;
-export interface MaskRenderer {
- buildPath?: (params: {
- resolvedParams: unknown;
- width: number;
- height: number;
- }) => Path2D;
- buildStrokePath?: (params: {
- resolvedParams: unknown;
- width: number;
- height: number;
- }) => Path2D;
- /** Renders the feathered mask directly onto ctx, bypassing JFA. */
- renderMask?: (params: {
- resolvedParams: unknown;
- ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
- width: number;
- height: number;
- feather: number;
- }) => void;
- renderMaskHandlesFeather?: boolean;
- renderStroke?: (params: {
- resolvedParams: unknown;
- ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
- width: number;
- height: number;
- }) => void;
+export type MaskByType<TType extends MaskType> = Extract<Mask, { type: TType }>;
+export type MaskParamsByType<TType extends MaskType> =
+ MaskByType<TType>["params"];
+
+type MaskPathArgs<TParams extends BaseMaskParams> = {
+ resolvedParams: TParams;
+ width: number;
+ height: number;
+};
+
+type MaskDrawArgs<TParams extends BaseMaskParams> = MaskPathArgs<TParams> & {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+};
+
+export type MaskBody<TParams extends BaseMaskParams = BaseMaskParams> =
+ | {
+ kind: "fillPath";
+ buildPath(args: MaskPathArgs<TParams>): Path2D;
+ }
+ | {
+ kind: "drawOpaque";
+ drawOpaque(args: MaskDrawArgs<TParams>): void;
+ }
+ | {
+ kind: "drawWithFeather";
+ drawWithFeather(args: MaskDrawArgs<TParams> & { feather: number }): void;
+ opaqueFastPath?: {
+ buildPath(args: MaskPathArgs<TParams>): Path2D;
+ };
+ };
+
+export type MaskStroke<TParams extends BaseMaskParams = BaseMaskParams> =
+ | {
+ kind: "strokeFromPath";
+ buildStrokePath(args: MaskPathArgs<TParams>): Path2D;
+ }
+ | {
+ kind: "renderStroke";
+ renderStroke(args: MaskDrawArgs<TParams>): void;
+ };
+
+export interface MaskRenderer<TParams extends BaseMaskParams = BaseMaskParams> {
+ body: MaskBody<TParams>;
+ stroke?: MaskStroke<TParams>;
}
export interface MaskFeatures {
@@ -282,17 +300,17 @@ export interface MaskInteractionDefinition<
snap?(args: MaskSnapArgs<TParams>): MaskSnapResult<TParams>;
}
-export interface MaskDefinition<
- TParams extends BaseMaskParams = BaseMaskParams,
-> {
- type: MaskType;
+export interface MaskDefinition<TType extends MaskType = MaskType> {
+ type: TType;
name: string;
features: MaskFeatures;
- params: ParamDefinition<keyof TParams & string>[];
- renderer: MaskRenderer;
- interaction: MaskInteractionDefinition<TParams>;
+ params: ParamDefinition<keyof MaskParamsByType<TType> & string>[];
+ renderer: MaskRenderer<MaskParamsByType<TType>>;
+ interaction: MaskInteractionDefinition<MaskParamsByType<TType>>;
/** When defined and returning false, the mask is not applied and the element renders fully visible. */
- isActive?: (params: TParams) => boolean;
- buildDefault(context: MaskDefaultContext): Omit<Mask, "id">;
- computeParamUpdate(args: MaskParamUpdateArgs<TParams>): Partial<TParams>;
+ isActive?(params: MaskParamsByType<TType>): boolean;
+ buildDefault(context: MaskDefaultContext): Omit<MaskByType<TType>, "id">;
+ computeParamUpdate(
+ args: MaskParamUpdateArgs<MaskParamsByType<TType>>,
+ ): Partial<MaskParamsByType<TType>>;
}
diff --git a/apps/web/src/services/renderer/compositor/frame-descriptor.ts b/apps/web/src/services/renderer/compositor/frame-descriptor.ts
index 1bc316ef..eeb0633c 100644
--- a/apps/web/src/services/renderer/compositor/frame-descriptor.ts
+++ b/apps/web/src/services/renderer/compositor/frame-descriptor.ts
@@ -396,23 +396,16 @@ function buildMaskArtifacts({
return { mask: null, strokeLayer: null };
}
- let feather = mask.params.feather;
- const canRenderMaskDirectly = Boolean(definition.renderer.renderMask);
- const shouldRenderMaskDirectly =
- canRenderMaskDirectly &&
- (!definition.renderer.buildPath ||
- (mask.params.feather > 0 &&
- definition.renderer.renderMaskHandlesFeather));
- if (
- shouldRenderMaskDirectly &&
- definition.renderer.renderMaskHandlesFeather
- ) {
- feather = 0;
- }
+ const { body } = definition.renderer;
+ const usesOpaqueFastPath =
+ body.kind === "drawWithFeather" &&
+ mask.params.feather === 0 &&
+ Boolean(body.opaqueFastPath);
+ const feather = body.kind === "drawWithFeather" ? 0 : mask.params.feather;
const maskTextureId = `${path}:mask`;
const { width: canvasWidth, height: canvasHeight } = renderer;
- const maskContentHash = `mask:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}:direct=${shouldRenderMaskDirectly}`;
+ const maskContentHash = `mask:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}:body=${body.kind}:fastPath=${usesOpaqueFastPath}`;
const drawMask: TextureCanvasDrawFn = (ctx) => {
const { canvas: elementMaskCanvas, context: elementMaskCtx } =
createCanvasSurface({
@@ -420,24 +413,44 @@ function buildMaskArtifacts({
height: Math.round(transform.height),
});
- if (shouldRenderMaskDirectly && definition.renderer.renderMask) {
- definition.renderer.renderMask({
- resolvedParams: mask.params,
- ctx: elementMaskCtx,
- width: Math.round(transform.width),
- height: Math.round(transform.height),
- feather: mask.params.feather,
- });
- } else if (definition.renderer.buildPath) {
- const path2d = definition.renderer.buildPath({
- resolvedParams: mask.params,
- width: transform.width,
- height: transform.height,
- });
- elementMaskCtx.fillStyle = "white";
- elementMaskCtx.fill(path2d);
- } else {
- return;
+ switch (body.kind) {
+ case "fillPath": {
+ const path2d = body.buildPath({
+ resolvedParams: mask.params,
+ width: transform.width,
+ height: transform.height,
+ });
+ elementMaskCtx.fillStyle = "white";
+ elementMaskCtx.fill(path2d);
+ break;
+ }
+ case "drawOpaque":
+ body.drawOpaque({
+ resolvedParams: mask.params,
+ ctx: elementMaskCtx,
+ width: Math.round(transform.width),
+ height: Math.round(transform.height),
+ });
+ break;
+ case "drawWithFeather":
+ if (usesOpaqueFastPath && body.opaqueFastPath) {
+ const path2d = body.opaqueFastPath.buildPath({
+ resolvedParams: mask.params,
+ width: transform.width,
+ height: transform.height,
+ });
+ elementMaskCtx.fillStyle = "white";
+ elementMaskCtx.fill(path2d);
+ } else {
+ body.drawWithFeather({
+ resolvedParams: mask.params,
+ ctx: elementMaskCtx,
+ width: Math.round(transform.width),
+ height: Math.round(transform.height),
+ feather: mask.params.feather,
+ });
+ }
+ break;
}
drawTransformedCanvas({ ctx, source: elementMaskCanvas, transform });
@@ -451,45 +464,38 @@ function buildMaskArtifacts({
draw: drawMask,
});
- const hasStroke =
- mask.params.strokeWidth > 0 &&
- (definition.renderer.renderStroke ||
- definition.renderer.buildStrokePath ||
- definition.renderer.buildPath);
+ const stroke = definition.renderer.stroke;
+ const hasStroke = mask.params.strokeWidth > 0 && Boolean(stroke);
let strokeLayer: FrameItemDescriptor | null = null;
- if (hasStroke) {
+ if (hasStroke && stroke) {
const strokeTextureId = `${path}:mask-stroke`;
- const strokeContentHash = `stroke:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}`;
+ const strokeContentHash = `stroke:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}:stroke=${stroke.kind}`;
const drawStroke: TextureCanvasDrawFn = (ctx) => {
const { canvas: strokeCanvas, context: strokeCtx } = createCanvasSurface({
width: Math.round(transform.width),
height: Math.round(transform.height),
});
- if (definition.renderer.renderStroke) {
- definition.renderer.renderStroke({
- resolvedParams: mask.params,
- ctx: strokeCtx,
- width: transform.width,
- height: transform.height,
- });
- } else {
- const strokePath =
- definition.renderer.buildStrokePath?.({
+ switch (stroke.kind) {
+ case "renderStroke":
+ stroke.renderStroke({
resolvedParams: mask.params,
+ ctx: strokeCtx,
width: transform.width,
height: transform.height,
- }) ??
- definition.renderer.buildPath?.({
+ });
+ break;
+ case "strokeFromPath": {
+ const strokePath = stroke.buildStrokePath({
resolvedParams: mask.params,
width: transform.width,
height: transform.height,
- }) ??
- null;
- if (!strokePath) return;
- strokeCtx.strokeStyle = mask.params.strokeColor;
- strokeCtx.lineWidth = mask.params.strokeWidth;
- strokeCtx.stroke(strokePath);
+ });
+ strokeCtx.strokeStyle = mask.params.strokeColor;
+ strokeCtx.lineWidth = mask.params.strokeWidth;
+ strokeCtx.stroke(strokePath);
+ break;
+ }
}
drawTransformedCanvas({ ctx, source: strokeCanvas, transform });