// Cubic interpolation source from https://www.paulinternet.nl/?page=bicubic function cubicInterpolate(p, x) { return p[1] + 0.5 * x * (p[2] - p[0] + x * (2.0 * p[0] - 5.0 * p[1] + 4.0 * p[2] - p[3] + x * (3.0 * (p[1] - p[2]) + p[3] - p[0]))); } export const LEFT_SIDE = 1; export const RIGHT_SIDE = 2; export const BOTH_SIDES = 3; /** * @param {Object} arrowData - Object with data for arrow * @param {{x: number, y: number}[]} arrowData.points - List of points defining the arrow's path. * @param {number} arrowData.splineStep - The density factor for the arrow's points. * @param {number} arrowData.spacing - The spacing between the points along the arrow. * @param {number} arrowData.offsetDistance - The offset distance for the arrow's path (width). * @param {number} arrowData.protrusionSize - Length of the square protrusion from the offset side. * * @returns {{x: number, y: number}[]} - An array of points representing the arrow polygon. */ export function getFrontline(protrusionData) { const style = protrusionData.style ?? LEFT_SIDE; const splinePoints = computeSplinePoints(protrusionData.points, protrusionData.splineStep); let bodyPolygonLeft = []; let bodyPolygonRight = []; if (style === BOTH_SIDES) { const { leftSidePoints: leftSidePointsLeftSide, rightSidePoints: rightSidePointsLeftSide } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, LEFT_SIDE); bodyPolygonLeft = [...leftSidePointsLeftSide, ...rightSidePointsLeftSide.reverse()]; const { leftSidePoints: leftSidePointsRightSide, rightSidePoints: rightSidePointsRightSide } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, RIGHT_SIDE); bodyPolygonRight = [...leftSidePointsRightSide, ...rightSidePointsRightSide.reverse()]; } const { leftSidePoints, rightSidePoints } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, protrusionData.style); const bodyPolygon = [...leftSidePoints, ...rightSidePoints.reverse()]; let protrusionPolygons = []; if (style === LEFT_SIDE) { protrusionPolygons = computeProtrusion(leftSidePoints, protrusionData); } else if (style === RIGHT_SIDE) { protrusionPolygons = computeProtrusion(rightSidePoints, protrusionData); } else if (style === BOTH_SIDES){ let protrusionPolygonsLeft = computeProtrusion(leftSidePoints, protrusionData); let protrusionPolygonsRight = computeProtrusion(rightSidePoints, protrusionData); //protrusionPolygonsA = [...computeProtrusion(leftSidePoints, protrusionData), ...computeProtrusion(rightSidePoints, protrusionData)]; return { bodyLeft: bodyPolygonLeft, bodyRight: bodyPolygonRight, protrusionsLeft: protrusionPolygonsLeft, protrusionsRight: protrusionPolygonsRight }; } return { body: bodyPolygon, protrusions: protrusionPolygons }; } function computeSplinePoints(points, density) { let splinePoints = []; for (let i = 0; i < points.length - 1; i++) { const p0 = points[i === 0 ? i : i - 1]; const p1 = points[i]; const p2 = points[i + 1]; const p3 = points[i + 2] || p2; for (let t = 0; t <= 1; t += density) { splinePoints.push({ x: cubicInterpolate([p0.x, p1.x, p2.x, p3.x], t), y: cubicInterpolate([p0.y, p1.y, p2.y, p3.y], t) }); } } return splinePoints; } function computeSides(splinePoints, spacing, offsetDistance, style = LEFT_SIDE) { let dots = []; let leftSidePoints = []; let rightSidePoints = []; let accumulatedDistance = 0; for (let i = 1; i < splinePoints.length; i++) { const previousPoint = splinePoints[i - 1]; const currentPoint = splinePoints[i]; const segmentLength = Math.hypot(currentPoint.x - previousPoint.x, currentPoint.y - previousPoint.y); accumulatedDistance += segmentLength; if (accumulatedDistance >= spacing || i === 1 || i === splinePoints.length - 1) { const distanceX = currentPoint.y - previousPoint.y; const distanceY = previousPoint.x - currentPoint.x; const length = Math.hypot(distanceX, distanceY); let localOffsetDistance = offsetDistance; const offsetX = (distanceX / length) * localOffsetDistance; const offsetY = (distanceY / length) * localOffsetDistance; dots.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY }); dots.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY }); accumulatedDistance = 0; if (style === LEFT_SIDE) { leftSidePoints.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY}); rightSidePoints.push({ x: currentPoint.x, y: currentPoint.y}); } else if (style === RIGHT_SIDE) { leftSidePoints.push({ x: currentPoint.x, y: currentPoint.y}); rightSidePoints.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY}); } else if (style === BOTH_SIDES) { leftSidePoints.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY }); rightSidePoints.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY}); } } } return { leftSidePoints, rightSidePoints }; } function computeProtrusion(leftSidePoints, protrusionData) { let protrusions = []; const segments = []; let totalLength = 0; for (let i = 0; i < leftSidePoints.length - 1; i++) { const p0 = leftSidePoints[i]; const p1 = leftSidePoints[i + 1]; const dx = p1.x - p0.x; const dy = p1.y - p0.y; const length = Math.hypot(dx, dy); segments.push({ p0, p1, dx, dy, length }); totalLength += length; } const positions = []; for (let d = 0; d <= totalLength - (protrusionData.protrusionGap + protrusionData.protrusionStartSize); d += protrusionData.protrusionGap) { positions.push(d + protrusionData.protrusionStartSize); } if (positions[positions.length - 1] < totalLength) { positions.push(totalLength - protrusionData.protrusionStartSize); } let currentSegmentIndex = 0; let currentSegmentPos = 0; for (const distance of positions) { while (currentSegmentIndex < segments.length && currentSegmentPos + segments[currentSegmentIndex].length < distance) { currentSegmentPos += segments[currentSegmentIndex].length; currentSegmentIndex++; } if (currentSegmentIndex >= segments.length) break; const seg = segments[currentSegmentIndex]; const localDistance = distance - currentSegmentPos; const t = localDistance / seg.length; const x = seg.p0.x + seg.dx * t; const y = seg.p0.y + seg.dy * t; const nx = -seg.dy / seg.length; const ny = seg.dx / seg.length; const centerX = x - nx * protrusionData.protrusionLength; const centerY = y - ny * protrusionData.protrusionLength; const ux = seg.dx / seg.length; const uy = seg.dy / seg.length; const corner1 = { x: x - ux * protrusionData.protrusionStartSize - nx * 0, y: y - uy * protrusionData.protrusionStartSize - ny * 0 }; const corner2 = { x: x + ux * protrusionData.protrusionStartSize - nx * 0, y: y + uy * protrusionData.protrusionStartSize - ny * 0 }; const corner3 = { x: centerX + ux * protrusionData.protrusionEndSize, y: centerY + uy * protrusionData.protrusionEndSize }; const corner4 = { x: centerX - ux * protrusionData.protrusionEndSize, y: centerY - uy * protrusionData.protrusionEndSize }; protrusions.push([corner1, corner2, corner3, corner4]); } return protrusions; }