UNPKG

toosoon-utils

Version:
176 lines (175 loc) 5.39 kB
import { distance, isCoincident } from '../../geometry'; /** * Utility abstract class for manipulating curves * * @exports * @class Curve * @abstract */ export default class Curve { isCurve = true; type = 'Curve'; /** * Amount of divisions when calculating the cumulative segment lengths of a curve */ arcLengthDivisions = 200; /** * Must be set to `true` if the curve parameters have changed */ needsUpdate = false; _cacheArcLengths = []; /** * Interpolate a point on the curve * * @param {number} u Normalized position value to interpolate * @returns {Point} Interpolated coordinates on the curve */ getPointAt(u) { const t = this.getUtoTmapping(u); return this.getPoint(t); } /** * Compute the curve shape into an array of points * * @param {number} [divisions=5] Number of divisions * @returns {Point[]} */ getPoints(divisions = 5) { const points = []; for (let i = 0; i <= divisions; i++) { points.push(this.getPoint(i / divisions)); } return points; } /** * Compute the curve shape into an array of equi-spaced points across the entire curve * * @param {number} [divisions=5] Number of divisions * @returns {Point[]} */ getSpacedPoints(divisions = 5) { const points = []; for (let i = 0; i <= divisions; i++) { points.push(this.getPointAt(i / divisions)); } return points; } /** * Compute the total arc length of the curve * * @returns {number} */ getLength() { const lengths = this.getLengths(); return lengths[lengths.length - 1]; } /** * Compute the cumulative segment lengths of the curve * * @param {number} [divisions=this.arcLengthDivisions] Number of divisions * @returns {number[]} */ getLengths(divisions = this.arcLengthDivisions) { if (this._cacheArcLengths.length === divisions + 1 && !this.needsUpdate) { return this._cacheArcLengths; } this.needsUpdate = false; const lengths = [0]; let currentPoint; let lastPoint = this.getPoint(0); let sum = 0; for (let i = 1; i <= divisions; i++) { currentPoint = this.getPoint(i / divisions); sum += distance(...currentPoint, ...lastPoint); lengths.push(sum); lastPoint = currentPoint; } this._cacheArcLengths = lengths; return lengths; } /** * Update the cached cumulative segment lengths */ updateArcLengths() { this.needsUpdate = true; this.getLengths(); } /** * Re-map a normalized position value into normalized time * * @param {number} u Normalized position value to interpolate * @param {number} [targetArcLength] Distance on the curve * @returns {number} Updated interpolation value */ getUtoTmapping(u, targetArcLength) { const arcLengths = this.getLengths(); let i = 0; const length = arcLengths.length; targetArcLength = targetArcLength ?? u * arcLengths[length - 1]; let low = 0; let high = length - 1; let comparison; while (low <= high) { i = Math.floor(low + (high - low) / 2); comparison = arcLengths[i] - targetArcLength; if (comparison < 0) { low = i + 1; } else if (comparison > 0) { high = i - 1; } else { high = i; break; } } i = high; if (arcLengths[i] === targetArcLength) { return i / (length - 1); } const lengthBefore = arcLengths[i]; const lengthAfter = arcLengths[i + 1]; const segmentLength = lengthAfter - lengthBefore; const segmentFraction = (targetArcLength - lengthBefore) / segmentLength; const t = (i + segmentFraction) / (length - 1); return t; } /** * Compute an unit vector tangent for the given normalized time value * * @param {number} t Normalized time value * @returns {[number, number]} Tangent vector */ getTangent(t) { const delta = 0.0001; let t1 = Math.max(0, t - delta); let t2 = Math.min(1, t + delta); const p1 = this.getPoint(t1); const p2 = this.getPoint(t2); let x = p2[0] - p1[0]; let y = p2[1] - p1[1]; const length = Math.sqrt(x * x + y * y); x /= length; y /= length; return [x, y]; } /** * Compute an unit vector tangent for the given normalized position value * * @param {number} u Normalized position value * @returns {[number, number]} Tangent vector */ getTangentAt(u) { const t = this.getUtoTmapping(u); return this.getTangent(t); } /** * Static method to check if given points are defining a closed curve * * @param {Point[]} points Points to check * @returns {boolean} True if the curve is closed, false otherwise */ static isClosed(points) { return points.length > 1 && !isCoincident(...points[0], ...points[points.length - 1]); } }