Bug 1406287 - Part 2: Implement animation time tick and label. r?gl
MozReview-Commit-ID: GlkOal5ClHu
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -19,17 +19,21 @@ class AnimationListContainer extends Pur
render() {
const { animations } = this.props;
return dom.div(
{
className: "animation-list-container"
},
- AnimationListHeader(),
+ AnimationListHeader(
+ {
+ animations
+ }
+ ),
AnimationList(
{
animations
}
)
);
}
}
--- a/devtools/client/inspector/animation/components/AnimationListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimationListHeader.js
@@ -1,23 +1,35 @@
/* 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 { createFactory, DOM: dom, PureComponent } =
+const { createFactory, DOM: dom, PropTypes, PureComponent } =
require("devtools/client/shared/vendor/react");
const AnimationTimelineTickList = createFactory(require("./AnimationTimelineTickList"));
class AnimationListHeader extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ };
+ }
+
render() {
+ const { animations } = this.props;
+
return dom.div(
{
className: "animation-list-header devtools-toolbar"
},
- AnimationTimelineTickList()
+ AnimationTimelineTickList(
+ {
+ animations
+ }
+ )
);
}
}
module.exports = AnimationListHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTimelineTickItem.js
@@ -0,0 +1,31 @@
+/* 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 { DOM: dom, PropTypes, PureComponent } =
+ require("devtools/client/shared/vendor/react");
+
+class AnimationTimeTickItem extends PureComponent {
+ static get propTypes() {
+ return {
+ position: PropTypes.number.isRequired,
+ timeTickLabel: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { position, timeTickLabel } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-timeline-tick-item",
+ style: { left: `${ position }%` }
+ },
+ timeTickLabel
+ );
+ }
+}
+
+module.exports = AnimationTimeTickItem;
--- a/devtools/client/inspector/animation/components/AnimationTimelineTickList.js
+++ b/devtools/client/inspector/animation/components/AnimationTimelineTickList.js
@@ -1,20 +1,73 @@
/* 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 { DOM: dom, PureComponent } =
+const { createFactory, DOM: dom, PropTypes, PureComponent } =
require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const AnimationTimelineTickItem = createFactory(require("./AnimationTimelineTickItem"));
+
+const TimeScale = require("../utils/timescale");
+const { findOptimalTimeInterval } = require("../utils/utils");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
class AnimationTimelineTickList extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ tickList: [],
+ };
+ }
+
+ componentDidMount() {
+ this.updateTickList();
+ }
+
+ updateTickList() {
+ const { animations } = this.props;
+ const timeScale = new TimeScale(animations);
+ const tickListEl = ReactDOM.findDOMNode(this);
+ const width = tickListEl.offsetWidth;
+ const animationDuration = timeScale.getDuration();
+ const minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
+ const intervalLength = findOptimalTimeInterval(minTimeInterval);
+ const intervalWidth = intervalLength * width / animationDuration;
+ const tickCount = width / intervalWidth;
+ const intervalPositionPercentage = 100 * intervalWidth / width;
+
+ const tickList = [];
+ for (let i = 0; i <= tickCount; i++) {
+ const position = i * intervalPositionPercentage;
+ const timeTickLabel =
+ timeScale.formatTime(timeScale.distanceToRelativeTime(position));
+ tickList.push({ position, timeTickLabel });
+ }
+
+ this.setState({ tickList });
+ }
+
render() {
+ const { tickList } = this.state;
+
return dom.div(
{
className: "animation-timeline-tick-list"
- }
+ },
+ tickList.map(tickItem => AnimationTimelineTickItem(tickItem))
);
}
}
module.exports = AnimationTimelineTickList;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -2,12 +2,13 @@
# 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/.
DevToolsModules(
'AnimationItem.js',
'AnimationList.js',
'AnimationListContainer.js',
'AnimationListHeader.js',
+ 'AnimationTimelineTickItem.js',
'AnimationTimelineTickList.js',
'App.js',
'NoAnimationPanel.js'
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/l10n.js
@@ -0,0 +1,13 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+module.exports = {
+ getFormatStr: (...args) => L10N.getFormatStr(...args),
+};
--- a/devtools/client/inspector/animation/utils/moz.build
+++ b/devtools/client/inspector/animation/utils/moz.build
@@ -1,7 +1,9 @@
# 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/.
DevToolsModules(
- 'utils.js'
+ 'l10n.js',
+ 'timescale.js',
+ 'utils.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -0,0 +1,121 @@
+/* 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 { getFormatStr } = require("./l10n");
+
+// If total duration for all animations is eqaul to or less than
+// TIME_FORMAT_MAX_DURATION_IN_MS, the text which expresses time is in milliseconds,
+// and seconds otherwise. Use in formatTime function.
+const TIME_FORMAT_MAX_DURATION_IN_MS = 4000;
+
+/**
+ * TimeScale object holds the total duration, start time and end time information for all
+ * animations which should be displayed, and is used to calculate the displayed area for
+ * each animation.
+ *
+ * For the helper to know how to convert, it needs to know all the animations.
+ * Whenever a new animation is added to the panel, addAnimation(state) should be
+ * called.
+ */
+class TimeScale {
+ constructor(animations) {
+ this.minStartTime = Infinity;
+ this.maxEndTime = 0;
+ for (const animation of animations) {
+ this.addAnimation(animation.state);
+ }
+ }
+
+ /**
+ * Add a new animation to time scale.
+ *
+ * @param {Object} state
+ * A PlayerFront.state object.
+ */
+ addAnimation(state) {
+ let {
+ delay,
+ duration,
+ endDelay = 0,
+ iterationCount,
+ playbackRate,
+ previousStartTime,
+ } = state;
+
+ const toRate = v => v / playbackRate;
+ const minZero = v => Math.max(v, 0);
+ const rateRelativeDuration =
+ toRate(duration * (!iterationCount ? 1 : iterationCount));
+ // Negative-delayed animations have their startTimes set such that we would
+ // be displaying the delay outside the time window if we didn't take it into
+ // account here.
+ const relevantDelay = delay < 0 ? toRate(delay) : 0;
+ previousStartTime = previousStartTime || 0;
+
+ const startTime = toRate(minZero(delay)) +
+ rateRelativeDuration +
+ endDelay;
+ this.minStartTime = Math.min(
+ this.minStartTime,
+ previousStartTime +
+ relevantDelay +
+ Math.min(startTime, 0)
+ );
+ const length = toRate(delay) + rateRelativeDuration + toRate(minZero(endDelay));
+ const endTime = previousStartTime + length;
+ this.maxEndTime = Math.max(this.maxEndTime, endTime);
+ }
+
+ /**
+ * Convert a distance in % to a time, in the current time scale.
+ *
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToTime(distance) {
+ return this.minStartTime + (this.getDuration() * distance / 100);
+ }
+
+ /**
+ * Convert a distance in % to a time, in the current time scale.
+ * The time will be relative to the current minimum start time.
+ *
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToRelativeTime(distance) {
+ const time = this.distanceToTime(distance);
+ return time - this.minStartTime;
+ }
+
+ /**
+ * Depending on the time scale, format the given time as milliseconds or
+ * seconds.
+ *
+ * @param {Number} time
+ * @return {String} The formatted time string.
+ */
+ formatTime(time) {
+ // Format in milliseconds if the total duration is short enough.
+ if (this.getDuration() <= TIME_FORMAT_MAX_DURATION_IN_MS) {
+ return getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+ }
+
+ // Otherwise format in seconds.
+ return getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
+ }
+
+ /**
+ * Return entire animations duration.
+ *
+ * @return {Number} duration
+ */
+ getDuration() {
+ return this.maxEndTime - this.minStartTime;
+ }
+}
+
+module.exports = TimeScale;
--- a/devtools/client/inspector/animation/utils/utils.js
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -1,14 +1,54 @@
/* 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";
+// The maximum number of times we can loop before we find the optimal time interval in the
+// timeline graph.
+const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
+// Time graduations should be multiple of one of these number.
+const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
+
+/**
+ * Find the optimal interval between time graduations in the animation timeline
+ * graph based on a minimum time interval.
+ *
+ * @param {Number} minTimeInterval
+ * Minimum time in ms in one interval
+ * @return {Number} The optimal interval time in ms
+ */
+function findOptimalTimeInterval(minTimeInterval) {
+ if (!minTimeInterval) {
+ return 0;
+ }
+
+ let numIters = 0;
+ let multiplier = 1;
+ let interval;
+
+ while (true) {
+ for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
+ interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
+
+ if (minTimeInterval <= interval) {
+ return interval;
+ }
+ }
+
+ if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
+ return interval;
+ }
+
+ multiplier *= 10;
+ }
+}
+
/**
* Check the equality timing effects from given animations.
*
* @param {Array} animations.
* @param {Array} same to avobe.
* @return {Boolean} true: same timing effects
*/
function isAllTimingEffectEqual(animationsA, animationsB) {
@@ -16,16 +56,17 @@ function isAllTimingEffectEqual(animatio
return false;
}
for (let i = 0; i < animationsA.length; i++) {
if (!isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
return false;
}
}
+
return true;
}
/**
* Check the equality given states as effect timing.
*
* @param {Object} state of animation.
* @param {Object} same to avobe.
@@ -37,10 +78,11 @@ function isTimingEffectEqual(stateA, sta
stateA.duration === stateB.duration &&
stateA.easing === stateB.easing &&
stateA.endDelay === stateB.endDelay &&
stateA.fill === stateB.fill &&
stateA.iterationCount === stateB.iterationCount &&
stateA.iterationStart === stateB.iterationStart;
}
-module.exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
-module.exports.isTimingEffectEqual = isTimingEffectEqual;
+exports.findOptimalTimeInterval = findOptimalTimeInterval;
+exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
+exports.isTimingEffectEqual = isTimingEffectEqual;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -1,55 +1,64 @@
/* 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/. */
/* Animation-inspector specific theme variables */
:root {
- --animation-even-background-color: rgba(0,0,0,0.05);
+ --animation-even-background-color: rgba(0, 0, 0, 0.05);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
}
:root.theme-dark {
- --animation-even-background-color: rgba(255,255,255,0.05);
+ --animation-even-background-color: rgba(255, 255, 255, 0.05);
}
:root.theme-firebug {
--command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
}
-/* Settings for animation-list-header */
+/* Animation List Header */
.animation-list-header {
display: flex;
justify-content: flex-end;
+ padding: 0;
}
+/* Animation Timeline Tick List */
.animation-timeline-tick-list {
margin-right: 10px;
+ position: relative;
width: calc(100% - 210px);
}
-/* Settings for animations element */
+.animation-timeline-tick-item {
+ border-left: 0.5px solid rgba(128, 136, 144, .5);
+ height: 100vh;
+ position: absolute;
+}
+
+/* Animation List */
.animation-list {
list-style-type: none;
margin-top: 0;
padding: 0;
}
-/* Settings for each animation element */
+/* Animation Item */
.animation-item {
height: 30px;
}
.animation-item:nth-child(2n+1) {
background-color: var(--animation-even-background-color);
}
-/* Settings for no animation message */
+/* No Animation Panel */
.animation-error-message {
overflow: auto;
}
.animation-error-message > p {
white-space: pre;
}