/** * @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() * * This file contains following classes: * * -- 2d classes -- * THREE.Curve * THREE.LineCurve * THREE.QuadraticBezierCurve * THREE.CubicBezierCurve * THREE.SplineCurve * THREE.ArcCurve * * -- 3d classes -- * THREE.LineCurve3 * THREE.QuadraticBezierCurve3 * THREE.CubicBezierCurve3 * THREE.SplineCurve3 * **/ /************************************************************** * 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 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 = 200; if ( this.cacheArcLengths && ( this.cacheArcLengths.length == divisions + 1 ) ) { //console.log( "cached", this.cacheArcLengths ); return this.cacheArcLengths; } 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. }; // 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 t = ( i + segmentFraction ) / ( il -1 ); return t; }; // In case any sub curve does not implement its tangent / normal finding, // we get 2 points with a small delta and find a gradient of the 2 points // which seems to make a reasonable approximation THREE.Curve.prototype.getNormalVector = function( t ) { var vec = this.getTangent( t ); return new THREE.Vector2( -vec.y , vec.x ); }; // Returns a unit vector tangent at t 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 = pt1.clone().subSelf(pt2); return vec.normalize(); }; THREE.Curve.prototype.getTangentAt = function ( u ) { var t = this.getUtoTmapping( u ); return this.getTangent( t ); }; /************************************************************** * Line **************************************************************/ THREE.LineCurve = function ( v1, v2 ) { if ( ! ( v1 instanceof THREE.Vector2 ) ) { // Fall back for old constuctor signature - should be removed over time THREE.LineCurve.oldConstructor.apply( this, arguments ); return; } this.v1 = v1; this.v2 = v2; }; THREE.LineCurve.oldConstructor = function ( x1, y1, x2, y2 ) { this.constructor( new THREE.Vector2( x1, y1 ), new THREE.Vector2( x2, y2 ) ); }; THREE.LineCurve.prototype = new THREE.Curve(); THREE.LineCurve.prototype.constructor = THREE.LineCurve; THREE.LineCurve.prototype.getPoint = function ( t ) { var point = new THREE.Vector2(); point.sub( this.v2, this.v1 ); point.multiplyScalar( t ).addSelf( 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 = new THREE.Vector2(); tangent.sub( this.v2, this.v1 ); tangent.normalize(); return tangent; }; /************************************************************** * Quadratic Bezier curve **************************************************************/ THREE.QuadraticBezierCurve = function ( v0, v1, v2 ) { if ( !( v1 instanceof THREE.Vector2 ) ) { var args = Array.prototype.slice.call( arguments ); v0 = new THREE.Vector2( args[ 0 ], args[ 1 ] ); v1 = new THREE.Vector2( args[ 2 ], args[ 3 ] ); v2 = new THREE.Vector2( args[ 4 ], args[ 5 ] ); } this.v0 = v0; this.v1 = v1; this.v2 = v2; }; THREE.QuadraticBezierCurve.prototype = new THREE.Curve(); THREE.QuadraticBezierCurve.prototype.constructor = THREE.QuadraticBezierCurve; 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 ) { // iterate sub segments // get lengths for sub segments // if segment is bezier // perform subdivisions 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 ) { if ( ! ( v1 instanceof THREE.Vector2 ) ) { var args = Array.prototype.slice.call( arguments ); v0 = new THREE.Vector2( args[ 0 ], args[ 1 ] ); v1 = new THREE.Vector2( args[ 2 ], args[ 3 ] ); v2 = new THREE.Vector2( args[ 4 ], args[ 5 ] ); v3 = new THREE.Vector2( args[ 6 ], args[ 7 ] ); } this.v0 = v0; this.v1 = v1; this.v2 = v2; this.v3 = v3; }; THREE.CubicBezierCurve.prototype = new THREE.Curve(); THREE.CubicBezierCurve.prototype.constructor = THREE.CubicBezierCurve; 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 ); // return normal unit vector 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 = new THREE.Curve(); THREE.SplineCurve.prototype.constructor = THREE.SplineCurve; 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 ? intPoint : intPoint + 1; c[ 3 ] = intPoint > points.length - 3 ? intPoint : 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; }; /************************************************************** * Arc curve **************************************************************/ THREE.ArcCurve = function ( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) { this.aX = aX; this.aY = aY; this.aRadius = aRadius; this.aStartAngle = aStartAngle; this.aEndAngle = aEndAngle; this.aClockwise = aClockwise; }; THREE.ArcCurve.prototype = new THREE.Curve(); THREE.ArcCurve.prototype.constructor = THREE.ArcCurve; THREE.ArcCurve.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.aRadius * Math.cos( angle ); var ty = this.aY + this.aRadius * Math.sin( angle ); return new THREE.Vector2( tx, ty ); }; /************************************************************** * 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; } }; /* getPoint DONE getLength DONE getLengths DONE curve.getPoints(); DONE curve.getPointAtArcLength(t); DONE curve.transform(params); curve.getTangentAt(t); DONE */ /************************************************************** * 3D Curves **************************************************************/ // A Factory method for creating new curve subclasses THREE.Curve.create = function( constructor, getPointFunc ) { var subClass = constructor; subClass.prototype = new THREE.Curve(); subClass.prototype.constructor = constructor; subClass.prototype.getPoint = getPointFunc; return subClass; }; /************************************************************** * 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.addSelf( 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 ? intPoint : intPoint + 1; c[ 3 ] = intPoint > points.length - 3 ? intPoint : 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 ); 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; } );