diff --git a/src/animation-constructor.js b/src/animation-constructor.js index 7afe249..1f4ee89 100644 --- a/src/animation-constructor.js +++ b/src/animation-constructor.js @@ -40,16 +40,18 @@ }; var pendingGroups = []; - function addPendingGroup(group) { + scope.awaitStartTime = function(groupPlayer) { + if (!isNaN(groupPlayer.startTime) || !groupPlayer._isGroup) + return; if (pendingGroups.length == 0) { requestAnimationFrame(updatePendingGroups); } - pendingGroups.push(group); - } + pendingGroups.push(groupPlayer); + }; function updatePendingGroups() { var updated = false; while (pendingGroups.length) { - pendingGroups.shift()(); + pendingGroups.shift()._updateChildren(); updated = true; } return updated; @@ -66,18 +68,44 @@ }, }); + // TODO: Call into this less frequently. + scope.Player.prototype._updateChildren = function() { + if (isNaN(this.startTime) || !this.source || !this._isGroup) + return; + var offset = 0; + for (var i = 0; i < this.source.children.length; i++) { + var child = this.source.children[i]; + var childPlayer; + + if (i >= this._childPlayers.length) { + childPlayer = window.document.timeline.play(child); + child.player = this.source.player; + this._childPlayers.push(childPlayer); + } else { + childPlayer = this._childPlayers[i]; + } + + if (childPlayer.startTime != this.startTime + offset) { + childPlayer.startTime = this.startTime + offset; + childPlayer._updateChildren(); + } + + if (this.playbackRate == -1 && this.currentTime < offset && childPlayer.currentTime !== -1) { + childPlayer.currentTime = -1; + } + + if (this.source instanceof window.AnimationSequence) + offset += child.activeDuration; + } + }; + window.document.timeline.play = function(source) { + // TODO: Handle effect callback. if (source instanceof window.Animation) { + // TODO: Handle null target. var player = source.target.animate(source._effect, source.timing); - // TODO: make source setter call cancel. player.source = source; source.player = player; - source._nativePlayer = player; - var cancel = player.cancel; - player.cancel = function() { - player.source = null; - cancel.call(this); - }; return player; } // FIXME: Move this code out of this module @@ -89,122 +117,20 @@ player._removePlayers(); return; } - if (isNaN(player._startTime)) + if (isNaN(player.startTime)) return; - updateChildPlayers(player); + player._updateChildren(); }; - function updateChildPlayers(updatingPlayer) { - var offset = 0; - - // TODO: Call into this less frequently. - - for (var i = 0; i < updatingPlayer.source.children.length; i++) { - var child = updatingPlayer.source.children[i]; - - if (i >= updatingPlayer._childPlayers.length) { - var newPlayer = window.document.timeline.play(child); - newPlayer.startTime = updatingPlayer.startTime + offset; - child.player = updatingPlayer.source.player; - updatingPlayer._childPlayers.push(newPlayer); - if (!(child instanceof window.Animation)) - updateChildPlayers(newPlayer); - } - - var childPlayer = updatingPlayer._childPlayers[i]; - if (updatingPlayer.playbackRate == -1 && updatingPlayer.currentTime < offset && childPlayer.currentTime !== -1) { - childPlayer.currentTime = -1; - } - - if (updatingPlayer.source instanceof window.AnimationSequence) - offset += child.activeDuration; - } - }; - - addPendingGroup(function() { - if (player.source) - updateChildPlayers(player); - }); // TODO: Use a single static element rather than one per group. var player = document.createElement('div').animate(ticker, source.timing); - player._childPlayers = []; player.source = source; - source._nativePlayer = player; + player._isGroup = true; source.player = player; - - player._removePlayers = function() { - while (this._childPlayers.length) - this._childPlayers.pop().cancel(); - }; - + scope.awaitStartTime(player); return player; } }; - - function isGroupPlayer(player) { - return !!player._childPlayers; - } - - var playerProto = Object.getPrototypeOf(document.documentElement.animate([])); - scope.hookMethod(playerProto, 'reverse', function() { - if (isGroupPlayer(this)) { - var offset = 0; - this._childPlayers.forEach(function(child) { - child.reverse(); - child.startTime = this.startTime + offset * this.playbackRate; - child.currentTime = this.currentTime + offset * this.playbackRate; - if (this.source instanceof window.AnimationSequence) - offset += child.source.activeDuration; - }.bind(this)); - } - }); - - scope.hookMethod(playerProto, 'pause', function() { - if (isGroupPlayer(this)) { - this._childPlayers.forEach(function(child) { - child.pause(); - }); - } - }); - - scope.hookMethod(playerProto, 'play', function() { - if (isGroupPlayer(this)) { - this._childPlayers.forEach(function(child) { - var time = child.currentTime; - child.play(); - child.currentTime = time; - }); - } - }); - - scope.hookMethod(playerProto, 'cancel', function() { - if (isGroupPlayer(this)) { - this.source = null; - this._removePlayers(); - } - }); - - scope.hookSetter(playerProto, 'currentTime', function(v) { - if (isGroupPlayer(this)) { - var offset = 0; - this._childPlayers.forEach(function(child) { - child.currentTime = v - offset; - if (this.source instanceof window.AnimationSequence) - offset += child.source.activeDuration; - }.bind(this)); - } - }); - - scope.hookSetter(playerProto, 'startTime', function(v) { - if (isGroupPlayer(this)) { - var offset = 0; - this._childPlayers.forEach(function(child) { - child.startTime = v + offset; - if (this.source instanceof window.AnimationSequence) - offset += child.source.activeDuration; - }.bind(this)); - } - }); }(webAnimationsShared, webAnimationsMaxifill, webAnimationsTesting)); diff --git a/src/effect-callback.js b/src/effect-callback.js index 0fcfa56..8261f60 100644 --- a/src/effect-callback.js +++ b/src/effect-callback.js @@ -18,11 +18,11 @@ Element.prototype.animate = function(effect, timing) { if (typeof effect == 'function') { - var player = originalAnimate.call(element, [], timing); + var player = new scope.Player(originalAnimate.call(element, [], timing)); bind(player, this, effect, timing); return player; } - return originalAnimate.call(this, effect, timing); + return new scope.Player(originalAnimate.call(this, effect, timing)); }; var sequenceNumber = 0; @@ -88,20 +88,9 @@ } } - var playerProto = Object.getPrototypeOf(document.documentElement.animate([])); - function registerHook() { + scope.Player.prototype._register = function() { if (this._callback) register(this._callback); }; - scope.hookMethod(playerProto, 'play', registerHook); - scope.hookMethod(playerProto, 'reverse', registerHook); - scope.hookMethod(playerProto, 'cancel', function() { - if (this._callback) { - register(this._callback); - this._callback._player = null; - } - }); - scope.hookSetter(playerProto, 'currentTime', registerHook); - scope.hookSetter(playerProto, 'startTime', registerHook); })(webAnimationsShared, webAnimationsMaxifill, webAnimationsTesting); diff --git a/src/hooks.js b/src/hooks.js deleted file mode 100644 index 133b9c4..0000000 --- a/src/hooks.js +++ /dev/null @@ -1,36 +0,0 @@ -// 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(scope) { - scope.hookSetter = function(proto, property, setter) { - var original = Object.getOwnPropertyDescriptor(proto, property); - Object.defineProperty(proto, property, { - enumerable: true, - configurable: true, - get: original.get, - set: function(v) { - original.set.call(this, v); - setter.call(this, v); - } - }); - }; - - scope.hookMethod = function(proto, property, f) { - var original = proto[property]; - proto[property] = function() { - var result = original.call(this); - f.call(this); - return result; - }; - }; -})(webAnimationsMaxifill); diff --git a/src/maxifill-player.js b/src/maxifill-player.js new file mode 100644 index 0000000..8fe6f6b --- /dev/null +++ b/src/maxifill-player.js @@ -0,0 +1,139 @@ +// 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.Player = function(player) { + this.source = null; + this._isGroup = false; + this._player = player; + this._childPlayers = []; + this._callback = null; + }; + + // TODO: add a source getter/setter + scope.Player.prototype = { + get paused() { + return this._player.paused; + }, + get onfinish() { + return this._onfinish; + }, + set onfinish(v) { + if (typeof v == 'function') { + this._onfinish = v; + this._player.onfinish = (function(e) { + e.target = this; + v.call(this, e); + }).bind(this); + } else { + this._player.onfinish = v; + this.onfinish = this._player.onfinish; + } + }, + get currentTime() { + return this._player.currentTime; + }, + set currentTime(v) { + this._player.currentTime = v; + this._register(); + this._forEachChild(function(child, offset) { + child.currentTime = v - offset; + }); + }, + get startTime() { + return this._player.startTime; + }, + set startTime(v) { + this._player.startTime = v; + this._register(); + this._forEachChild(function(child, offset) { + child.startTime = v + offset; + }); + }, + get playbackRate() { + return this._player.playbackRate; + }, + get finished() { + return this._player.finished; + }, + play: function() { + this._player.play(); + this._register(); + scope.awaitStartTime(this); + this._forEachChild(function(child) { + var time = child.currentTime; + child.play(); + child.currentTime = time; + }); + }, + pause: function() { + this._player.pause(); + this._register(); + this._forEachChild(function(child) { + child.pause(); + }); + }, + finish: function() { + this._player.finish(); + this._register(); + // TODO: child players?? + }, + cancel: function() { + this._player.cancel(); + if (this._callback) { + this._register(); + this._callback._player = null; + } + this.source = null; + this._removePlayers(); + }, + reverse: function() { + this._player.reverse(); + scope.awaitStartTime(this); + this._register(); + this._forEachChild(function(child, offset) { + child.reverse(); + child.startTime = this.startTime + offset * this.playbackRate; + child.currentTime = this.currentTime + offset * this.playbackRate; + }); + }, + 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._player.addEventListener(type, wrapped); + }, + removeEventListener: function(type, handler) { + this._player.removeEventListener(type, (handler && handler._wrapper) || handler); + }, + _removePlayers: function() { + while (this._childPlayers.length) + this._childPlayers.pop().cancel(); + }, + _forEachChild: function(f) { + var offset = 0; + this._childPlayers.forEach(function(child) { + f.call(this, child, offset); + if (this.source instanceof window.AnimationSequence) + offset += child.source.activeDuration; + }.bind(this)); + }, + }; + +})(webAnimationsShared, webAnimationsMaxifill, webAnimationsTesting); diff --git a/target-config.js b/target-config.js index a02ae1e..4e6ab50 100644 --- a/target-config.js +++ b/target-config.js @@ -19,7 +19,7 @@ 'src/transform-handler.js'); var maxifillSrc = minifillSrc.concat( - 'src/hooks.js', + 'src/maxifill-player.js', 'src/animation-constructor.js', 'src/effect-callback.js', 'src/group-constructors.js'); diff --git a/test/js/group-constructors.js b/test/js/group-constructors.js index 26bdbe8..bab7748 100644 --- a/test/js/group-constructors.js +++ b/test/js/group-constructors.js @@ -22,11 +22,5 @@ suite('group-constructors', function() { tick(2100); assert.equal(p._childPlayers[1]._childPlayers[0].source.player, p); assert.equal(p._childPlayers[1]._childPlayers[1].source.player, p); - - assert.equal(p.source._nativePlayer.source, p.source); - assert.equal(p._childPlayers[0].source._nativePlayer.source, p._childPlayers[0].source); - assert.equal(p._childPlayers[1].source._nativePlayer.source, p._childPlayers[1].source); - assert.equal(p._childPlayers[1]._childPlayers[0].source._nativePlayer.source, p._childPlayers[1]._childPlayers[0].source); - assert.equal(p._childPlayers[1]._childPlayers[1].source._nativePlayer.source, p._childPlayers[1]._childPlayers[1].source); }); }); diff --git a/test/js/group-player-finish-event.js b/test/js/group-player-finish-event.js index b131d7a..d3857a3 100644 --- a/test/js/group-player-finish-event.js +++ b/test/js/group-player-finish-event.js @@ -10,7 +10,7 @@ suite('group-player-finish-event', function() { new Animation(this.element, [], 500), ]), ]); - this.player = this.element.animate([], 1000); + this.player = document.timeline.play(animation, 1000); }); teardown(function() { if (this.element.parent) diff --git a/test/js/group-player.js b/test/js/group-player.js index 0e12b9a..4182278 100644 --- a/test/js/group-player.js +++ b/test/js/group-player.js @@ -167,9 +167,9 @@ suite('group-player', function() { } if (typeof timingList[0] == 'number') { if (isNaN(timingList[0])) - assert.ok(isNaN(player._startTime), trace + 'expected NaN startTime'); + assert.ok(isNaN(player.startTime), trace + 'expected NaN startTime'); else - assert.equal(player._startTime, timingList[0], trace + ' startTime'); + assert.equal(player.startTime, timingList[0], trace + ' startTime'); assert.equal(player.currentTime, timingList[1], trace + ' currentTime'); } else { _checkTimes(player._childPlayers[index], timingList[0], 0, trace + ' ' + index); diff --git a/test/js/player.js b/test/js/player.js index 60b0ffb..575510a 100644 --- a/test/js/player.js +++ b/test/js/player.js @@ -88,7 +88,7 @@ suite('player', function() { tick(100); tick(1200); assert.equal(p.finished, true); - assert.equal(p._startTime, 100); + assert.equal(p.startTime, 100); assert.equal(p.currentTime, 1000); tick(1500); assert.equal(p.currentTime, 1000); @@ -97,7 +97,7 @@ suite('player', function() { assert.ok(isNaN(p._startTime)); assert.equal(p.currentTime, 1000); tick(1600); - assert.equal(p._startTime, 2600); + assert.equal(p.startTime, 2600); assert.equal(p.currentTime, 1000); }); test('playing after finishing works as expected', function() { @@ -106,16 +106,16 @@ suite('player', function() { tick(100); tick(1200); assert.equal(p.finished, true); - assert.equal(p._startTime, 100); + assert.equal(p.startTime, 100); assert.equal(p.currentTime, 1000); tick(1500); assert.equal(p.currentTime, 1000); assert.equal(isTicking(), false); p.play(); - assert.ok(isNaN(p._startTime)); + assert.ok(isNaN(p.startTime)); assert.equal(p.currentTime, 0); tick(1600); - assert.equal(p._startTime, 1600); + assert.equal(p.startTime, 1600); assert.equal(p.currentTime, 0); }); test('limiting works as expected', function() { @@ -259,11 +259,11 @@ suite('player', function() { tick(90); var nofill = document.body.animate([], 100); var fill = document.body.animate([], {duration: 100, fill: 'forwards'}); - assert.deepEqual(document.timeline.players, [nofill, fill]); + assert.deepEqual(document.timeline.players, [nofill._player || nofill, fill._player || fill]); tick(100); - assert.deepEqual(document.timeline.players, [nofill, fill]); + assert.deepEqual(document.timeline.players, [nofill._player || nofill, fill._player || fill]); tick(400); - assert.deepEqual(document.timeline.players, [fill]); + assert.deepEqual(document.timeline.players, [fill._player || fill]); }); test('discarded players get re-added on modification', function() { tick(90); @@ -272,14 +272,14 @@ suite('player', function() { tick(400); assert.deepEqual(document.timeline.players, []); player.currentTime = 0; - assert.deepEqual(document.timeline.players, [player]); + assert.deepEqual(document.timeline.players, [player._player || player]); }); test('players in the before phase are not discarded', function() { tick(100); var player = document.body.animate([], 100); player.currentTime = -50; tick(110); - assert.deepEqual(document.timeline.players, [player]); + assert.deepEqual(document.timeline.players, [player._player || player]); }); test('players that go out of effect should not clear the effect of players that are in effect', function() { var target = document.createElement('div'); diff --git a/web-animations.html b/web-animations.html index 5d3d3a4..f4083cc 100644 --- a/web-animations.html +++ b/web-animations.html @@ -28,7 +28,7 @@ - +