| // Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKModel} |
| * @param {!WebInspector.Target} target |
| */ |
| WebInspector.AnimationModel = function(target) |
| { |
| WebInspector.SDKModel.call(this, WebInspector.AnimationModel, target); |
| this._agent = target.animationAgent(); |
| target.registerAnimationDispatcher(new WebInspector.AnimationDispatcher(this)); |
| /** @type {!Map.<string, !WebInspector.AnimationModel.Animation>} */ |
| this._animationsById = new Map(); |
| /** @type {!Map.<string, !WebInspector.AnimationModel.AnimationGroup>} */ |
| this._animationGroups = new Map(); |
| /** @type {!Array.<string>} */ |
| this._pendingAnimations = []; |
| target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._mainFrameNavigated, this); |
| } |
| |
| WebInspector.AnimationModel.Events = { |
| AnimationGroupStarted: "AnimationGroupStarted" |
| } |
| |
| WebInspector.AnimationModel.prototype = { |
| _mainFrameNavigated: function() |
| { |
| this._animationsById.clear(); |
| this._animationGroups.clear(); |
| this._pendingAnimations = []; |
| }, |
| |
| /** |
| * @param {string} id |
| */ |
| animationCreated: function(id) |
| { |
| this._pendingAnimations.push(id); |
| }, |
| |
| /** |
| * @param {string} id |
| */ |
| _animationCanceled: function(id) |
| { |
| this._pendingAnimations.remove(id); |
| this._flushPendingAnimationsIfNeeded(); |
| }, |
| |
| /** |
| * @param {!AnimationAgent.Animation} payload |
| */ |
| animationStarted: function(payload) |
| { |
| var animation = WebInspector.AnimationModel.Animation.parsePayload(this.target(), payload); |
| |
| // Ignore Web Animations custom effects & groups. |
| if (animation.type() === "WebAnimation" && animation.source().keyframesRule().keyframes().length === 0) { |
| this._pendingAnimations.remove(animation.id()); |
| } else { |
| this._animationsById.set(animation.id(), animation); |
| if (this._pendingAnimations.indexOf(animation.id()) === -1) |
| this._pendingAnimations.push(animation.id()); |
| } |
| |
| this._flushPendingAnimationsIfNeeded(); |
| }, |
| |
| _flushPendingAnimationsIfNeeded: function() |
| { |
| for (var id of this._pendingAnimations) { |
| if (!this._animationsById.get(id)) |
| return; |
| } |
| |
| while (this._pendingAnimations.length) |
| this._matchExistingGroups(this._createGroupFromPendingAnimations()); |
| }, |
| |
| /** |
| * @param {!WebInspector.AnimationModel.AnimationGroup} incomingGroup |
| * @return {boolean} |
| */ |
| _matchExistingGroups: function(incomingGroup) |
| { |
| var matchedGroup = null; |
| for (var group of this._animationGroups.values()) { |
| if (group._matches(incomingGroup)) { |
| matchedGroup = group; |
| group._update(incomingGroup); |
| break; |
| } |
| } |
| |
| if (!matchedGroup) |
| this._animationGroups.set(incomingGroup.id(), incomingGroup); |
| this.dispatchEventToListeners(WebInspector.AnimationModel.Events.AnimationGroupStarted, matchedGroup || incomingGroup); |
| return !!matchedGroup; |
| }, |
| |
| /** |
| * @return {!WebInspector.AnimationModel.AnimationGroup} |
| */ |
| _createGroupFromPendingAnimations: function() |
| { |
| console.assert(this._pendingAnimations.length); |
| var groupedAnimations = [this._animationsById.get(this._pendingAnimations.shift())]; |
| var remainingAnimations = []; |
| for (var id of this._pendingAnimations) { |
| var anim = this._animationsById.get(id); |
| if (anim.startTime() === groupedAnimations[0].startTime()) |
| groupedAnimations.push(anim); |
| else |
| remainingAnimations.push(id); |
| } |
| this._pendingAnimations = remainingAnimations; |
| return new WebInspector.AnimationModel.AnimationGroup(this.target(), groupedAnimations[0].id(), groupedAnimations); |
| }, |
| |
| /** |
| * @return {!Promise.<number>} |
| */ |
| playbackRatePromise: function() |
| { |
| /** |
| * @param {?Protocol.Error} error |
| * @param {number} playbackRate |
| * @return {number} |
| */ |
| function callback(error, playbackRate) |
| { |
| if (error) |
| return 1; |
| return playbackRate; |
| } |
| |
| return this._agent.getPlaybackRate(callback).catchException(1); |
| }, |
| |
| /** |
| * @param {number} playbackRate |
| */ |
| setPlaybackRate: function(playbackRate) |
| { |
| this._agent.setPlaybackRate(playbackRate); |
| }, |
| |
| ensureEnabled: function() |
| { |
| if (this._enabled) |
| return; |
| this._agent.enable(); |
| this._enabled = true; |
| }, |
| |
| __proto__: WebInspector.SDKModel.prototype |
| } |
| |
| WebInspector.AnimationModel._symbol = Symbol("AnimationModel"); |
| |
| /** |
| * @param {!WebInspector.Target} target |
| * @return {!WebInspector.AnimationModel} |
| */ |
| WebInspector.AnimationModel.fromTarget = function(target) |
| { |
| if (!target[WebInspector.AnimationModel._symbol]) |
| target[WebInspector.AnimationModel._symbol] = new WebInspector.AnimationModel(target); |
| |
| return target[WebInspector.AnimationModel._symbol]; |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKObject} |
| * @param {!WebInspector.Target} target |
| * @param {!AnimationAgent.Animation} payload |
| */ |
| WebInspector.AnimationModel.Animation = function(target, payload) |
| { |
| WebInspector.SDKObject.call(this, target); |
| this._payload = payload; |
| this._source = new WebInspector.AnimationModel.AnimationEffect(this.target(), this._payload.source); |
| } |
| |
| /** |
| * @param {!WebInspector.Target} target |
| * @param {!AnimationAgent.Animation} payload |
| * @return {!WebInspector.AnimationModel.Animation} |
| */ |
| WebInspector.AnimationModel.Animation.parsePayload = function(target, payload) |
| { |
| return new WebInspector.AnimationModel.Animation(target, payload); |
| } |
| |
| /** @enum {string} */ |
| WebInspector.AnimationModel.Animation.Type = { |
| CSSTransition: "CSSTransition", |
| CSSAnimation: "CSSAnimation", |
| WebAnimation: "WebAnimation" |
| } |
| |
| WebInspector.AnimationModel.Animation.prototype = { |
| /** |
| * @return {!AnimationAgent.Animation} |
| */ |
| payload: function() |
| { |
| return this._payload; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| id: function() |
| { |
| return this._payload.id; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| name: function() |
| { |
| return this.source().name(); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| paused: function() |
| { |
| return this._payload.pausedState; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| playState: function() |
| { |
| return this._playState || this._payload.playState; |
| }, |
| |
| /** |
| * @param {string} playState |
| */ |
| setPlayState: function(playState) |
| { |
| this._playState = playState; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| playbackRate: function() |
| { |
| return this._payload.playbackRate; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| startTime: function() |
| { |
| return this._payload.startTime; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| endTime: function() |
| { |
| if (!this.source().iterations) |
| return Infinity; |
| return this.startTime() + this.source().delay() + this.source().duration() * this.source().iterations() + this.source().endDelay(); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| currentTime: function() |
| { |
| return this._payload.currentTime; |
| }, |
| |
| /** |
| * @return {!WebInspector.AnimationModel.AnimationEffect} |
| */ |
| source: function() |
| { |
| return this._source; |
| }, |
| |
| /** |
| * @return {!WebInspector.AnimationModel.Animation.Type} |
| */ |
| type: function() |
| { |
| return /** @type {!WebInspector.AnimationModel.Animation.Type} */(this._payload.type); |
| }, |
| |
| /** |
| * @param {!WebInspector.AnimationModel.Animation} animation |
| * @return {boolean} |
| */ |
| overlaps: function(animation) |
| { |
| // Infinite animations |
| if (!this.source().iterations() || !animation.source().iterations()) |
| return true; |
| |
| var firstAnimation = this.startTime() < animation.startTime() ? this : animation; |
| var secondAnimation = firstAnimation === this ? animation : this; |
| return firstAnimation.endTime() >= secondAnimation.startTime(); |
| }, |
| |
| /** |
| * @param {number} duration |
| * @param {number} delay |
| */ |
| setTiming: function(duration, delay) |
| { |
| this._source.node().then(this._updateNodeStyle.bind(this, duration, delay)); |
| this._source._duration = duration; |
| this._source._delay = delay; |
| this.target().animationAgent().setTiming(this.id(), duration, delay); |
| }, |
| |
| /** |
| * @param {number} duration |
| * @param {number} delay |
| * @param {!WebInspector.DOMNode} node |
| */ |
| _updateNodeStyle: function(duration, delay, node) |
| { |
| var animationPrefix; |
| if (this.type() == WebInspector.AnimationModel.Animation.Type.CSSTransition) |
| animationPrefix = "transition-"; |
| else if (this.type() == WebInspector.AnimationModel.Animation.Type.CSSAnimation) |
| animationPrefix = "animation-"; |
| else |
| return; |
| |
| var cssModel = WebInspector.CSSStyleModel.fromTarget(node.target()); |
| if (!cssModel) |
| return; |
| cssModel.setEffectivePropertyValueForNode(node.id, animationPrefix + "duration", duration + "ms"); |
| cssModel.setEffectivePropertyValueForNode(node.id, animationPrefix + "delay", delay + "ms"); |
| }, |
| |
| /** |
| * @return {!Promise.<?WebInspector.RemoteObject>} |
| */ |
| remoteObjectPromise: function() |
| { |
| /** |
| * @param {?Protocol.Error} error |
| * @param {!RuntimeAgent.RemoteObject} payload |
| * @return {?WebInspector.RemoteObject} |
| * @this {!WebInspector.AnimationModel.Animation} |
| */ |
| function callback(error, payload) |
| { |
| return !error ? this.target().runtimeModel.createRemoteObject(payload) : null; |
| } |
| |
| return this.target().animationAgent().resolveAnimation(this.id(), callback.bind(this)); |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| _cssId: function() |
| { |
| return this._payload.cssId || ""; |
| }, |
| |
| __proto__: WebInspector.SDKObject.prototype |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKObject} |
| * @param {!WebInspector.Target} target |
| * @param {!AnimationAgent.AnimationEffect} payload |
| */ |
| WebInspector.AnimationModel.AnimationEffect = function(target, payload) |
| { |
| WebInspector.SDKObject.call(this, target); |
| this._payload = payload; |
| if (payload.keyframesRule) |
| this._keyframesRule = new WebInspector.AnimationModel.KeyframesRule(target, payload.keyframesRule); |
| this._delay = this._payload.delay; |
| this._duration = this._payload.duration; |
| } |
| |
| WebInspector.AnimationModel.AnimationEffect.prototype = { |
| /** |
| * @return {number} |
| */ |
| delay: function() |
| { |
| return this._delay; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| endDelay: function() |
| { |
| return this._payload.endDelay; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| playbackRate: function() |
| { |
| return this._payload.playbackRate; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| iterationStart: function() |
| { |
| return this._payload.iterationStart; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| iterations: function() |
| { |
| // Animations with zero duration, zero delays and infinite iterations can't be shown. |
| if (!this.delay() && !this.endDelay() && !this.duration()) |
| return 0; |
| return this._payload.iterations || Infinity; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| duration: function() |
| { |
| return this._duration; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| direction: function() |
| { |
| return this._payload.direction; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| fill: function() |
| { |
| return this._payload.fill; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| name: function() |
| { |
| return this._payload.name; |
| }, |
| |
| /** |
| * @return {!Promise.<!WebInspector.DOMNode>} |
| */ |
| node: function() |
| { |
| if (!this._deferredNode) |
| this._deferredNode = new WebInspector.DeferredDOMNode(this.target(), this.backendNodeId()); |
| return this._deferredNode.resolvePromise(); |
| }, |
| |
| /** |
| * @return {!WebInspector.DeferredDOMNode} |
| */ |
| deferredNode: function() |
| { |
| return new WebInspector.DeferredDOMNode(this.target(), this.backendNodeId()); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| backendNodeId: function() |
| { |
| return this._payload.backendNodeId; |
| }, |
| |
| /** |
| * @return {?WebInspector.AnimationModel.KeyframesRule} |
| */ |
| keyframesRule: function() |
| { |
| return this._keyframesRule; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| easing: function() |
| { |
| return this._payload.easing; |
| }, |
| |
| __proto__: WebInspector.SDKObject.prototype |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKObject} |
| * @param {!WebInspector.Target} target |
| * @param {!AnimationAgent.KeyframesRule} payload |
| */ |
| WebInspector.AnimationModel.KeyframesRule = function(target, payload) |
| { |
| WebInspector.SDKObject.call(this, target); |
| this._payload = payload; |
| this._keyframes = this._payload.keyframes.map(function (keyframeStyle) { |
| return new WebInspector.AnimationModel.KeyframeStyle(target, keyframeStyle); |
| }); |
| } |
| |
| WebInspector.AnimationModel.KeyframesRule.prototype = { |
| /** |
| * @param {!Array.<!AnimationAgent.KeyframeStyle>} payload |
| */ |
| _setKeyframesPayload: function(payload) |
| { |
| this._keyframes = payload.map(function (keyframeStyle) { |
| return new WebInspector.AnimationModel.KeyframeStyle(this._target, keyframeStyle); |
| }); |
| }, |
| |
| /** |
| * @return {string|undefined} |
| */ |
| name: function() |
| { |
| return this._payload.name; |
| }, |
| |
| /** |
| * @return {!Array.<!WebInspector.AnimationModel.KeyframeStyle>} |
| */ |
| keyframes: function() |
| { |
| return this._keyframes; |
| }, |
| |
| __proto__: WebInspector.SDKObject.prototype |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKObject} |
| * @param {!WebInspector.Target} target |
| * @param {!AnimationAgent.KeyframeStyle} payload |
| */ |
| WebInspector.AnimationModel.KeyframeStyle = function(target, payload) |
| { |
| WebInspector.SDKObject.call(this, target); |
| this._payload = payload; |
| this._offset = this._payload.offset; |
| } |
| |
| WebInspector.AnimationModel.KeyframeStyle.prototype = { |
| /** |
| * @return {string} |
| */ |
| offset: function() |
| { |
| return this._offset; |
| }, |
| |
| /** |
| * @param {number} offset |
| */ |
| setOffset: function(offset) |
| { |
| this._offset = offset * 100 + "%"; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| offsetAsNumber: function() |
| { |
| return parseFloat(this._offset) / 100; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| easing: function() |
| { |
| return this._payload.easing; |
| }, |
| |
| __proto__: WebInspector.SDKObject.prototype |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.SDKObject} |
| * @param {!WebInspector.Target} target |
| * @param {string} id |
| * @param {!Array.<!WebInspector.AnimationModel.Animation>} animations |
| */ |
| WebInspector.AnimationModel.AnimationGroup = function(target, id, animations) |
| { |
| WebInspector.SDKObject.call(this, target); |
| this._id = id; |
| this._animations = animations; |
| this._paused = false; |
| } |
| |
| WebInspector.AnimationModel.AnimationGroup.prototype = { |
| /** |
| * @return {string} |
| */ |
| id: function() |
| { |
| return this._id; |
| }, |
| |
| /** |
| * @return {!Array.<!WebInspector.AnimationModel.Animation>} |
| */ |
| animations: function() |
| { |
| return this._animations; |
| }, |
| |
| /** |
| * @return {!Array.<string>} |
| */ |
| _animationIds: function() |
| { |
| /** |
| * @param {!WebInspector.AnimationModel.Animation} animation |
| * @return {string} |
| */ |
| function extractId(animation) |
| { |
| return animation.id(); |
| } |
| |
| return this._animations.map(extractId); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| startTime: function() |
| { |
| return this._animations[0].startTime(); |
| }, |
| |
| /** |
| * @param {number} currentTime |
| */ |
| seekTo: function(currentTime) |
| { |
| this.target().animationAgent().seekAnimations(this._animationIds(), currentTime); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| paused: function() |
| { |
| return this._paused; |
| }, |
| |
| /** |
| * @param {boolean} paused |
| */ |
| togglePause: function(paused) |
| { |
| if (paused === this._paused) |
| return; |
| this._paused = paused; |
| this.target().animationAgent().setPaused(this._animationIds(), paused); |
| }, |
| |
| /** |
| * @return {!Promise.<number>} |
| */ |
| currentTimePromise: function() |
| { |
| /** |
| * @param {?Protocol.Error} error |
| * @param {number} currentTime |
| * @return {number} |
| */ |
| function callback(error, currentTime) |
| { |
| return !error ? currentTime : 0; |
| } |
| |
| return this.target().animationAgent().getCurrentTime(this._animations[0].id(), callback).catchException(0); |
| }, |
| |
| /** |
| * @param {!WebInspector.AnimationModel.AnimationGroup} group |
| * @return {boolean} |
| */ |
| _matches: function(group) |
| { |
| /** |
| * @param {!WebInspector.AnimationModel.Animation} anim |
| * @return {string} |
| */ |
| function extractId(anim) |
| { |
| if (anim.type() === WebInspector.AnimationModel.Animation.Type.WebAnimation) |
| return anim.type() + anim.id(); |
| else |
| return anim._cssId(); |
| } |
| |
| if (this._animations.length !== group._animations.length) |
| return false; |
| var left = this._animations.map(extractId).sort(); |
| var right = group._animations.map(extractId).sort(); |
| for (var i = 0; i < left.length; i++) { |
| if (left[i] !== right[i]) |
| return false; |
| } |
| return true; |
| }, |
| |
| /** |
| * @param {!WebInspector.AnimationModel.AnimationGroup} group |
| */ |
| _update: function(group) |
| { |
| this._animations = group._animations; |
| }, |
| |
| __proto__: WebInspector.SDKObject.prototype |
| } |
| |
| |
| /** |
| * @constructor |
| * @implements {AnimationAgent.Dispatcher} |
| */ |
| WebInspector.AnimationDispatcher = function(animationModel) |
| { |
| this._animationModel = animationModel; |
| } |
| |
| WebInspector.AnimationDispatcher.prototype = { |
| /** |
| * @override |
| * @param {string} id |
| */ |
| animationCreated: function(id) |
| { |
| this._animationModel.animationCreated(id); |
| }, |
| |
| /** |
| * @override |
| * @param {string} id |
| */ |
| animationCanceled: function(id) |
| { |
| this._animationModel._animationCanceled(id); |
| }, |
| |
| /** |
| * @override |
| * @param {!AnimationAgent.Animation} payload |
| */ |
| animationStarted: function(payload) |
| { |
| this._animationModel.animationStarted(payload); |
| } |
| } |