refactor: split builtin and freeform masks
Made-with: Cursor
This commit is contained in:
parent
97f8d1968f
commit
79dcec8a2e
|
|
@ -305,7 +305,7 @@ export function useEditorActions() {
|
||||||
if (!selectedMaskPointSelection) {
|
if (!selectedMaskPointSelection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editor.timeline.deleteCustomMaskPoints({
|
editor.timeline.deleteFreeformPathMaskPoints({
|
||||||
trackId: selectedMaskPointSelection.trackId,
|
trackId: selectedMaskPointSelection.trackId,
|
||||||
elementId: selectedMaskPointSelection.elementId,
|
elementId: selectedMaskPointSelection.elementId,
|
||||||
maskId: selectedMaskPointSelection.maskId,
|
maskId: selectedMaskPointSelection.maskId,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command, type CommandResult } from "@/commands/base-command";
|
import { Command, type CommandResult } from "@/commands/base-command";
|
||||||
import {
|
import {
|
||||||
getCustomMaskClosedStateAfterPointRemoval,
|
getFreeformPathClosedStateAfterPointRemoval,
|
||||||
removeCustomMaskPoints,
|
removeFreeformPathPoints,
|
||||||
} from "@/masks/custom-path";
|
} from "@/masks/freeform/path";
|
||||||
import type { CustomMask } from "@/masks/types";
|
import type { FreeformPathMask } from "@/masks/types";
|
||||||
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
|
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
|
||||||
import type { MaskableElement, SceneTracks } from "@/timeline";
|
import type { MaskableElement, SceneTracks } from "@/timeline";
|
||||||
|
|
||||||
function deletePointsFromCustomMask({
|
function deletePointsFromFreeformPathMask({
|
||||||
mask,
|
mask,
|
||||||
pointIds,
|
pointIds,
|
||||||
}: {
|
}: {
|
||||||
mask: CustomMask;
|
mask: FreeformPathMask;
|
||||||
pointIds: string[];
|
pointIds: string[];
|
||||||
}): CustomMask {
|
}): FreeformPathMask {
|
||||||
const points = mask.params.path;
|
const points = mask.params.path;
|
||||||
const nextPoints = removeCustomMaskPoints({ points, pointIds });
|
const nextPoints = removeFreeformPathPoints({ points, pointIds });
|
||||||
if (nextPoints.length === points.length) {
|
if (nextPoints.length === points.length) {
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ function deletePointsFromCustomMask({
|
||||||
params: {
|
params: {
|
||||||
...mask.params,
|
...mask.params,
|
||||||
path: nextPoints,
|
path: nextPoints,
|
||||||
closed: getCustomMaskClosedStateAfterPointRemoval({
|
closed: getFreeformPathClosedStateAfterPointRemoval({
|
||||||
wasClosed: mask.params.closed,
|
wasClosed: mask.params.closed,
|
||||||
remainingPointCount: nextPoints.length,
|
remainingPointCount: nextPoints.length,
|
||||||
}),
|
}),
|
||||||
|
|
@ -46,11 +46,11 @@ function deletePointsFromElementMask({
|
||||||
const currentMasks = element.masks ?? [];
|
const currentMasks = element.masks ?? [];
|
||||||
let didDeletePoints = false;
|
let didDeletePoints = false;
|
||||||
const nextMasks = currentMasks.map((mask) => {
|
const nextMasks = currentMasks.map((mask) => {
|
||||||
if (mask.id !== maskId || mask.type !== "custom") {
|
if (mask.id !== maskId || mask.type !== "freeform") {
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextMask = deletePointsFromCustomMask({
|
const nextMask = deletePointsFromFreeformPathMask({
|
||||||
mask,
|
mask,
|
||||||
pointIds,
|
pointIds,
|
||||||
});
|
});
|
||||||
|
|
@ -64,7 +64,7 @@ function deletePointsFromElementMask({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeleteCustomMaskPointsCommand extends Command {
|
export class DeleteFreeformPathMaskPointsCommand extends Command {
|
||||||
private savedState: SceneTracks | null = null;
|
private savedState: SceneTracks | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
|
|
@ -100,8 +100,9 @@ export class DeleteCustomMaskPointsCommand extends Command {
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
|
if (!isMaskableElement(element)) return element;
|
||||||
const result = deletePointsFromElementMask({
|
const result = deletePointsFromElementMask({
|
||||||
element: element as MaskableElement,
|
element,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
pointIds: this.pointIds,
|
pointIds: this.pointIds,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { DeleteCustomMaskPointsCommand } from "./delete-custom-mask-points";
|
export { DeleteFreeformPathMaskPointsCommand } from "./delete-custom-mask-points";
|
||||||
export { InsertCustomMaskPointCommand } from "./insert-custom-mask-point";
|
export { InsertFreeformPathMaskPointCommand } from "./insert-custom-mask-point";
|
||||||
export { RemoveMaskCommand } from "./remove-mask";
|
export { RemoveMaskCommand } from "./remove-mask";
|
||||||
export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted";
|
export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted";
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { Command, type CommandResult } from "@/commands/base-command";
|
import { Command, type CommandResult } from "@/commands/base-command";
|
||||||
import { insertPointOnCustomMaskSegment } from "@/masks/definitions/custom";
|
import { insertPointOnFreeformSegment } from "@/masks/freeform/definition";
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
import type { CustomMask } from "@/masks/types";
|
import type { FreeformPathMask } from "@/masks/types";
|
||||||
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
|
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
|
||||||
import type { MaskableElement, SceneTracks } from "@/timeline";
|
import type { MaskableElement, SceneTracks } from "@/timeline";
|
||||||
|
|
||||||
function insertPointIntoCustomMask({
|
function insertPointIntoFreeformPathMask({
|
||||||
mask,
|
mask,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
mask: CustomMask;
|
mask: FreeformPathMask;
|
||||||
segmentIndex: number;
|
segmentIndex: number;
|
||||||
canvasPoint: { x: number; y: number };
|
canvasPoint: { x: number; y: number };
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): { mask: CustomMask; insertedPointId: string | null } {
|
}): { mask: FreeformPathMask; insertedPointId: string | null } {
|
||||||
const result = insertPointOnCustomMaskSegment({
|
const result = insertPointOnFreeformSegment({
|
||||||
params: mask.params,
|
params: mask.params,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
|
|
@ -61,11 +61,11 @@ function insertPointIntoElementMask({
|
||||||
let didInsertPoint = false;
|
let didInsertPoint = false;
|
||||||
|
|
||||||
const nextMasks = currentMasks.map((mask) => {
|
const nextMasks = currentMasks.map((mask) => {
|
||||||
if (mask.id !== maskId || mask.type !== "custom") {
|
if (mask.id !== maskId || mask.type !== "freeform") {
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = insertPointIntoCustomMask({
|
const result = insertPointIntoFreeformPathMask({
|
||||||
mask,
|
mask,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
|
|
@ -85,7 +85,7 @@ function insertPointIntoElementMask({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InsertCustomMaskPointCommand extends Command {
|
export class InsertFreeformPathMaskPointCommand extends Command {
|
||||||
private savedState: SceneTracks | null = null;
|
private savedState: SceneTracks | null = null;
|
||||||
private readonly trackId: string;
|
private readonly trackId: string;
|
||||||
private readonly elementId: string;
|
private readonly elementId: string;
|
||||||
|
|
@ -130,8 +130,9 @@ export class InsertCustomMaskPointCommand extends Command {
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) => {
|
update: (element) => {
|
||||||
|
if (!isMaskableElement(element)) return element;
|
||||||
const result = insertPointIntoElementMask({
|
const result = insertPointIntoElementMask({
|
||||||
element: element as MaskableElement,
|
element,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
segmentIndex: this.segmentIndex,
|
segmentIndex: this.segmentIndex,
|
||||||
canvasPoint: this.canvasPoint,
|
canvasPoint: this.canvasPoint,
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,13 @@ export class RemoveMaskCommand extends Command {
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) =>
|
update: (element) => {
|
||||||
removeMaskFromElement({
|
if (!isMaskableElement(element)) return element;
|
||||||
element: element as MaskableElement,
|
return removeMaskFromElement({
|
||||||
|
element,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,13 @@ export class ToggleMaskInvertedCommand extends Command {
|
||||||
trackId: this.trackId,
|
trackId: this.trackId,
|
||||||
elementId: this.elementId,
|
elementId: this.elementId,
|
||||||
elementPredicate: isMaskableElement,
|
elementPredicate: isMaskableElement,
|
||||||
update: (element) =>
|
update: (element) => {
|
||||||
toggleMaskInvertedOnElement({
|
if (!isMaskableElement(element)) return element;
|
||||||
element: element as MaskableElement,
|
return toggleMaskInvertedOnElement({
|
||||||
|
element,
|
||||||
maskId: this.maskId,
|
maskId: this.maskId,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ import {
|
||||||
RetimeKeyframeCommand,
|
RetimeKeyframeCommand,
|
||||||
UpdateScalarKeyframeCurveCommand,
|
UpdateScalarKeyframeCurveCommand,
|
||||||
AddClipEffectCommand,
|
AddClipEffectCommand,
|
||||||
DeleteCustomMaskPointsCommand,
|
DeleteFreeformPathMaskPointsCommand,
|
||||||
InsertCustomMaskPointCommand,
|
InsertFreeformPathMaskPointCommand,
|
||||||
RemoveClipEffectCommand,
|
RemoveClipEffectCommand,
|
||||||
UpdateClipEffectParamsCommand,
|
UpdateClipEffectParamsCommand,
|
||||||
ToggleClipEffectCommand,
|
ToggleClipEffectCommand,
|
||||||
|
|
@ -349,7 +349,7 @@ export class TimelineManager {
|
||||||
this.editor.command.execute({ command });
|
this.editor.command.execute({ command });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCustomMaskPoints({
|
deleteFreeformPathMaskPoints({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
|
|
@ -363,7 +363,7 @@ export class TimelineManager {
|
||||||
if (pointIds.length === 0) {
|
if (pointIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const command = new DeleteCustomMaskPointsCommand({
|
const command = new DeleteFreeformPathMaskPointsCommand({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
|
|
@ -372,7 +372,7 @@ export class TimelineManager {
|
||||||
this.editor.command.execute({ command });
|
this.editor.command.execute({ command });
|
||||||
}
|
}
|
||||||
|
|
||||||
insertCustomMaskPoint({
|
insertFreeformPathMaskPoint({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
|
|
@ -387,7 +387,7 @@ export class TimelineManager {
|
||||||
canvasPoint: { x: number; y: number };
|
canvasPoint: { x: number; y: number };
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): void {
|
}): void {
|
||||||
const command = new InsertCustomMaskPointCommand({
|
const command = new InsertFreeformPathMaskPointCommand({
|
||||||
trackId,
|
trackId,
|
||||||
elementId,
|
elementId,
|
||||||
maskId,
|
maskId,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
findClosestPointOnCustomMaskSegment,
|
findClosestPointOnFreeformSegment,
|
||||||
getCustomMaskClosedStateAfterPointRemoval,
|
getFreeformPathClosedStateAfterPointRemoval,
|
||||||
insertPointIntoCustomMaskSegment,
|
insertPointIntoFreeformSegment,
|
||||||
removeCustomMaskPoints,
|
removeFreeformPathPoints,
|
||||||
} from "@/masks/custom-path";
|
} from "@/masks/freeform/path";
|
||||||
import {
|
import {
|
||||||
appendPointToCustomMask,
|
appendPointToFreeformPathMask,
|
||||||
customMaskDefinition,
|
freeformMaskDefinition,
|
||||||
insertPointOnCustomMaskSegment,
|
insertPointOnFreeformSegment,
|
||||||
} from "@/masks/definitions/custom";
|
} from "@/masks/freeform/definition";
|
||||||
import { getSplitMaskStrokeSegment } from "@/masks/definitions/split";
|
import { getSplitMaskStrokeSegment } from "@/masks/builtin/definitions/split";
|
||||||
import { textMaskDefinition } from "@/masks/definitions/text";
|
import { textMaskDefinition } from "@/masks/builtin/definitions/text";
|
||||||
import { getMaskSnapGeometry } from "@/masks/geometry";
|
import { getMaskSnapGeometry } from "@/masks/geometry";
|
||||||
import { snapBoxMaskInteraction, snapSplitMaskInteraction } from "@/masks/snap";
|
import { snapBoxMaskInteraction, snapSplitMaskInteraction } from "@/masks/snap";
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
import type {
|
import type {
|
||||||
CustomMaskParams,
|
FreeformPathMaskParams,
|
||||||
RectangleMaskParams,
|
RectangleMaskParams,
|
||||||
SplitMaskParams,
|
SplitMaskParams,
|
||||||
TextMaskParams,
|
TextMaskParams,
|
||||||
|
|
@ -100,9 +100,9 @@ function buildTextMaskParams(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCustomMaskParams(
|
function buildFreeformPathMaskParams(
|
||||||
overrides: Partial<CustomMaskParams> = {},
|
overrides: Partial<FreeformPathMaskParams> = {},
|
||||||
): CustomMaskParams {
|
): FreeformPathMaskParams {
|
||||||
return {
|
return {
|
||||||
feather: 0,
|
feather: 0,
|
||||||
inverted: false,
|
inverted: false,
|
||||||
|
|
@ -347,11 +347,11 @@ describe("mask snapping", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("snaps custom mask movement using path geometry bounds", () => {
|
test("snaps custom mask movement using path geometry bounds", () => {
|
||||||
const params = buildCustomMaskParams({
|
const params = buildFreeformPathMaskParams({
|
||||||
centerX: 0.03,
|
centerX: 0.03,
|
||||||
centerY: -0.04,
|
centerY: -0.04,
|
||||||
});
|
});
|
||||||
const result = customMaskDefinition.interaction.snap?.({
|
const result = freeformMaskDefinition.interaction.snap?.({
|
||||||
handleId: { kind: "position" },
|
handleId: { kind: "position" },
|
||||||
startParams: params,
|
startParams: params,
|
||||||
proposedParams: params,
|
proposedParams: params,
|
||||||
|
|
@ -381,11 +381,11 @@ describe("mask snapping", () => {
|
||||||
|
|
||||||
describe("custom mask creation", () => {
|
describe("custom mask creation", () => {
|
||||||
test("anchors the first point at the click position", () => {
|
test("anchors the first point at the click position", () => {
|
||||||
const params = buildCustomMaskParams({
|
const params = buildFreeformPathMaskParams({
|
||||||
path: [],
|
path: [],
|
||||||
closed: false,
|
closed: false,
|
||||||
});
|
});
|
||||||
const next = appendPointToCustomMask({
|
const next = appendPointToFreeformPathMask({
|
||||||
params,
|
params,
|
||||||
canvasPoint: { x: bounds.cx + 20, y: bounds.cy - 10 },
|
canvasPoint: { x: bounds.cx + 20, y: bounds.cy - 10 },
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -401,8 +401,8 @@ describe("custom mask creation", () => {
|
||||||
|
|
||||||
describe("custom mask point deletion", () => {
|
describe("custom mask point deletion", () => {
|
||||||
test("removes the selected points by id", () => {
|
test("removes the selected points by id", () => {
|
||||||
const points = buildCustomMaskParams().path;
|
const points = buildFreeformPathMaskParams().path;
|
||||||
const nextPoints = removeCustomMaskPoints({
|
const nextPoints = removeFreeformPathPoints({
|
||||||
points,
|
points,
|
||||||
pointIds: ["b"],
|
pointIds: ["b"],
|
||||||
});
|
});
|
||||||
|
|
@ -411,14 +411,14 @@ describe("custom mask point deletion", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("reopens a closed path once fewer than three points remain", () => {
|
test("reopens a closed path once fewer than three points remain", () => {
|
||||||
const points = buildCustomMaskParams().path;
|
const points = buildFreeformPathMaskParams().path;
|
||||||
const nextPoints = removeCustomMaskPoints({
|
const nextPoints = removeFreeformPathPoints({
|
||||||
points,
|
points,
|
||||||
pointIds: ["c"],
|
pointIds: ["c"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getCustomMaskClosedStateAfterPointRemoval({
|
getFreeformPathClosedStateAfterPointRemoval({
|
||||||
wasClosed: true,
|
wasClosed: true,
|
||||||
remainingPointCount: nextPoints.length,
|
remainingPointCount: nextPoints.length,
|
||||||
}),
|
}),
|
||||||
|
|
@ -428,9 +428,9 @@ describe("custom mask point deletion", () => {
|
||||||
|
|
||||||
describe("custom mask point insertion", () => {
|
describe("custom mask point insertion", () => {
|
||||||
test("finds the closest point on the clicked segment", () => {
|
test("finds the closest point on the clicked segment", () => {
|
||||||
const params = buildCustomMaskParams();
|
const params = buildFreeformPathMaskParams();
|
||||||
const points = params.path;
|
const points = params.path;
|
||||||
const closestPoint = findClosestPointOnCustomMaskSegment({
|
const closestPoint = findClosestPointOnFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex: 0,
|
segmentIndex: 0,
|
||||||
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
|
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
|
||||||
|
|
@ -449,8 +449,8 @@ describe("custom mask point insertion", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("splits a segment into two segments at the insertion point", () => {
|
test("splits a segment into two segments at the insertion point", () => {
|
||||||
const points = buildCustomMaskParams().path;
|
const points = buildFreeformPathMaskParams().path;
|
||||||
const nextPoints = insertPointIntoCustomMaskSegment({
|
const nextPoints = insertPointIntoFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex: 0,
|
segmentIndex: 0,
|
||||||
pointId: "new",
|
pointId: "new",
|
||||||
|
|
@ -471,8 +471,8 @@ describe("custom mask point insertion", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("builds updated custom mask params for a clicked segment", () => {
|
test("builds updated custom mask params for a clicked segment", () => {
|
||||||
const result = insertPointOnCustomMaskSegment({
|
const result = insertPointOnFreeformSegment({
|
||||||
params: buildCustomMaskParams(),
|
params: buildFreeformPathMaskParams(),
|
||||||
segmentIndex: 0,
|
segmentIndex: 0,
|
||||||
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
|
canvasPoint: { x: bounds.cx, y: bounds.cy - 10 },
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
getDefaultBaseMaskParams,
|
getDefaultBaseMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
rotatePoint,
|
rotatePoint,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
function getDefaultCinematicBarsMaskParams({
|
function getDefaultCinematicBarsMaskParams({
|
||||||
elementSize,
|
elementSize,
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
getDefaultSquareMaskParams,
|
getDefaultSquareMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
rotatePoint,
|
rotatePoint,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
function buildDiamondPath({
|
function buildDiamondPath({
|
||||||
centerX,
|
centerX,
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
getBoxLikeGeometry,
|
getBoxLikeGeometry,
|
||||||
getDefaultSquareMaskParams,
|
getDefaultSquareMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
export const ellipseMaskDefinition: MaskDefinition<"ellipse"> = {
|
export const ellipseMaskDefinition: MaskDefinition<"ellipse"> = {
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
getDefaultSquareMaskParams,
|
getDefaultSquareMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
rotatePoint,
|
rotatePoint,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
function buildHeartPath({
|
function buildHeartPath({
|
||||||
centerX,
|
centerX,
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import {
|
import {
|
||||||
masksRegistry,
|
builtinMasksRegistry,
|
||||||
type MaskDefinitionForRegistration,
|
type BuiltinMaskDefinitionForRegistration,
|
||||||
type MaskIconProps,
|
type MaskIconProps,
|
||||||
} from "../registry";
|
} from "../../registry";
|
||||||
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
|
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
|
||||||
import { customMaskDefinition } from "./custom";
|
|
||||||
import { diamondMaskDefinition } from "./diamond";
|
import { diamondMaskDefinition } from "./diamond";
|
||||||
import { ellipseMaskDefinition } from "./ellipse";
|
import { ellipseMaskDefinition } from "./ellipse";
|
||||||
import { heartMaskDefinition } from "./heart";
|
import { heartMaskDefinition } from "./heart";
|
||||||
|
|
@ -27,17 +26,17 @@ function registerDefaultMask({
|
||||||
definition,
|
definition,
|
||||||
icon,
|
icon,
|
||||||
}: {
|
}: {
|
||||||
definition: MaskDefinitionForRegistration;
|
definition: BuiltinMaskDefinitionForRegistration;
|
||||||
icon: MaskIconProps;
|
icon: MaskIconProps;
|
||||||
}) {
|
}) {
|
||||||
if (masksRegistry.has(definition.type)) {
|
if (builtinMasksRegistry.has(definition.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
masksRegistry.registerMask({ definition, icon });
|
builtinMasksRegistry.registerMask({ definition, icon });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerDefaultMasks(): void {
|
export function registerBuiltinMasks(): void {
|
||||||
registerDefaultMask({
|
registerDefaultMask({
|
||||||
definition: splitMaskDefinition,
|
definition: splitMaskDefinition,
|
||||||
icon: { icon: PanelRightDashedIcon, strokeWidth: 1 },
|
icon: { icon: PanelRightDashedIcon, strokeWidth: 1 },
|
||||||
|
|
@ -70,8 +69,4 @@ export function registerDefaultMasks(): void {
|
||||||
definition: textMaskDefinition,
|
definition: textMaskDefinition,
|
||||||
icon: { icon: TextFontIcon },
|
icon: { icon: TextFontIcon },
|
||||||
});
|
});
|
||||||
registerDefaultMask({
|
|
||||||
definition: customMaskDefinition,
|
|
||||||
icon: { icon: SquareIcon },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
getDefaultSquareMaskParams,
|
getDefaultSquareMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
rotatePoint,
|
rotatePoint,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
function buildRectanglePath({
|
function buildRectanglePath({
|
||||||
centerX,
|
centerX,
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { computeFeatherUpdate } from "../param-update";
|
import { computeFeatherUpdate } from "@/masks/param-update";
|
||||||
import type {
|
import type {
|
||||||
MaskDefinition,
|
MaskDefinition,
|
||||||
MaskParamUpdateArgs,
|
MaskParamUpdateArgs,
|
||||||
SplitMaskParams,
|
SplitMaskParams,
|
||||||
} from "@/masks/types";
|
} from "@/masks/types";
|
||||||
import { halfPlaneSign, lineEdgeIntersection } from "../utils";
|
import { halfPlaneSign, lineEdgeIntersection } from "@/masks/utils";
|
||||||
import {
|
import {
|
||||||
getLineMaskHandlePositions,
|
getLineMaskHandlePositions,
|
||||||
getLineMaskOverlay,
|
getLineMaskOverlay,
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
getDefaultSquareMaskParams,
|
getDefaultSquareMaskParams,
|
||||||
getStrokeOffset,
|
getStrokeOffset,
|
||||||
rotatePoint,
|
rotatePoint,
|
||||||
} from "./box-like";
|
} from "../box-like";
|
||||||
|
|
||||||
const STAR_INNER_RADIUS_RATIO = 0.45;
|
const STAR_INNER_RADIUS_RATIO = 0.45;
|
||||||
const STAR_VERTEX_COUNT = 10;
|
const STAR_VERTEX_COUNT = 10;
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
import type { MaskableElement } from "@/timeline";
|
import type { MaskableElement } from "@/timeline";
|
||||||
import type { Mask, MaskType, TextMask } from "@/masks/types";
|
import type { Mask, MaskType, TextMask } from "@/masks/types";
|
||||||
import type { NumberParamDefinition, SelectParamDefinition } from "@/params";
|
import type { NumberParamDefinition, SelectParamDefinition } from "@/params";
|
||||||
import { masksRegistry, buildDefaultMaskInstance } from "@/masks";
|
import {
|
||||||
|
buildDefaultMaskInstance,
|
||||||
|
getMaskDefinition,
|
||||||
|
getMaskDefinitionsForMenu,
|
||||||
|
} from "@/masks";
|
||||||
import { useEditor } from "@/editor/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useElementPreview } from "@/timeline/hooks/use-element-preview";
|
import { useElementPreview } from "@/timeline/hooks/use-element-preview";
|
||||||
import { useMenuPreview } from "@/editor/use-menu-preview";
|
import { useMenuPreview } from "@/editor/use-menu-preview";
|
||||||
|
|
@ -85,12 +89,43 @@ type PreviewParamHandler = (
|
||||||
key: string,
|
key: string,
|
||||||
) => (value: number | string | boolean) => void;
|
) => (value: number | string | boolean) => void;
|
||||||
|
|
||||||
type RegisteredMaskDefinition = ReturnType<(typeof masksRegistry)["get"]>;
|
type RegisteredMaskDefinition = ReturnType<typeof getMaskDefinition>;
|
||||||
|
|
||||||
function isTextMask(mask: Mask): mask is TextMask {
|
function isTextMask(mask: Mask): mask is TextMask {
|
||||||
return mask.type === "text";
|
return mask.type === "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withPreviewedMaskParam({
|
||||||
|
mask,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
mask: Mask;
|
||||||
|
key: string;
|
||||||
|
value: number | string | boolean;
|
||||||
|
}): Mask {
|
||||||
|
switch (mask.type) {
|
||||||
|
case "split":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "cinematic-bars":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "rectangle":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "ellipse":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "heart":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "diamond":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "star":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "text":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
case "freeform":
|
||||||
|
return { ...mask, params: { ...mask.params, [key]: value } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function MasksTab({ element, trackId }: MasksTabProps) {
|
export function MasksTab({ element, trackId }: MasksTabProps) {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const { renderElement, previewUpdates, commit } =
|
const { renderElement, previewUpdates, commit } =
|
||||||
|
|
@ -99,7 +134,7 @@ export function MasksTab({ element, trackId }: MasksTabProps) {
|
||||||
elementId: element.id,
|
elementId: element.id,
|
||||||
fallback: element,
|
fallback: element,
|
||||||
});
|
});
|
||||||
const maskDefs = masksRegistry.getAll();
|
const maskDefs = getMaskDefinitionsForMenu();
|
||||||
const tracks = useEditor(
|
const tracks = useEditor(
|
||||||
(e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks,
|
(e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks,
|
||||||
);
|
);
|
||||||
|
|
@ -210,16 +245,10 @@ export function MasksTab({ element, trackId }: MasksTabProps) {
|
||||||
const updatedMasks = renderMasks.map((existingMask, maskIndex) =>
|
const updatedMasks = renderMasks.map((existingMask, maskIndex) =>
|
||||||
maskIndex !== index
|
maskIndex !== index
|
||||||
? existingMask
|
? existingMask
|
||||||
: {
|
: withPreviewedMaskParam({ mask: existingMask, key, value }),
|
||||||
...existingMask,
|
|
||||||
params: {
|
|
||||||
...existingMask.params,
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
previewUpdates({ masks: updatedMasks } as Partial<MaskableElement>);
|
previewUpdates({ masks: updatedMasks });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -301,7 +330,7 @@ function MaskItem({
|
||||||
onCommit,
|
onCommit,
|
||||||
}: MaskItemProps) {
|
}: MaskItemProps) {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const definition = masksRegistry.get(mask.type);
|
const definition = getMaskDefinition(mask.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section sectionKey={`mask-item:${mask.id}`} showTopBorder={false}>
|
<Section sectionKey={`mask-item:${mask.id}`} showTopBorder={false}>
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,26 @@ import type { ParamDefinition } from "@/params";
|
||||||
import { PEN_CURSOR } from "@/preview/components/cursors";
|
import { PEN_CURSOR } from "@/preview/components/cursors";
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
import type {
|
import type {
|
||||||
CustomMask,
|
FreeformPathMask,
|
||||||
CustomMaskParams,
|
FreeformPathMaskParams,
|
||||||
MaskDefinition,
|
MaskDefinition,
|
||||||
MaskHandlePosition,
|
MaskHandlePosition,
|
||||||
MaskOverlay,
|
MaskOverlay,
|
||||||
MaskParamUpdateArgs,
|
MaskParamUpdateArgs,
|
||||||
} from "@/masks/types";
|
} from "@/masks/types";
|
||||||
import {
|
import {
|
||||||
buildCustomMaskPath2D,
|
buildFreeformPath2D,
|
||||||
buildCustomMaskSvgPath,
|
buildFreeformSvgPath,
|
||||||
customMaskCanvasPointToLocal,
|
freeformCanvasPointToLocal,
|
||||||
findClosestPointOnCustomMaskSegment,
|
findClosestPointOnFreeformSegment,
|
||||||
getCustomMaskCanvasSegments,
|
getFreeformCanvasSegments,
|
||||||
getCustomMaskCanvasGeometry,
|
getFreeformCanvasGeometry,
|
||||||
getCustomMaskLocalBounds,
|
getFreeformLocalBounds,
|
||||||
getCustomMaskSegmentCount,
|
getFreeformSegmentCount,
|
||||||
insertPointIntoCustomMaskSegment,
|
insertPointIntoFreeformSegment,
|
||||||
recenterCustomMaskPath,
|
recenterFreeformPath,
|
||||||
type CustomMaskPathPoint,
|
type FreeformPathPoint,
|
||||||
} from "@/masks/custom-path";
|
} from "@/masks/freeform/path";
|
||||||
import { getBoxMaskHandlePositions } from "@/masks/handle-positions";
|
import { getBoxMaskHandlePositions } from "@/masks/handle-positions";
|
||||||
import { computeFeatherUpdate } from "@/masks/param-update";
|
import { computeFeatherUpdate } from "@/masks/param-update";
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,7 +40,7 @@ const PERCENTAGE_DISPLAY = {
|
||||||
step: 1,
|
step: 1,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const CUSTOM_MASK_PARAMS: ParamDefinition<keyof CustomMaskParams & string>[] = [
|
const FREEFORM_PATH_MASK_PARAMS: ParamDefinition<keyof FreeformPathMaskParams & string>[] = [
|
||||||
{
|
{
|
||||||
key: "centerX",
|
key: "centerX",
|
||||||
label: "X",
|
label: "X",
|
||||||
|
|
@ -79,12 +79,12 @@ const CUSTOM_MASK_PARAMS: ParamDefinition<keyof CustomMaskParams & string>[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function getCustomMaskDisplayHandles({
|
function getFreeformDisplayHandles({
|
||||||
params,
|
params,
|
||||||
displayScale,
|
displayScale,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
params: CustomMaskParams;
|
params: FreeformPathMaskParams;
|
||||||
displayScale: number;
|
displayScale: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): {
|
}): {
|
||||||
|
|
@ -92,7 +92,7 @@ function getCustomMaskDisplayHandles({
|
||||||
overlays: MaskOverlay[];
|
overlays: MaskOverlay[];
|
||||||
} {
|
} {
|
||||||
const points = params.path;
|
const points = params.path;
|
||||||
const geometry = getCustomMaskCanvasGeometry({
|
const geometry = getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -108,7 +108,7 @@ function getCustomMaskDisplayHandles({
|
||||||
overlays.push({
|
overlays.push({
|
||||||
id: "path",
|
id: "path",
|
||||||
type: "canvas-path",
|
type: "canvas-path",
|
||||||
pathData: buildCustomMaskSvgPath({
|
pathData: buildFreeformSvgPath({
|
||||||
points,
|
points,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -124,7 +124,7 @@ function getCustomMaskDisplayHandles({
|
||||||
if (params.closed) {
|
if (params.closed) {
|
||||||
const segmentStrokeWidth = 12;
|
const segmentStrokeWidth = 12;
|
||||||
overlays.push(
|
overlays.push(
|
||||||
...getCustomMaskCanvasSegments({
|
...getFreeformCanvasSegments({
|
||||||
points,
|
points,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -145,7 +145,7 @@ function getCustomMaskDisplayHandles({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const localBounds = getCustomMaskLocalBounds({ points, bounds });
|
const localBounds = getFreeformLocalBounds({ points, bounds });
|
||||||
if (params.closed && localBounds) {
|
if (params.closed && localBounds) {
|
||||||
handles.push(
|
handles.push(
|
||||||
...getBoxMaskHandlePositions({
|
...getBoxMaskHandlePositions({
|
||||||
|
|
@ -179,19 +179,19 @@ function getCustomMaskDisplayHandles({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCustomMaskPoint({
|
function updateFreeformPathMaskPoint({
|
||||||
points,
|
points,
|
||||||
pointId,
|
pointId,
|
||||||
updater,
|
updater,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
pointId: string;
|
pointId: string;
|
||||||
updater: (point: CustomMaskPathPoint) => CustomMaskPathPoint;
|
updater: (point: FreeformPathPoint) => FreeformPathPoint;
|
||||||
}) {
|
}) {
|
||||||
return points.map((point) => (point.id === pointId ? updater(point) : point));
|
return points.map((point) => (point.id === pointId ? updater(point) : point));
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeCustomMaskParamUpdate({
|
function computeFreeformParamUpdate({
|
||||||
handleId,
|
handleId,
|
||||||
startParams,
|
startParams,
|
||||||
deltaX,
|
deltaX,
|
||||||
|
|
@ -199,7 +199,7 @@ function computeCustomMaskParamUpdate({
|
||||||
startCanvasX,
|
startCanvasX,
|
||||||
startCanvasY,
|
startCanvasY,
|
||||||
bounds,
|
bounds,
|
||||||
}: MaskParamUpdateArgs<CustomMaskParams>): Partial<CustomMaskParams> {
|
}: MaskParamUpdateArgs<FreeformPathMaskParams>): Partial<FreeformPathMaskParams> {
|
||||||
if (handleId.kind === "position") {
|
if (handleId.kind === "position") {
|
||||||
return {
|
return {
|
||||||
centerX: startParams.centerX + deltaX / bounds.width,
|
centerX: startParams.centerX + deltaX / bounds.width,
|
||||||
|
|
@ -266,7 +266,7 @@ function computeCustomMaskParamUpdate({
|
||||||
x: startCanvasX + deltaX,
|
x: startCanvasX + deltaX,
|
||||||
y: startCanvasY + deltaY,
|
y: startCanvasY + deltaY,
|
||||||
};
|
};
|
||||||
const localPoint = customMaskCanvasPointToLocal({
|
const localPoint = freeformCanvasPointToLocal({
|
||||||
point: currentPoint,
|
point: currentPoint,
|
||||||
centerX: startParams.centerX,
|
centerX: startParams.centerX,
|
||||||
centerY: startParams.centerY,
|
centerY: startParams.centerY,
|
||||||
|
|
@ -276,7 +276,7 @@ function computeCustomMaskParamUpdate({
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: updateCustomMaskPoint({
|
path: updateFreeformPathMaskPoint({
|
||||||
points,
|
points,
|
||||||
pointId: handleId.pointId,
|
pointId: handleId.pointId,
|
||||||
updater: (point) => {
|
updater: (point) => {
|
||||||
|
|
@ -306,15 +306,15 @@ function computeCustomMaskParamUpdate({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customMaskDefinition: MaskDefinition<"custom"> = {
|
export const freeformMaskDefinition: MaskDefinition<"freeform"> = {
|
||||||
type: "custom",
|
type: "freeform",
|
||||||
name: "Custom",
|
name: "Pen tool",
|
||||||
features: {
|
features: {
|
||||||
hasPosition: true,
|
hasPosition: true,
|
||||||
hasRotation: true,
|
hasRotation: true,
|
||||||
sizeMode: "uniform",
|
sizeMode: "uniform",
|
||||||
},
|
},
|
||||||
params: CUSTOM_MASK_PARAMS,
|
params: FREEFORM_PATH_MASK_PARAMS,
|
||||||
interaction: {
|
interaction: {
|
||||||
getInteraction({
|
getInteraction({
|
||||||
params,
|
params,
|
||||||
|
|
@ -323,7 +323,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
scaleX: _scaleX,
|
scaleX: _scaleX,
|
||||||
scaleY: _scaleY,
|
scaleY: _scaleY,
|
||||||
}) {
|
}) {
|
||||||
return getCustomMaskDisplayHandles({ params, bounds, displayScale });
|
return getFreeformDisplayHandles({ params, bounds, displayScale });
|
||||||
},
|
},
|
||||||
snap({
|
snap({
|
||||||
handleId,
|
handleId,
|
||||||
|
|
@ -334,7 +334,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
snapThreshold,
|
snapThreshold,
|
||||||
}) {
|
}) {
|
||||||
const points = startParams.path;
|
const points = startParams.path;
|
||||||
const localBounds = getCustomMaskLocalBounds({ points, bounds });
|
const localBounds = getFreeformLocalBounds({ points, bounds });
|
||||||
if (!startParams.closed || !localBounds) {
|
if (!startParams.closed || !localBounds) {
|
||||||
return {
|
return {
|
||||||
params: proposedParams,
|
params: proposedParams,
|
||||||
|
|
@ -422,9 +422,9 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
buildDefault(): Omit<CustomMask, "id"> {
|
buildDefault(): Omit<FreeformPathMask, "id"> {
|
||||||
return {
|
return {
|
||||||
type: "custom",
|
type: "freeform",
|
||||||
params: {
|
params: {
|
||||||
feather: 0,
|
feather: 0,
|
||||||
inverted: false,
|
inverted: false,
|
||||||
|
|
@ -440,7 +440,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computeParamUpdate: computeCustomMaskParamUpdate,
|
computeParamUpdate: computeFreeformParamUpdate,
|
||||||
isActive(params) {
|
isActive(params) {
|
||||||
return params.closed;
|
return params.closed;
|
||||||
},
|
},
|
||||||
|
|
@ -454,7 +454,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
return new Path2D();
|
return new Path2D();
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildCustomMaskPath2D({
|
return buildFreeformPath2D({
|
||||||
points,
|
points,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -480,7 +480,7 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = params.path;
|
const points = params.path;
|
||||||
const path = buildCustomMaskPath2D({
|
const path = buildFreeformPath2D({
|
||||||
points,
|
points,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -520,15 +520,15 @@ export const customMaskDefinition: MaskDefinition<"custom"> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function appendPointToCustomMask({
|
export function appendPointToFreeformPathMask({
|
||||||
params,
|
params,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
params: CustomMaskParams;
|
params: FreeformPathMaskParams;
|
||||||
canvasPoint: { x: number; y: number };
|
canvasPoint: { x: number; y: number };
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): CustomMaskParams {
|
}): FreeformPathMaskParams {
|
||||||
const points = params.path;
|
const points = params.path;
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
|
|
@ -554,7 +554,7 @@ export function appendPointToCustomMask({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const localPoint = customMaskCanvasPointToLocal({
|
const localPoint = freeformCanvasPointToLocal({
|
||||||
point: canvasPoint,
|
point: canvasPoint,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -574,7 +574,7 @@ export function appendPointToCustomMask({
|
||||||
outY: 0,
|
outY: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const recentered = recenterCustomMaskPath({
|
const recentered = recenterFreeformPath({
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -591,25 +591,25 @@ export function appendPointToCustomMask({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertPointOnCustomMaskSegment({
|
export function insertPointOnFreeformSegment({
|
||||||
params,
|
params,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
bounds,
|
bounds,
|
||||||
pointId = generateUUID(),
|
pointId = generateUUID(),
|
||||||
}: {
|
}: {
|
||||||
params: CustomMaskParams;
|
params: FreeformPathMaskParams;
|
||||||
segmentIndex: number;
|
segmentIndex: number;
|
||||||
canvasPoint: { x: number; y: number };
|
canvasPoint: { x: number; y: number };
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
pointId?: string;
|
pointId?: string;
|
||||||
}): { params: CustomMaskParams; pointId: string } | null {
|
}): { params: FreeformPathMaskParams; pointId: string } | null {
|
||||||
const points = params.path;
|
const points = params.path;
|
||||||
if (getCustomMaskSegmentCount({ points, closed: params.closed }) === 0) {
|
if (getFreeformSegmentCount({ points, closed: params.closed }) === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closestPoint = findClosestPointOnCustomMaskSegment({
|
const closestPoint = findClosestPointOnFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
|
|
@ -624,7 +624,7 @@ export function insertPointOnCustomMaskSegment({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = insertPointIntoCustomMaskSegment({
|
const nextPoints = insertPointIntoFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
pointId,
|
pointId,
|
||||||
|
|
@ -635,7 +635,7 @@ export function insertPointOnCustomMaskSegment({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recentered = recenterCustomMaskPath({
|
const recentered = recenterFreeformPath({
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
centerX: params.centerX,
|
centerX: params.centerX,
|
||||||
centerY: params.centerY,
|
centerY: params.centerY,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
|
|
||||||
export interface CustomMaskPathPoint {
|
export interface FreeformPathPoint {
|
||||||
id: string;
|
id: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -10,7 +10,7 @@ export interface CustomMaskPathPoint {
|
||||||
outY: number;
|
outY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
function isFreeformPathPoint(value: unknown): value is FreeformPathPoint {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -33,38 +33,38 @@ function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCustomMaskPath({
|
export function parseFreeformPath({
|
||||||
path,
|
path,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
}): CustomMaskPathPoint[] {
|
}): FreeformPathPoint[] {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(path);
|
const parsed = JSON.parse(path);
|
||||||
return Array.isArray(parsed) ? parsed.filter(isCustomMaskPathPoint) : [];
|
return Array.isArray(parsed) ? parsed.filter(isFreeformPathPoint) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeCustomMaskPath({
|
export function serializeFreeformPath({
|
||||||
points,
|
points,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
}): string {
|
}): string {
|
||||||
return JSON.stringify(points);
|
return JSON.stringify(points);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeCustomMaskPoints({
|
export function removeFreeformPathPoints({
|
||||||
points,
|
points,
|
||||||
pointIds,
|
pointIds,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
pointIds: string[];
|
pointIds: string[];
|
||||||
}): CustomMaskPathPoint[] {
|
}): FreeformPathPoint[] {
|
||||||
if (pointIds.length === 0) {
|
if (pointIds.length === 0) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ export function removeCustomMaskPoints({
|
||||||
return points.filter((point) => !pointIdsToRemove.has(point.id));
|
return points.filter((point) => !pointIdsToRemove.has(point.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskClosedStateAfterPointRemoval({
|
export function getFreeformPathClosedStateAfterPointRemoval({
|
||||||
wasClosed,
|
wasClosed,
|
||||||
remainingPointCount,
|
remainingPointCount,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -100,7 +100,7 @@ function rotatePoint({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskCenterCanvasPoint({
|
export function getFreeformCenterCanvasPoint({
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -115,7 +115,7 @@ export function getCustomMaskCenterCanvasPoint({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function customMaskLocalPointToCanvas({
|
export function freeformLocalPointToCanvas({
|
||||||
point,
|
point,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -130,7 +130,7 @@ export function customMaskLocalPointToCanvas({
|
||||||
scale: number;
|
scale: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): { x: number; y: number } {
|
}): { x: number; y: number } {
|
||||||
const center = getCustomMaskCenterCanvasPoint({ centerX, centerY, bounds });
|
const center = getFreeformCenterCanvasPoint({ centerX, centerY, bounds });
|
||||||
const scaledLocal = {
|
const scaledLocal = {
|
||||||
x: point.x * bounds.width * scale,
|
x: point.x * bounds.width * scale,
|
||||||
y: point.y * bounds.height * scale,
|
y: point.y * bounds.height * scale,
|
||||||
|
|
@ -147,7 +147,7 @@ export function customMaskLocalPointToCanvas({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function customMaskCanvasPointToLocal({
|
export function freeformCanvasPointToLocal({
|
||||||
point,
|
point,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -162,7 +162,7 @@ export function customMaskCanvasPointToLocal({
|
||||||
scale: number;
|
scale: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}): { x: number; y: number } {
|
}): { x: number; y: number } {
|
||||||
const center = getCustomMaskCenterCanvasPoint({ centerX, centerY, bounds });
|
const center = getFreeformCenterCanvasPoint({ centerX, centerY, bounds });
|
||||||
const translated = {
|
const translated = {
|
||||||
x: point.x - center.x,
|
x: point.x - center.x,
|
||||||
y: point.y - center.y,
|
y: point.y - center.y,
|
||||||
|
|
@ -179,7 +179,7 @@ export function customMaskCanvasPointToLocal({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskCanvasGeometry({
|
export function getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -187,7 +187,7 @@ export function getCustomMaskCanvasGeometry({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
|
|
@ -196,7 +196,7 @@ export function getCustomMaskCanvasGeometry({
|
||||||
}) {
|
}) {
|
||||||
const anchors = points.map((point) => ({
|
const anchors = points.map((point) => ({
|
||||||
id: point.id,
|
id: point.id,
|
||||||
anchor: customMaskLocalPointToCanvas({
|
anchor: freeformLocalPointToCanvas({
|
||||||
point: { x: point.x, y: point.y },
|
point: { x: point.x, y: point.y },
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -204,7 +204,7 @@ export function getCustomMaskCanvasGeometry({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
}),
|
}),
|
||||||
inHandle: customMaskLocalPointToCanvas({
|
inHandle: freeformLocalPointToCanvas({
|
||||||
point: { x: point.x + point.inX, y: point.y + point.inY },
|
point: { x: point.x + point.inX, y: point.y + point.inY },
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -212,7 +212,7 @@ export function getCustomMaskCanvasGeometry({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
}),
|
}),
|
||||||
outHandle: customMaskLocalPointToCanvas({
|
outHandle: freeformLocalPointToCanvas({
|
||||||
point: { x: point.x + point.outX, y: point.y + point.outY },
|
point: { x: point.x + point.outX, y: point.y + point.outY },
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -288,7 +288,7 @@ function getCanvasPointBounds({ points }: { points: CanvasPoint[] }): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMaskCanvasSegment {
|
export interface FreeformCanvasSegment {
|
||||||
index: number;
|
index: number;
|
||||||
startPointId: string;
|
startPointId: string;
|
||||||
endPointId: string;
|
endPointId: string;
|
||||||
|
|
@ -358,16 +358,16 @@ function evaluateCubicBezier({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomMaskSegmentIndices({
|
function getFreeformSegmentIndices({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
segmentIndex: number;
|
segmentIndex: number;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
}): { startIndex: number; endIndex: number } | null {
|
}): { startIndex: number; endIndex: number } | null {
|
||||||
const segmentCount = getCustomMaskSegmentCount({ points, closed });
|
const segmentCount = getFreeformSegmentCount({ points, closed });
|
||||||
if (segmentIndex < 0 || segmentIndex >= segmentCount) {
|
if (segmentIndex < 0 || segmentIndex >= segmentCount) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -378,11 +378,11 @@ function getCustomMaskSegmentIndices({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskSegmentCount({
|
export function getFreeformSegmentCount({
|
||||||
points,
|
points,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
}): number {
|
}): number {
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
|
|
@ -391,7 +391,7 @@ export function getCustomMaskSegmentCount({
|
||||||
return closed ? points.length : points.length - 1;
|
return closed ? points.length : points.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskCanvasSegments({
|
export function getFreeformCanvasSegments({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -400,15 +400,15 @@ export function getCustomMaskCanvasSegments({
|
||||||
bounds,
|
bounds,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
}): CustomMaskCanvasSegment[] {
|
}): FreeformCanvasSegment[] {
|
||||||
const geometry = getCustomMaskCanvasGeometry({
|
const geometry = getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -416,7 +416,7 @@ export function getCustomMaskCanvasSegments({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
});
|
});
|
||||||
const segmentCount = getCustomMaskSegmentCount({ points, closed });
|
const segmentCount = getFreeformSegmentCount({ points, closed });
|
||||||
|
|
||||||
return Array.from({ length: segmentCount }, (_, segmentIndex) => {
|
return Array.from({ length: segmentCount }, (_, segmentIndex) => {
|
||||||
const start = geometry.anchors[segmentIndex];
|
const start = geometry.anchors[segmentIndex];
|
||||||
|
|
@ -434,7 +434,7 @@ export function getCustomMaskCanvasSegments({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findClosestPointOnCustomMaskSegment({
|
export function findClosestPointOnFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
canvasPoint,
|
canvasPoint,
|
||||||
|
|
@ -445,7 +445,7 @@ export function findClosestPointOnCustomMaskSegment({
|
||||||
bounds,
|
bounds,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
segmentIndex: number;
|
segmentIndex: number;
|
||||||
canvasPoint: CanvasPoint;
|
canvasPoint: CanvasPoint;
|
||||||
centerX: number;
|
centerX: number;
|
||||||
|
|
@ -455,7 +455,7 @@ export function findClosestPointOnCustomMaskSegment({
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
}): { t: number; point: CanvasPoint } | null {
|
}): { t: number; point: CanvasPoint } | null {
|
||||||
const segment = getCustomMaskCanvasSegments({
|
const segment = getFreeformCanvasSegments({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -531,20 +531,20 @@ export function findClosestPointOnCustomMaskSegment({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertPointIntoCustomMaskSegment({
|
export function insertPointIntoFreeformSegment({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
pointId,
|
pointId,
|
||||||
t,
|
t,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
segmentIndex: number;
|
segmentIndex: number;
|
||||||
pointId: string;
|
pointId: string;
|
||||||
t: number;
|
t: number;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
}): CustomMaskPathPoint[] {
|
}): FreeformPathPoint[] {
|
||||||
const indices = getCustomMaskSegmentIndices({
|
const indices = getFreeformSegmentIndices({
|
||||||
points,
|
points,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
closed,
|
closed,
|
||||||
|
|
@ -596,11 +596,11 @@ export function insertPointIntoCustomMaskSegment({
|
||||||
return nextPoints;
|
return nextPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomMaskLocalBounds({
|
export function getFreeformLocalBounds({
|
||||||
points,
|
points,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
bounds: ElementBounds;
|
bounds: ElementBounds;
|
||||||
}) {
|
}) {
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
|
|
@ -632,7 +632,7 @@ export function getCustomMaskLocalBounds({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recenterCustomMaskPath({
|
export function recenterFreeformPath({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -640,7 +640,7 @@ export function recenterCustomMaskPath({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
|
|
@ -651,7 +651,7 @@ export function recenterCustomMaskPath({
|
||||||
return { centerX, centerY, points };
|
return { centerX, centerY, points };
|
||||||
}
|
}
|
||||||
|
|
||||||
const geometry = getCustomMaskCanvasGeometry({
|
const geometry = getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -676,7 +676,7 @@ export function recenterCustomMaskPath({
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextPoints = geometry.anchors.map((point) => {
|
const nextPoints = geometry.anchors.map((point) => {
|
||||||
const anchor = customMaskCanvasPointToLocal({
|
const anchor = freeformCanvasPointToLocal({
|
||||||
point: point.anchor,
|
point: point.anchor,
|
||||||
centerX: nextCenterLocal.x,
|
centerX: nextCenterLocal.x,
|
||||||
centerY: nextCenterLocal.y,
|
centerY: nextCenterLocal.y,
|
||||||
|
|
@ -684,7 +684,7 @@ export function recenterCustomMaskPath({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
});
|
});
|
||||||
const inHandle = customMaskCanvasPointToLocal({
|
const inHandle = freeformCanvasPointToLocal({
|
||||||
point: point.inHandle,
|
point: point.inHandle,
|
||||||
centerX: nextCenterLocal.x,
|
centerX: nextCenterLocal.x,
|
||||||
centerY: nextCenterLocal.y,
|
centerY: nextCenterLocal.y,
|
||||||
|
|
@ -692,7 +692,7 @@ export function recenterCustomMaskPath({
|
||||||
scale,
|
scale,
|
||||||
bounds,
|
bounds,
|
||||||
});
|
});
|
||||||
const outHandle = customMaskCanvasPointToLocal({
|
const outHandle = freeformCanvasPointToLocal({
|
||||||
point: point.outHandle,
|
point: point.outHandle,
|
||||||
centerX: nextCenterLocal.x,
|
centerX: nextCenterLocal.x,
|
||||||
centerY: nextCenterLocal.y,
|
centerY: nextCenterLocal.y,
|
||||||
|
|
@ -719,7 +719,7 @@ export function recenterCustomMaskPath({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCustomMaskPath2D({
|
export function buildFreeformPath2D({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -728,7 +728,7 @@ export function buildCustomMaskPath2D({
|
||||||
bounds,
|
bounds,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
|
|
@ -741,7 +741,7 @@ export function buildCustomMaskPath2D({
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const geometry = getCustomMaskCanvasGeometry({
|
const geometry = getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -782,7 +782,7 @@ export function buildCustomMaskPath2D({
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCustomMaskSvgPath({
|
export function buildFreeformSvgPath({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -791,7 +791,7 @@ export function buildCustomMaskSvgPath({
|
||||||
bounds,
|
bounds,
|
||||||
closed,
|
closed,
|
||||||
}: {
|
}: {
|
||||||
points: CustomMaskPathPoint[];
|
points: FreeformPathPoint[];
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
|
|
@ -803,7 +803,7 @@ export function buildCustomMaskSvgPath({
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const geometry = getCustomMaskCanvasGeometry({
|
const geometry = getFreeformCanvasGeometry({
|
||||||
points,
|
points,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
|
import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
|
||||||
import { masksRegistry } from "./registry";
|
import {
|
||||||
|
BASE_MASK_PARAM_DEFINITIONS,
|
||||||
|
builtinMasksRegistry,
|
||||||
|
type RegisteredBuiltinMaskDefinition,
|
||||||
|
} from "./registry";
|
||||||
|
import { freeformMaskDefinition } from "./freeform/definition";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
|
import { SquareIcon } from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
export { masksRegistry } from "./registry";
|
export { builtinMasksRegistry } from "./registry";
|
||||||
export { registerDefaultMasks } from "./definitions";
|
export { registerBuiltinMasks as registerDefaultMasks } from "./builtin/definitions";
|
||||||
|
|
||||||
type MaskWithoutId = Mask extends infer TMask
|
type MaskWithoutId = Mask extends infer TMask
|
||||||
? TMask extends Mask
|
? TMask extends Mask
|
||||||
|
|
@ -29,11 +35,35 @@ function withMaskId({ mask, id }: { mask: MaskWithoutId; id: string }): Mask {
|
||||||
return { ...mask, id };
|
return { ...mask, id };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...mask, id };
|
return { ...mask, id };
|
||||||
case "custom":
|
case "freeform":
|
||||||
return { ...mask, id };
|
return { ...mask, id };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMaskDefinition(maskType: MaskType): RegisteredBuiltinMaskDefinition {
|
||||||
|
if (maskType === "freeform") {
|
||||||
|
return {
|
||||||
|
...freeformMaskDefinition,
|
||||||
|
params: [...freeformMaskDefinition.params, ...BASE_MASK_PARAM_DEFINITIONS],
|
||||||
|
icon: { icon: SquareIcon },
|
||||||
|
} as RegisteredBuiltinMaskDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builtinMasksRegistry.get(maskType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMaskDefinitionsForMenu() {
|
||||||
|
return [
|
||||||
|
...builtinMasksRegistry.getAll(),
|
||||||
|
{
|
||||||
|
...freeformMaskDefinition,
|
||||||
|
name: "Pen tool",
|
||||||
|
params: [...freeformMaskDefinition.params, ...BASE_MASK_PARAM_DEFINITIONS],
|
||||||
|
icon: { icon: SquareIcon },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function buildDefaultMaskInstance({
|
export function buildDefaultMaskInstance({
|
||||||
maskType,
|
maskType,
|
||||||
elementSize,
|
elementSize,
|
||||||
|
|
@ -41,7 +71,7 @@ export function buildDefaultMaskInstance({
|
||||||
maskType: MaskType;
|
maskType: MaskType;
|
||||||
elementSize?: { width: number; height: number };
|
elementSize?: { width: number; height: number };
|
||||||
}): Mask {
|
}): Mask {
|
||||||
const definition = masksRegistry.get(maskType);
|
const definition = getMaskDefinition(maskType);
|
||||||
const context: MaskDefaultContext = { elementSize };
|
const context: MaskDefaultContext = { elementSize };
|
||||||
return withMaskId({
|
return withMaskId({
|
||||||
mask: definition.buildDefault(context),
|
mask: definition.buildDefault(context),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { MAX_FEATHER } from "@/masks/feather";
|
||||||
import type { ParamDefinition } from "@/params";
|
import type { ParamDefinition } from "@/params";
|
||||||
import type {
|
import type {
|
||||||
BaseMaskParams,
|
BaseMaskParams,
|
||||||
|
BuiltinMaskType,
|
||||||
Mask,
|
Mask,
|
||||||
MaskDefaultContext,
|
MaskDefaultContext,
|
||||||
MaskDefinition,
|
MaskDefinition,
|
||||||
|
|
@ -23,11 +24,11 @@ type RegisteredMaskWithoutId = Mask extends infer TMask
|
||||||
: never
|
: never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type MaskDefinitionForRegistration = {
|
export type BuiltinMaskDefinitionForRegistration = {
|
||||||
[TType in MaskType]: MaskDefinition<TType>;
|
[TType in BuiltinMaskType]: MaskDefinition<TType>;
|
||||||
}[MaskType];
|
}[BuiltinMaskType];
|
||||||
|
|
||||||
const BASE_MASK_PARAM_DEFINITIONS: ParamDefinition<
|
export const BASE_MASK_PARAM_DEFINITIONS: ParamDefinition<
|
||||||
keyof BaseMaskParams & string
|
keyof BaseMaskParams & string
|
||||||
>[] = [
|
>[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +58,7 @@ const BASE_MASK_PARAM_DEFINITIONS: ParamDefinition<
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface RegisteredMaskDefinition {
|
export interface RegisteredBuiltinMaskDefinition {
|
||||||
type: MaskType;
|
type: MaskType;
|
||||||
name: string;
|
name: string;
|
||||||
features: MaskDefinition["features"];
|
features: MaskDefinition["features"];
|
||||||
|
|
@ -72,9 +73,9 @@ export interface RegisteredMaskDefinition {
|
||||||
icon: MaskIconProps;
|
icon: MaskIconProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MasksRegistry extends DefinitionRegistry<
|
export class BuiltinMasksRegistry extends DefinitionRegistry<
|
||||||
MaskType,
|
BuiltinMaskType,
|
||||||
RegisteredMaskDefinition
|
RegisteredBuiltinMaskDefinition
|
||||||
> {
|
> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("mask");
|
super("mask");
|
||||||
|
|
@ -84,10 +85,10 @@ export class MasksRegistry extends DefinitionRegistry<
|
||||||
definition,
|
definition,
|
||||||
icon,
|
icon,
|
||||||
}: {
|
}: {
|
||||||
definition: MaskDefinitionForRegistration;
|
definition: BuiltinMaskDefinitionForRegistration;
|
||||||
icon: MaskIconProps;
|
icon: MaskIconProps;
|
||||||
}): void {
|
}): void {
|
||||||
const withBaseParams: RegisteredMaskDefinition = {
|
const withBaseParams: RegisteredBuiltinMaskDefinition = {
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
name: definition.name,
|
name: definition.name,
|
||||||
features: definition.features,
|
features: definition.features,
|
||||||
|
|
@ -106,4 +107,4 @@ export class MasksRegistry extends DefinitionRegistry<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const masksRegistry = new MasksRegistry();
|
export const builtinMasksRegistry = new BuiltinMasksRegistry();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import type { ElementBounds } from "@/preview/element-bounds";
|
import type { ElementBounds } from "@/preview/element-bounds";
|
||||||
import type { SnapLine } from "@/preview/preview-snap";
|
import type { SnapLine } from "@/preview/preview-snap";
|
||||||
import type { ParamDefinition } from "@/params";
|
import type { ParamDefinition } from "@/params";
|
||||||
import type { CustomMaskPathPoint } from "@/masks/custom-path";
|
import type { FreeformPathPoint } from "@/masks/freeform/path";
|
||||||
import type {
|
import type {
|
||||||
TextDecoration,
|
TextDecoration,
|
||||||
TextFontStyle,
|
TextFontStyle,
|
||||||
TextFontWeight,
|
TextFontWeight,
|
||||||
} from "@/text/primitives";
|
} from "@/text/primitives";
|
||||||
|
|
||||||
export type MaskType =
|
export type BuiltinMaskType =
|
||||||
| "split"
|
| "split"
|
||||||
| "cinematic-bars"
|
| "cinematic-bars"
|
||||||
| "rectangle"
|
| "rectangle"
|
||||||
|
|
@ -16,8 +16,9 @@ export type MaskType =
|
||||||
| "heart"
|
| "heart"
|
||||||
| "diamond"
|
| "diamond"
|
||||||
| "star"
|
| "star"
|
||||||
| "text"
|
| "text";
|
||||||
| "custom";
|
|
||||||
|
export type MaskType = BuiltinMaskType | "freeform";
|
||||||
|
|
||||||
export interface BaseMaskParams {
|
export interface BaseMaskParams {
|
||||||
feather: number;
|
feather: number;
|
||||||
|
|
@ -57,8 +58,8 @@ export interface TextMaskParams extends BaseMaskParams {
|
||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMaskParams extends BaseMaskParams {
|
export interface FreeformPathMaskParams extends BaseMaskParams {
|
||||||
path: CustomMaskPathPoint[];
|
path: FreeformPathPoint[];
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
centerX: number;
|
centerX: number;
|
||||||
centerY: number;
|
centerY: number;
|
||||||
|
|
@ -114,13 +115,7 @@ export interface TextMask {
|
||||||
params: TextMaskParams;
|
params: TextMaskParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomMask {
|
export type BuiltinShapeMask =
|
||||||
id: string;
|
|
||||||
type: "custom";
|
|
||||||
params: CustomMaskParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Mask =
|
|
||||||
| SplitMask
|
| SplitMask
|
||||||
| CinematicBarsMask
|
| CinematicBarsMask
|
||||||
| RectangleMask
|
| RectangleMask
|
||||||
|
|
@ -128,8 +123,15 @@ export type Mask =
|
||||||
| HeartMask
|
| HeartMask
|
||||||
| DiamondMask
|
| DiamondMask
|
||||||
| StarMask
|
| StarMask
|
||||||
| TextMask
|
| TextMask;
|
||||||
| CustomMask;
|
|
||||||
|
export interface FreeformPathMask {
|
||||||
|
id: string;
|
||||||
|
type: "freeform";
|
||||||
|
params: FreeformPathMaskParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mask = BuiltinShapeMask | FreeformPathMask;
|
||||||
|
|
||||||
export type MaskByType<TType extends MaskType> = Extract<Mask, { type: TType }>;
|
export type MaskByType<TType extends MaskType> = Extract<Mask, { type: TType }>;
|
||||||
export type MaskParamsByType<TType extends MaskType> =
|
export type MaskParamsByType<TType extends MaskType> =
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
import { usePreviewViewport } from "@/preview/components/preview-viewport";
|
||||||
import { useEditor } from "@/editor/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||||
import { masksRegistry } from "@/masks";
|
import { getMaskDefinition } from "@/masks";
|
||||||
import { appendPointToCustomMask } from "@/masks/definitions/custom";
|
import { appendPointToFreeformPathMask } from "@/masks/freeform/definition";
|
||||||
import {
|
import {
|
||||||
getVisibleElementsWithBounds,
|
getVisibleElementsWithBounds,
|
||||||
type ElementBounds,
|
type ElementBounds,
|
||||||
|
|
@ -158,7 +158,7 @@ export function useMaskHandles({
|
||||||
const { handles: baseHandlePositions, overlays }: MaskInteractionResult =
|
const { handles: baseHandlePositions, overlays }: MaskInteractionResult =
|
||||||
selectedWithMask
|
selectedWithMask
|
||||||
? (() => {
|
? (() => {
|
||||||
const def = masksRegistry.get(selectedWithMask.mask.type);
|
const def = getMaskDefinition(selectedWithMask.mask.type);
|
||||||
const { x: scaleX, y: scaleY } = viewport.getDisplayScale();
|
const { x: scaleX, y: scaleY } = viewport.getDisplayScale();
|
||||||
const displayScale = (scaleX + scaleY) / 2;
|
const displayScale = (scaleX + scaleY) / 2;
|
||||||
return def.interaction.getInteraction({
|
return def.interaction.getInteraction({
|
||||||
|
|
@ -195,7 +195,7 @@ export function useMaskHandles({
|
||||||
|
|
||||||
const customMaskPointIds = useMemo(
|
const customMaskPointIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedWithMask?.mask.type === "custom"
|
selectedWithMask?.mask.type === "freeform"
|
||||||
? handlePositions
|
? handlePositions
|
||||||
.filter((h) => h.kind === "point")
|
.filter((h) => h.kind === "point")
|
||||||
.map((h) => {
|
.map((h) => {
|
||||||
|
|
@ -205,8 +205,8 @@ export function useMaskHandles({
|
||||||
: [],
|
: [],
|
||||||
[handlePositions, selectedWithMask?.mask.type],
|
[handlePositions, selectedWithMask?.mask.type],
|
||||||
);
|
);
|
||||||
const isCreatingCustomMask =
|
const isCreatingFreeformPathMask =
|
||||||
selectedWithMask?.mask.type === "custom" &&
|
selectedWithMask?.mask.type === "freeform" &&
|
||||||
(!selectedWithMask.mask.params.closed || customMaskPointIds.length === 0);
|
(!selectedWithMask.mask.params.closed || customMaskPointIds.length === 0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -215,7 +215,7 @@ export function useMaskHandles({
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!selectedWithMask ||
|
!selectedWithMask ||
|
||||||
selectedWithMask.mask.type !== "custom" ||
|
selectedWithMask.mask.type !== "freeform" ||
|
||||||
!isMaskSelectionForElement({
|
!isMaskSelectionForElement({
|
||||||
trackId: selectedWithMask.trackId,
|
trackId: selectedWithMask.trackId,
|
||||||
elementId: selectedWithMask.elementId,
|
elementId: selectedWithMask.elementId,
|
||||||
|
|
@ -254,7 +254,7 @@ export function useMaskHandles({
|
||||||
selectedWithMask,
|
selectedWithMask,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateCustomMaskPointSelection = useCallback(
|
const updateFreeformPathMaskPointSelection = useCallback(
|
||||||
({
|
({
|
||||||
pointId,
|
pointId,
|
||||||
toggleSelection,
|
toggleSelection,
|
||||||
|
|
@ -262,7 +262,7 @@ export function useMaskHandles({
|
||||||
pointId: string;
|
pointId: string;
|
||||||
toggleSelection: boolean;
|
toggleSelection: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!selectedWithMask || selectedWithMask.mask.type !== "custom") {
|
if (!selectedWithMask || selectedWithMask.mask.type !== "freeform") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,21 +347,21 @@ export function useMaskHandles({
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const anchorHandle =
|
const anchorHandle =
|
||||||
selectedWithMask.mask.type === "custom" && handleId.kind === "anchor"
|
selectedWithMask.mask.type === "freeform" && handleId.kind === "anchor"
|
||||||
? handleId
|
? handleId
|
||||||
: null;
|
: null;
|
||||||
const segmentHandle =
|
const segmentHandle =
|
||||||
selectedWithMask.mask.type === "custom" && handleId.kind === "segment"
|
selectedWithMask.mask.type === "freeform" && handleId.kind === "segment"
|
||||||
? handleId
|
? handleId
|
||||||
: null;
|
: null;
|
||||||
if (isCreatingCustomMask) {
|
if (isCreatingFreeformPathMask) {
|
||||||
const firstPointId = customMaskPointIds[0];
|
const firstPointId = customMaskPointIds[0];
|
||||||
if (
|
if (
|
||||||
firstPointId &&
|
firstPointId &&
|
||||||
handleId.kind === "anchor" &&
|
handleId.kind === "anchor" &&
|
||||||
handleId.pointId === firstPointId &&
|
handleId.pointId === firstPointId &&
|
||||||
customMaskPointIds.length >= 3 &&
|
customMaskPointIds.length >= 3 &&
|
||||||
selectedWithMask.mask.type === "custom"
|
selectedWithMask.mask.type === "freeform"
|
||||||
) {
|
) {
|
||||||
const updatedMask = withUpdatedMaskParams({
|
const updatedMask = withUpdatedMaskParams({
|
||||||
mask: selectedWithMask.mask,
|
mask: selectedWithMask.mask,
|
||||||
|
|
@ -394,7 +394,7 @@ export function useMaskHandles({
|
||||||
});
|
});
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
|
|
||||||
if (segmentHandle && selectedWithMask.mask.type === "custom") {
|
if (segmentHandle && selectedWithMask.mask.type === "freeform") {
|
||||||
setActiveHandleId(handleId);
|
setActiveHandleId(handleId);
|
||||||
pendingSegmentInsertRef.current = {
|
pendingSegmentInsertRef.current = {
|
||||||
trackId: selectedWithMask.trackId,
|
trackId: selectedWithMask.trackId,
|
||||||
|
|
@ -418,14 +418,14 @@ export function useMaskHandles({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchorHandle) {
|
if (anchorHandle) {
|
||||||
updateCustomMaskPointSelection({
|
updateFreeformPathMaskPointSelection({
|
||||||
pointId: anchorHandle.pointId,
|
pointId: anchorHandle.pointId,
|
||||||
toggleSelection: event.shiftKey,
|
toggleSelection: event.shiftKey,
|
||||||
});
|
});
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (selectedWithMask.mask.type === "custom") {
|
} else if (selectedWithMask.mask.type === "freeform") {
|
||||||
editor.selection.clearMaskPointSelection();
|
editor.selection.clearMaskPointSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,22 +449,22 @@ export function useMaskHandles({
|
||||||
customMaskPointIds,
|
customMaskPointIds,
|
||||||
editor.selection,
|
editor.selection,
|
||||||
editor.timeline,
|
editor.timeline,
|
||||||
isCreatingCustomMask,
|
isCreatingFreeformPathMask,
|
||||||
selectedWithMask,
|
selectedWithMask,
|
||||||
updateCustomMaskPointSelection,
|
updateFreeformPathMaskPointSelection,
|
||||||
viewport,
|
viewport,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCanvasPointerDown = useCallback(
|
const handleCanvasPointerDown = useCallback(
|
||||||
({ event }: { event: React.PointerEvent }) => {
|
({ event }: { event: React.PointerEvent }) => {
|
||||||
if (!selectedWithMask || !isCreatingCustomMask) {
|
if (!selectedWithMask || !isCreatingFreeformPathMask) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedWithMask.mask.type !== "custom") {
|
if (selectedWithMask.mask.type !== "freeform") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,7 +477,7 @@ export function useMaskHandles({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextParams = appendPointToCustomMask({
|
const nextParams = appendPointToFreeformPathMask({
|
||||||
params: selectedWithMask.mask.params,
|
params: selectedWithMask.mask.params,
|
||||||
canvasPoint: pos,
|
canvasPoint: pos,
|
||||||
bounds: selectedWithMask.bounds,
|
bounds: selectedWithMask.bounds,
|
||||||
|
|
@ -502,7 +502,7 @@ export function useMaskHandles({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[editor.timeline, isCreatingCustomMask, selectedWithMask, viewport],
|
[editor.timeline, isCreatingFreeformPathMask, selectedWithMask, viewport],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
|
|
@ -538,7 +538,7 @@ export function useMaskHandles({
|
||||||
|
|
||||||
const deltaX = pos.x - drag.startCanvasX;
|
const deltaX = pos.x - drag.startCanvasX;
|
||||||
const deltaY = pos.y - drag.startCanvasY;
|
const deltaY = pos.y - drag.startCanvasY;
|
||||||
const def = masksRegistry.get(selectedWithMask.mask.type);
|
const def = getMaskDefinition(selectedWithMask.mask.type);
|
||||||
|
|
||||||
const rawParams = def.computeParamUpdate({
|
const rawParams = def.computeParamUpdate({
|
||||||
handleId: drag.handleId,
|
handleId: drag.handleId,
|
||||||
|
|
@ -600,7 +600,7 @@ export function useMaskHandles({
|
||||||
const handlePointerUp = useCallback(() => {
|
const handlePointerUp = useCallback(() => {
|
||||||
const pendingSegmentInsert = pendingSegmentInsertRef.current;
|
const pendingSegmentInsert = pendingSegmentInsertRef.current;
|
||||||
if (pendingSegmentInsert && !dragStateRef.current) {
|
if (pendingSegmentInsert && !dragStateRef.current) {
|
||||||
editor.timeline.insertCustomMaskPoint({
|
editor.timeline.insertFreeformPathMaskPoint({
|
||||||
trackId: pendingSegmentInsert.trackId,
|
trackId: pendingSegmentInsert.trackId,
|
||||||
elementId: pendingSegmentInsert.elementId,
|
elementId: pendingSegmentInsert.elementId,
|
||||||
maskId: pendingSegmentInsert.maskId,
|
maskId: pendingSegmentInsert.maskId,
|
||||||
|
|
@ -627,7 +627,7 @@ export function useMaskHandles({
|
||||||
selectedWithMask,
|
selectedWithMask,
|
||||||
handlePositions,
|
handlePositions,
|
||||||
overlays,
|
overlays,
|
||||||
isCreatingCustomMask,
|
isCreatingFreeformPathMask,
|
||||||
handleCanvasPointerDown,
|
handleCanvasPointerDown,
|
||||||
activeHandleId,
|
activeHandleId,
|
||||||
handlePointerDown,
|
handlePointerDown,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function MaskHandles({
|
||||||
selectedWithMask,
|
selectedWithMask,
|
||||||
handlePositions,
|
handlePositions,
|
||||||
overlays,
|
overlays,
|
||||||
isCreatingCustomMask,
|
isCreatingFreeformPathMask,
|
||||||
handleCanvasPointerDown,
|
handleCanvasPointerDown,
|
||||||
handlePointerDown,
|
handlePointerDown,
|
||||||
handlePointerMove,
|
handlePointerMove,
|
||||||
|
|
@ -94,7 +94,7 @@ export function MaskHandles({
|
||||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{isCreatingCustomMask ? (
|
{isCreatingFreeformPathMask ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 pointer-events-auto"
|
className="absolute inset-0 pointer-events-auto"
|
||||||
style={{ cursor: PEN_CURSOR }}
|
style={{ cursor: PEN_CURSOR }}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { drawCssBackground } from "@/gradients";
|
import { drawCssBackground } from "@/gradients";
|
||||||
import { masksRegistry } from "@/masks";
|
import { getMaskDefinition } from "@/masks";
|
||||||
import { incrementCounter } from "@/diagnostics/render-perf";
|
import { incrementCounter } from "@/diagnostics/render-perf";
|
||||||
import type { AnyBaseNode } from "../nodes/base-node";
|
import type { AnyBaseNode } from "../nodes/base-node";
|
||||||
import type { CanvasRenderer } from "../canvas-renderer";
|
import type { CanvasRenderer } from "../canvas-renderer";
|
||||||
|
|
@ -390,7 +390,7 @@ function buildMaskArtifacts({
|
||||||
return { mask: null, strokeLayer: null };
|
return { mask: null, strokeLayer: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = masksRegistry.get(mask.type);
|
const definition = getMaskDefinition(mask.type);
|
||||||
|
|
||||||
if (definition.isActive?.(mask.params) === false) {
|
if (definition.isActive?.(mask.params) === false) {
|
||||||
return { mask: null, strokeLayer: null };
|
return { mask: null, strokeLayer: null };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { transformProjectV30ToV31 } from "../transformers/v30-to-v31";
|
||||||
|
import { asRecord, asRecordArray } from "./helpers";
|
||||||
|
|
||||||
|
describe("V30 to V31 Migration", () => {
|
||||||
|
test("renames custom masks to freeform without changing params", () => {
|
||||||
|
const params = {
|
||||||
|
feather: 0,
|
||||||
|
inverted: false,
|
||||||
|
strokeColor: "#ffffff",
|
||||||
|
strokeWidth: 0,
|
||||||
|
strokeAlign: "center",
|
||||||
|
path: [{ id: "p1", x: 0, y: 0, inX: 0, inY: 0, outX: 0, outY: 0 }],
|
||||||
|
closed: true,
|
||||||
|
centerX: 0,
|
||||||
|
centerY: 0,
|
||||||
|
rotation: 0,
|
||||||
|
scale: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformProjectV30ToV31({
|
||||||
|
project: {
|
||||||
|
id: "project-v30-freeform",
|
||||||
|
version: 30,
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
tracks: {
|
||||||
|
main: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "image-1",
|
||||||
|
type: "image",
|
||||||
|
masks: [{ id: "mask-1", type: "custom", params }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.skipped).toBe(false);
|
||||||
|
expect(result.project.version).toBe(31);
|
||||||
|
const scene = asRecordArray(result.project.scenes)[0];
|
||||||
|
const tracks = asRecord(scene.tracks);
|
||||||
|
const main = asRecord(tracks.main);
|
||||||
|
const element = asRecordArray(main.elements)[0];
|
||||||
|
const mask = asRecordArray(asRecord(element).masks)[0];
|
||||||
|
expect(mask).toMatchObject({
|
||||||
|
id: "mask-1",
|
||||||
|
type: "freeform",
|
||||||
|
legacyType: "custom",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -29,10 +29,11 @@ import { V26toV27Migration } from "./v26-to-v27";
|
||||||
import { V27toV28Migration } from "./v27-to-v28";
|
import { V27toV28Migration } from "./v27-to-v28";
|
||||||
import { V28toV29Migration } from "./v28-to-v29";
|
import { V28toV29Migration } from "./v28-to-v29";
|
||||||
import { V29toV30Migration } from "./v29-to-v30";
|
import { V29toV30Migration } from "./v29-to-v30";
|
||||||
|
import { V30toV31Migration } from "./v30-to-v31";
|
||||||
export { runStorageMigrations } from "./runner";
|
export { runStorageMigrations } from "./runner";
|
||||||
export type { MigrationProgress } from "./runner";
|
export type { MigrationProgress } from "./runner";
|
||||||
|
|
||||||
export const CURRENT_PROJECT_VERSION = 30;
|
export const CURRENT_PROJECT_VERSION = 31;
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
new V0toV1Migration(),
|
new V0toV1Migration(),
|
||||||
|
|
@ -65,4 +66,5 @@ export const migrations = [
|
||||||
new V27toV28Migration(),
|
new V27toV28Migration(),
|
||||||
new V28toV29Migration(),
|
new V28toV29Migration(),
|
||||||
new V29toV30Migration(),
|
new V29toV30Migration(),
|
||||||
|
new V30toV31Migration(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { MigrationResult, ProjectRecord } from "./types";
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
import { getProjectId, isRecord } from "./utils";
|
import { getProjectId, isRecord } from "./utils";
|
||||||
|
|
||||||
interface CustomMaskPathPoint {
|
interface FreeformPathPoint {
|
||||||
id: string;
|
id: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -11,7 +11,7 @@ interface CustomMaskPathPoint {
|
||||||
outY: number;
|
outY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
function isFreeformPathPoint(value: unknown): value is FreeformPathPoint {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -27,18 +27,18 @@ function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCustomMaskPath({
|
function parseFreeformPath({
|
||||||
path,
|
path,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
}): CustomMaskPathPoint[] {
|
}): FreeformPathPoint[] {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(path);
|
const parsed = JSON.parse(path);
|
||||||
return Array.isArray(parsed) ? parsed.filter(isCustomMaskPathPoint) : [];
|
return Array.isArray(parsed) ? parsed.filter(isFreeformPathPoint) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +150,7 @@ function migrateMask({ mask }: { mask: unknown }): unknown {
|
||||||
...mask,
|
...mask,
|
||||||
params: {
|
params: {
|
||||||
...mask.params,
|
...mask.params,
|
||||||
path: parseCustomMaskPath({ path }),
|
path: parseFreeformPath({ path }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
import { getProjectId, isRecord } from "./utils";
|
||||||
|
|
||||||
|
export function transformProjectV30ToV31({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): MigrationResult<ProjectRecord> {
|
||||||
|
if (!getProjectId({ project })) {
|
||||||
|
return { project, skipped: true, reason: "no project id" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = project.version;
|
||||||
|
if (typeof version !== "number") {
|
||||||
|
return { project, skipped: true, reason: "invalid version" };
|
||||||
|
}
|
||||||
|
if (version >= 31) {
|
||||||
|
return { project, skipped: true, reason: "already v31" };
|
||||||
|
}
|
||||||
|
if (version !== 30) {
|
||||||
|
return { project, skipped: true, reason: "not v30" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: {
|
||||||
|
...migrateProject({ project }),
|
||||||
|
version: 31,
|
||||||
|
},
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateProject({ project }: { project: ProjectRecord }): ProjectRecord {
|
||||||
|
const nextProject = { ...project };
|
||||||
|
if (Array.isArray(project.scenes)) {
|
||||||
|
nextProject.scenes = project.scenes.map((scene) => migrateScene({ scene }));
|
||||||
|
}
|
||||||
|
return nextProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateScene({ scene }: { scene: unknown }): unknown {
|
||||||
|
if (!isRecord(scene)) {
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextScene = { ...scene };
|
||||||
|
if (isRecord(scene.tracks)) {
|
||||||
|
nextScene.tracks = migrateTracks({ tracks: scene.tracks });
|
||||||
|
}
|
||||||
|
return nextScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateTracks({ tracks }: { tracks: ProjectRecord }): ProjectRecord {
|
||||||
|
const nextTracks = { ...tracks };
|
||||||
|
if (isRecord(tracks.main)) {
|
||||||
|
nextTracks.main = migrateTrack({ track: tracks.main });
|
||||||
|
}
|
||||||
|
if (Array.isArray(tracks.overlay)) {
|
||||||
|
nextTracks.overlay = tracks.overlay.map((track) => migrateTrack({ track }));
|
||||||
|
}
|
||||||
|
if (Array.isArray(tracks.audio)) {
|
||||||
|
nextTracks.audio = tracks.audio.map((track) => migrateTrack({ track }));
|
||||||
|
}
|
||||||
|
return nextTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateTrack({ track }: { track: unknown }): unknown {
|
||||||
|
if (!isRecord(track) || !Array.isArray(track.elements)) {
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
elements: track.elements.map((element) => migrateElement({ element })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateElement({ element }: { element: unknown }): unknown {
|
||||||
|
if (!isRecord(element) || !Array.isArray(element.masks)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...element,
|
||||||
|
masks: element.masks.map((mask) => migrateMask({ mask })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateMask({ mask }: { mask: unknown }): unknown {
|
||||||
|
if (!isRecord(mask) || mask.type !== "custom") {
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mask,
|
||||||
|
type: "freeform",
|
||||||
|
legacyType: "custom",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { StorageMigration, type StorageMigrationRunArgs } from "./base";
|
||||||
|
import type { MigrationResult, ProjectRecord } from "./transformers/types";
|
||||||
|
import { transformProjectV30ToV31 } from "./transformers/v30-to-v31";
|
||||||
|
|
||||||
|
export class V30toV31Migration extends StorageMigration {
|
||||||
|
from = 30;
|
||||||
|
to = 31;
|
||||||
|
|
||||||
|
async run({
|
||||||
|
project,
|
||||||
|
}: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>> {
|
||||||
|
return transformProjectV30ToV31({ project });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue