blob: 698532cd972ba42dae9f4c1ecec0fcefc4115462 [file] [log] [blame]
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(shared, scope, testing) {
scope.animationsWithPromises = [];
scope.Animation = function(effect, timeline) {
this.id = '';
if (effect && effect._id) {
this.id = effect._id;
}
this.effect = effect;
if (effect) {
effect._animation = this;
}
if (!timeline) {
throw new Error('Animation with null timeline is not supported');
}
this._timeline = timeline;
this._sequenceNumber = shared.sequenceNumber++;
this._holdTime = 0;
this._paused = false;
this._isGroup = false;
this._animation = null;
this._childAnimations = [];
this._callback = null;
this._oldPlayState = 'idle';
this._rebuildUnderlyingAnimation();
// Animations are constructed in the idle state.
this._animation.cancel();
this._updatePromises();
};
scope.Animation.prototype = {
_updatePromises: function() {
var oldPlayState = this._oldPlayState;
var newPlayState = this.playState;
if (this._readyPromise && newPlayState !== oldPlayState) {
if (newPlayState == 'idle') {
this._rejectReadyPromise();
this._readyPromise = undefined;
} else if (oldPlayState == 'pending') {
this._resolveReadyPromise();
} else if (newPlayState == 'pending') {
this._readyPromise = undefined;
}
}
if (this._finishedPromise && newPlayState !== oldPlayState) {
if (newPlayState == 'idle') {
this._rejectFinishedPromise();
this._finishedPromise = undefined;
} else if (newPlayState == 'finished') {
this._resolveFinishedPromise();
} else if (oldPlayState == 'finished') {
this._finishedPromise = undefined;
}
}
this._oldPlayState = this.playState;
return (this._readyPromise || this._finishedPromise);
},
_rebuildUnderlyingAnimation: function() {
this._updatePromises();
var oldPlaybackRate;
var oldPaused;
var oldStartTime;
var oldCurrentTime;
var hadUnderlying = this._animation ? true : false;
if (hadUnderlying) {
oldPlaybackRate = this.playbackRate;
oldPaused = this._paused;
oldStartTime = this.startTime;
oldCurrentTime = this.currentTime;
this._animation.cancel();
this._animation._wrapper = null;
this._animation = null;
}
if (!this.effect || this.effect instanceof window.KeyframeEffect) {
this._animation = scope.newUnderlyingAnimationForKeyframeEffect(this.effect);
scope.bindAnimationForKeyframeEffect(this);
}
if (this.effect instanceof window.SequenceEffect || this.effect instanceof window.GroupEffect) {
this._animation = scope.newUnderlyingAnimationForGroup(this.effect);
scope.bindAnimationForGroup(this);
}
if (this.effect && this.effect._onsample) {
scope.bindAnimationForCustomEffect(this);
}
if (hadUnderlying) {
if (oldPlaybackRate != 1) {
this.playbackRate = oldPlaybackRate;
}
if (oldStartTime !== null) {
this.startTime = oldStartTime;
} else if (oldCurrentTime !== null) {
this.currentTime = oldCurrentTime;
} else if (this._holdTime !== null) {
this.currentTime = this._holdTime;
}
if (oldPaused) {
this.pause();
}
}
this._updatePromises();
},
_updateChildren: function() {
if (!this.effect || this.playState == 'idle')
return;
var offset = this.effect._timing.delay;
this._childAnimations.forEach(function(childAnimation) {
this._arrangeChildren(childAnimation, offset);
if (this.effect instanceof window.SequenceEffect)
offset += scope.groupChildDuration(childAnimation.effect);
}.bind(this));
},
_setExternalAnimation: function(animation) {
if (!this.effect || !this._isGroup)
return;
for (var i = 0; i < this.effect.children.length; i++) {
this.effect.children[i]._animation = animation;
this._childAnimations[i]._setExternalAnimation(animation);
}
},
_constructChildAnimations: function() {
if (!this.effect || !this._isGroup)
return;
var offset = this.effect._timing.delay;
this._removeChildAnimations();
this.effect.children.forEach(function(child) {
var childAnimation = scope.timeline._play(child);
this._childAnimations.push(childAnimation);
childAnimation.playbackRate = this.playbackRate;
if (this._paused)
childAnimation.pause();
child._animation = this.effect._animation;
this._arrangeChildren(childAnimation, offset);
if (this.effect instanceof window.SequenceEffect)
offset += scope.groupChildDuration(child);
}.bind(this));
},
_arrangeChildren: function(childAnimation, offset) {
if (this.startTime === null) {
childAnimation.currentTime = this.currentTime - offset / this.playbackRate;
} else if (childAnimation.startTime !== this.startTime + offset / this.playbackRate) {
childAnimation.startTime = this.startTime + offset / this.playbackRate;
}
},
get timeline() {
return this._timeline;
},
get playState() {
return this._animation ? this._animation.playState : 'idle';
},
get finished() {
if (!window.Promise) {
console.warn('Animation Promises require JavaScript Promise constructor');
return null;
}
if (!this._finishedPromise) {
if (scope.animationsWithPromises.indexOf(this) == -1) {
scope.animationsWithPromises.push(this);
}
this._finishedPromise = new Promise(
function(resolve, reject) {
this._resolveFinishedPromise = function() {
resolve(this);
};
this._rejectFinishedPromise = function() {
reject({type: DOMException.ABORT_ERR, name: 'AbortError'});
};
}.bind(this));
if (this.playState == 'finished') {
this._resolveFinishedPromise();
}
}
return this._finishedPromise;
},
get ready() {
if (!window.Promise) {
console.warn('Animation Promises require JavaScript Promise constructor');
return null;
}
if (!this._readyPromise) {
if (scope.animationsWithPromises.indexOf(this) == -1) {
scope.animationsWithPromises.push(this);
}
this._readyPromise = new Promise(
function(resolve, reject) {
this._resolveReadyPromise = function() {
resolve(this);
};
this._rejectReadyPromise = function() {
reject({type: DOMException.ABORT_ERR, name: 'AbortError'});
};
}.bind(this));
if (this.playState !== 'pending') {
this._resolveReadyPromise();
}
}
return this._readyPromise;
},
get onfinish() {
return this._animation.onfinish;
},
set onfinish(v) {
if (typeof v == 'function') {
this._animation.onfinish = (function(e) {
e.target = this;
v.call(this, e);
}).bind(this);
} else {
this._animation.onfinish = v;
}
},
get oncancel() {
return this._animation.oncancel;
},
set oncancel(v) {
if (typeof v == 'function') {
this._animation.oncancel = (function(e) {
e.target = this;
v.call(this, e);
}).bind(this);
} else {
this._animation.oncancel = v;
}
},
get currentTime() {
this._updatePromises();
var currentTime = this._animation.currentTime;
this._updatePromises();
return currentTime;
},
set currentTime(v) {
this._updatePromises();
this._animation.currentTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE;
this._register();
this._forEachChild(function(child, offset) {
child.currentTime = v - offset;
});
this._updatePromises();
},
get startTime() {
return this._animation.startTime;
},
set startTime(v) {
this._updatePromises();
this._animation.startTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE;
this._register();
this._forEachChild(function(child, offset) {
child.startTime = v + offset;
});
this._updatePromises();
},
get playbackRate() {
return this._animation.playbackRate;
},
set playbackRate(value) {
this._updatePromises();
var oldCurrentTime = this.currentTime;
this._animation.playbackRate = value;
this._forEachChild(function(childAnimation) {
childAnimation.playbackRate = value;
});
if (oldCurrentTime !== null) {
this.currentTime = oldCurrentTime;
}
this._updatePromises();
},
play: function() {
this._updatePromises();
this._paused = false;
this._animation.play();
if (this._timeline._animations.indexOf(this) == -1) {
this._timeline._animations.push(this);
}
this._register();
scope.awaitStartTime(this);
this._forEachChild(function(child) {
var time = child.currentTime;
child.play();
child.currentTime = time;
});
this._updatePromises();
},
pause: function() {
this._updatePromises();
if (this.currentTime) {
this._holdTime = this.currentTime;
}
this._animation.pause();
this._register();
this._forEachChild(function(child) {
child.pause();
});
this._paused = true;
this._updatePromises();
},
finish: function() {
this._updatePromises();
this._animation.finish();
this._register();
this._updatePromises();
},
cancel: function() {
this._updatePromises();
this._animation.cancel();
this._register();
this._removeChildAnimations();
this._updatePromises();
},
reverse: function() {
this._updatePromises();
var oldCurrentTime = this.currentTime;
this._animation.reverse();
this._forEachChild(function(childAnimation) {
childAnimation.reverse();
});
if (oldCurrentTime !== null) {
this.currentTime = oldCurrentTime;
}
this._updatePromises();
},
addEventListener: function(type, handler) {
var wrapped = handler;
if (typeof handler == 'function') {
wrapped = (function(e) {
e.target = this;
handler.call(this, e);
}).bind(this);
handler._wrapper = wrapped;
}
this._animation.addEventListener(type, wrapped);
},
removeEventListener: function(type, handler) {
this._animation.removeEventListener(type, (handler && handler._wrapper) || handler);
},
_removeChildAnimations: function() {
while (this._childAnimations.length)
this._childAnimations.pop().cancel();
},
_forEachChild: function(f) {
var offset = 0;
if (this.effect.children && this._childAnimations.length < this.effect.children.length)
this._constructChildAnimations();
this._childAnimations.forEach(function(child) {
f.call(this, child, offset);
if (this.effect instanceof window.SequenceEffect)
offset += child.effect.activeDuration;
}.bind(this));
if (this.playState == 'pending')
return;
var timing = this.effect._timing;
var t = this.currentTime;
if (t !== null)
t = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), t, timing);
if (t == null || isNaN(t))
this._removeChildAnimations();
},
};
window.Animation = scope.Animation;
if (WEB_ANIMATIONS_TESTING) {
testing.webAnimationsNextAnimation = scope.Animation;
}
})(webAnimationsShared, webAnimationsNext, webAnimationsTesting);