import Clip from './Clip';
import easingFuncs from './easing';
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) {
this._maxTime = Math.max(time, this._maxTime);
easing = (typeof easing === 'function' ? easing : easingFuncs[easing]) || 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
});
}
}
this._tracks[propName].push({
time: parseInt(time),
value: props[propName],
easing: easing
});
}
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) {
this.when(duringTime + this._lastKFTime, props, easing);
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;
var clipMaxTime = 0;
for (var propName in this._tracks) {
var clip = createTrackClip(
this, globalEasing, oneTrackDone,
this._tracks[propName], propName, self._interpolater, self._maxTime
);
if (clip) {
clipMaxTime = Math.max(clipMaxTime, clip.life);
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;
}
};
export default Animator;