/** * @author zz85 / http://www.lab4games.net/zz85/blog * Extensible curve object * * Some common of Curve methods * .getPoint(t), getTangent(t) * .getPointAt(u), getTagentAt(u) * .getPoints(), .getSpacedPoints() * .getLength() * .updateArcLengths() * * This file contains following classes: * * -- 2d classes -- * THREE.Curve * THREE.LineCurve * THREE.QuadraticBezierCurve * THREE.CubicBezierCurve * THREE.SplineCurve * THREE.ArcCurve * THREE.EllipseCurve * * -- 3d classes -- * THREE.LineCurve3 * THREE.QuadraticBezierCurve3 * THREE.CubicBezierCurve3 * THREE.SplineCurve3 * THREE.ClosedSplineCurve3 * * A series of curves can be represented as a THREE.CurvePath * **/ /************************************************************** * Abstract Curve base class **************************************************************/ THREE.Curve = function () { }; // Virtual base class method to overwrite and implement in subclasses // - t [0 .. 1] THREE.Curve.prototype.getPoint = function ( t ) { console.log( "Warning, getPoint() not implemented!" ); return null; }; // Get point at relative position in curve according to arc length // - u [0 .. 1] THREE.Curve.prototype.getPointAt = function ( u ) { var t = this.getUtoTmapping( u ); return this.getPoint( t ); }; // Get sequence of points using getPoint( t ) THREE.Curve.prototype.getPoints = function ( divisions ) { if ( !divisions ) divisions = 5; var d, pts = []; for ( d = 0; d <= divisions; d ++ ) { pts.push( this.getPoint( d / divisions ) ); } return pts; }; // Get sequence of points using getPointAt( u ) THREE.Curve.prototype.getSpacedPoints = function ( divisions ) { if ( !divisions ) divisions = 5; var d, pts = []; for ( d = 0; d <= divisions; d ++ ) { pts.push( this.getPointAt( d / divisions ) ); } return pts; }; // Get total curve arc length THREE.Curve.prototype.getLength = function () { var lengths = this.getLengths(); return lengths[ lengths.length - 1 ]; }; // Get list of cumulative segment lengths THREE.Curve.prototype.getLengths = function ( divisions ) { if ( !divisions ) divisions = (this.__arcLengthDivisions) ? (this.__arcLengthDivisions): 200; if ( this.cacheArcLengths && ( this.cacheArcLengths.length == divisions + 1 ) && !this.needsUpdate) { //console.log( "cached", this.cacheArcLengths ); return this.cacheArcLengths; } this.needsUpdate = false; var cache = []; var current, last = this.getPoint( 0 ); var p, sum = 0; cache.push( 0 ); for ( p = 1; p <= divisions; p ++ ) { current = this.getPoint ( p / divisions ); sum += current.distanceTo( last ); cache.push( sum ); last = current; } this.cacheArcLengths = cache; return cache; // { sums: cache, sum:sum }; Sum is in the last element. }; THREE.Curve.prototype.updateArcLengths = function() { this.needsUpdate = true; this.getLengths(); }; // Given u ( 0 .. 1 ), get a t to find p. This gives you points which are equi distance THREE.Curve.prototype.getUtoTmapping = function ( u, distance ) { var arcLengths = this.getLengths(); var i = 0, il = arcLengths.length; var targetArcLength; // The targeted u distance value to get if ( distance ) { targetArcLength = distance; } else { targetArcLength = u * arcLengths[ il - 1 ]; } //var time = Date.now(); // binary search for the index with largest value smaller than target u distance var low = 0, high = il - 1, comparison; while ( low <= high ) { i = Math.floor( low + ( high - low ) / 2 ); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats comparison = arcLengths[ i ] - targetArcLength; if ( comparison < 0 ) { low = i + 1; continue; } else if ( comparison > 0 ) { high = i - 1; continue; } else { high = i; break; // DONE } } i = high; //console.log('b' , i, low, high, Date.now()- time); if ( arcLengths[ i ] == targetArcLength ) { var t = i / ( il - 1 ); return t; } // we could get finer grain at lengths, or use simple interpolatation between two points var lengthBefore = arcLengths[ i ]; var lengthAfter = arcLengths[ i + 1 ]; var segmentLength = lengthAfter - lengthBefore; // determine where we are between the 'before' and 'after' points var segmentFraction = ( targetArcLength - lengthBefore ) / segmentLength; // add that fractional amount to t var t = ( i + segmentFraction ) / ( il -1 ); return t; }; // Returns a unit vector tangent at t // In case any sub curve does not implement its tangent derivation, // 2 points a small delta apart will be used to find its gradient // which seems to give a reasonable approximation THREE.Curve.prototype.getTangent = function( t ) { var delta = 0.0001; var t1 = t - delta; var t2 = t + delta; // Capping in case of danger if ( t1 < 0 ) t1 = 0; if ( t2 > 1 ) t2 = 1; var pt1 = this.getPoint( t1 ); var pt2 = this.getPoint( t2 ); var vec = pt2.clone().sub(pt1); return vec.normalize(); }; THREE.Curve.prototype.getTangentAt = function ( u ) { var t = this.getUtoTmapping( u ); return this.getTangent( t ); }; /************************************************************** * Line **************************************************************/ THREE.LineCurve = function ( v1, v2 ) { this.v1 = v1; this.v2 = v2; }; THREE.LineCurve.prototype = Object.create( THREE.Curve.prototype ); THREE.LineCurve.prototype.getPoint = function ( t ) { var point = this.v2.clone().sub(this.v1); point.multiplyScalar( t ).add( this.v1 ); return point; }; // Line curve is linear, so we can overwrite default getPointAt THREE.LineCurve.prototype.getPointAt = function ( u ) { return this.getPoint( u ); }; THREE.LineCurve.prototype.getTangent = function( t ) { var tangent = this.v2.clone().sub(this.v1); return tangent.normalize(); }; /************************************************************** * Quadratic Bezier curve **************************************************************/ THREE.QuadraticBezierCurve = function ( v0, v1, v2 ) { this.v0 = v0; this.v1 = v1; this.v2 = v2; }; THREE.QuadraticBezierCurve.prototype = Object.create( THREE.Curve.prototype ); THREE.QuadraticBezierCurve.prototype.getPoint = function ( t ) { var tx, ty; tx = THREE.Shape.Utils.b2( t, this.v0.x, this.v1.x, this.v2.x ); ty = THREE.Shape.Utils.b2( t, this.v0.y, this.v1.y, this.v2.y ); return new THREE.Vector2( tx, ty ); }; THREE.QuadraticBezierCurve.prototype.getTangent = function( t ) { var tx, ty; tx = THREE.Curve.Utils.tangentQuadraticBezier( t, this.v0.x, this.v1.x, this.v2.x ); ty = THREE.Curve.Utils.tangentQuadraticBezier( t, this.v0.y, this.v1.y, this.v2.y ); // returns unit vector var tangent = new THREE.Vector2( tx, ty ); tangent.normalize(); return tangent; }; /************************************************************** * Cubic Bezier curve **************************************************************/ THREE.CubicBezierCurve = function ( v0, v1, v2, v3 ) { this.v0 = v0; this.v1 = v1; this.v2 = v2; this.v3 = v3; }; THREE.CubicBezierCurve.prototype = Object.create( THREE.Curve.prototype ); THREE.CubicBezierCurve.prototype.getPoint = function ( t ) { var tx, ty; tx = THREE.Shape.Utils.b3( t, this.v0.x, this.v1.x, this.v2.x, this.v3.x ); ty = THREE.Shape.Utils.b3( t, this.v0.y, this.v1.y, this.v2.y, this.v3.y ); return new THREE.Vector2( tx, ty ); }; THREE.CubicBezierCurve.prototype.getTangent = function( t ) { var tx, ty; tx = THREE.Curve.Utils.tangentCubicBezier( t, this.v0.x, this.v1.x, this.v2.x, this.v3.x ); ty = THREE.Curve.Utils.tangentCubicBezier( t, this.v0.y, this.v1.y, this.v2.y, this.v3.y ); var tangent = new THREE.Vector2( tx, ty ); tangent.normalize(); return tangent; }; /************************************************************** * Spline curve **************************************************************/ THREE.SplineCurve = function ( points /* array of Vector2 */ ) { this.points = (points == undefined) ? [] : points; }; THREE.SplineCurve.prototype = Object.create( THREE.Curve.prototype ); THREE.SplineCurve.prototype.getPoint = function ( t ) { var v = new THREE.Vector2(); var c = []; var points = this.points, point, intPoint, weight; point = ( points.length - 1 ) * t; intPoint = Math.floor( point ); weight = point - intPoint; c[ 0 ] = intPoint == 0 ? intPoint : intPoint - 1; c[ 1 ] = intPoint; c[ 2 ] = intPoint > points.length - 2 ? points.length -1 : intPoint + 1; c[ 3 ] = intPoint > points.length - 3 ? points.length -1 : intPoint + 2; v.x = THREE.Curve.Utils.interpolate( points[ c[ 0 ] ].x, points[ c[ 1 ] ].x, points[ c[ 2 ] ].x, points[ c[ 3 ] ].x, weight ); v.y = THREE.Curve.Utils.interpolate( points[ c[ 0 ] ].y, points[ c[ 1 ] ].y, points[ c[ 2 ] ].y, points[ c[ 3 ] ].y, weight ); return v; }; /************************************************************** * Ellipse curve **************************************************************/ THREE.EllipseCurve = function ( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise ) { this.aX = aX; this.aY = aY; this.xRadius = xRadius; this.yRadius = yRadius; this.aStartAngle = aStartAngle; this.aEndAngle = aEndAngle; this.aClockwise = aClockwise; }; THREE.EllipseCurve.prototype = Object.create( THREE.Curve.prototype ); THREE.EllipseCurve.prototype.getPoint = function ( t ) { var deltaAngle = this.aEndAngle - this.aStartAngle; if ( !this.aClockwise ) { t = 1 - t; } var angle = this.aStartAngle + t * deltaAngle; var tx = this.aX + this.xRadius * Math.cos( angle ); var ty = this.aY + this.yRadius * Math.sin( angle ); return new THREE.Vector2( tx, ty ); }; /************************************************************** * Arc curve **************************************************************/ THREE.ArcCurve = function ( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) { THREE.EllipseCurve.call( this, aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise ); }; THREE.ArcCurve.prototype = Object.create( THREE.EllipseCurve.prototype ); /************************************************************** * Utils **************************************************************/ THREE.Curve.Utils = { tangentQuadraticBezier: function ( t, p0, p1, p2 ) { return 2 * ( 1 - t ) * ( p1 - p0 ) + 2 * t * ( p2 - p1 ); }, // Puay Bing, thanks for helping with this derivative! tangentCubicBezier: function (t, p0, p1, p2, p3 ) { return -3 * p0 * (1 - t) * (1 - t) + 3 * p1 * (1 - t) * (1-t) - 6 *t *p1 * (1-t) + 6 * t * p2 * (1-t) - 3 * t * t * p2 + 3 * t * t * p3; }, tangentSpline: function ( t, p0, p1, p2, p3 ) { // To check if my formulas are correct var h00 = 6 * t * t - 6 * t; // derived from 2t^3 − 3t^2 + 1 var h10 = 3 * t * t - 4 * t + 1; // t^3 − 2t^2 + t var h01 = -6 * t * t + 6 * t; // − 2t3 + 3t2 var h11 = 3 * t * t - 2 * t; // t3 − t2 return h00 + h10 + h01 + h11; }, // Catmull-Rom 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; } }; // TODO: Transformation for Curves? /************************************************************** * 3D Curves **************************************************************/ // A Factory method for creating new curve subclasses THREE.Curve.create = function ( constructor, getPointFunc ) { constructor.prototype = Object.create( THREE.Curve.prototype ); constructor.prototype.getPoint = getPointFunc; return constructor; }; /************************************************************** * Line3D **************************************************************/ THREE.LineCurve3 = THREE.Curve.create( function ( v1, v2 ) { this.v1 = v1; this.v2 = v2; }, function ( t ) { var r = new THREE.Vector3(); r.sub( this.v2, this.v1 ); // diff r.multiplyScalar( t ); r.add( this.v1 ); return r; } ); /************************************************************** * Quadratic Bezier 3D curve **************************************************************/ THREE.QuadraticBezierCurve3 = THREE.Curve.create( function ( v0, v1, v2 ) { this.v0 = v0; this.v1 = v1; this.v2 = v2; }, function ( t ) { var tx, ty, tz; tx = THREE.Shape.Utils.b2( t, this.v0.x, this.v1.x, this.v2.x ); ty = THREE.Shape.Utils.b2( t, this.v0.y, this.v1.y, this.v2.y ); tz = THREE.Shape.Utils.b2( t, this.v0.z, this.v1.z, this.v2.z ); return new THREE.Vector3( tx, ty, tz ); } ); /************************************************************** * Cubic Bezier 3D curve **************************************************************/ THREE.CubicBezierCurve3 = THREE.Curve.create( function ( v0, v1, v2, v3 ) { this.v0 = v0; this.v1 = v1; this.v2 = v2; this.v3 = v3; }, function ( t ) { var tx, ty, tz; tx = THREE.Shape.Utils.b3( t, this.v0.x, this.v1.x, this.v2.x, this.v3.x ); ty = THREE.Shape.Utils.b3( t, this.v0.y, this.v1.y, this.v2.y, this.v3.y ); tz = THREE.Shape.Utils.b3( t, this.v0.z, this.v1.z, this.v2.z, this.v3.z ); return new THREE.Vector3( tx, ty, tz ); } ); /************************************************************** * Spline 3D curve **************************************************************/ THREE.SplineCurve3 = THREE.Curve.create( function ( points /* array of Vector3 */) { this.points = (points == undefined) ? [] : points; }, function ( t ) { var v = new THREE.Vector3(); var c = []; var points = this.points, point, intPoint, weight; point = ( points.length - 1 ) * t; intPoint = Math.floor( point ); weight = point - intPoint; c[ 0 ] = intPoint == 0 ? intPoint : intPoint - 1; c[ 1 ] = intPoint; c[ 2 ] = intPoint > points.length - 2 ? points.length - 1 : intPoint + 1; c[ 3 ] = intPoint > points.length - 3 ? points.length - 1 : intPoint + 2; var pt0 = points[ c[0] ], pt1 = points[ c[1] ], pt2 = points[ c[2] ], pt3 = points[ c[3] ]; v.x = THREE.Curve.Utils.interpolate(pt0.x, pt1.x, pt2.x, pt3.x, weight); v.y = THREE.Curve.Utils.interpolate(pt0.y, pt1.y, pt2.y, pt3.y, weight); v.z = THREE.Curve.Utils.interpolate(pt0.z, pt1.z, pt2.z, pt3.z, weight); return v; } ); // THREE.SplineCurve3.prototype.getTangent = function(t) { // var v = new THREE.Vector3(); // var c = []; // var points = this.points, point, intPoint, weight; // point = ( points.length - 1 ) * t; // intPoint = Math.floor( point ); // weight = point - intPoint; // c[ 0 ] = intPoint == 0 ? intPoint : intPoint - 1; // c[ 1 ] = intPoint; // c[ 2 ] = intPoint > points.length - 2 ? points.length - 1 : intPoint + 1; // c[ 3 ] = intPoint > points.length - 3 ? points.length - 1 : intPoint + 2; // var pt0 = points[ c[0] ], // pt1 = points[ c[1] ], // pt2 = points[ c[2] ], // pt3 = points[ c[3] ]; // // t = weight; // v.x = THREE.Curve.Utils.tangentSpline( t, pt0.x, pt1.x, pt2.x, pt3.x ); // v.y = THREE.Curve.Utils.tangentSpline( t, pt0.y, pt1.y, pt2.y, pt3.y ); // v.z = THREE.Curve.Utils.tangentSpline( t, pt0.z, pt1.z, pt2.z, pt3.z ); // return v; // } /************************************************************** * Closed Spline 3D curve **************************************************************/ THREE.ClosedSplineCurve3 = THREE.Curve.create( function ( points /* array of Vector3 */) { this.points = (points == undefined) ? [] : points; }, function ( t ) { var v = new THREE.Vector3(); var c = []; var points = this.points, point, intPoint, weight; point = ( points.length - 0 ) * t; // This needs to be from 0-length +1 intPoint = Math.floor( point ); weight = point - intPoint; intPoint += intPoint > 0 ? 0 : ( Math.floor( Math.abs( intPoint ) / points.length ) + 1 ) * points.length; c[ 0 ] = ( intPoint - 1 ) % points.length; c[ 1 ] = ( intPoint ) % points.length; c[ 2 ] = ( intPoint + 1 ) % points.length; c[ 3 ] = ( intPoint + 2 ) % points.length; v.x = THREE.Curve.Utils.interpolate( points[ c[ 0 ] ].x, points[ c[ 1 ] ].x, points[ c[ 2 ] ].x, points[ c[ 3 ] ].x, weight ); v.y = THREE.Curve.Utils.interpolate( points[ c[ 0 ] ].y, points[ c[ 1 ] ].y, points[ c[ 2 ] ].y, points[ c[ 3 ] ].y, weight ); v.z = THREE.Curve.Utils.interpolate( points[ c[ 0 ] ].z, points[ c[ 1 ] ].z, points[ c[ 2 ] ].z, points[ c[ 3 ] ].z, weight ); return v; } );