/**
A **Path** is a complex curved path constructed from various {{#crossLink "Curve"}}{{/crossLink}} subtypes.
## Overview
* A Path can be constructed from these {{#crossLink "Curve"}}{{/crossLink}} subtypes: {{#crossLink "SplineCurve"}}{{/crossLink}},
{{#crossLink "CubicBezierCurve"}}{{/crossLink}} and {{#crossLink "QuadraticBezierCurve"}}{{/crossLink}}.
* You can sample a {{#crossLink "Path/point:property"}}{{/crossLink}} and a {{#crossLink "Curve/tangent:property"}}{{/crossLink}}
vector on a Path for any given value of {{#crossLink "Path/t:property"}}{{/crossLink}} in the range [0..1].
* When you set {{#crossLink "Path/t:property"}}{{/crossLink}} on a Path, its
{{#crossLink "Path/point:property"}}{{/crossLink}} and {{#crossLink "Curve/tangent:property"}}{{/crossLink}} properties
will update accordingly.
## Examples
* [CubicBezierCurve example](../../examples/#animation_curves_cubicBezier)
* [Tweening position along a QuadraticBezierCurve](../../examples/#animation_curves_quadraticBezier)
* [Tweening color along a QuadraticBezierCurve](../../examples/#animation_curves_quadraticBezier_color)
* [SplineCurve example](../../examples/#animation_curves_spline)
* [Path example](../../examples/#curves_Path)
## Usage
#### Animation along a SplineCurve
Create a Path containing a {{#crossLink "CubicBezierCurve"}}{{/crossLink}}, a {{#crossLink "QuadraticBezierCurve"}}{{/crossLink}}
and a {{#crossLink "SplineCurve"}}{{/crossLink}}, subscribe to updates on its {{#crossLink "Path/point:property"}}{{/crossLink}} and
{{#crossLink "Curve/tangent:property"}}{{/crossLink}} properties, then vary its {{#crossLink "Path/t:property"}}{{/crossLink}}
property over time:
````javascript
var path = new xeogl.Path({
curves: [
new xeogl.CubicBezierCurve({
v0: [-10, 0, 0],
v1: [-5, 15, 0],
v2: [20, 15, 0],
v3: [10, 0, 0]
}),
new xeogl.QuadraticBezierCurve({
v0: [10, 0, 0],
v1: [20, 15, 0],
v2: [10, 0, 0]
}),
new xeogl.SplineCurve({
points: [
[10, 0, 0],
[-5, 15, 0],
[20, 15, 0],
[10, 0, 0]
]
})
]
});
path.on("point", function(point) {
this.log("path.point=" + JSON.stringify(point));
});
path.on("tangent", function(tangent) {
this.log("path.tangent=" + JSON.stringify(tangent));
});
path.on("t", function(t) {
this.log("path.t=" + t);
});
path.scene.on("tick", function(e) {
path.t = (e.time - e.startTime) * 0.01;
});
````
#### Randomly sampling points
Use Path's {{#crossLink "Path/getPoint:method"}}{{/crossLink}} and
{{#crossLink "path/getTangent:method"}}{{/crossLink}} methods to sample the point and vector
at a given **t**:
````javascript
path.scene.on("tick", function(e) {
var t = (e.time - e.startTime) * 0.01;
var point = path.getPoint(t);
var tangent = path.getTangent(t);
this.log("t=" + t + ", point=" + JSON.stringify(point) + ", tangent=" + JSON.stringify(tangent));
});
````
#### Sampling multiple points
Use Path's {{#crossLink "path/getPoints:method"}}{{/crossLink}} method to sample a list of equidistant points
along it. In the snippet below, we'll build a {{#crossLink "Geometry"}}{{/crossLink}} that renders a line along the
path. Note that we need to flatten the points array for consumption by the {{#crossLink "Geometry"}}{{/crossLink}}.
````javascript
var geometry = new xeogl.Geometry({
positions: xeogl.math.flatten(path.getPoints(50))
});
````
@class Path
@module xeogl
@submodule curves
@constructor
@param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}}.
@param [cfg] {*} Fly configuration
@param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}}, generated automatically when omitted.
@param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Path.
@param [cfg.paths=[]] IDs or instances of {{#crossLink "path"}}{{/crossLink}} subtypes to add to this Path.
@param [cfg.t=0] Current position on this Path, in range between 0..1.
@extends path
*/
xeogl.Path = class xeoglPath extends xeogl.Curve {
init(cfg) {
super.init(cfg);
this._cachedLengths = [];
this._dirty = true;
this._curves = []; // Array of child Curve components
this._t = 0;
this._dirtySubs = []; // Subscriptions to "dirty" events from child Curve components
this._destroyedSubs = []; // Subscriptions to "destroyed" events from child Curve components
this.curves = cfg.curves || []; // Add initial curves
this.t = cfg.t; // Set initial progress
}
/**
* Adds a {{#crossLink "Curve"}}{{/crossLink}} to this Path.
*
* Fires a {{#crossLink "Path/curves:event"}}{{/crossLink}} event on change.
*
* @param {Curve} curve The {{#crossLink "Curve"}}{{/crossLink}} to add.
*/
addCurve(curve) {
this._curves.push(curve);
this._dirty = true;
/**
* Fired whenever this Path's
* {{#crossLink "Path/curves:property"}}{{/crossLink}} property changes.
* @event curves
* @param value The property's new value
*/
this.fire("curves", this._curves);
}
/**
The {{#crossLink "Curve"}}Curves{{/crossLink}} in this Path.
Fires a {{#crossLink "Path/curves:event"}}{{/crossLink}} event on change.
@property curves
@default []
@type {{Array of Spline, Path, QuadraticBezierCurve or CubicBezierCurve}}
*/
set curves(value) {
value = value || [];
var curve;
// Unsubscribe from events on old curves
var i;
var len;
for (i = 0, len = this._curves.length; i < len; i++) {
curve = this._curves[i];
curve.off(this._dirtySubs[i]);
curve.off(this._destroyedSubs[i]);
}
this._curves = [];
this._dirtySubs = [];
this._destroyedSubs = [];
var self = this;
function curveDirty() {
self._dirty = true;
}
function curveDestroyed() {
var id = this.id;
for (i = 0, len = self._curves.length; i < len; i++) {
if (self._curves[i].id === id) {
self._curves = self._curves.slice(i, i + 1);
self._dirtySubs = self._dirtySubs.slice(i, i + 1);
self._destroyedSubs = self._destroyedSubs.slice(i, i + 1);
self._dirty = true;
self.fire("curves", self._curves);
return;
}
}
}
for (i = 0, len = value.length; i < len; i++) {
curve = value[i];
if (xeogl._isNumeric(curve) || xeogl._isString(curve)) {
// ID given for curve - find the curve component
var id = curve;
curve = this.scene.components[id];
if (!curve) {
this.error("Component not found: " + xeogl._inQuotes(id));
continue;
}
}
var type = curve.type;
if (type !== "xeogl.SplineCurve" &&
type !== "xeogl.Path" &&
type !== "xeogl.CubicBezierCurve" &&
type !== "xeogl.QuadraticBezierCurve") {
this.error("Component " + xeogl._inQuotes(curve.id)
+ " is not a xeogl.SplineCurve, xeogl.Path or xeogl.QuadraticBezierCurve");
continue;
}
this._curves.push(curve);
this._dirtySubs.push(curve.on("dirty", curveDirty));
this._destroyedSubs.push(curve.on("destroyed", curveDestroyed));
}
this._dirty = true;
this.fire("curves", this._curves);
}
get curves() {
return this._curves;
}
/**
Current point of progress along this Path.
Automatically clamps to range [0..1].
Fires a {{#crossLink "Path/t:event"}}{{/crossLink}} event on change.
@property t
@default 0
@type Number
*/
set t(value) {
value = value || 0;
this._t = value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value);
/**
* Fired whenever this Path's
* {{#crossLink "Path/t:property"}}{{/crossLink}} property changes.
* @event t
* @param value The property's new value
*/
this.fire("t", this._t);
}
get t() {
return this._t;
}
/**
Point on this Path corresponding to the current value of {{#crossLink "Path/t:property"}}{{/crossLink}}.
@property point
@type {{Array of Number}}
*/
get point() {
return this.getPoint(this._t);
}
/**
Length of this Path, which is the cumulative length of all {{#crossLink "Curve/t:property"}}Curves{{/crossLink}}
currently in {{#crossLink "Path/curves:property"}}{{/crossLink}}.
@property length
@type {Number}
*/
get length() {
var lens = this._getCurveLengths();
return lens[lens.length - 1];
}
/**
* Gets a point on this Path corresponding to the given progress position.
* @param {Number} t Indicates point of progress along this curve, in the range [0..1].
* @returns {{Array of Number}}
*/
getPoint(t) {
var d = t * this.length;
var curveLengths = this._getCurveLengths();
var i = 0, diff, curve;
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;
}
_getCurveLengths() {
if (!this._dirty) {
return this._cachedLengths;
}
var lengths = [];
var sums = 0;
var i, il = this._curves.length;
for (i = 0; i < il; i++) {
sums += this._curves[i].length;
lengths.push(sums);
}
this._cachedLengths = lengths;
this._dirty = false;
return lengths;
}
_getJSON() {
var curveIds = [];
for (var i = 0, len = this._curves.length; i < len; i++) {
curveIds.push(this._curves[i].id);
}
return {
curves: curveIds,
t: this._t
};
}
destroy() {
super.destroy();
var i;
var len;
var curve;
for (i = 0, len = this._curves.length; i < len; i++) {
curve = this._curves[i];
curve.off(this._dirtySubs[i]);
curve.off(this._destroyedSubs[i]);
}
}
};