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
@@ -4,36 +4,43 @@
"use strict";
const { createFactory, DOM: dom, PropTypes, PureComponent } =
require("devtools/client/shared/vendor/react");
const AnimationList = createFactory(require("./AnimationList"));
const AnimationListHeader = createFactory(require("./AnimationListHeader"));
+const TimeScale = require("../utils/timescale");
class AnimationListContainer extends PureComponent {
static get displayName() {
return "AnimationListContainer";
}
static get propTypes() {
return {
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
};
}
render() {
const { animations } = this.props;
+ const timescale = new TimeScale(animations);
+
return dom.div(
{
className: "animation-list-container"
},
- AnimationListHeader(),
+ AnimationListHeader(
+ {
+ timescale
+ }
+ ),
AnimationList(
{
animations
}
)
);
}
}
--- a/devtools/client/inspector/animation/components/AnimationListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimationListHeader.js
@@ -1,27 +1,39 @@
/* 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 AnimationTimeTickList = createFactory(require("./AnimationTimeTickList"));
class AnimationListHeader extends PureComponent {
static get displayName() {
return "AnimationListHeader";
}
+ static get propTypes() {
+ return {
+ timescale: PropTypes.object.isRequired,
+ };
+ }
+
render() {
+ const { timescale } = this.props;
+
return dom.div(
{
className: "animation-list-header"
},
- AnimationTimeTickList()
+ AnimationTimeTickList(
+ {
+ timescale
+ }
+ )
);
}
}
module.exports = AnimationListHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTimeTickItem.js
@@ -0,0 +1,36 @@
+/* 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 displayName() {
+ return "AnimationTimeTickItem";
+ }
+
+ static get propTypes() {
+ return {
+ position: PropTypes.number.isRequired,
+ text: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { position, text } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-time-tick-item",
+ style: { left: `${ position }%` }
+ },
+ dom.div(),
+ dom.label(null, text)
+ );
+ }
+}
+
+module.exports = AnimationTimeTickItem;
--- a/devtools/client/inspector/animation/components/AnimationTimeTickList.js
+++ b/devtools/client/inspector/animation/components/AnimationTimeTickList.js
@@ -1,24 +1,82 @@
/* 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 AnimationTimeTickItem = createFactory(require("./AnimationTimeTickItem"));
+const { findOptimalTimeInterval } = require("../utils/utils");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
class AnimationTimeTickList extends PureComponent {
static get displayName() {
return "AnimationTimeTickList";
}
+ static get propTypes() {
+ return {
+ timescale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor() {
+ super();
+
+ this.state = {
+ tickList: []
+ };
+ }
+
+ componentDidMount() {
+ this.updateTickList();
+ }
+
+ createTickList() {
+ const { timescale } = this.props;
+
+ const tickListEl = ReactDOM.findDOMNode(this);
+ if (!tickListEl) {
+ // Not mounted yet.
+ return [];
+ }
+ 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 tickList = [];
+ for (let i = 0; i <= width / intervalWidth; i++) {
+ const position = 100 * i * intervalWidth / width;
+ const text = timescale.formatTime(timescale.distanceToRelativeTime(position));
+ tickList.push({ position, text });
+ }
+ return tickList;
+ }
+
+ updateTickList() {
+ const tickList = this.createTickList();
+ this.setState({ tickList });
+ }
+
render() {
+ const { tickList } = this.state;
+
return dom.div(
{
className: "animation-time-tick-list"
- }
+ },
+ tickList.map(tickItem => {
+ return AnimationTimeTickItem(tickItem);
+ })
);
}
}
module.exports = AnimationTimeTickList;
--- 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',
+ 'AnimationTimeTickItem.js',
'AnimationTimeTickList.js',
'App.js',
'NoAnimationPanel.js'
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/l10n.js
@@ -0,0 +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/. */
+
+"use strict";
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+module.exports =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
--- 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(
+ 'l10n.js',
+ 'timescale.js',
'utils.js'
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -0,0 +1,112 @@
+/* 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 L10N = require("./l10n");
+
+const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
+
+/**
+ * The TimeScale helper object is used to know which size should something be
+ * displayed with in the animation panel, depending on the animations that are
+ * currently displayed.
+ * If there are 5 animations displayed, and the first one starts at 10000ms and
+ * the last one ends at 20000ms, then this helper can be used to convert any
+ * time in this range to a distance in pixels.
+ *
+ * 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;
+ animations.forEach(animation => {
+ this.addAnimation(animation.state);
+ });
+ }
+
+ /**
+ * Add a new animation to time scale.
+ * @param {Object} state A PlayerFront.state object.
+ */
+ addAnimation(state) {
+ let {previousStartTime, delay, duration, endDelay,
+ iterationCount, playbackRate} = state;
+
+ endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
+ let toRate = v => v / playbackRate;
+ let minZero = v => Math.max(v, 0);
+ let 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.
+ let relevantDelay = delay < 0 ? toRate(delay) : 0;
+ previousStartTime = previousStartTime || 0;
+
+ let startTime = toRate(minZero(delay)) +
+ rateRelativeDuration +
+ endDelay;
+ this.minStartTime = Math.min(
+ this.minStartTime,
+ previousStartTime +
+ relevantDelay +
+ Math.min(startTime, 0)
+ );
+ let length = toRate(delay) +
+ rateRelativeDuration +
+ toRate(minZero(endDelay));
+ let 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() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
+ return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+ }
+
+ // Otherwise format in seconds.
+ return L10N.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,49 @@
/* 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";
+// How many times, maximum, can we 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) {
@@ -37,10 +72,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.findOptimalTimeInterval = findOptimalTimeInterval;
module.exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
module.exports.isTimingEffectEqual = isTimingEffectEqual;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -7,38 +7,54 @@
:root {
--animation-even-background-color: rgba(0,0,0,0.05);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
/* How wide should the sidebar be (should be wide enough to contain long
property names like 'border-bottom-right-radius' without ellipsis) */
--default-sidebar-width: 200px;
/* The size of a keyframe marker in the keyframes diagram */
--keyframes-marker-size: 10px;
+ /* The color of the time tick borders */
+ --time-tick-border-color: rgba(128, 136, 144, .5);
}
:root.theme-dark {
--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 {
height: 20px;
}
.animation-time-tick-list {
- height: 100%;
left: var(--default-sidebar-width);
position: absolute;
right: var(--keyframes-marker-size);
}
+.animation-time-tick-item {
+ position: absolute;
+}
+
+.animation-time-tick-item > div {
+ border-left: 0.5px solid var(--time-tick-border-color);
+ height: 100vh;
+ position: absolute;
+}
+
+.animation-time-tick-item > label {
+ position: relative;
+ top: 3px;
+}
+
/* Settings for animations element */
.animation-list {
list-style-type: none;
margin-top: 0;
padding: 0;
}
/* Settings for each animation element */