225 lines
7.1 KiB
JavaScript
225 lines
7.1 KiB
JavaScript
/**
|
|
* Curve drawing functions
|
|
*/
|
|
|
|
import { hexToRgb } from "../utils/helpers.js";
|
|
|
|
/**
|
|
* Calculate distance from point to line segment
|
|
* @param {{x: number, y: number}} point - Point coordinates
|
|
* @param {Array<number>} lineStart - Line start point [x, y]
|
|
* @param {Array<number>} lineEnd - Line end point [x, y]
|
|
* @returns {number} Distance from point to line segment
|
|
*/
|
|
export function pointToLineSegmentDistance(point, lineStart, lineEnd) {
|
|
const A = point.x - lineStart[0];
|
|
const B = point.y - lineStart[1];
|
|
const C = lineEnd[0] - lineStart[0];
|
|
const D = lineEnd[1] - lineStart[1];
|
|
|
|
const dot = A * C + B * D;
|
|
const lenSq = C * C + D * D;
|
|
let param = -1;
|
|
if (lenSq !== 0) param = dot / lenSq;
|
|
|
|
let xx, yy;
|
|
|
|
if (param < 0) {
|
|
xx = lineStart[0];
|
|
yy = lineStart[1];
|
|
} else if (param > 1) {
|
|
xx = lineEnd[0];
|
|
yy = lineEnd[1];
|
|
} else {
|
|
xx = lineStart[0] + param * C;
|
|
yy = lineStart[1] + param * D;
|
|
}
|
|
|
|
const dx = point.x - xx;
|
|
const dy = point.y - yy;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
/**
|
|
* Draw control points for a curve
|
|
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
* @param {Object} curve - Curve object
|
|
* @param {Object} state - Editor state
|
|
* @param {boolean} isSelected - Whether curve is selected
|
|
*/
|
|
export function drawPoints(ctx, curve, state, isSelected = false) {
|
|
const pointRadius = 5;
|
|
const selectedRadius = 7;
|
|
|
|
curve.points.forEach((point, index) => {
|
|
const pointKey = `${curve.id}:${index}`;
|
|
const isPointSelected = state.selectedPoints.has(pointKey);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(point[0], point[1], isPointSelected ? selectedRadius : pointRadius, 0, Math.PI * 2);
|
|
|
|
if (isPointSelected || isSelected) {
|
|
ctx.fillStyle = "#4a9eff";
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.fill();
|
|
ctx.strokeStyle = "#000000";
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Add stroke outline only for specifically selected points (not just selected curves)
|
|
if (isPointSelected) {
|
|
ctx.beginPath();
|
|
ctx.arc(point[0], point[1], selectedRadius, 0, Math.PI * 2);
|
|
ctx.strokeStyle = "#ffffff";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Draw a single curve (polygon)
|
|
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
* @param {Object} curve - Curve object
|
|
* @param {Object} state - Editor state
|
|
* @param {boolean} isSelected - Whether curve is selected
|
|
*/
|
|
export function drawCurve(ctx, curve, state, isSelected = false) {
|
|
if (!curve || !curve.points || curve.points.length < 2) return;
|
|
|
|
ctx.save();
|
|
|
|
// Get pointTypes with validation (default to all "hard" if missing or invalid)
|
|
let pointTypes;
|
|
if (Array.isArray(curve.pointTypes) && curve.pointTypes.length === curve.points.length) {
|
|
pointTypes = curve.pointTypes;
|
|
} else {
|
|
pointTypes = curve.points.map(() => "hard");
|
|
}
|
|
|
|
// Draw polygon path with smooth curves where needed using Catmull-Rom style interpolation
|
|
ctx.beginPath();
|
|
|
|
const n = curve.points.length;
|
|
const points = curve.points;
|
|
|
|
// Helper to get point with wrap-around for closed curves
|
|
const getPoint = (index) => {
|
|
if (curve.closed) {
|
|
return points[((index % n) + n) % n];
|
|
} else {
|
|
if (index < 0) return points[0];
|
|
if (index >= n) return points[n - 1];
|
|
return points[index];
|
|
}
|
|
};
|
|
|
|
// Helper to get point type with wrap-around
|
|
const getType = (index) => {
|
|
if (curve.closed) {
|
|
return pointTypes[((index % n) + n) % n];
|
|
} else {
|
|
if (index < 0) return pointTypes[0];
|
|
if (index >= n) return pointTypes[n - 1];
|
|
return pointTypes[index];
|
|
}
|
|
};
|
|
|
|
// Start at first point
|
|
ctx.moveTo(points[0][0], points[0][1]);
|
|
|
|
// For each segment
|
|
const segmentCount = curve.closed ? n : n - 1;
|
|
for (let i = 0; i < segmentCount; i++) {
|
|
const nextI = (i + 1) % n;
|
|
|
|
const p0 = points[i];
|
|
const p1 = points[nextI];
|
|
const type0 = getType(i);
|
|
const type1 = getType(nextI);
|
|
|
|
// If both points are hard, use straight line
|
|
if (type0 === "hard" && type1 === "hard") {
|
|
ctx.lineTo(p1[0], p1[1]);
|
|
} else {
|
|
// At least one point is smooth - use cubic curve
|
|
// Calculate control points based on neighboring points (Catmull-Rom style)
|
|
|
|
const prevP = getPoint(i - 1);
|
|
const nextNextP = getPoint(i + 2);
|
|
|
|
// Tension factor (0.5 is Catmull-Rom default)
|
|
const tension = 0.5;
|
|
|
|
// Calculate control point 1 (based on p0's tangent if smooth)
|
|
let cp1x, cp1y;
|
|
if (type0 === "smooth") {
|
|
// Tangent at p0 based on prevP to p1
|
|
cp1x = p0[0] + (p1[0] - prevP[0]) * tension / 2;
|
|
cp1y = p0[1] + (p1[1] - prevP[1]) * tension / 2;
|
|
} else {
|
|
// Hard point - control point is closer to p0
|
|
cp1x = p0[0] + (p1[0] - p0[0]) * 0.25;
|
|
cp1y = p0[1] + (p1[1] - p0[1]) * 0.25;
|
|
}
|
|
|
|
// Calculate control point 2 (based on p1's tangent if smooth)
|
|
let cp2x, cp2y;
|
|
if (type1 === "smooth") {
|
|
// Tangent at p1 based on p0 to nextNextP
|
|
cp2x = p1[0] - (nextNextP[0] - p0[0]) * tension / 2;
|
|
cp2y = p1[1] - (nextNextP[1] - p0[1]) * tension / 2;
|
|
} else {
|
|
// Hard point - control point is closer to p1
|
|
cp2x = p1[0] - (p1[0] - p0[0]) * 0.25;
|
|
cp2y = p1[1] - (p1[1] - p0[1]) * 0.25;
|
|
}
|
|
|
|
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p1[0], p1[1]);
|
|
}
|
|
}
|
|
|
|
if (curve.closed) {
|
|
ctx.closePath();
|
|
}
|
|
|
|
// Fill with semi-transparent overlay
|
|
if (curve.closed) {
|
|
// Convert maskOpacity from 0-100 to 0-1 alpha
|
|
const alpha = (state.maskOpacity || 10) / 100;
|
|
|
|
if (isSelected) {
|
|
// Selected curves use blue color with custom opacity
|
|
ctx.fillStyle = `rgba(74, 158, 255, ${alpha})`;
|
|
} else {
|
|
// Unselected curves use mask color with custom opacity
|
|
const rgb = hexToRgb(state.maskColor || "#ffffff");
|
|
// Convert from 0-1 to 0-255 range
|
|
const r = Math.round(rgb[0] * 255);
|
|
const g = Math.round(rgb[1] * 255);
|
|
const b = Math.round(rgb[2] * 255);
|
|
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|
|
ctx.fill();
|
|
}
|
|
|
|
// Stroke style
|
|
if (isSelected) {
|
|
ctx.strokeStyle = "#4a9eff";
|
|
ctx.lineWidth = 3;
|
|
} else {
|
|
ctx.strokeStyle = "#ffffff";
|
|
ctx.lineWidth = 2;
|
|
}
|
|
|
|
ctx.stroke();
|
|
|
|
// Draw control points
|
|
drawPoints(ctx, curve, state, isSelected);
|
|
|
|
ctx.restore();
|
|
}
|