From 162722dd52f1af62d96aff4625722738a1fadd7d Mon Sep 17 00:00:00 2001 From: Daisuke Akatsuka Date: Tue, 13 Mar 2018 16:45:19 +0900 Subject: [PATCH] Bug 1431573 - Part 4: Implement time label. r=gl MozReview-Commit-ID: Cg6A4hNLXnO --- .../client/inspector/animation/animation.js | 92 ++++++++++++++++++- .../components/AnimationListContainer.js | 5 +- .../animation/components/AnimationToolbar.js | 11 +++ .../inspector/animation/components/App.js | 10 ++ .../animation/components/CurrentTimeLabel.js | 83 +++++++++++++++++ .../animation/components/PauseResumeButton.js | 3 +- .../inspector/animation/components/moz.build | 1 + .../animation/reducers/animations.js | 4 + .../inspector/animation/utils/timescale.js | 5 + .../client/inspector/animation/utils/utils.js | 11 +++ 10 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 devtools/client/inspector/animation/components/CurrentTimeLabel.js diff --git a/devtools/client/inspector/animation/animation.js b/devtools/client/inspector/animation/animation.js index a54283323f01..3f5ea93d3ed7 100644 --- a/devtools/client/inspector/animation/animation.js +++ b/devtools/client/inspector/animation/animation.js @@ -19,16 +19,23 @@ const { updateSelectedAnimation, updateSidebarSize } = require("./actions/animations"); -const { isAllAnimationEqual } = require("./utils/utils"); +const { + isAllAnimationEqual, + hasPlayingAnimation, +} = require("./utils/utils"); class AnimationInspector { constructor(inspector, win) { this.inspector = inspector; this.win = win; + this.addAnimationsCurrentTimeListener = + this.addAnimationsCurrentTimeListener.bind(this); this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this); this.getComputedStyle = this.getComputedStyle.bind(this); this.getNodeFromActor = this.getNodeFromActor.bind(this); + this.removeAnimationsCurrentTimeListener = + this.removeAnimationsCurrentTimeListener.bind(this); this.rewindAnimationsCurrentTime = this.rewindAnimationsCurrentTime.bind(this); this.selectAnimation = this.selectAnimation.bind(this); this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this); @@ -36,6 +43,7 @@ class AnimationInspector { this.simulateAnimation = this.simulateAnimation.bind(this); this.toggleElementPicker = this.toggleElementPicker.bind(this); this.update = this.update.bind(this); + this.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this); this.onElementPickerStarted = this.onElementPickerStarted.bind(this); this.onElementPickerStopped = this.onElementPickerStopped.bind(this); this.onSidebarResized = this.onSidebarResized.bind(this); @@ -58,10 +66,13 @@ class AnimationInspector { } = this.inspector.getPanel("boxmodel").getComponentProps(); const { + addAnimationsCurrentTimeListener, emit: emitEventForTest, getAnimatedPropertyMap, getComputedStyle, getNodeFromActor, + isAnimationsRunning, + removeAnimationsCurrentTimeListener, rewindAnimationsCurrentTime, selectAnimation, setAnimationsPlayState, @@ -73,6 +84,8 @@ class AnimationInspector { const target = this.inspector.target; this.animationsFront = new AnimationsFront(target.client, target.form); + this.animationsCurrentTimeListeners = []; + const provider = createElement(Provider, { id: "newanimationinspector", @@ -81,12 +94,15 @@ class AnimationInspector { }, App( { + addAnimationsCurrentTimeListener, emitEventForTest, getAnimatedPropertyMap, getComputedStyle, getNodeFromActor, + isAnimationsRunning, onHideBoxModelHighlighter, onShowBoxModelHighlighterForNode, + removeAnimationsCurrentTimeListener, rewindAnimationsCurrentTime, selectAnimation, setAnimationsPlayState, @@ -123,6 +139,8 @@ class AnimationInspector { this.simulatedElement = null; } + this.stopAnimationsCurrentTimeTimer(); + this.inspector = null; this.win = null; } @@ -131,6 +149,10 @@ class AnimationInspector { return this.inspector.store.getState().animations; } + addAnimationsCurrentTimeListener(listener) { + this.animationsCurrentTimeListeners.push(listener); + } + /** * Return a map of animated property from given animation actor. * @@ -201,6 +223,12 @@ class AnimationInspector { this.inspector.sidebar.getCurrentTabID() === "newanimationinspector"; } + onAnimationsCurrentTimeUpdated(currentTime) { + for (const listener of this.animationsCurrentTimeListeners) { + listener(currentTime); + } + } + onElementPickerStarted() { this.inspector.store.dispatch(updateElementPickerEnabled(true)); } @@ -222,10 +250,16 @@ class AnimationInspector { this.inspector.store.dispatch(updateSidebarSize(size)); } + removeAnimationsCurrentTimeListener(listener) { + this.animationsCurrentTimeListeners = + this.animationsCurrentTimeListeners.filter(l => l !== listener); + } + async rewindAnimationsCurrentTime() { const animations = this.state.animations; await this.animationsFront.setCurrentTimes(animations, 0, true); - this.updateAnimations(animations); + await this.updateAnimations(animations); + this.onAnimationsCurrentTimeUpdated(0); } selectAnimation(animation) { @@ -288,6 +322,19 @@ class AnimationInspector { return this.simulatedAnimation; } + stopAnimationsCurrentTimeTimer() { + if (this.currentTimeTimer) { + this.currentTimeTimer.destroy(); + this.currentTimeTimer = null; + } + } + + startAnimationsCurrentTimeTimer() { + const currentTimeTimer = new CurrentTimeTimer(this); + currentTimeTimer.start(); + this.currentTimeTimer = currentTimeTimer; + } + toggleElementPicker() { this.inspector.toolbox.highlighterUtils.togglePicker(); } @@ -325,6 +372,8 @@ class AnimationInspector { } updateState(animations) { + this.stopAnimationsCurrentTimeTimer(); + this.inspector.store.dispatch(updateAnimations(animations)); // If number of displayed animations is one, we select the animation automatically. // But if selected animation is in given animations, ignores. @@ -334,6 +383,45 @@ class AnimationInspector { !animations.find(animation => animation.actorID === selectedAnimation.actorID)) { this.selectAnimation(animations.length === 1 ? animations[0] : null); } + + if (hasPlayingAnimation(animations)) { + this.startAnimationsCurrentTimeTimer(); + } + } +} + +class CurrentTimeTimer { + constructor(animationInspector) { + const timeScale = animationInspector.state.timeScale; + this.baseCurrentTime = timeScale.documentCurrentTime - timeScale.minStartTime; + this.startTime = animationInspector.win.performance.now(); + this.animationInspector = animationInspector; + + this.next = this.next.bind(this); + } + + destroy() { + this.stop(); + this.animationInspector = null; + } + + next() { + if (this.doStop) { + return; + } + + const { onAnimationsCurrentTimeUpdated, win } = this.animationInspector; + const currentTime = this.baseCurrentTime + win.performance.now() - this.startTime; + onAnimationsCurrentTimeUpdated(currentTime); + win.requestAnimationFrame(this.next); + } + + start() { + this.next(); + } + + stop() { + this.doStop = true; } } diff --git a/devtools/client/inspector/animation/components/AnimationListContainer.js b/devtools/client/inspector/animation/components/AnimationListContainer.js index 40242a993672..c9fab9c84013 100644 --- a/devtools/client/inspector/animation/components/AnimationListContainer.js +++ b/devtools/client/inspector/animation/components/AnimationListContainer.js @@ -12,8 +12,6 @@ const dom = require("devtools/client/shared/vendor/react-dom-factories"); const AnimationList = createFactory(require("./AnimationList")); const AnimationListHeader = createFactory(require("./AnimationListHeader")); -const TimeScale = require("../utils/timescale"); - class AnimationListContainer extends PureComponent { static get propTypes() { return { @@ -26,6 +24,7 @@ class AnimationListContainer extends PureComponent { selectAnimation: PropTypes.func.isRequired, setSelectedNode: PropTypes.func.isRequired, simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, }; } @@ -40,8 +39,8 @@ class AnimationListContainer extends PureComponent { selectAnimation, setSelectedNode, simulateAnimation, + timeScale, } = this.props; - const timeScale = new TimeScale(animations); return dom.div( { diff --git a/devtools/client/inspector/animation/components/AnimationToolbar.js b/devtools/client/inspector/animation/components/AnimationToolbar.js index 47001cd0eb71..29fd88243a95 100644 --- a/devtools/client/inspector/animation/components/AnimationToolbar.js +++ b/devtools/client/inspector/animation/components/AnimationToolbar.js @@ -8,13 +8,16 @@ const { createFactory, PureComponent } = require("devtools/client/shared/vendor/ const dom = require("devtools/client/shared/vendor/react-dom-factories"); const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const CurrentTimeLabel = createFactory(require("./CurrentTimeLabel")); const PauseResumeButton = createFactory(require("./PauseResumeButton")); const RewindButton = createFactory(require("./RewindButton")); class AnimationToolbar extends PureComponent { static get propTypes() { return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, animations: PropTypes.arrayOf(PropTypes.object).isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, rewindAnimationsCurrentTime: PropTypes.func.isRequired, setAnimationsPlayState: PropTypes.func.isRequired, }; @@ -22,7 +25,9 @@ class AnimationToolbar extends PureComponent { render() { const { + addAnimationsCurrentTimeListener, animations, + removeAnimationsCurrentTimeListener, rewindAnimationsCurrentTime, setAnimationsPlayState, } = this.props; @@ -41,6 +46,12 @@ class AnimationToolbar extends PureComponent { animations, setAnimationsPlayState, } + ), + CurrentTimeLabel( + { + addAnimationsCurrentTimeListener, + removeAnimationsCurrentTimeListener, + } ) ); } diff --git a/devtools/client/inspector/animation/components/App.js b/devtools/client/inspector/animation/components/App.js index ef62f683777e..3df9dc23dd2e 100644 --- a/devtools/client/inspector/animation/components/App.js +++ b/devtools/client/inspector/animation/components/App.js @@ -18,6 +18,7 @@ const SplitBox = createFactory(require("devtools/client/shared/components/splitt class App extends PureComponent { static get propTypes() { return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, animations: PropTypes.arrayOf(PropTypes.object).isRequired, detailVisibility: PropTypes.bool.isRequired, emitEventForTest: PropTypes.func.isRequired, @@ -26,12 +27,14 @@ class App extends PureComponent { getNodeFromActor: PropTypes.func.isRequired, onHideBoxModelHighlighter: PropTypes.func.isRequired, onShowBoxModelHighlighterForNode: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, rewindAnimationsCurrentTime: PropTypes.func.isRequired, selectAnimation: PropTypes.func.isRequired, setAnimationsPlayState: PropTypes.func.isRequired, setDetailVisibility: PropTypes.func.isRequired, setSelectedNode: PropTypes.func.isRequired, simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, toggleElementPicker: PropTypes.func.isRequired, }; } @@ -42,6 +45,7 @@ class App extends PureComponent { render() { const { + addAnimationsCurrentTimeListener, animations, detailVisibility, emitEventForTest, @@ -50,12 +54,14 @@ class App extends PureComponent { getNodeFromActor, onHideBoxModelHighlighter, onShowBoxModelHighlighterForNode, + removeAnimationsCurrentTimeListener, rewindAnimationsCurrentTime, selectAnimation, setAnimationsPlayState, setDetailVisibility, setSelectedNode, simulateAnimation, + timeScale, toggleElementPicker, } = this.props; @@ -68,7 +74,9 @@ class App extends PureComponent { [ AnimationToolbar( { + addAnimationsCurrentTimeListener, animations, + removeAnimationsCurrentTimeListener, rewindAnimationsCurrentTime, setAnimationsPlayState, } @@ -98,6 +106,7 @@ class App extends PureComponent { selectAnimation, setSelectedNode, simulateAnimation, + timeScale, } ), vert: false, @@ -117,6 +126,7 @@ const mapStateToProps = state => { return { animations: state.animations.animations, detailVisibility: state.animations.detailVisibility, + timeScale: state.animations.timeScale, }; }; diff --git a/devtools/client/inspector/animation/components/CurrentTimeLabel.js b/devtools/client/inspector/animation/components/CurrentTimeLabel.js new file mode 100644 index 000000000000..fa25b2c46a1b --- /dev/null +++ b/devtools/client/inspector/animation/components/CurrentTimeLabel.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const dom = require("devtools/client/shared/vendor/react-dom-factories"); +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); + +class CurrentTimeLabel extends PureComponent { + static get propTypes() { + return { + addAnimationsCurrentTimeListener: PropTypes.func.isRequired, + removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + const { addAnimationsCurrentTimeListener } = props; + this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this); + + this.state = { + currentTime: 0, + }; + + addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + componentWillUnmount() { + const { removeAnimationsCurrentTimeListener } = this.props; + removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated); + } + + onCurrentTimeUpdated(currentTime) { + this.setState({ currentTime }); + } + + render() { + const { currentTime } = this.state; + + return dom.label( + { + className: "current-time-label", + }, + formatStopwatchTime(currentTime) + ); + } +} + +/** + * Format a timestamp (in ms) as a mm:ss.mmm string. + * + * @param {Number} time + * @return {String} + */ +function formatStopwatchTime(time) { + // Format falsy values as 0 + if (!time) { + return "00:00.000"; + } + + let milliseconds = parseInt(time % 1000, 10); + let seconds = parseInt((time / 1000) % 60, 10); + let minutes = parseInt((time / (1000 * 60)), 10); + + let pad = (nb, max) => { + if (nb < max) { + return new Array((max + "").length - (nb + "").length + 1).join("0") + nb; + } + return nb; + }; + + minutes = pad(minutes, 10); + seconds = pad(seconds, 10); + milliseconds = pad(milliseconds, 100); + + return `${minutes}:${seconds}.${milliseconds}`; +} + +module.exports = CurrentTimeLabel; diff --git a/devtools/client/inspector/animation/components/PauseResumeButton.js b/devtools/client/inspector/animation/components/PauseResumeButton.js index 93ff85f25e38..2cdaa502fd3a 100644 --- a/devtools/client/inspector/animation/components/PauseResumeButton.js +++ b/devtools/client/inspector/animation/components/PauseResumeButton.js @@ -9,6 +9,7 @@ const dom = require("devtools/client/shared/vendor/react-dom-factories"); const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); const { getStr } = require("../utils/l10n"); +const { hasPlayingAnimation } = require("../utils/utils"); class PauseResumeButton extends PureComponent { static get propTypes() { @@ -43,7 +44,7 @@ class PauseResumeButton extends PureComponent { updateState() { const { animations } = this.props; - const isPlaying = animations.some(({state}) => state.playState === "running"); + const isPlaying = hasPlayingAnimation(animations); this.setState({ isPlaying }); } diff --git a/devtools/client/inspector/animation/components/moz.build b/devtools/client/inspector/animation/components/moz.build index 39c7bbdea172..3aeb402294fa 100644 --- a/devtools/client/inspector/animation/components/moz.build +++ b/devtools/client/inspector/animation/components/moz.build @@ -24,6 +24,7 @@ DevToolsModules( 'AnimationTimelineTickList.js', 'AnimationToolbar.js', 'App.js', + 'CurrentTimeLabel.js', 'KeyframesProgressTickItem.js', 'KeyframesProgressTickList.js', 'NoAnimationPanel.js', diff --git a/devtools/client/inspector/animation/reducers/animations.js b/devtools/client/inspector/animation/reducers/animations.js index 1591b7e36f88..fe20e2129bfc 100644 --- a/devtools/client/inspector/animation/reducers/animations.js +++ b/devtools/client/inspector/animation/reducers/animations.js @@ -12,6 +12,8 @@ const { UPDATE_SIDEBAR_SIZE, } = require("../actions/index"); +const TimeScale = require("../utils/timescale"); + const INITIAL_STATE = { animations: [], detailVisibility: false, @@ -21,12 +23,14 @@ const INITIAL_STATE = { height: 0, width: 0, }, + timeScale: null, }; const reducers = { [UPDATE_ANIMATIONS](state, { animations }) { return Object.assign({}, state, { animations, + timeScale: new TimeScale(animations), }); }, diff --git a/devtools/client/inspector/animation/utils/timescale.js b/devtools/client/inspector/animation/utils/timescale.js index 17d58eb74b83..cb16b1c67172 100644 --- a/devtools/client/inspector/animation/utils/timescale.js +++ b/devtools/client/inspector/animation/utils/timescale.js @@ -24,6 +24,8 @@ class TimeScale { constructor(animations) { this.minStartTime = Infinity; this.maxEndTime = 0; + this.documentCurrentTime = 0; + for (const animation of animations) { this.addAnimation(animation.state); } @@ -38,6 +40,7 @@ class TimeScale { addAnimation(state) { let { delay, + documentCurrentTime, duration, endDelay = 0, iterationCount, @@ -67,6 +70,8 @@ class TimeScale { const length = toRate(delay) + rateRelativeDuration + toRate(minZero(endDelay)); const endTime = previousStartTime + length; this.maxEndTime = Math.max(this.maxEndTime, endTime); + + this.documentCurrentTime = Math.max(this.documentCurrentTime, documentCurrentTime); } /** diff --git a/devtools/client/inspector/animation/utils/utils.js b/devtools/client/inspector/animation/utils/utils.js index c7969a8cbadc..9512e43c6db4 100644 --- a/devtools/client/inspector/animation/utils/utils.js +++ b/devtools/client/inspector/animation/utils/utils.js @@ -69,6 +69,16 @@ function isAllAnimationEqual(animationsA, animationsB) { return true; } +/** + * Check wether the animations are running at least one. + * + * @param {Array} animations. + * @return {Boolean} true: playing + */ +function hasPlayingAnimation(animations) { + return animations.some(({state}) => state.playState === "running"); +} + /** * Check the equality given states as effect timing. * @@ -88,5 +98,6 @@ function isTimingEffectEqual(stateA, stateB) { } exports.findOptimalTimeInterval = findOptimalTimeInterval; +exports.hasPlayingAnimation = hasPlayingAnimation; exports.isAllAnimationEqual = isAllAnimationEqual; exports.isTimingEffectEqual = isTimingEffectEqual;