frame.akima
Version:
A package for Akima interpolation
287 lines (286 loc) • 11.9 kB
JavaScript
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;
}
}