claygl
Version:
WebGL graphic library
1,892 lines (1,696 loc) • 1.11 MB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.clay = {})));
}(this, (function (exports) { 'use strict';
// 缓动函数来自 https://github.com/sole/tween.js/blob/master/src/Tween.js
/**
* @namespace clay.animation.easing
*/
var easing = {
/**
* @alias clay.animation.easing.linear
* @param {number} k
* @return {number}
*/
linear: function(k) {
return k;
},
/**
* @alias clay.animation.easing.quadraticIn
* @param {number} k
* @return {number}
*/
quadraticIn: function(k) {
return k * k;
},
/**
* @alias clay.animation.easing.quadraticOut
* @param {number} k
* @return {number}
*/
quadraticOut: function(k) {
return k * (2 - k);
},
/**
* @alias clay.animation.easing.quadraticInOut
* @param {number} k
* @return {number}
*/
quadraticInOut: function(k) {
if ((k *= 2) < 1) {
return 0.5 * k * k;
}
return - 0.5 * (--k * (k - 2) - 1);
},
/**
* @alias clay.animation.easing.cubicIn
* @param {number} k
* @return {number}
*/
cubicIn: function(k) {
return k * k * k;
},
/**
* @alias clay.animation.easing.cubicOut
* @param {number} k
* @return {number}
*/
cubicOut: function(k) {
return --k * k * k + 1;
},
/**
* @alias clay.animation.easing.cubicInOut
* @param {number} k
* @return {number}
*/
cubicInOut: function(k) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k;
}
return 0.5 * ((k -= 2) * k * k + 2);
},
/**
* @alias clay.animation.easing.quarticIn
* @param {number} k
* @return {number}
*/
quarticIn: function(k) {
return k * k * k * k;
},
/**
* @alias clay.animation.easing.quarticOut
* @param {number} k
* @return {number}
*/
quarticOut: function(k) {
return 1 - (--k * k * k * k);
},
/**
* @alias clay.animation.easing.quarticInOut
* @param {number} k
* @return {number}
*/
quarticInOut: function(k) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k * k;
}
return - 0.5 * ((k -= 2) * k * k * k - 2);
},
/**
* @alias clay.animation.easing.quinticIn
* @param {number} k
* @return {number}
*/
quinticIn: function(k) {
return k * k * k * k * k;
},
/**
* @alias clay.animation.easing.quinticOut
* @param {number} k
* @return {number}
*/
quinticOut: function(k) {
return --k * k * k * k * k + 1;
},
/**
* @alias clay.animation.easing.quinticInOut
* @param {number} k
* @return {number}
*/
quinticInOut: function(k) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k * k * k;
}
return 0.5 * ((k -= 2) * k * k * k * k + 2);
},
/**
* @alias clay.animation.easing.sinusoidalIn
* @param {number} k
* @return {number}
*/
sinusoidalIn: function(k) {
return 1 - Math.cos(k * Math.PI / 2);
},
/**
* @alias clay.animation.easing.sinusoidalOut
* @param {number} k
* @return {number}
*/
sinusoidalOut: function(k) {
return Math.sin(k * Math.PI / 2);
},
/**
* @alias clay.animation.easing.sinusoidalInOut
* @param {number} k
* @return {number}
*/
sinusoidalInOut: function(k) {
return 0.5 * (1 - Math.cos(Math.PI * k));
},
/**
* @alias clay.animation.easing.exponentialIn
* @param {number} k
* @return {number}
*/
exponentialIn: function(k) {
return k === 0 ? 0 : Math.pow(1024, k - 1);
},
/**
* @alias clay.animation.easing.exponentialOut
* @param {number} k
* @return {number}
*/
exponentialOut: function(k) {
return k === 1 ? 1 : 1 - Math.pow(2, - 10 * k);
},
/**
* @alias clay.animation.easing.exponentialInOut
* @param {number} k
* @return {number}
*/
exponentialInOut: function(k) {
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if ((k *= 2) < 1) {
return 0.5 * Math.pow(1024, k - 1);
}
return 0.5 * (- Math.pow(2, - 10 * (k - 1)) + 2);
},
/**
* @alias clay.animation.easing.circularIn
* @param {number} k
* @return {number}
*/
circularIn: function(k) {
return 1 - Math.sqrt(1 - k * k);
},
/**
* @alias clay.animation.easing.circularOut
* @param {number} k
* @return {number}
*/
circularOut: function(k) {
return Math.sqrt(1 - (--k * k));
},
/**
* @alias clay.animation.easing.circularInOut
* @param {number} k
* @return {number}
*/
circularInOut: function(k) {
if ((k *= 2) < 1) {
return - 0.5 * (Math.sqrt(1 - k * k) - 1);
}
return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1);
},
/**
* @alias clay.animation.easing.elasticIn
* @param {number} k
* @return {number}
*/
elasticIn: function(k) {
var s, a = 0.1, p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1; s = p / 4;
}else{
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
return - (a * Math.pow(2, 10 * (k -= 1)) *
Math.sin((k - s) * (2 * Math.PI) / p));
},
/**
* @alias clay.animation.easing.elasticOut
* @param {number} k
* @return {number}
*/
elasticOut: function(k) {
var s, a = 0.1, p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1; s = p / 4;
}
else{
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
return (a * Math.pow(2, - 10 * k) *
Math.sin((k - s) * (2 * Math.PI) / p) + 1);
},
/**
* @alias clay.animation.easing.elasticInOut
* @param {number} k
* @return {number}
*/
elasticInOut: function(k) {
var s, a = 0.1, p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1; s = p / 4;
}
else{
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
if ((k *= 2) < 1) {
return - 0.5 * (a * Math.pow(2, 10 * (k -= 1))
* Math.sin((k - s) * (2 * Math.PI) / p));
}
return a * Math.pow(2, -10 * (k -= 1))
* Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1;
},
/**
* @alias clay.animation.easing.backIn
* @param {number} k
* @return {number}
*/
backIn: function(k) {
var s = 1.70158;
return k * k * ((s + 1) * k - s);
},
/**
* @alias clay.animation.easing.backOut
* @param {number} k
* @return {number}
*/
backOut: function(k) {
var s = 1.70158;
return --k * k * ((s + 1) * k + s) + 1;
},
/**
* @alias clay.animation.easing.backInOut
* @param {number} k
* @return {number}
*/
backInOut: function(k) {
var s = 1.70158 * 1.525;
if ((k *= 2) < 1) {
return 0.5 * (k * k * ((s + 1) * k - s));
}
return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2);
},
/**
* @alias clay.animation.easing.bounceIn
* @param {number} k
* @return {number}
*/
bounceIn: function(k) {
return 1 - easing.bounceOut(1 - k);
},
/**
* @alias clay.animation.easing.bounceOut
* @param {number} k
* @return {number}
*/
bounceOut: function(k) {
if (k < (1 / 2.75)) {
return 7.5625 * k * k;
}
else if (k < (2 / 2.75)) {
return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
} else if (k < (2.5 / 2.75)) {
return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
} else {
return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
}
},
/**
* @alias clay.animation.easing.bounceInOut
* @param {number} k
* @return {number}
*/
bounceInOut: function(k) {
if (k < 0.5) {
return easing.bounceIn(k * 2) * 0.5;
}
return easing.bounceOut(k * 2 - 1) * 0.5 + 0.5;
}
};
function noop () {}
/**
* @constructor
* @alias clay.animation.Clip
* @param {Object} [opts]
* @param {Object} [opts.target]
* @param {number} [opts.life]
* @param {number} [opts.delay]
* @param {number} [opts.gap]
* @param {number} [opts.playbackRate]
* @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation
* @param {string|Function} [opts.easing]
* @param {Function} [opts.onframe]
* @param {Function} [opts.onfinish]
* @param {Function} [opts.onrestart]
*/
var Clip = function (opts) {
opts = opts || {};
/**
* @type {string}
*/
this.name = opts.name || '';
/**
* @type {Object}
*/
this.target = opts.target;
/**
* @type {number}
*/
this.life = opts.life || 1000;
/**
* @type {number}
*/
this.delay = opts.delay || 0;
/**
* @type {number}
*/
this.gap = opts.gap || 0;
/**
* @type {number}
*/
this.playbackRate = opts.playbackRate || 1;
this._initialized = false;
this._elapsedTime = 0;
this._loop = opts.loop == null ? false : opts.loop;
this.setLoop(this._loop);
if (opts.easing != null) {
this.setEasing(opts.easing);
}
/**
* @type {Function}
*/
this.onframe = opts.onframe || noop;
/**
* @type {Function}
*/
this.onfinish = opts.onfinish || noop;
/**
* @type {Function}
*/
this.onrestart = opts.onrestart || noop;
this._paused = false;
};
Clip.prototype = {
gap: 0,
life: 0,
delay: 0,
/**
* @param {number|boolean} loop
*/
setLoop: function (loop) {
this._loop = loop;
if (loop) {
if (typeof loop === 'number') {
this._loopRemained = loop;
}
else {
this._loopRemained = Infinity;
}
}
},
/**
* @param {string|Function} easing
*/
setEasing: function (easing$$1) {
if (typeof(easing$$1) === 'string') {
easing$$1 = easing[easing$$1];
}
this.easing = easing$$1;
},
/**
* @param {number} time
* @return {string}
*/
step: function (time, deltaTime, silent) {
if (!this._initialized) {
this._startTime = time + this.delay;
this._initialized = true;
}
if (this._currentTime != null) {
deltaTime = time - this._currentTime;
}
this._currentTime = time;
if (this._paused) {
return 'paused';
}
if (time < this._startTime) {
return;
}
// PENDIGN Sync ?
this._elapse(time, deltaTime);
var percent = Math.min(this._elapsedTime / this.life, 1);
if (percent < 0) {
return;
}
var schedule;
if (this.easing) {
schedule = this.easing(percent);
}
else {
schedule = percent;
}
if (!silent) {
this.fire('frame', schedule);
}
if (percent === 1) {
if (this._loop && this._loopRemained > 0) {
this._restartInLoop(time);
this._loopRemained--;
return 'restart';
}
else {
// Mark this clip to be deleted
// In the animation.update
this._needsRemove = true;
return 'finish';
}
}
else {
return null;
}
},
/**
* @param {number} time
* @return {string}
*/
setTime: function (time) {
return this.step(time + this._startTime);
},
restart: function (time) {
// If user leave the page for a while, when he gets back
// All clips may be expired and all start from the beginning value(position)
// It is clearly wrong, so we use remainder to add a offset
var remainder = 0;
// Remainder ignored if restart is invoked manually
if (time) {
this._elapse(time);
remainder = this._elapsedTime % this.life;
}
time = time || Date.now();
this._startTime = time - remainder + this.delay;
this._elapsedTime = 0;
this._needsRemove = false;
this._paused = false;
},
getElapsedTime: function () {
return this._elapsedTime;
},
_restartInLoop: function (time) {
this._startTime = time + this.gap;
this._elapsedTime = 0;
},
_elapse: function (time, deltaTime) {
this._elapsedTime += deltaTime * this.playbackRate;
},
fire: function (eventType, arg) {
var eventName = 'on' + eventType;
if (this[eventName]) {
this[eventName](this.target, arg);
}
},
clone: function () {
var clip = new this.constructor();
clip.name = this.name;
clip._loop = this._loop;
clip._loopRemained = this._loopRemained;
clip.life = this.life;
clip.gap = this.gap;
clip.delay = this.delay;
return clip;
},
/**
* Pause the clip.
*/
pause: function () {
this._paused = true;
},
/**
* Resume the clip.
*/
resume: function () {
this._paused = false;
}
};
Clip.prototype.constructor = Clip;
var arraySlice = Array.prototype.slice;
function defaultGetter(target, key) {
return target[key];
}
function defaultSetter(target, key, value) {
target[key] = value;
}
function interpolateNumber(p0, p1, percent) {
return (p1 - p0) * percent + p0;
}
function interpolateArray(p0, p1, percent, out, arrDim) {
var len = p0.length;
if (arrDim == 1) {
for (var i = 0; i < len; i++) {
out[i] = interpolateNumber(p0[i], p1[i], percent);
}
}
else {
var len2 = p0[0].length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len2; j++) {
out[i][j] = interpolateNumber(
p0[i][j], p1[i][j], percent
);
}
}
}
}
function isArrayLike(data) {
if (typeof(data) == 'undefined') {
return false;
} else if (typeof(data) == 'string') {
return false;
} else {
return typeof(data.length) == 'number';
}
}
function cloneValue(value) {
if (isArrayLike(value)) {
var len = value.length;
if (isArrayLike(value[0])) {
var ret = [];
for (var i = 0; i < len; i++) {
ret.push(arraySlice.call(value[i]));
}
return ret;
} else {
return arraySlice.call(value);
}
} else {
return value;
}
}
function catmullRomInterpolateArray(
p0, p1, p2, p3, t, t2, t3, out, arrDim
) {
var len = p0.length;
if (arrDim == 1) {
for (var i = 0; i < len; i++) {
out[i] = catmullRomInterpolate(
p0[i], p1[i], p2[i], p3[i], t, t2, t3
);
}
} else {
var len2 = p0[0].length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len2; j++) {
out[i][j] = catmullRomInterpolate(
p0[i][j], p1[i][j], p2[i][j], p3[i][j],
t, t2, t3
);
}
}
}
}
function catmullRomInterpolate(p0, p1, p2, p3, t, t2, t3) {
var v0 = (p2 - p0) * 0.5;
var v1 = (p3 - p1) * 0.5;
return (2 * (p1 - p2) + v0 + v1) * t3
+ (- 3 * (p1 - p2) - 2 * v0 - v1) * t2
+ v0 * t + p1;
}
// arr0 is source array, arr1 is target array.
// Do some preprocess to avoid error happened when interpolating from arr0 to arr1
function fillArr(arr0, arr1, arrDim) {
var arr0Len = arr0.length;
var arr1Len = arr1.length;
if (arr0Len !== arr1Len) {
// FIXME Not work for TypedArray
var isPreviousLarger = arr0Len > arr1Len;
if (isPreviousLarger) {
// Cut the previous
arr0.length = arr1Len;
}
else {
// Fill the previous
for (var i = arr0Len; i < arr1Len; i++) {
arr0.push(
arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i])
);
}
}
}
// Handling NaN value
var len2 = arr0[0] && arr0[0].length;
for (var i = 0; i < arr0.length; i++) {
if (arrDim === 1) {
if (isNaN(arr0[i])) {
arr0[i] = arr1[i];
}
}
else {
for (var j = 0; j < len2; j++) {
if (isNaN(arr0[i][j])) {
arr0[i][j] = arr1[i][j];
}
}
}
}
}
function isArraySame(arr0, arr1, arrDim) {
if (arr0 === arr1) {
return true;
}
var len = arr0.length;
if (len !== arr1.length) {
return false;
}
if (arrDim === 1) {
for (var i = 0; i < len; i++) {
if (arr0[i] !== arr1[i]) {
return false;
}
}
}
else {
var len2 = arr0[0].length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len2; j++) {
if (arr0[i][j] !== arr1[i][j]) {
return false;
}
}
}
}
return true;
}
function createTrackClip(animator, globalEasing, oneTrackDone, keyframes, propName, interpolater, maxTime) {
var getter = animator._getter;
var setter = animator._setter;
var useSpline = globalEasing === 'spline';
var trackLen = keyframes.length;
if (!trackLen) {
return;
}
// Guess data type
var firstVal = keyframes[0].value;
var isValueArray = isArrayLike(firstVal);
// For vertices morphing
var arrDim = (
isValueArray
&& isArrayLike(firstVal[0])
)
? 2 : 1;
// Sort keyframe as ascending
keyframes.sort(function(a, b) {
return a.time - b.time;
});
// Percents of each keyframe
var kfPercents = [];
// Value of each keyframe
var kfValues = [];
// Easing funcs of each keyframe.
var kfEasings = [];
var prevValue = keyframes[0].value;
var isAllValueEqual = true;
for (var i = 0; i < trackLen; i++) {
kfPercents.push(keyframes[i].time / maxTime);
// Assume value is a color when it is a string
var value = keyframes[i].value;
// Check if value is equal, deep check if value is array
if (!((isValueArray && isArraySame(value, prevValue, arrDim))
|| (!isValueArray && value === prevValue))) {
isAllValueEqual = false;
}
prevValue = value;
kfValues.push(value);
kfEasings.push(keyframes[i].easing);
}
if (isAllValueEqual) {
return;
}
var lastValue = kfValues[trackLen - 1];
// Polyfill array and NaN value
for (var i = 0; i < trackLen - 1; i++) {
if (isValueArray) {
fillArr(kfValues[i], lastValue, arrDim);
}
else {
if (isNaN(kfValues[i]) && !isNaN(lastValue)) {
kfValues[i] = lastValue;
}
}
}
isValueArray && fillArr(getter(animator._target, propName), lastValue, arrDim);
// Cache the key of last frame to speed up when
// animation playback is sequency
var cacheKey = 0;
var cachePercent = 0;
var start;
var i, w;
var p0, p1, p2, p3;
var onframe = function(target, percent) {
// Find the range keyframes
// kf1-----kf2---------current--------kf3
// find kf2(i) and kf3(i + 1) and do interpolation
if (percent < cachePercent) {
// Start from next key
start = Math.min(cacheKey + 1, trackLen - 1);
for (i = start; i >= 0; i--) {
if (kfPercents[i] <= percent) {
break;
}
}
i = Math.min(i, trackLen - 2);
}
else {
for (i = cacheKey; i < trackLen; i++) {
if (kfPercents[i] > percent) {
break;
}
}
i = Math.min(i - 1, trackLen - 2);
}
cacheKey = i;
cachePercent = percent;
var range = (kfPercents[i + 1] - kfPercents[i]);
if (range === 0) {
return;
}
else {
w = (percent - kfPercents[i]) / range;
// Clamp 0 - 1
w = Math.max(Math.min(1, w), 0);
}
w = kfEasings[i + 1](w);
if (useSpline) {
p1 = kfValues[i];
p0 = kfValues[i === 0 ? i : i - 1];
p2 = kfValues[i > trackLen - 2 ? trackLen - 1 : i + 1];
p3 = kfValues[i > trackLen - 3 ? trackLen - 1 : i + 2];
if (interpolater) {
setter(
target,
propName,
interpolater(
getter(target, propName),
p0, p1, p2, p3, w
)
);
}
else if (isValueArray) {
catmullRomInterpolateArray(
p0, p1, p2, p3, w, w*w, w*w*w,
getter(target, propName),
arrDim
);
}
else {
setter(
target,
propName,
catmullRomInterpolate(p0, p1, p2, p3, w, w*w, w*w*w)
);
}
}
else {
if (interpolater) {
setter(
target,
propName,
interpolater(
getter(target, propName),
kfValues[i],
kfValues[i + 1],
w
)
);
}
else if (isValueArray) {
interpolateArray(
kfValues[i], kfValues[i+1], w,
getter(target, propName),
arrDim
);
}
else {
setter(
target,
propName,
interpolateNumber(kfValues[i], kfValues[i+1], w)
);
}
}
};
var clip = new Clip({
target: animator._target,
life: maxTime,
loop: animator._loop,
delay: animator._delay,
onframe: onframe,
onfinish: oneTrackDone
});
if (globalEasing && globalEasing !== 'spline') {
clip.setEasing(globalEasing);
}
return clip;
}
/**
* @description Animator object can only be created by Animation.prototype.animate method.
* After created, we can use {@link clay.animation.Animator#when} to add all keyframes and {@link clay.animation.Animator#start} it.
* Clips will be automatically created and added to the animation instance which created this deferred object.
*
* @constructor clay.animation.Animator
*
* @param {Object} target
* @param {boolean} loop
* @param {Function} getter
* @param {Function} setter
* @param {Function} interpolater
*/
function Animator(target, loop, getter, setter, interpolater) {
this._tracks = {};
this._target = target;
this._loop = loop || false;
this._getter = getter || defaultGetter;
this._setter = setter || defaultSetter;
this._interpolater = interpolater || null;
this._delay = 0;
this._doneList = [];
this._onframeList = [];
this._clipList = [];
this._maxTime = 0;
this._lastKFTime = 0;
}
function noopEasing(w) {
return w;
}
Animator.prototype = {
constructor: Animator,
/**
* @param {number} time Keyframe time using millisecond
* @param {Object} props A key-value object. Value can be number, 1d and 2d array
* @param {string|Function} [easing]
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
when: function (time, props, easing$$1) {
this._maxTime = Math.max(time, this._maxTime);
easing$$1 = (typeof easing$$1 === 'function' ? easing$$1 : easing[easing$$1]) || noopEasing;
for (var propName in props) {
if (!this._tracks[propName]) {
this._tracks[propName] = [];
// If time is 0
// Then props is given initialize value
// Else
// Initialize value from current prop value
if (time !== 0) {
this._tracks[propName].push({
time: 0,
value: cloneValue(
this._getter(this._target, propName)
),
easing: easing$$1
});
}
}
this._tracks[propName].push({
time: parseInt(time),
value: props[propName],
easing: easing$$1
});
}
return this;
},
/**
* @param {number} time During time since last keyframe
* @param {Object} props A key-value object. Value can be number, 1d and 2d array
* @param {string|Function} [easing]
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
then: function (duringTime, props, easing$$1) {
this.when(duringTime + this._lastKFTime, props, easing$$1);
this._lastKFTime += duringTime;
return this;
},
/**
* callback when running animation
* @param {Function} callback callback have two args, animating target and current percent
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
during: function (callback) {
this._onframeList.push(callback);
return this;
},
_doneCallback: function () {
// Clear all tracks
this._tracks = {};
// Clear all clips
this._clipList.length = 0;
var doneList = this._doneList;
var len = doneList.length;
for (var i = 0; i < len; i++) {
doneList[i].call(this);
}
},
/**
* Start the animation
* @param {string|Function} easing
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
start: function (globalEasing) {
var self = this;
var clipCount = 0;
var oneTrackDone = function() {
clipCount--;
if (clipCount === 0) {
self._doneCallback();
}
};
var lastClip;
for (var propName in this._tracks) {
var clip = createTrackClip(
this, globalEasing, oneTrackDone,
this._tracks[propName], propName, self._interpolater, self._maxTime
);
if (clip) {
this._clipList.push(clip);
clipCount++;
// If start after added to animation
if (this.animation) {
this.animation.addClip(clip);
}
lastClip = clip;
}
}
// Add during callback on the last clip
if (lastClip) {
var oldOnFrame = lastClip.onframe;
lastClip.onframe = function (target, percent) {
oldOnFrame(target, percent);
for (var i = 0; i < self._onframeList.length; i++) {
self._onframeList[i](target, percent);
}
};
}
if (!clipCount) {
this._doneCallback();
}
return this;
},
/**
* Stop the animation
* @memberOf clay.animation.Animator.prototype
*/
stop: function () {
for (var i = 0; i < this._clipList.length; i++) {
var clip = this._clipList[i];
this.animation.removeClip(clip);
}
this._clipList = [];
},
/**
* Delay given milliseconds
* @param {number} time
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
delay: function (time){
this._delay = time;
return this;
},
/**
* Callback after animation finished
* @param {Function} func
* @return {clay.animation.Animator}
* @memberOf clay.animation.Animator.prototype
*/
done: function (func) {
if (func) {
this._doneList.push(func);
}
return this;
},
/**
* Get all clips created in start method.
* @return {clay.animation.Clip[]}
* @memberOf clay.animation.Animator.prototype
*/
getClips: function () {
return this._clipList;
}
};
// 1D Blend clip of blend tree
// http://docs.unity3d.com/Documentation/Manual/1DBlending.html
var clipSortFunc = function (a, b) {
return a.position < b.position;
};
/**
* @typedef {Object} clay.animation.Blend1DClip.IClipInput
* @property {number} position
* @property {clay.animation.Clip} clip
* @property {number} offset
*/
/**
* 1d blending node in animation blend tree.
* output clip must have blend1D and copy method
* @constructor
* @alias clay.animation.Blend1DClip
* @extends clay.animation.Clip
*
* @param {Object} [opts]
* @param {string} [opts.name]
* @param {Object} [opts.target]
* @param {number} [opts.life]
* @param {number} [opts.delay]
* @param {number} [opts.gap]
* @param {number} [opts.playbackRatio]
* @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation
* @param {string|Function} [opts.easing]
* @param {Function} [opts.onframe]
* @param {Function} [opts.onfinish]
* @param {Function} [opts.onrestart]
* @param {object[]} [opts.inputs]
* @param {number} [opts.position]
* @param {clay.animation.Clip} [opts.output]
*/
var Blend1DClip = function (opts) {
opts = opts || {};
Clip.call(this, opts);
/**
* Output clip must have blend1D and copy method
* @type {clay.animation.Clip}
*/
this.output = opts.output || null;
/**
* @type {clay.animation.Blend1DClip.IClipInput[]}
*/
this.inputs = opts.inputs || [];
/**
* @type {number}
*/
this.position = 0;
this._cacheKey = 0;
this._cachePosition = -Infinity;
this.inputs.sort(clipSortFunc);
};
Blend1DClip.prototype = new Clip();
Blend1DClip.prototype.constructor = Blend1DClip;
/**
* @param {number} position
* @param {clay.animation.Clip} inputClip
* @param {number} [offset]
* @return {clay.animation.Blend1DClip.IClipInput}
*/
Blend1DClip.prototype.addInput = function (position, inputClip, offset) {
var obj = {
position: position,
clip: inputClip,
offset: offset || 0
};
this.life = Math.max(inputClip.life, this.life);
if (!this.inputs.length) {
this.inputs.push(obj);
return obj;
}
var len = this.inputs.length;
if (this.inputs[0].position > position) {
this.inputs.unshift(obj);
} else if (this.inputs[len - 1].position <= position) {
this.inputs.push(obj);
} else {
var key = this._findKey(position);
this.inputs.splice(key, obj);
}
return obj;
};
Blend1DClip.prototype.step = function (time, dTime, silent) {
var ret = Clip.prototype.step.call(this, time);
if (ret !== 'finish') {
this.setTime(this.getElapsedTime());
}
// PENDING Schedule
if (!silent && ret !== 'paused') {
this.fire('frame');
}
return ret;
};
Blend1DClip.prototype.setTime = function (time) {
var position = this.position;
var inputs = this.inputs;
var len = inputs.length;
var min = inputs[0].position;
var max = inputs[len-1].position;
if (position <= min || position >= max) {
var in0 = position <= min ? inputs[0] : inputs[len-1];
var clip = in0.clip;
var offset = in0.offset;
clip.setTime((time + offset) % clip.life);
// Input clip is a blend clip
// PENDING
if (clip.output instanceof Clip) {
this.output.copy(clip.output);
} else {
this.output.copy(clip);
}
} else {
var key = this._findKey(position);
var in1 = inputs[key];
var in2 = inputs[key + 1];
var clip1 = in1.clip;
var clip2 = in2.clip;
// Set time on input clips
clip1.setTime((time + in1.offset) % clip1.life);
clip2.setTime((time + in2.offset) % clip2.life);
var w = (this.position - in1.position) / (in2.position - in1.position);
var c1 = clip1.output instanceof Clip ? clip1.output : clip1;
var c2 = clip2.output instanceof Clip ? clip2.output : clip2;
this.output.blend1D(c1, c2, w);
}
};
/**
* Clone a new Blend1D clip
* @param {boolean} cloneInputs True if clone the input clips
* @return {clay.animation.Blend1DClip}
*/
Blend1DClip.prototype.clone = function (cloneInputs) {
var clip = Clip.prototype.clone.call(this);
clip.output = this.output.clone();
for (var i = 0; i < this.inputs.length; i++) {
var inputClip = cloneInputs ? this.inputs[i].clip.clone(true) : this.inputs[i].clip;
clip.addInput(this.inputs[i].position, inputClip, this.inputs[i].offset);
}
return clip;
};
// Find the key where position in range [inputs[key].position, inputs[key+1].position)
Blend1DClip.prototype._findKey = function (position) {
var key = -1;
var inputs = this.inputs;
var len = inputs.length;
if (this._cachePosition < position) {
for (var i = this._cacheKey; i < len-1; i++) {
if (position >= inputs[i].position && position < inputs[i+1].position) {
key = i;
}
}
} else {
var s = Math.min(len-2, this._cacheKey);
for (var i = s; i >= 0; i--) {
if (position >= inputs[i].position && position < inputs[i+1].position) {
key = i;
}
}
}
if (key >= 0) {
this._cacheKey = key;
this._cachePosition = position;
}
return key;
};
// Delaunay Triangulation
// Modified from https://github.com/ironwallaby/delaunay
var EPSILON = 1.0 / 1048576.0;
function supertriangle(vertices) {
var xmin = Number.POSITIVE_INFINITY;
var ymin = Number.POSITIVE_INFINITY;
var xmax = Number.NEGATIVE_INFINITY;
var ymax = Number.NEGATIVE_INFINITY;
var i, dx, dy, dmax, xmid, ymid;
for (i = vertices.length; i--; ) {
if (vertices[i][0] < xmin) { xmin = vertices[i][0]; }
if (vertices[i][0] > xmax) { xmax = vertices[i][0]; }
if (vertices[i][1] < ymin) { ymin = vertices[i][1]; }
if (vertices[i][1] > ymax) { ymax = vertices[i][1]; }
}
dx = xmax - xmin;
dy = ymax - ymin;
dmax = Math.max(dx, dy);
xmid = xmin + dx * 0.5;
ymid = ymin + dy * 0.5;
return [
[xmid - 20 * dmax, ymid - dmax],
[xmid , ymid + 20 * dmax],
[xmid + 20 * dmax, ymid - dmax]
];
}
function circumcircle(vertices, i, j, k) {
var x1 = vertices[i][0],
y1 = vertices[i][1],
x2 = vertices[j][0],
y2 = vertices[j][1],
x3 = vertices[k][0],
y3 = vertices[k][1],
fabsy1y2 = Math.abs(y1 - y2),
fabsy2y3 = Math.abs(y2 - y3),
xc, yc, m1, m2, mx1, mx2, my1, my2, dx, dy;
/* Check for coincident points */
if (fabsy1y2 < EPSILON && fabsy2y3 < EPSILON) {
throw new Error('Eek! Coincident points!');
}
if (fabsy1y2 < EPSILON) {
m2 = -((x3 - x2) / (y3 - y2));
mx2 = (x2 + x3) / 2.0;
my2 = (y2 + y3) / 2.0;
xc = (x2 + x1) / 2.0;
yc = m2 * (xc - mx2) + my2;
}
else if (fabsy2y3 < EPSILON) {
m1 = -((x2 - x1) / (y2 - y1));
mx1 = (x1 + x2) / 2.0;
my1 = (y1 + y2) / 2.0;
xc = (x3 + x2) / 2.0;
yc = m1 * (xc - mx1) + my1;
}
else {
m1 = -((x2 - x1) / (y2 - y1));
m2 = -((x3 - x2) / (y3 - y2));
mx1 = (x1 + x2) / 2.0;
mx2 = (x2 + x3) / 2.0;
my1 = (y1 + y2) / 2.0;
my2 = (y2 + y3) / 2.0;
xc = (m1 * mx1 - m2 * mx2 + my2 - my1) / (m1 - m2);
yc = (fabsy1y2 > fabsy2y3) ?
m1 * (xc - mx1) + my1 :
m2 * (xc - mx2) + my2;
}
dx = x2 - xc;
dy = y2 - yc;
return {i: i, j: j, k: k, x: xc, y: yc, r: dx * dx + dy * dy};
}
function dedup(edges) {
var i, j, a, b, m, n;
for (j = edges.length; j; ) {
b = edges[--j];
a = edges[--j];
for (i = j; i; ) {
n = edges[--i];
m = edges[--i];
if ((a === m && b === n) || (a === n && b === m)) {
edges.splice(j, 2);
edges.splice(i, 2);
break;
}
}
}
}
var delaunay = {
triangulate: function(vertices, key) {
var n = vertices.length;
var i, j, indices, st, open, closed, edges, dx, dy, a, b, c;
/* Bail if there aren't enough vertices to form any triangles. */
if (n < 3) {
return [];
}
/* Slice out the actual vertices from the passed objects. (Duplicate the
* array even if we don't, though, since we need to make a supertriangle
* later on!) */
vertices = vertices.slice(0);
if (key) {
for (i = n; i--; ) {
vertices[i] = vertices[i][key];
}
}
/* Make an array of indices into the vertex array, sorted by the
* vertices' x-position. Force stable sorting by comparing indices if
* the x-positions are equal. */
indices = new Array(n);
for (i = n; i--; ) {
indices[i] = i;
}
indices.sort(function(i, j) {
var diff = vertices[j][0] - vertices[i][0];
return diff !== 0 ? diff : i - j;
});
/* Next, find the vertices of the supertriangle (which contains all other
* triangles), and append them onto the end of a (copy of) the vertex
* array. */
st = supertriangle(vertices);
vertices.push(st[0], st[1], st[2]);
/* Initialize the open list (containing the supertriangle and nothing
* else) and the closed list (which is empty since we havn't processed
* any triangles yet). */
open = [circumcircle(vertices, n + 0, n + 1, n + 2)];
closed = [];
edges = [];
/* Incrementally add each vertex to the mesh. */
for (i = indices.length; i--; edges.length = 0) {
c = indices[i];
/* For each open triangle, check to see if the current point is
* inside it's circumcircle. If it is, remove the triangle and add
* it's edges to an edge list. */
for (j = open.length; j--; ) {
/* If this point is to the right of this triangle's circumcircle,
* then this triangle should never get checked again. Remove it
* from the open list, add it to the closed list, and skip. */
dx = vertices[c][0] - open[j].x;
if (dx > 0.0 && dx * dx > open[j].r) {
closed.push(open[j]);
open.splice(j, 1);
continue;
}
/* If we're outside the circumcircle, skip this triangle. */
dy = vertices[c][1] - open[j].y;
if (dx * dx + dy * dy - open[j].r > EPSILON) {
continue;
}
/* Remove the triangle and add it's edges to the edge list. */
edges.push(
open[j].i, open[j].j,
open[j].j, open[j].k,
open[j].k, open[j].i
);
open.splice(j, 1);
}
/* Remove any doubled edges. */
dedup(edges);
/* Add a new triangle for each edge. */
for (j = edges.length; j; ) {
b = edges[--j];
a = edges[--j];
open.push(circumcircle(vertices, a, b, c));
}
}
/* Copy any remaining open triangles to the closed list, and then
* remove any triangles that share a vertex with the supertriangle,
* building a list of triplets that represent triangles. */
for (i = open.length; i--; ) {
closed.push(open[i]);
}
open.length = 0;
for (i = closed.length; i--; ) {
if (closed[i].i < n && closed[i].j < n && closed[i].k < n) {
open.push(closed[i].i, closed[i].j, closed[i].k);
}
}
/* Yay, we're done! */
return open;
},
contains: function(tri, p) {
/* Bounding box test first, for quick rejections. */
if ((p[0] < tri[0][0] && p[0] < tri[1][0] && p[0] < tri[2][0]) ||
(p[0] > tri[0][0] && p[0] > tri[1][0] && p[0] > tri[2][0]) ||
(p[1] < tri[0][1] && p[1] < tri[1][1] && p[1] < tri[2][1]) ||
(p[1] > tri[0][1] && p[1] > tri[1][1] && p[1] > tri[2][1])) {
return null;
}
var a = tri[1][0] - tri[0][0];
var b = tri[2][0] - tri[0][0];
var c = tri[1][1] - tri[0][1];
var d = tri[2][1] - tri[0][1];
var i = a * d - b * c;
/* Degenerate tri. */
if (i === 0.0) {
return null;
}
var u = (d * (p[0] - tri[0][0]) - b * (p[1] - tri[0][1])) / i,
v = (a * (p[1] - tri[0][1]) - c * (p[0] - tri[0][0])) / i;
/* If we're outside the tri, fail. */
if (u < 0.0 || v < 0.0 || (u + v) > 1.0) {
return null;
}
return [u, v];
}
};
var GLMAT_EPSILON = 0.000001;
// Use Array instead of Float32Array. It seems to be much faster and higher precision.
var GLMAT_ARRAY_TYPE = Array;
// if(!GLMAT_ARRAY_TYPE) {
// GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
// }
var GLMAT_RANDOM$1 = Math.random;
/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
/**
* @class 2 Dimensional Vector
* @name vec2
*/
var vec2 = {};
/**
* Creates a new, empty vec2
*
* @returns {vec2} a new 2D vector
*/
vec2.create = function() {
var out = new GLMAT_ARRAY_TYPE(2);
out[0] = 0;
out[1] = 0;
return out;
};
/**
* Creates a new vec2 initialized with values from an existing vector
*
* @param {vec2} a vector to clone
* @returns {vec2} a new 2D vector
*/
vec2.clone = function(a) {
var out = new GLMAT_ARRAY_TYPE(2);
out[0] = a[0];
out[1] = a[1];
return out;
};
/**
* Creates a new vec2 initialized with the given values
*
* @param {Number} x X component
* @param {Number} y Y component
* @returns {vec2} a new 2D vector
*/
vec2.fromValues = function(x, y) {
var out = new GLMAT_ARRAY_TYPE(2);
out[0] = x;
out[1] = y;
return out;
};
/**
* Copy the values from one vec2 to another
*
* @param {vec2} out the receiving vector
* @param {vec2} a the source vector
* @returns {vec2} out
*/
vec2.copy = function(out, a) {
out[0] = a[0];
out[1] = a[1];
return out;
};
/**
* Set the components of a vec2 to the given values
*
* @param {vec2} out the receiving vector
* @param {Number} x X component
* @param {Number} y Y component
* @returns {vec2} out
*/
vec2.set = function(out, x, y) {
out[0] = x;
out[1] = y;
return out;
};
/**
* Adds two vec2's
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.add = function(out, a, b) {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
return out;
};
/**
* Subtracts vector b from vector a
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.subtract = function(out, a, b) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
return out;
};
/**
* Alias for {@link vec2.subtract}
* @function
*/
vec2.sub = vec2.subtract;
/**
* Multiplies two vec2's
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.multiply = function(out, a, b) {
out[0] = a[0] * b[0];
out[1] = a[1] * b[1];
return out;
};
/**
* Alias for {@link vec2.multiply}
* @function
*/
vec2.mul = vec2.multiply;
/**
* Divides two vec2's
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.divide = function(out, a, b) {
out[0] = a[0] / b[0];
out[1] = a[1] / b[1];
return out;
};
/**
* Alias for {@link vec2.divide}
* @function
*/
vec2.div = vec2.divide;
/**
* Returns the minimum of two vec2's
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.min = function(out, a, b) {
out[0] = Math.min(a[0], b[0]);
out[1] = Math.min(a[1], b[1]);
return out;
};
/**
* Returns the maximum of two vec2's
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {vec2} out
*/
vec2.max = function(out, a, b) {
out[0] = Math.max(a[0], b[0]);
out[1] = Math.max(a[1], b[1]);
return out;
};
/**
* Scales a vec2 by a scalar number
*
* @param {vec2} out the receiving vector
* @param {vec2} a the vector to scale
* @param {Number} b amount to scale the vector by
* @returns {vec2} out
*/
vec2.scale = function(out, a, b) {
out[0] = a[0] * b;
out[1] = a[1] * b;
return out;
};
/**
* Adds two vec2's after scaling the second operand by a scalar value
*
* @param {vec2} out the receiving vector
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @param {Number} scale the amount to scale b by before adding
* @returns {vec2} out
*/
vec2.scaleAndAdd = function(out, a, b, scale) {
out[0] = a[0] + (b[0] * scale);
out[1] = a[1] + (b[1] * scale);
return out;
};
/**
* Calculates the euclidian distance between two vec2's
*
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {Number} distance between a and b
*/
vec2.distance = function(a, b) {
var x = b[0] - a[0],
y = b[1] - a[1];
return Math.sqrt(x*x + y*y);
};
/**
* Alias for {@link vec2.distance}
* @function
*/
vec2.dist = vec2.distance;
/**
* Calculates the squared euclidian distance between two vec2's
*
* @param {vec2} a the first operand
* @param {vec2} b the second operand
* @returns {Number} squared distance between a and b
*/
vec2.squaredDistance = function(a, b) {
var x = b[0] - a[0],
y = b[1] - a[1];
return x*x + y*y;
};
/**
* Alias for {@link vec2.squaredDistance}
* @function
*/
vec2.sqrDist = vec2.squaredDistance;
/**
* Calculates the length of a vec2
*
* @param {vec2} a vector to calculate length of
* @returns {Number} length of a
*/
vec2.length = function (a) {
var x = a[0],
y = a[1];
return Math.sqrt(x*x + y*y);
};
/**
* Alias for {@link vec2.length}
* @function
*/
vec2.len = vec2.length;
/**
* Calculates the squared length of a vec2
*
* @param {vec2} a vector to calculate squared length of
* @returns {Number} squared length of a
*/
vec2.squaredLength = f