mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: split builtin and freeform masks
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,835 @@
|
||||
import type { ElementBounds } from "@/preview/element-bounds";
|
||||
|
||||
export interface FreeformPathPoint {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
inX: number;
|
||||
inY: number;
|
||||
outX: number;
|
||||
outY: number;
|
||||
}
|
||||
|
||||
function isFreeformPathPoint(value: unknown): value is FreeformPathPoint {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
"id" in value &&
|
||||
typeof value.id === "string" &&
|
||||
"x" in value &&
|
||||
typeof value.x === "number" &&
|
||||
"y" in value &&
|
||||
typeof value.y === "number" &&
|
||||
"inX" in value &&
|
||||
typeof value.inX === "number" &&
|
||||
"inY" in value &&
|
||||
typeof value.inY === "number" &&
|
||||
"outX" in value &&
|
||||
typeof value.outX === "number" &&
|
||||
"outY" in value &&
|
||||
typeof value.outY === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function parseFreeformPath({
|
||||
path,
|
||||
}: {
|
||||
path: string;
|
||||
}): FreeformPathPoint[] {
|
||||
if (!path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(path);
|
||||
return Array.isArray(parsed) ? parsed.filter(isFreeformPathPoint) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeFreeformPath({
|
||||
points,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
}): string {
|
||||
return JSON.stringify(points);
|
||||
}
|
||||
|
||||
export function removeFreeformPathPoints({
|
||||
points,
|
||||
pointIds,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
pointIds: string[];
|
||||
}): FreeformPathPoint[] {
|
||||
if (pointIds.length === 0) {
|
||||
return points;
|
||||
}
|
||||
const pointIdsToRemove = new Set(pointIds);
|
||||
return points.filter((point) => !pointIdsToRemove.has(point.id));
|
||||
}
|
||||
|
||||
export function getFreeformPathClosedStateAfterPointRemoval({
|
||||
wasClosed,
|
||||
remainingPointCount,
|
||||
}: {
|
||||
wasClosed: boolean;
|
||||
remainingPointCount: number;
|
||||
}): boolean {
|
||||
return wasClosed && remainingPointCount >= 3;
|
||||
}
|
||||
|
||||
function rotatePoint({
|
||||
x,
|
||||
y,
|
||||
rotationDegrees,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
rotationDegrees: number;
|
||||
}): { x: number; y: number } {
|
||||
const angleRad = (rotationDegrees * Math.PI) / 180;
|
||||
const cos = Math.cos(angleRad);
|
||||
const sin = Math.sin(angleRad);
|
||||
return {
|
||||
x: x * cos - y * sin,
|
||||
y: x * sin + y * cos,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFreeformCenterCanvasPoint({
|
||||
centerX,
|
||||
centerY,
|
||||
bounds,
|
||||
}: {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
bounds: ElementBounds;
|
||||
}): { x: number; y: number } {
|
||||
return {
|
||||
x: bounds.cx + centerX * bounds.width,
|
||||
y: bounds.cy + centerY * bounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
export function freeformLocalPointToCanvas({
|
||||
point,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}: {
|
||||
point: { x: number; y: number };
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
}): { x: number; y: number } {
|
||||
const center = getFreeformCenterCanvasPoint({ centerX, centerY, bounds });
|
||||
const scaledLocal = {
|
||||
x: point.x * bounds.width * scale,
|
||||
y: point.y * bounds.height * scale,
|
||||
};
|
||||
const rotated = rotatePoint({
|
||||
x: scaledLocal.x,
|
||||
y: scaledLocal.y,
|
||||
rotationDegrees: rotation,
|
||||
});
|
||||
|
||||
return {
|
||||
x: center.x + rotated.x,
|
||||
y: center.y + rotated.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function freeformCanvasPointToLocal({
|
||||
point,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}: {
|
||||
point: { x: number; y: number };
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
}): { x: number; y: number } {
|
||||
const center = getFreeformCenterCanvasPoint({ centerX, centerY, bounds });
|
||||
const translated = {
|
||||
x: point.x - center.x,
|
||||
y: point.y - center.y,
|
||||
};
|
||||
const rotated = rotatePoint({
|
||||
x: translated.x,
|
||||
y: translated.y,
|
||||
rotationDegrees: -rotation,
|
||||
});
|
||||
|
||||
return {
|
||||
x: bounds.width === 0 ? 0 : rotated.x / (bounds.width * scale),
|
||||
y: bounds.height === 0 ? 0 : rotated.y / (bounds.height * scale),
|
||||
};
|
||||
}
|
||||
|
||||
export function getFreeformCanvasGeometry({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
}) {
|
||||
const anchors = points.map((point) => ({
|
||||
id: point.id,
|
||||
anchor: freeformLocalPointToCanvas({
|
||||
point: { x: point.x, y: point.y },
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}),
|
||||
inHandle: freeformLocalPointToCanvas({
|
||||
point: { x: point.x + point.inX, y: point.y + point.inY },
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}),
|
||||
outHandle: freeformLocalPointToCanvas({
|
||||
point: { x: point.x + point.outX, y: point.y + point.outY },
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}),
|
||||
}));
|
||||
|
||||
const geometryPoints = anchors.flatMap((point) => [
|
||||
point.anchor,
|
||||
point.inHandle,
|
||||
point.outHandle,
|
||||
]);
|
||||
if (geometryPoints.length === 0) {
|
||||
return {
|
||||
anchors,
|
||||
bounds: null,
|
||||
};
|
||||
}
|
||||
|
||||
const geometryBounds = getCanvasPointBounds({
|
||||
points: geometryPoints,
|
||||
});
|
||||
|
||||
return {
|
||||
anchors,
|
||||
bounds: geometryBounds,
|
||||
};
|
||||
}
|
||||
|
||||
export interface CanvasPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function getCanvasPointBounds({ points }: { points: CanvasPoint[] }): {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
} | null {
|
||||
if (points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [firstPoint, ...restPoints] = points;
|
||||
let minX = firstPoint.x;
|
||||
let maxX = firstPoint.x;
|
||||
let minY = firstPoint.y;
|
||||
let maxY = firstPoint.y;
|
||||
|
||||
for (const point of restPoints) {
|
||||
minX = Math.min(minX, point.x);
|
||||
maxX = Math.max(maxX, point.x);
|
||||
minY = Math.min(minY, point.y);
|
||||
maxY = Math.max(maxY, point.y);
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
maxX,
|
||||
minY,
|
||||
maxY,
|
||||
width: Math.max(1, maxX - minX),
|
||||
height: Math.max(1, maxY - minY),
|
||||
centerX: (minX + maxX) / 2,
|
||||
centerY: (minY + maxY) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export interface FreeformCanvasSegment {
|
||||
index: number;
|
||||
startPointId: string;
|
||||
endPointId: string;
|
||||
start: CanvasPoint;
|
||||
startOut: CanvasPoint;
|
||||
endIn: CanvasPoint;
|
||||
end: CanvasPoint;
|
||||
pathData: string;
|
||||
}
|
||||
|
||||
function clampUnit(value: number): number {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
function getDistanceSquared({
|
||||
a,
|
||||
b,
|
||||
}: {
|
||||
a: CanvasPoint;
|
||||
b: CanvasPoint;
|
||||
}): number {
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function lerpPoint({
|
||||
a,
|
||||
b,
|
||||
t,
|
||||
}: {
|
||||
a: CanvasPoint;
|
||||
b: CanvasPoint;
|
||||
t: number;
|
||||
}): CanvasPoint {
|
||||
return {
|
||||
x: a.x + (b.x - a.x) * t,
|
||||
y: a.y + (b.y - a.y) * t,
|
||||
};
|
||||
}
|
||||
|
||||
function evaluateCubicBezier({
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
t,
|
||||
}: {
|
||||
p0: CanvasPoint;
|
||||
p1: CanvasPoint;
|
||||
p2: CanvasPoint;
|
||||
p3: CanvasPoint;
|
||||
t: number;
|
||||
}): CanvasPoint {
|
||||
const oneMinusT = 1 - t;
|
||||
return {
|
||||
x:
|
||||
oneMinusT ** 3 * p0.x +
|
||||
3 * oneMinusT ** 2 * t * p1.x +
|
||||
3 * oneMinusT * t ** 2 * p2.x +
|
||||
t ** 3 * p3.x,
|
||||
y:
|
||||
oneMinusT ** 3 * p0.y +
|
||||
3 * oneMinusT ** 2 * t * p1.y +
|
||||
3 * oneMinusT * t ** 2 * p2.y +
|
||||
t ** 3 * p3.y,
|
||||
};
|
||||
}
|
||||
|
||||
function getFreeformSegmentIndices({
|
||||
points,
|
||||
segmentIndex,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
segmentIndex: number;
|
||||
closed: boolean;
|
||||
}): { startIndex: number; endIndex: number } | null {
|
||||
const segmentCount = getFreeformSegmentCount({ points, closed });
|
||||
if (segmentIndex < 0 || segmentIndex >= segmentCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex: segmentIndex,
|
||||
endIndex: (segmentIndex + 1) % points.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFreeformSegmentCount({
|
||||
points,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
closed: boolean;
|
||||
}): number {
|
||||
if (points.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
return closed ? points.length : points.length - 1;
|
||||
}
|
||||
|
||||
export function getFreeformCanvasSegments({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
closed: boolean;
|
||||
}): FreeformCanvasSegment[] {
|
||||
const geometry = getFreeformCanvasGeometry({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
const segmentCount = getFreeformSegmentCount({ points, closed });
|
||||
|
||||
return Array.from({ length: segmentCount }, (_, segmentIndex) => {
|
||||
const start = geometry.anchors[segmentIndex];
|
||||
const end = geometry.anchors[(segmentIndex + 1) % geometry.anchors.length];
|
||||
return {
|
||||
index: segmentIndex,
|
||||
startPointId: start.id,
|
||||
endPointId: end.id,
|
||||
start: start.anchor,
|
||||
startOut: start.outHandle,
|
||||
endIn: end.inHandle,
|
||||
end: end.anchor,
|
||||
pathData: `M ${start.anchor.x},${start.anchor.y} C ${start.outHandle.x},${start.outHandle.y} ${end.inHandle.x},${end.inHandle.y} ${end.anchor.x},${end.anchor.y}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function findClosestPointOnFreeformSegment({
|
||||
points,
|
||||
segmentIndex,
|
||||
canvasPoint,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
segmentIndex: number;
|
||||
canvasPoint: CanvasPoint;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
closed: boolean;
|
||||
}): { t: number; point: CanvasPoint } | null {
|
||||
const segment = getFreeformCanvasSegments({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
closed,
|
||||
}).find((candidate) => candidate.index === segmentIndex);
|
||||
if (!segment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sampleCount = 24;
|
||||
let bestT = 0;
|
||||
let bestDistanceSquared = getDistanceSquared({
|
||||
a: canvasPoint,
|
||||
b: segment.start,
|
||||
});
|
||||
|
||||
for (let step = 0; step <= sampleCount; step++) {
|
||||
const t = step / sampleCount;
|
||||
const point = evaluateCubicBezier({
|
||||
p0: segment.start,
|
||||
p1: segment.startOut,
|
||||
p2: segment.endIn,
|
||||
p3: segment.end,
|
||||
t,
|
||||
});
|
||||
const distanceSquared = getDistanceSquared({ a: canvasPoint, b: point });
|
||||
if (distanceSquared < bestDistanceSquared) {
|
||||
bestDistanceSquared = distanceSquared;
|
||||
bestT = t;
|
||||
}
|
||||
}
|
||||
|
||||
let searchStep = 1 / sampleCount;
|
||||
for (let iteration = 0; iteration < 8; iteration++) {
|
||||
const candidates = [bestT - searchStep, bestT, bestT + searchStep]
|
||||
.map(clampUnit)
|
||||
.map((t) => ({
|
||||
t,
|
||||
point: evaluateCubicBezier({
|
||||
p0: segment.start,
|
||||
p1: segment.startOut,
|
||||
p2: segment.endIn,
|
||||
p3: segment.end,
|
||||
t,
|
||||
}),
|
||||
}));
|
||||
for (const candidate of candidates) {
|
||||
const distanceSquared = getDistanceSquared({
|
||||
a: canvasPoint,
|
||||
b: candidate.point,
|
||||
});
|
||||
if (distanceSquared < bestDistanceSquared) {
|
||||
bestDistanceSquared = distanceSquared;
|
||||
bestT = candidate.t;
|
||||
}
|
||||
}
|
||||
searchStep /= 2;
|
||||
}
|
||||
|
||||
const clampedT = Math.min(0.999, Math.max(0.001, bestT));
|
||||
return {
|
||||
t: clampedT,
|
||||
point: evaluateCubicBezier({
|
||||
p0: segment.start,
|
||||
p1: segment.startOut,
|
||||
p2: segment.endIn,
|
||||
p3: segment.end,
|
||||
t: clampedT,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function insertPointIntoFreeformSegment({
|
||||
points,
|
||||
segmentIndex,
|
||||
pointId,
|
||||
t,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
segmentIndex: number;
|
||||
pointId: string;
|
||||
t: number;
|
||||
closed: boolean;
|
||||
}): FreeformPathPoint[] {
|
||||
const indices = getFreeformSegmentIndices({
|
||||
points,
|
||||
segmentIndex,
|
||||
closed,
|
||||
});
|
||||
if (!indices) {
|
||||
return points;
|
||||
}
|
||||
|
||||
const startPoint = points[indices.startIndex];
|
||||
const endPoint = points[indices.endIndex];
|
||||
const clampedT = Math.min(0.999, Math.max(0.001, t));
|
||||
const p0 = { x: startPoint.x, y: startPoint.y };
|
||||
const p1 = {
|
||||
x: startPoint.x + startPoint.outX,
|
||||
y: startPoint.y + startPoint.outY,
|
||||
};
|
||||
const p2 = {
|
||||
x: endPoint.x + endPoint.inX,
|
||||
y: endPoint.y + endPoint.inY,
|
||||
};
|
||||
const p3 = { x: endPoint.x, y: endPoint.y };
|
||||
const p01 = lerpPoint({ a: p0, b: p1, t: clampedT });
|
||||
const p12 = lerpPoint({ a: p1, b: p2, t: clampedT });
|
||||
const p23 = lerpPoint({ a: p2, b: p3, t: clampedT });
|
||||
const p012 = lerpPoint({ a: p01, b: p12, t: clampedT });
|
||||
const p123 = lerpPoint({ a: p12, b: p23, t: clampedT });
|
||||
const splitPoint = lerpPoint({ a: p012, b: p123, t: clampedT });
|
||||
|
||||
const nextPoints = [...points];
|
||||
nextPoints[indices.startIndex] = {
|
||||
...startPoint,
|
||||
outX: p01.x - startPoint.x,
|
||||
outY: p01.y - startPoint.y,
|
||||
};
|
||||
nextPoints[indices.endIndex] = {
|
||||
...endPoint,
|
||||
inX: p23.x - endPoint.x,
|
||||
inY: p23.y - endPoint.y,
|
||||
};
|
||||
nextPoints.splice(indices.endIndex, 0, {
|
||||
id: pointId,
|
||||
x: splitPoint.x,
|
||||
y: splitPoint.y,
|
||||
inX: p012.x - splitPoint.x,
|
||||
inY: p012.y - splitPoint.y,
|
||||
outX: p123.x - splitPoint.x,
|
||||
outY: p123.y - splitPoint.y,
|
||||
});
|
||||
return nextPoints;
|
||||
}
|
||||
|
||||
export function getFreeformLocalBounds({
|
||||
points,
|
||||
bounds,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
bounds: ElementBounds;
|
||||
}) {
|
||||
if (points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = points.flatMap((point) => [
|
||||
{ x: point.x * bounds.width, y: point.y * bounds.height },
|
||||
{
|
||||
x: (point.x + point.inX) * bounds.width,
|
||||
y: (point.y + point.inY) * bounds.height,
|
||||
},
|
||||
{
|
||||
x: (point.x + point.outX) * bounds.width,
|
||||
y: (point.y + point.outY) * bounds.height,
|
||||
},
|
||||
]);
|
||||
|
||||
const localBounds = getCanvasPointBounds({
|
||||
points: values,
|
||||
});
|
||||
if (!localBounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
width: localBounds.width,
|
||||
height: localBounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
export function recenterFreeformPath({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
}) {
|
||||
if (points.length === 0) {
|
||||
return { centerX, centerY, points };
|
||||
}
|
||||
|
||||
const geometry = getFreeformCanvasGeometry({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
if (!geometry.bounds) {
|
||||
return { centerX, centerY, points };
|
||||
}
|
||||
|
||||
const nextCenterCanvas = {
|
||||
x: geometry.bounds.centerX,
|
||||
y: geometry.bounds.centerY,
|
||||
};
|
||||
const nextCenterLocal = {
|
||||
x: bounds.width === 0 ? 0 : (nextCenterCanvas.x - bounds.cx) / bounds.width,
|
||||
y:
|
||||
bounds.height === 0
|
||||
? 0
|
||||
: (nextCenterCanvas.y - bounds.cy) / bounds.height,
|
||||
};
|
||||
|
||||
const nextPoints = geometry.anchors.map((point) => {
|
||||
const anchor = freeformCanvasPointToLocal({
|
||||
point: point.anchor,
|
||||
centerX: nextCenterLocal.x,
|
||||
centerY: nextCenterLocal.y,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
const inHandle = freeformCanvasPointToLocal({
|
||||
point: point.inHandle,
|
||||
centerX: nextCenterLocal.x,
|
||||
centerY: nextCenterLocal.y,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
const outHandle = freeformCanvasPointToLocal({
|
||||
point: point.outHandle,
|
||||
centerX: nextCenterLocal.x,
|
||||
centerY: nextCenterLocal.y,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
|
||||
return {
|
||||
id: point.id,
|
||||
x: anchor.x,
|
||||
y: anchor.y,
|
||||
inX: inHandle.x - anchor.x,
|
||||
inY: inHandle.y - anchor.y,
|
||||
outX: outHandle.x - anchor.x,
|
||||
outY: outHandle.y - anchor.y,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
centerX: nextCenterLocal.x,
|
||||
centerY: nextCenterLocal.y,
|
||||
points: nextPoints,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFreeformPath2D({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
closed: boolean;
|
||||
}): Path2D {
|
||||
const path = new Path2D();
|
||||
if (points.length === 0) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const geometry = getFreeformCanvasGeometry({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
const anchors = geometry.anchors;
|
||||
path.moveTo(anchors[0].anchor.x, anchors[0].anchor.y);
|
||||
|
||||
for (let index = 1; index < anchors.length; index++) {
|
||||
const previous = anchors[index - 1];
|
||||
const current = anchors[index];
|
||||
path.bezierCurveTo(
|
||||
previous.outHandle.x,
|
||||
previous.outHandle.y,
|
||||
current.inHandle.x,
|
||||
current.inHandle.y,
|
||||
current.anchor.x,
|
||||
current.anchor.y,
|
||||
);
|
||||
}
|
||||
|
||||
if (closed && anchors.length > 1) {
|
||||
const last = anchors[anchors.length - 1];
|
||||
const first = anchors[0];
|
||||
path.bezierCurveTo(
|
||||
last.outHandle.x,
|
||||
last.outHandle.y,
|
||||
first.inHandle.x,
|
||||
first.inHandle.y,
|
||||
first.anchor.x,
|
||||
first.anchor.y,
|
||||
);
|
||||
path.closePath();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export function buildFreeformSvgPath({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
closed,
|
||||
}: {
|
||||
points: FreeformPathPoint[];
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
bounds: ElementBounds;
|
||||
closed: boolean;
|
||||
}): string {
|
||||
if (points.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const geometry = getFreeformCanvasGeometry({
|
||||
points,
|
||||
centerX,
|
||||
centerY,
|
||||
rotation,
|
||||
scale,
|
||||
bounds,
|
||||
});
|
||||
const anchors = geometry.anchors;
|
||||
const segments = [`M ${anchors[0].anchor.x},${anchors[0].anchor.y}`];
|
||||
|
||||
for (let index = 1; index < anchors.length; index++) {
|
||||
const previous = anchors[index - 1];
|
||||
const current = anchors[index];
|
||||
segments.push(
|
||||
`C ${previous.outHandle.x},${previous.outHandle.y} ${current.inHandle.x},${current.inHandle.y} ${current.anchor.x},${current.anchor.y}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (closed && anchors.length > 1) {
|
||||
const last = anchors[anchors.length - 1];
|
||||
const first = anchors[0];
|
||||
segments.push(
|
||||
`C ${last.outHandle.x},${last.outHandle.y} ${first.inHandle.x},${first.inHandle.y} ${first.anchor.x},${first.anchor.y}`,
|
||||
);
|
||||
segments.push("Z");
|
||||
}
|
||||
|
||||
return segments.join(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user