Parallax-Bicycle/BezierCurve.js

118 lines
4.7 KiB
JavaScript

"use strict";
/******************************************************************************
*************************** ES6 Bezier Curve Class ***************************
******************************************************************************
* v0.4
******************************************************************************
* A bezier curve class that supports an arbitrary number of control points.
* Originally written for my 2D Graphics coursework at University.
*
* Bug reports can be made as a comment on this gist, or
* sent to <bugs@starbeamrainbowlabs.com>. Alternatively, you can tweet
* me at @SBRLabs.
******************************************************************************
* Author: Starbeamrainbowlabs <bugs@starbeamrainbowlabs.com>
*
* Revisions:
* v0.1: Initial revision.
* v0.2: Include example test page.
* v0.3: Minor comment tweaks.
* v0.4: Fixed a few bugs.
*/
class BezierCurve
{
constructor(inControlPoints)
{
this.controlPoints = inControlPoints;
// The interpolated points cache
this.lastSegmentCount = 0;
this.lastPoints = null;
}
/**
* Interpolate to find the point at a specific percentage along the bezier curve.
* @param {number} time The time between 0-1 for which to calculate the point along the bezier curve.
* @param {[Vector]} controlPoints An array of control points to operate on. You shouldn't need to set this parameter - it's only here due to the recursive native of the interpolation algorithm.
* @return {Vector} The interpolated point.
*/
interpolate(time, controlPoints)
{
// If the control points are undefined, then pick up our own control points
if(typeof controlPoints == "undefined")
controlPoints = this.controlPoints;
// Make sure that the time is between 0 and 1.
if(time < 0 || time > 1)
throw new Error(`The time specified was out of bounds! It should be between 0 and 1, but a value of ${time} was provided.`);
// Create an array to store the interpolated points in
var interpolatedPoints = [];
// Loop over all the control points, except the last one
for(let i = 0; i < controlPoints.length - 1; i++)
{
// Find the difference between the current point and the next one along
// To get the vector of the line between 2 points, you do b - a for the points a and b.
let difference = controlPoints[i + 1].clone().subtract(controlPoints[i]);
// Multiply the line's vector by the time in order to extract a percentage along the line
difference.multiply(time);
// Add the first point on to put the vector back in the right place,
// and then add it to the interpolated pionts array.
// It's important to add the first control point on again here as we
// made the vector relative to 0 in order to perform the
// interpolation rather than relative to the first point on the line
// as it should be.
interpolatedPoints.push(difference.add(controlPoints[i]));
}
if(interpolatedPoints.length > 1)
{
// We have more than one interpolated point left, recurse to cut it
// down to a single point.
interpolatedPoints = this.interpolate(time, interpolatedPoints);
}
// Return the first item of the array if we still have an array. If it
// isn't an array, it must mean that one of our recursive calls has
// already broken the vector out of the array.
if(Array.isArray(interpolatedPoints))
return interpolatedPoints[0];
else
return interpolatedPoints;
}
/**
* Add the bezier curve to the current path on context. Remember that this method doesn't call context.beginPath(), nor does it fill or stroke anything. In addition, it does a lineTo the first point, so you'll need to do a moveTo manually if it is desired.
* @param {CanvasRenderingContext2D} context The context to render to.
* @param {number} segmentCount The number of segments to use when rendering the bezier curve. Note that this function caches the interpolation calculations for the last segment value you pass in, so you may want to have multiple different BezierCurve objects if you want to regularly change this value.
*/
curve(context, segmentCount)
{
if(segmentCount != this.lastSegmentCount)
{
// The interpolated points cache doesn't match the specified segment
// count - update it now
this.lastPoints = [];
for(let i = 0; i <= 1; i += 1 / segmentCount)
{
this.lastPoints.push(this.interpolate(i));
}
// Update the cached segment count to match the new contents of the
// cache.
this.lastSegmentCount = segmentCount;
}
// Add the bezier curve to the current path using the cached points
// (that may have been recalculated above).
for(let point of this.lastPoints)
{
context.lineTo(point.x, point.y);
}
}
}