/** * 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} lineStart - Line start point [x, y] * @param {Array} 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(); }