UNPKG

frame.akima

Version:

A package for Akima interpolation

287 lines (286 loc) 11.9 kB
import { AkimaPoint } from './AkimaPoint'; // Hilfsfunktion für Zentroid (bereits vorhanden) export function getCenter(points) { const xMax = Math.max(...points.map((p) => p.x)); const xMin = Math.min(...points.map((p) => p.x)); const yMax = Math.max(...points.map((p) => p.y)); const yMin = Math.min(...points.map((p) => p.y)); const centerX = (xMax + xMin) / 2; const centerY = (yMax + yMin) / 2; return { x: centerX, y: centerY }; } export function sortPointsCounterClockwise(points) { const center = getCenter(points); return [...points].sort((a, b) => { const angleA = (Math.atan2(a.y - center.y, a.x - center.x) + 2 * Math.PI) % (2 * Math.PI); const angleB = (Math.atan2(b.y - center.y, b.x - center.x) + 2 * Math.PI) % (2 * Math.PI); return angleA - angleB; }); } // Catmull-Rom Spline Funktion export function createCatmullRomSpline(points) { if (points.length < 3) { throw new Error('Mindestens 3 Punkte erforderlich für Catmull-Rom Spline'); } // Sortiere Punkte nach Winkel um Zentroid für geschlossene Kurve // const center = getCentroid(points); const center = getCenter(points); // const center = { x: 0, y: 0 }; const sortedPoints = [...points].sort((a, b) => { const angleA = (Math.atan2(a.y - center.y, a.x - center.x) + 2 * Math.PI) % (2 * Math.PI); const angleB = (Math.atan2(b.y - center.y, b.x - center.x) + 2 * Math.PI) % (2 * Math.PI); return angleA - angleB; }); console.log(`sortedPoints: ${JSON.stringify(sortedPoints, null, 2)}`); // Erweitere Array für geschlossene Kurve const extended = [ sortedPoints[sortedPoints.length - 1], ...sortedPoints, sortedPoints[0], sortedPoints[1], ]; console.log(`extended sortedPoints: ${JSON.stringify(extended, null, 2)}`); // Catmull-Rom Interpolation const catmullRomInterpolate = (p0, p1, p2, p3, t) => { const t2 = t * t; const t3 = t2 * t; const x = 0.5 * (2 * p1.x + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3); const y = 0.5 * (2 * p1.y + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3); return { x, y, radius: Math.sqrt(x * x + y * y) }; }; // Berechne den Startwinkel des ersten sortierten Punkts const startAngle = Math.atan2(sortedPoints[0].y - center.y, sortedPoints[0].x - center.x); console.log(`center: (${center.x}, ${center.y})`); console.log(`sortedPoints[0]: (${sortedPoints[0].x}, ${sortedPoints[0].y})`); const normalizedStartAngle = ((startAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); console.log(`startAngle: ${startAngle}, normalizedStartAngle: ${normalizedStartAngle}`); console.log(); // Rückgabe-Funktion: Winkel → interpolierter Punkt return (angle) => { // Normalisiere Winkel auf [0, 2*PI] const normalizedAngle = ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); // Adjustiere den Winkel relativ zum Startwinkel let adjustedAngle = normalizedAngle - normalizedStartAngle; if (adjustedAngle < 0) { adjustedAngle += 2 * Math.PI; } // console.log( // `normalizedAngle: ${ // Math.round(normalizedAngle * 100) / 100 // }, adjustedAngle: ${Math.round(adjustedAngle * 100) / 100}` // ); // Berechne Position im Spline (0 bis sortedPoints.length) const position = (adjustedAngle / (2 * Math.PI)) * sortedPoints.length; // Finde das entsprechende Segment const segmentIndex = Math.floor(position) % sortedPoints.length; const t = position - Math.floor(position); // Hole die 4 Kontrollpunkte für Catmull-Rom const p0 = extended[segmentIndex]; const p1 = extended[segmentIndex + 1]; const p2 = extended[segmentIndex + 2]; const p3 = extended[segmentIndex + 3]; return catmullRomInterpolate(p0, p1, p2, p3, t); }; } /** * Baut aus einer Punktwolke einen geschlossenen Catmull-Rom-Spline in Polarkoordinaten * und liefert eine Funktion sample(angle) => [x, y]. * * @param points Array von {x, y} – idealerweise rundum verteilt, ein geschlossener Umriss. * @returns Funktion, die für angle in [0, 2π) den interpolierten Punkt zurückgibt. */ export function createPolarCatmullRom(points) { if (points.length < 2) { throw new Error('Mindestens 2 Punkte benötigt.'); } const TWO_PI = 2 * Math.PI; const polar = points.map((p) => { let a = Math.atan2(p.y, p.x); if (a < 0) a += TWO_PI; return { angle: a, radius: Math.hypot(p.x, p.y), orig: new AkimaPoint(p.x, p.y), }; }); // 2) Nach Winkel aufsteigend sortieren polar.sort((a, b) => a.angle - b.angle); // 3) Rotieren, so dass der Punkt mit Winkel 0 an Index 0 steht // Wenn kein exakter 0°-Punkt existiert, nehmen wir den kleinsten Winkel. let idx0 = polar.findIndex((p) => Math.abs(p.angle) < 1e-9); if (idx0 < 0) idx0 = 0; const rotated = []; for (let i = 0; i < polar.length; i++) { const src = polar[(idx0 + i) % polar.length]; // Falls wir über das Ende hinausgehen, addiere 2π const angle = src.angle + (idx0 + i >= polar.length ? TWO_PI : 0); rotated.push({ angle, radius: src.radius, orig: src.orig }); } // Basiswinkel subtrahieren, damit erster Punkt genau bei 0 liegt // const baseAngle = rotated[0].angle; // rotated.forEach((p) => (p.angle -= baseAngle)); const n = rotated.length; // 4) Die Rückgabefunktion return function sample(inputAngle) { // a) Normiere auf [0, 2π) let a = inputAngle % TWO_PI; if (a < 0) a += TWO_PI; // b) Wenn genau 0, liefere Originalpunkt // if (Math.abs(a) < 1e-9) { // const p0 = rotated[0].orig; // return { x: p0.x, y: p0.y }; // } // c) Segment finden: p1.angle < a <= p2.angle let i2 = rotated.findIndex((p) => p.angle > a); if (i2 < 0) { // a > letzter Winkel ⇒ wrap auf erstes Segment i2 = 0; } const i1 = (i2 - 1 + n) % n; const i0 = (i1 - 1 + n) % n; const i3 = (i2 + 1) % n; const p0 = rotated[i0]; const p1 = rotated[i1]; const p2 = rotated[i2]; const p3 = rotated[i3]; // d) t im Intervall [0,1] const segmentLen = (p2.angle - p1.angle + TWO_PI) % TWO_PI; const t = ((a - p1.angle + TWO_PI) % TWO_PI) / segmentLen; // e) Catmull-Rom (uniform, tension = 0.5) nur auf Radius const t2 = t * t; const t3 = t2 * t; const r = 0.5 * (2 * p1.radius + (p2.radius - p0.radius) * t + (2 * p0.radius - 5 * p1.radius + 4 * p2.radius - p3.radius) * t2 + (-p0.radius + 3 * p1.radius - 3 * p2.radius + p3.radius) * t3); // f) zurück in kartesisch const x = r * Math.cos(a); const y = r * Math.sin(a); return { x, y, radius: r }; }; } /** * Catmull-Rom Spline class for creating equidistant points */ export class Catmull { splineFunc; center; constructor(points) { if (points.length < 3) { throw new Error('At least 3 points required for Catmull-Rom spline'); } this.center = getCenter(points); this.splineFunc = createCatmullRomSpline(points); } /** * Returns an array of 128 points with equidistant angles from the center * @returns Array of 128 points with angles evenly distributed from 0 to 2π */ getEquidistantAnglePoints() { const numPoints = 128; const points = []; // Generate 128 evenly spaced angles from 0 to 2π for (let i = 0; i < numPoints; i++) { const angle = (i * 2 * Math.PI) / numPoints; // Get the point from the spline at this angle const point = this.splineFunc(angle); points.push(point); } return points; } /** * Returns an array of 128 points with equidistant radii from the center * @returns Array of 128 points with consistent radius distribution */ getEquidistantRadiusPoints() { const numPoints = 128; const points = []; // Sample the spline at regular angular intervals const initialSamples = []; for (let i = 0; i < numPoints * 4; i++) { // Oversample for better accuracy const angle = (i * 2 * Math.PI) / (numPoints * 4); const point = this.splineFunc(angle); initialSamples.push({ angle, point }); } // Find the minimum and maximum radii const radii = initialSamples.map((s) => s.point.radius); const minRadius = Math.min(...radii); const maxRadius = Math.max(...radii); // Create 128 equidistant radius values const targetRadii = []; for (let i = 0; i < numPoints; i++) { const t = i / (numPoints - 1); const radius = minRadius + t * (maxRadius - minRadius); targetRadii.push(radius); } // For each target radius, find the best matching angle(s) for (let i = 0; i < numPoints; i++) { const targetRadius = targetRadii[i]; // Find the sample with radius closest to target let bestSample = initialSamples[0]; let minRadiusDiff = Math.abs(bestSample.point.radius - targetRadius); for (const sample of initialSamples) { const radiusDiff = Math.abs(sample.point.radius - targetRadius); if (radiusDiff < minRadiusDiff) { minRadiusDiff = radiusDiff; bestSample = sample; } } // Use binary search to refine the angle for exact radius match const refinedPoint = this.findAngleForRadius(targetRadius, bestSample.angle); points.push(refinedPoint); } return points; } /** * Binary search to find the angle that produces the closest radius to target * @param targetRadius The desired radius * @param initialAngle Starting angle for search * @returns Point with radius closest to target */ findAngleForRadius(targetRadius, initialAngle) { const tolerance = 0.001; const maxIterations = 20; let angle = initialAngle; let bestPoint = this.splineFunc(angle); let bestRadiusDiff = Math.abs(bestPoint.radius - targetRadius); // Search in both directions around the initial angle const searchRange = Math.PI / 64; // Small search range for (let iter = 0; iter < maxIterations; iter++) { const step = searchRange / Math.pow(2, iter); // Try angles in both directions const angles = [angle - step, angle + step]; for (const testAngle of angles) { const testPoint = this.splineFunc(testAngle); const radiusDiff = Math.abs(testPoint.radius - targetRadius); if (radiusDiff < bestRadiusDiff) { bestRadiusDiff = radiusDiff; bestPoint = testPoint; angle = testAngle; } } // If we're close enough, stop searching if (bestRadiusDiff < tolerance) { break; } } return bestPoint; } }