var Vec2 = require('./Vec2');
Camtull-Rom spline implementation
Inspired by code from Tween.js
var points = [
new Vec2(-2, 0),
new Vec2(-1, 0),
new Vec2( 1, 1),
new Vec2( 2, -1)
];
var spline = new Spline2D(points);
spline.getPointAt(0.25);
var Vec2 = require('./Vec2');
points
- { Array of Vec2 } = [ ]closed
- is the spline a closed loop? { Boolean } = false
function Spline2D(points, closed) {
this.points = points || [];
this.dirtyLength = true;
this.closed = closed || false;
this.samplesCount = 100;
}
Gets position based on t-value. It is fast, but resulting points will not be evenly distributed.
t
- { Number } <0, 1>
returns Vec2
Spline2D.prototype.getPoint = function (t) {
if (this.closed) {
t = (t + 1) % 1;
} else {
t = Math.max(0, Math.min(t, 1));
}
var points = this.points;
var len = this.closed ? points.length : points.length - 1;
var point = t * len;
var intPoint = Math.floor(point);
var weight = point - intPoint;
var c0, c1, c2, c3;
if (this.closed) {
c0 = (intPoint - 1 + points.length) % points.length;
c1 = intPoint % points.length;
c2 = (intPoint + 1) % points.length;
c3 = (intPoint + 2) % points.length;
} else {
c0 = intPoint == 0 ? intPoint : intPoint - 1;
c1 = intPoint;
c2 = intPoint > points.length - 2 ? intPoint : intPoint + 1;
c3 = intPoint > points.length - 3 ? intPoint : intPoint + 2;
}
var vec = new Vec2();
vec.x = this.interpolate(points[c0].x, points[c1].x, points[c2].x, points[c3].x, weight);
vec.y = this.interpolate(points[c0].y, points[c1].y, points[c2].y, points[c3].y, weight);
return vec;
};
Spline2D.prototype.addPoint = function (p) {
this.dirtyLength = true;
this.points.push(p);
};
Gets position based on d-th of total length of the curve. Precise but might be slow at the first use due to need to precalculate length.
d
- { Number } <0, 1>
Spline2D.prototype.getPointAt = function (d) {
if (this.closed) {
d = (d + 1) % 1;
} else {
d = Math.max(0, Math.min(d, 1));
}
if (this.dirtyLength) {
this.precalculateLength();
}
TODO: try binary search
var k = 0;
for (var i = 0; i < this.accumulatedLengthRatios.length; i++) {
if (this.accumulatedLengthRatios[i] > d - 1/this.samplesCount) {
k = this.accumulatedRatios[i];
break;
}
}
return this.getPoint(k);
};
naive implementation
Spline2D.prototype.getClosestPoint = function(point) {
if (this.dirtyLength) {
this.precalculateLength();
}
var closesPoint = this.precalculatedPoints.reduce(function(best, p) {
var dist = point.squareDistance(p);
if (dist < best.dist) {
return { dist: dist, point: p };
}
else return best;
}, { dist: Infinity, point: null });
return closesPoint.point;
}
Spline2D.prototype.getClosestPointRatio = function(point) {
if (this.dirtyLength) {
this.precalculateLength();
}
var closesPoint = this.precalculatedPoints.reduce(function(best, p, pIndex) {
var dist = point.squareDistance(p);
if (dist < best.dist) {
return { dist: dist, point: p, index: pIndex };
}
else return best;
}, { dist: Infinity, point: null, index: -1 });
return this.accumulatedLengthRatios[closesPoint.index];
}
Returns position of i-th point forming the curve
i
- { Number } <0, Spline2D.points.length)
Spline2D.prototype.getPointAtIndex = function (i) {
if (i < this.points.length) {
return this.points[i];
} else {
return null;
}
};
Spline2D.prototype.getNumPoints = function () {
return this.points.length;
};
Spline2D.prototype.getLength = function () {
if (this.dirtyLength) {
this.precalculateLength();
}
return this.length;
};
Goes through all the segments of the curve and calculates total length and the ratio of each segment.
Spline2D.prototype.precalculateLength = function () {
var step = 1 / this.samplesCount;
var k = 0;
var totalLength = 0;
this.accumulatedRatios = [];
this.accumulatedLengthRatios = [];
this.accumulatedLengths = [];
this.precalculatedPoints = [];
var point;
var prevPoint;
for (var i = 0; i < this.samplesCount; i++) {
prevPoint = point;
point = this.getPoint(k);
if (i > 0) {
var len = point.dup().sub(prevPoint).length();
totalLength += len;
}
this.accumulatedRatios.push(k);
this.accumulatedLengths.push(totalLength);
this.precalculatedPoints.push(point);
k += step;
}
for (var i = 0; i < this.samplesCount; i++) {
this.accumulatedLengthRatios.push(this.accumulatedLengths[i] / totalLength);
}
this.length = totalLength;
this.dirtyLength = false;
};
Spline2D.prototype.close = function () {
this.closed = true;
};
Spline2D.prototype.isClosed = function () {
return this.closed;
};
Helper function to calculate Catmul-Rom spline equation
p0
- previous value { Number }p1
- current value { Number }p2
- next value { Number }p3
- next next value { Number }t
- parametric distance between p1 and p2 { Number } <0, 1>
Spline2D.prototype.interpolate = function (p0, p1, p2, p3, t) {
var v0 = (p2 - p0) * 0.5;
var v1 = (p3 - p1) * 0.5;
var t2 = t * t;
var t3 = t * t2;
return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
};
module.exports = Spline2D;