class LineInterpolator {
constructor(points) {
const amount = points.length;
const path = new Array(amount);
path[0] = {
point: points[0],
distance: 0,
relative: 0,
}
let length = 0;
for(let i = 1; i < amount; i++) {
const point = points[i];
length += Vector2.dist(point, path[i - 1].point);
path[i] = {
point,
distance: length,
};
}
for(let i = 1; i < amount; i++) {
const p = path[i];
p.relative = length === 0 ? 0 : p.distance / length;
}
this.amount = amount;
this.length = length;
this.singlePoint = (length <= 0.001);
// Set path for interpolator
this.path = path;
}
// Get point at relative position
// NB: Relative has to be between 0 and 1.
GetPoint(relative) {
// Return first point if line has no length
if(this.singlePoint) {
return { pos: this.path[0].point, dir: Vector2.Up, dist: 0 };
}
let base = 0;
let range = this.amount;
let idx = Math.floor(range / 2);
// Find target sub-section using divide-and-conquer algorithm
while(range > 10) {
if(relative < this.path[idx].relative) {
range = Math.floor(range / 2);
idx = base + Math.floor(range / 2);
} else {
base += Math.floor(range / 2);
range = Math.ceil(range / 2);
idx = base + Math.floor(range / 2);
}
}
const target = base + range;
for(let i = base + 1; i <= target; i++) {
const cur = this.path[i];
if (cur.relative >= relative) { // Between points
const prev = this.path[i - 1];
const dist = cur.relative - prev.relative;
const frac = (relative - prev.relative) / dist;
return {
pos: Vector2.lerp(prev.point, cur.point, frac),
dir: Vector2.sub(cur.point, prev.point).normalized,
dist: prev.distance * (1 - frac) + cur.distance * frac,
};
}
}
}
// Get section at relative position
// NB: Relative has to be between 0 and 1.
// Each point have position, direction and distance to point. { pos: __, dir: __, dist: __ }
GetSection(relativeStart, relativeEnd) {
// Return two points if line has no length
if(this.singlePoint) {
return [
{ pos: this.path[0].point, dir: Vector2.Up, dist: 0 },
{ pos: this.path[0].point, dir: Vector2.Up, dist: 0 },
];
}
let base = 0;
let range = this.amount;
let idx = Math.floor(range / 2);
// Find target sub-section using divide-and-conquer algorithm
while(range > 10) {
if(relativeStart < this.path[idx].relative) {
range = Math.floor(range / 2);
idx = base + Math.floor(range / 2);
} else {
base += Math.floor(range / 2);
range = Math.ceil(range / 2);
idx = base + Math.floor(range / 2);
}
}
const points = [];
let started = false;
for(let i = base + 1; i < this.amount; i++) {
const cur = this.path[i];
if (!started && cur.relative >= relativeStart) { // Between points
const prev = this.path[i - 1];
const dist = cur.relative - prev.relative;
const frac = (relativeStart - prev.relative) / dist;
const prevDir = this.GetDirection(i - 1);
const curDir = this.GetDirection(i);
points.push({
pos: Vector2.lerp(prev.point, cur.point, frac),
dir: Vector2.rotationLerp(prevDir, curDir, frac).normalized, // Interpolate direction
dist: prev.distance * (1 - frac) + cur.distance * frac, // Interpolate distance
}); // Push first point
started = true;
}
if(!started) continue; // Don't continue if not started
if (cur.relative >= relativeEnd) { // Between points
const prev = this.path[i - 1];
const dist = cur.relative - prev.relative;
const frac = (relativeEnd - prev.relative) / dist;
const prevDir = this.GetDirection(i - 1);
const curDir = this.GetDirection(i);
points.push({
pos: Vector2.lerp(prev.point, cur.point, frac),
dir: Vector2.rotationLerp(prevDir, curDir, frac).normalized, // Interpolate direction
dist: prev.distance * (1 - frac) + cur.distance * frac, // Interpolate distance
}); // Push last point
break;
}
points.push({
pos: cur.point,
dir: this.GetDirection(i),
dist: cur.distance,
}); // Add points between
}
return points;
}
// Get point with given distance from start in world space
GetPointFromStart(distance) {
let relative = distance / this.length;
if (relative < 0) relative = 0;
if (relative > 1) relative = 1;
return this.GetPoint(relative);
}
// Get point with given distance from the end in world space
GetPointFromEnd(distance) {
let relative = 1 - (distance / this.length);
if (relative < 0) relative = 0;
if (relative > 1) relative = 1;
return this.GetPoint(relative);
}
// Get collection of equally space points with given width in real distance
GetRangeFromStart(relative, width, resolution = 10) {
const relativeEnd = relative + (width / this.length);
const relativeDisp = (relativeEnd - relative) / resolution;
const points = [];
for(let i = 0; i <= resolution; i++) {
points.push(this.GetPoint(relative + relativeDisp * i));
}
return points;
}
// Get direction at index
GetDirection(idx) {
const end = this.amount - 1;
if (idx === 0) { // If first
return Vector2.sub(this.path[1].point, this.path[0].point).normalized;
} // If last
else if (idx === end) {
return Vector2.sub(this.path[end].point, this.path[end - 1].point).normalized;
}
else {
const cur = this.path[idx].point; // Current point
const to = Vector2.sub(cur, this.path[idx - 1].point); // Direction to current
const from = Vector2.sub(this.path[idx + 1].point, cur); // Direction from current
return Vector2.rotationLerp(to, from, 0.5).normalized; // Rotate halfway
}
}
}