CurvePath class
@author zz85 / http://www.lab4games.net/zz85/blog
Curved Path - a curve path is simply a array of connected
curves, but retains the api of a curve
class CurvePath extends Curve {
List curves;
List _bends;
bool autoClose; // Automatically closes the path
List cacheLengths = null;
CurvePath()
: curves = [],
_bends = [],
autoClose = false,
super();
add( curve ) => curves.add( curve );
checkConnection() {
// TODO
// If the ending of curve is not connected to the starting
// or the next curve, then, this is not a real path
}
closePath() {
// TODO Test
// and verify for vector3 (needs to implement equals)
// Add a line curve if start and end of lines are not connected
var startPoint = curves[0].getPoint(0);
var endPoint = curves[curves.length-1].getPoint(1);
if (!startPoint.equals(endPoint)) {
this.curves.add( new LineCurve(endPoint, startPoint) );
}
}
// To get accurate point with reference to
// entire path distance at time t,
// following has to be done:
// 1. Length of each sub path have to be known
// 2. Locate and identify type of curve
// 3. Get t for the curve
// 4. Return curve.getPointAt(t')
getPoint( num t ) {
var d = t * this.length;
var curveLengths = this.getCurveLengths();
var i = 0, diff;
Curve curve;
// To think about boundaries points.
while ( i < curveLengths.length ) {
if ( curveLengths[ i ] >= d ) {
diff = curveLengths[ i ] - d;
curve = this.curves[ i ];
var u = 1 - diff / curve.length;
return curve.getPointAt( u );
}
i ++;
}
return null;
// loop where sum != 0, sum > d , sum+1 <d
}
// We cannot use the default THREE.Curve getPoint() with getLength() because in
// THREE.Curve, getLength() depends on getPoint() but in THREE.CurvePath
// getPoint() depends on getLength
num get length => getCurveLengths().last;
// Compute lengths and cache them
// We cannot overwrite getLengths() because UtoT mapping uses it.
List<num> getCurveLengths() {
// We use cache values if curves and cache array are same length
if ( this.cacheLengths != null && this.cacheLengths.length == this.curves.length ) {
return this.cacheLengths;
}
// Get length of subsurve
// Push sums into cached array
var lengths = [], sums = 0;
var i, il = this.curves.length;
for ( i = 0; i < il; i ++ ) {
sums += this.curves[ i ].length;
lengths.add( sums );
}
this.cacheLengths = lengths;
return lengths;
}
// Returns min and max coordinates, as well as centroid
getBoundingBox() {
var points = getPoints();
var maxX, maxY, maxZ;
var minX, minY, minZ;
maxX = maxY = double.NEGATIVE_INFINITY;
minX = minY = double.INFINITY;
var p, i, sum;
var v3 = points[0] is Vector3;
sum = (v3) ? new Vector3.zero() : new Vector2.zero();
int il = points.length;
for ( i = 0; i < il; i ++ ) {
p = points[ i ];
if ( p.x > maxX ) { maxX = p.x;
} else if ( p.x < minX ) minX = p.x;
if ( p.y > maxY ) { maxY = p.y;
} else if ( p.y < minY ) minY = p.y;
if (v3) {
p = p as Vector3;
if ( p.z > maxZ ) { maxZ = p.z;
} else if ( p.z < minZ ) minZ = p.z;
(sum as Vector3).add( p );
} else {
(sum as Vector2).add( p );
}
}
var ret = {
"minX": minX,
"minY": minY,
"maxX": maxX,
"maxY": maxY,
"centroid": (sum as dynamic).scale( 1.0 / il )
};
if (v3) {
ret["maxZ"] = maxZ;
ret["minZ"] = minZ;
}
return ret;
}
/**************************************************************
* Create Geometries Helpers
**************************************************************/
/// Generate geometry from path points (for Line or ParticleSystem objects)
createPointsGeometry( {divisions} ) {
var pts = this.getPoints( divisions, true );
return this.createGeometry( pts );
}
// Generate geometry from equidistance sampling along the path
createSpacedPointsGeometry( [divisions] ) {
var pts = this.getSpacedPoints( divisions, true );
return this.createGeometry( pts );
}
createGeometry( points ) {
var geometry = new Geometry();
for ( var i = 0; i < points.length; i ++ ) {
var z = (points[i] is Vector3) ? points[ i ].z : 0.0;
geometry.vertices.add( new Vector3( points[ i ].x, points[ i ].y, z) );
}
return geometry;
}
/**************************************************************
* Bend / Wrap Helper Methods
**************************************************************/
// Wrap path / Bend modifiers?
addWrapPath( bendpath ) => _bends.add( bendpath );
getTransformedPoints( segments, {List bends: null} ) {
var oldPts = this.getPoints( segments ); // getPoints getSpacedPoints
var i, il;
if (bends == null) {
bends = _bends;
}
for ( i = 0; i < bends.length; i ++ ) {
oldPts = this.getWrapPoints( oldPts, bends[ i ] );
}
return oldPts;
}
getTransformedSpacedPoints( [num segments, List bends = null] ) {
var oldPts = getSpacedPoints( segments );
var i, il;
if (bends == null) {
bends = _bends;
}
for ( i = 0; i < bends.length; i ++ ) {
oldPts = this.getWrapPoints( oldPts, bends[ i ] );
}
return oldPts;
}
// This returns getPoints() bend/wrapped around the contour of a path.
// Read http://www.planetclegg.com/projects/WarpingTextToSplines.html
getWrapPoints( oldPts, path ) {
var bounds = getBoundingBox();
var i, il, p, oldX, oldY, xNorm;
for ( i = 0; i < oldPts.length; i ++ ) {
p = oldPts[ i ];
oldX = p.x;
oldY = p.y;
xNorm = oldX / bounds.maxX;
// If using actual distance, for length > path, requires line extrusions
//xNorm = path.getUtoTmapping(xNorm, oldX); // 3 styles. 1) wrap stretched. 2) wrap stretch by arc length 3) warp by actual distance
xNorm = path.getUtoTmapping( xNorm, oldX );
// check for out of bounds?
var pathPt = path.getPoint( xNorm );
var normal = path.getNormalVector( xNorm ).scale( oldY );
p.x = pathPt.x + normal.x;
p.y = pathPt.y + normal.y;
}
return oldPts;
}
}
Extends
Curve > CurvePath
Subclasses
Constructors
Properties
Methods
dynamic add(curve) #
add( curve ) => curves.add( curve );
dynamic addWrapPath(bendpath) #
Bend / Wrap Helper Methods
addWrapPath( bendpath ) => _bends.add( bendpath );
dynamic checkConnection() #
checkConnection() {
// TODO
// If the ending of curve is not connected to the starting
// or the next curve, then, this is not a real path
}
dynamic closePath() #
closePath() {
// TODO Test
// and verify for vector3 (needs to implement equals)
// Add a line curve if start and end of lines are not connected
var startPoint = curves[0].getPoint(0);
var endPoint = curves[curves.length-1].getPoint(1);
if (!startPoint.equals(endPoint)) {
this.curves.add( new LineCurve(endPoint, startPoint) );
}
}
dynamic createGeometry(points) #
createGeometry( points ) {
var geometry = new Geometry();
for ( var i = 0; i < points.length; i ++ ) {
var z = (points[i] is Vector3) ? points[ i ].z : 0.0;
geometry.vertices.add( new Vector3( points[ i ].x, points[ i ].y, z) );
}
return geometry;
}
dynamic createPointsGeometry({divisions}) #
Create Geometries Helpers
Generate geometry from path points (for Line or ParticleSystem objects)
createPointsGeometry( {divisions} ) {
var pts = this.getPoints( divisions, true );
return this.createGeometry( pts );
}
dynamic createSpacedPointsGeometry([divisions]) #
createSpacedPointsGeometry( [divisions] ) {
var pts = this.getSpacedPoints( divisions, true );
return this.createGeometry( pts );
}
dynamic getBoundingBox() #
getBoundingBox() {
var points = getPoints();
var maxX, maxY, maxZ;
var minX, minY, minZ;
maxX = maxY = double.NEGATIVE_INFINITY;
minX = minY = double.INFINITY;
var p, i, sum;
var v3 = points[0] is Vector3;
sum = (v3) ? new Vector3.zero() : new Vector2.zero();
int il = points.length;
for ( i = 0; i < il; i ++ ) {
p = points[ i ];
if ( p.x > maxX ) { maxX = p.x;
} else if ( p.x < minX ) minX = p.x;
if ( p.y > maxY ) { maxY = p.y;
} else if ( p.y < minY ) minY = p.y;
if (v3) {
p = p as Vector3;
if ( p.z > maxZ ) { maxZ = p.z;
} else if ( p.z < minZ ) minZ = p.z;
(sum as Vector3).add( p );
} else {
(sum as Vector2).add( p );
}
}
var ret = {
"minX": minX,
"minY": minY,
"maxX": maxX,
"maxY": maxY,
"centroid": (sum as dynamic).scale( 1.0 / il )
};
if (v3) {
ret["maxZ"] = maxZ;
ret["minZ"] = minZ;
}
return ret;
}
List<num> getCurveLengths() #
List<num> getCurveLengths() {
// We use cache values if curves and cache array are same length
if ( this.cacheLengths != null && this.cacheLengths.length == this.curves.length ) {
return this.cacheLengths;
}
// Get length of subsurve
// Push sums into cached array
var lengths = [], sums = 0;
var i, il = this.curves.length;
for ( i = 0; i < il; i ++ ) {
sums += this.curves[ i ].length;
lengths.add( sums );
}
this.cacheLengths = lengths;
return lengths;
}
List getLengths({num divisions: null}) #
inherited from Curve
List getLengths( {num divisions: null} ) {
if (divisions == null) divisions = (_arcLengthDivisions != null) ? (_arcLengthDivisions): 200;
if ( cacheArcLengths != null
&& ( cacheArcLengths.length == divisions + 1 )
&& !needsUpdate) {
//console.log( "cached", this.cacheArcLengths );
return cacheArcLengths;
}
needsUpdate = false;
var cache = [];
var current;
var last = getPoint( 0.0 );
var sum = 0;
cache.add( 0 );
for ( var p = 1; p <= divisions; p ++ ) {
current = getPoint ( p / divisions );
var distance;
// TODO(nelsonsilva) - Must move distanceTo to IVector interface os create a new IHasDistance
if (current is Vector3) {
distance = (current as Vector3).absoluteError( last as Vector3 );
} else {
distance = (current as Vector2).absoluteError( last as Vector2);
}
sum += distance;
cache.add( sum );
last = current;
}
cacheArcLengths = cache;
return cache; // { sums: cache, sum:sum }; Sum is in the last element.
}
dynamic getPoint(num t) #
getPoint( num t ) {
var d = t * this.length;
var curveLengths = this.getCurveLengths();
var i = 0, diff;
Curve curve;
// To think about boundaries points.
while ( i < curveLengths.length ) {
if ( curveLengths[ i ] >= d ) {
diff = curveLengths[ i ] - d;
curve = this.curves[ i ];
var u = 1 - diff / curve.length;
return curve.getPointAt( u );
}
i ++;
}
return null;
// loop where sum != 0, sum > d , sum+1 <d
}
V getPointAt(u) #
inherited from Curve
V getPointAt( u ) {
var t = getUtoTmapping( u );
return getPoint( t );
}
List<V> getPoints([num divisions = null, closedPath = false]) #
inherited from Curve
List<V> getPoints( [num divisions = null, closedPath = false] ) {
if (divisions == null) divisions = 5;
var d, pts = [];
for ( d = 0; d <= divisions; d ++ ) {
pts.add( this.getPoint( d / divisions ) );
}
return pts;
}
List<V> getSpacedPoints([num divisions = 5, closedPath = false]) #
inherited from Curve
List<V> getSpacedPoints( [num divisions = 5, closedPath = false] ) {
var d, pts = [];
for ( d = 0; d <= divisions; d ++ ) {
pts.add( this.getPointAt( d / divisions ) );
}
return pts;
}
V getTangent(t) #
inherited from Curve
V getTangent( 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 = getPoint( t1 );
var pt2 = getPoint( t2 );
var vec = pt2 - pt1;
return vec.normalize();
}
V getTangentAt(u) #
inherited from Curve
V getTangentAt( u ) {
var t = getUtoTmapping( u );
return getTangent( t );
}
dynamic getTransformedPoints(segments, {List bends: null}) #
getTransformedPoints( segments, {List bends: null} ) {
var oldPts = this.getPoints( segments ); // getPoints getSpacedPoints
var i, il;
if (bends == null) {
bends = _bends;
}
for ( i = 0; i < bends.length; i ++ ) {
oldPts = this.getWrapPoints( oldPts, bends[ i ] );
}
return oldPts;
}
dynamic getTransformedSpacedPoints([num segments, List bends = null]) #
getTransformedSpacedPoints( [num segments, List bends = null] ) {
var oldPts = getSpacedPoints( segments );
var i, il;
if (bends == null) {
bends = _bends;
}
for ( i = 0; i < bends.length; i ++ ) {
oldPts = this.getWrapPoints( oldPts, bends[ i ] );
}
return oldPts;
}
List<V> getUPoints([List uList, closedPath = false]) #
inherited from Curve
List<V> getUPoints( [List uList , closedPath = false] ) {
var pts = [];
for ( var u in uList ) {
pts.add( this.getPointAt( u ) );
}
return pts;
}
dynamic getUtoTmapping(u, {distance: null}) #
inherited from Curve
getUtoTmapping( u, {distance: null} ) {
var arcLengths = getLengths();
int i = 0, il = arcLengths.length;
var targetArcLength; // The targeted u distance value to get
if (distance != null) {
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 = ( low + ( high - low ) / 2 ).floor().toInt(); // 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;
}
dynamic getWrapPoints(oldPts, path) #
getWrapPoints( oldPts, path ) {
var bounds = getBoundingBox();
var i, il, p, oldX, oldY, xNorm;
for ( i = 0; i < oldPts.length; i ++ ) {
p = oldPts[ i ];
oldX = p.x;
oldY = p.y;
xNorm = oldX / bounds.maxX;
// If using actual distance, for length > path, requires line extrusions
//xNorm = path.getUtoTmapping(xNorm, oldX); // 3 styles. 1) wrap stretched. 2) wrap stretch by arc length 3) warp by actual distance
xNorm = path.getUtoTmapping( xNorm, oldX );
// check for out of bounds?
var pathPt = path.getPoint( xNorm );
var normal = path.getNormalVector( xNorm ).scale( oldY );
p.x = pathPt.x + normal.x;
p.y = pathPt.y + normal.y;
}
return oldPts;
}