summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/masks/definitions/text.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/masks/definitions/text.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/masks/definitions/text.ts')
-rw-r--r--apps/web/src/lib/masks/definitions/text.ts438
1 files changed, 0 insertions, 438 deletions
diff --git a/apps/web/src/lib/masks/definitions/text.ts b/apps/web/src/lib/masks/definitions/text.ts
deleted file mode 100644
index b774ed28..00000000
--- a/apps/web/src/lib/masks/definitions/text.ts
+++ /dev/null
@@ -1,438 +0,0 @@
-import type { ParamDefinition } from "@/lib/params";
-import type {
- MaskDefinition,
- MaskParamUpdateArgs,
- TextMask,
- TextMaskParams,
-} from "@/lib/masks/types";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/lib/text/typography";
-import {
- drawMeasuredTextLayout,
- measureTextLayout,
- strokeMeasuredTextLayout,
-} from "@/lib/text/primitives";
-import { getTextMeasurementContext } from "@/lib/text/measure-element";
-import { getTextVisualRect } from "@/lib/text/layout";
-import {
- getBoxMaskHandlePositions,
- getBoxMaskRectOverlay,
-} from "@/lib/masks/handle-positions";
-import { computeFeatherUpdate } from "@/lib/masks/param-update";
-import {
- setMaskLocalCenter,
- toGlobalMaskSnapLines,
-} from "@/lib/masks/geometry";
-import {
- snapPosition,
- snapRotation,
- snapScale,
- type ScaleEdgePreference,
-} from "@/lib/preview/preview-snap";
-
-const PERCENTAGE_DISPLAY = {
- displayMultiplier: 100,
- step: 1,
-} as const;
-
-const TEXT_MASK_ALIGNMENT = DEFAULTS.text.element.textAlign;
-
-const TEXT_MASK_PARAMS: ParamDefinition<keyof TextMaskParams & 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: "fontSize",
- label: "Size",
- type: "number",
- default: DEFAULTS.text.element.fontSize,
- min: MIN_FONT_SIZE,
- max: MAX_FONT_SIZE,
- step: 1,
- },
- {
- 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 measureTextMask({
- params,
- height,
-}: {
- params: TextMaskParams;
- height: number;
-}) {
- const layout = measureTextLayout({
- text: {
- content: params.content,
- fontSize: params.fontSize,
- fontFamily: params.fontFamily,
- fontWeight: params.fontWeight,
- fontStyle: params.fontStyle,
- textAlign: TEXT_MASK_ALIGNMENT,
- textDecoration: params.textDecoration,
- letterSpacing: params.letterSpacing,
- lineHeight: params.lineHeight,
- },
- canvasHeight: height,
- ctx: getTextMeasurementContext(),
- });
- const visualRect = getTextVisualRect({
- textAlign: layout.textAlign,
- block: layout.block,
- background: { enabled: false, color: "transparent" },
- fontSizeRatio: layout.fontSizeRatio,
- });
-
- return {
- layout,
- intrinsicWidth: Math.max(1, visualRect.width),
- intrinsicHeight: Math.max(1, visualRect.height),
- };
-}
-
-function getScalePreferredEdges({
- handleId,
-}: {
- handleId: string;
-}): ScaleEdgePreference | undefined {
- if (handleId !== "scale") {
- return undefined;
- }
-
- return {
- right: true,
- bottom: true,
- };
-}
-
-function computeTextMaskParamUpdate({
- handleId,
- startParams,
- deltaX,
- deltaY,
- startCanvasX,
- startCanvasY,
- bounds,
-}: MaskParamUpdateArgs<TextMaskParams>): Partial<TextMaskParams> {
- if (handleId === "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 === "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 === "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 === "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),
- };
- }
-
- return {};
-}
-
-export const textMaskDefinition: MaskDefinition<TextMaskParams> = {
- type: "text",
- name: "Text",
- features: {
- hasPosition: true,
- hasRotation: true,
- sizeMode: "uniform",
- },
- params: TEXT_MASK_PARAMS,
- interaction: {
- getInteraction({
- params,
- bounds,
- displayScale,
- scaleX: _scaleX,
- scaleY: _scaleY,
- }) {
- const { intrinsicWidth, intrinsicHeight } = measureTextMask({
- params,
- height: bounds.height,
- });
- const width = (intrinsicWidth * params.scale) / bounds.width;
- const height = (intrinsicHeight * params.scale) / bounds.height;
- return {
- handles: getBoxMaskHandlePositions({
- centerX: params.centerX,
- centerY: params.centerY,
- width,
- height,
- rotation: params.rotation,
- feather: params.feather,
- sizeMode: "uniform",
- bounds,
- displayScale,
- }),
- overlays: [
- getBoxMaskRectOverlay({
- centerX: params.centerX,
- centerY: params.centerY,
- width,
- height,
- rotation: params.rotation,
- bounds,
- }),
- ],
- };
- },
- snap({
- handleId,
- startParams,
- proposedParams,
- bounds,
- canvasSize,
- snapThreshold,
- }) {
- const { intrinsicWidth, intrinsicHeight } = measureTextMask({
- params: startParams,
- height: bounds.height,
- });
- const position = {
- x: proposedParams.centerX * bounds.width,
- y: proposedParams.centerY * bounds.height,
- };
-
- if (handleId === "position") {
- const { snappedPosition, activeLines } = snapPosition({
- proposedPosition: position,
- canvasSize: bounds,
- elementSize: {
- width: intrinsicWidth * proposedParams.scale,
- height: intrinsicHeight * proposedParams.scale,
- },
- rotation: proposedParams.rotation,
- snapThreshold,
- });
-
- return {
- params: {
- ...proposedParams,
- ...setMaskLocalCenter({
- center: snappedPosition,
- bounds,
- }),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- if (handleId === "rotation") {
- const { snappedRotation } = snapRotation({
- proposedRotation: proposedParams.rotation,
- });
- return {
- params: {
- ...proposedParams,
- rotation: snappedRotation,
- },
- activeLines: [],
- };
- }
-
- if (handleId === "scale") {
- const { snappedScale, activeLines } = snapScale({
- proposedScale: proposedParams.scale,
- position,
- baseWidth: intrinsicWidth,
- baseHeight: intrinsicHeight,
- rotation: proposedParams.rotation,
- canvasSize: bounds,
- snapThreshold,
- preferredEdges: getScalePreferredEdges({ handleId }),
- });
-
- return {
- params: {
- ...proposedParams,
- scale: Math.max(0.01, snappedScale),
- },
- activeLines: toGlobalMaskSnapLines({
- lines: activeLines,
- bounds,
- canvasSize,
- }),
- };
- }
-
- return {
- params: proposedParams,
- activeLines: [],
- };
- },
- },
- buildDefault(): Omit<TextMask, "id"> {
- return {
- type: "text",
- params: {
- feather: 0,
- inverted: false,
- strokeColor: "#ffffff",
- strokeWidth: 0,
- strokeAlign: "center",
- content: "Mask",
- fontSize: DEFAULTS.text.element.fontSize,
- fontFamily: DEFAULTS.text.element.fontFamily,
- fontWeight: DEFAULTS.text.element.fontWeight,
- fontStyle: DEFAULTS.text.element.fontStyle,
- textDecoration: DEFAULTS.text.element.textDecoration,
- letterSpacing: DEFAULTS.text.letterSpacing,
- lineHeight: DEFAULTS.text.lineHeight,
- centerX: 0,
- centerY: 0,
- rotation: 0,
- scale: 1,
- },
- };
- },
- computeParamUpdate: computeTextMaskParamUpdate,
- isActive(params) {
- return params.content.trim().length > 0;
- },
- renderer: {
- renderMask({ 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);
- }
- 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";
- drawMeasuredTextLayout({
- ctx,
- layout,
- textColor: "#ffffff",
- background: null,
- });
- }
-
- if (params.strokeAlign === "outside") {
- ctx.globalCompositeOperation = "destination-out";
- drawMeasuredTextLayout({
- ctx,
- layout,
- textColor: "#ffffff",
- background: null,
- });
- }
-
- ctx.restore();
- },
- },
-};