Bug 1210796 - Part 9: Add progress indicator. r=pbro
MozReview-Commit-ID: GRcj1tFIKZB
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -43,16 +43,17 @@ AnimationDetails.prototype = {
init: function (containerEl) {
this.containerEl = containerEl;
},
destroy: function () {
this.unrender();
this.containerEl = null;
this.serverTraits = null;
+ this.progressIndicatorEl = null;
},
unrender: function () {
for (let component of this.keyframeComponents) {
component.off("frame-selected", this.onFrameSelected);
component.destroy();
}
this.keyframeComponents = [];
@@ -181,21 +182,31 @@ AnimationDetails.prototype = {
}
// Build an element for each animated property track.
this.tracks = yield this.getTracks(animation, this.serverTraits);
// Get animation type for each CSS properties.
const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
+ // Render progress indicator.
+ this.renderProgressIndicator();
// Render animated properties header.
this.renderAnimatedPropertiesHeader();
// Render animated properties body.
this.renderAnimatedPropertiesBody(animationTypes);
+ // Create dummy animation to indicate the animation progress.
+ const timing = Object.assign({}, animation.state, {
+ iterations: animation.state.iterationCount
+ ? animation.state.iterationCount : Infinity
+ });
+ this.dummyAnimation =
+ new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
+
// Useful for tests to know when rendering of all animation detail UIs
// have been completed.
this.emit("animation-detail-rendering-completed");
}),
onFrameSelected: function (e, args) {
// Relay the event up, it's needed in parents too.
this.emit(e, args);
@@ -281,16 +292,54 @@ AnimationDetails.prototype = {
keyframes: this.tracks[propertyName],
propertyName: propertyName,
animation: this.animation,
animationType: animationTypes[propertyName]
});
keyframesComponent.on("frame-selected", this.onFrameSelected);
this.keyframeComponents.push(keyframesComponent);
}
+ },
+
+ renderProgressIndicator: function () {
+ // The wrapper represents the area which the indicator is displayable.
+ const progressIndicatorWrapperEl = createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "track-container progress-indicator-wrapper"
+ }
+ });
+ this.progressIndicatorEl = createNode({
+ parent: progressIndicatorWrapperEl,
+ attributes: {
+ "class": "progress-indicator"
+ }
+ });
+ createNode({
+ parent: this.progressIndicatorEl,
+ attributes: {
+ "class": "progress-indicator-shape"
+ }
+ });
+ },
+
+ indicateProgress: function (time) {
+ if (!this.progressIndicatorEl) {
+ // Not displayed yet.
+ return;
+ }
+ const startTime = this.animation.state.previousStartTime || 0;
+ this.dummyAnimation.currentTime =
+ (time - startTime) * this.animation.state.playbackRate;
+ this.progressIndicatorEl.style.left =
+ `${ this.dummyAnimation.effect.getComputedTiming().progress * 100 }%`;
+ },
+
+ get win() {
+ return this.containerEl.ownerDocument.defaultView;
}
};
/**
* Turn propertyName into property-name.
* @param {String} jsPropertyName A camelcased CSS property name. Typically
* something that comes out of computed styles. E.g. borderBottomColor
* @return {String} The corresponding CSS property name: border-bottom-color
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -1,16 +1,17 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {
createNode,
findOptimalTimeInterval,
getFormattedAnimationTitle,
TimeScale
} = require("devtools/client/animationinspector/utils");
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
@@ -53,16 +54,17 @@ function AnimationsTimeline(inspector, s
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
this.onScrubberMouseOut = this.onScrubberMouseOut.bind(this);
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
this.onAnimationSelected = this.onAnimationSelected.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onFrameSelected = this.onFrameSelected.bind(this);
+ this.onTimelineDataChanged = this.onTimelineDataChanged.bind(this);
EventEmitter.decorate(this);
}
exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function (containerEl) {
@@ -268,16 +270,17 @@ AnimationsTimeline.prototype = {
}
this.stopAnimatingScrubber();
TimeScale.reset();
this.destroySubComponents("targetNodes");
this.destroySubComponents("timeBlocks");
this.details.off("frame-selected", this.onFrameSelected);
this.details.unrender();
this.animationsEl.innerHTML = "";
+ this.off("timeline-data-changed", this.onTimelineDataChanged);
},
onWindowResize: function () {
// Don't do anything if the root element has a width of 0
if (this.rootWrapperEl.offsetWidth === 0) {
return;
}
@@ -285,17 +288,17 @@ AnimationsTimeline.prototype = {
this.win.clearTimeout(this.windowResizeTimer);
}
this.windowResizeTimer = this.win.setTimeout(() => {
this.drawHeaderAndBackground();
}, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
},
- onAnimationSelected: function (e, animation) {
+ onAnimationSelected: Task.async(function* (e, animation) {
let index = this.animations.indexOf(animation);
if (index === -1) {
return;
}
// Unselect an animation which was selected.
const animationEls = this.rootWrapperEl.querySelectorAll(".animation");
for (let i = 0; i < animationEls.length; i++) {
@@ -319,20 +322,21 @@ AnimationsTimeline.prototype = {
`animated-properties ${ animation.state.type }`;
}
// Select and render.
const selectedAnimationEl = animationEls[index];
selectedAnimationEl.classList.add("selected");
this.animationDetailEl.style.display =
this.animationDetailEl.dataset.defaultDisplayStyle;
- this.details.render(animation);
+ yield this.details.render(animation);
+ this.onTimelineDataChanged(null, { time: this.currentTime || 0 });
this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
this.emit("animation-selected", animation);
- },
+ }),
/**
* When a frame gets selected, move the scrubber to the corresponding position
*/
onFrameSelected: function (e, {x}) {
this.moveScrubberTo(x, true);
},
@@ -478,16 +482,19 @@ AnimationsTimeline.prototype = {
if (!documentCurrentTime) {
this.scrubberEl.style.display = "none";
} else {
this.scrubberEl.style.display = "block";
this.startAnimatingScrubber(this.wasRewound()
? TimeScale.minStartTime
: documentCurrentTime);
}
+
+ // To indicate the animation progress in AnimationDetails.
+ this.on("timeline-data-changed", this.onTimelineDataChanged);
},
isAtLeastOneAnimationPlaying: function () {
return this.animations.some(({state}) => state.playState === "running");
},
wasRewound: function () {
return !this.isAtLeastOneAnimationPlaying() &&
@@ -587,10 +594,17 @@ AnimationsTimeline.prototype = {
parent: this.timeTickEl,
nodeType: "span",
attributes: {
"class": "time-tick",
"style": `left:${pos}%`
}
});
}
+ },
+
+ onTimelineDataChanged: function (e, { time }) {
+ this.currentTime = time;
+ const indicateTime =
+ TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
+ this.details.indicateProgress(indicateTime);
}
};
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -376,28 +376,21 @@ function disableHighlighter(toolbox) {
function* clickOnAnimation(panel, index, shouldAlreadySelected) {
let timeline = panel.animationsTimelineComponent;
// Expect a selection event.
let onSelectionChanged = timeline.once(shouldAlreadySelected
? "animation-already-selected"
: "animation-selected");
- // If we're opening the animation, also wait for
- // the animation-detail-rendering-completed event.
- let onReady = shouldAlreadySelected
- ? Promise.resolve()
- : timeline.details.once("animation-detail-rendering-completed");
-
info("Click on animation " + index + " in the timeline");
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
EventUtils.sendMouseEvent({type: "click"}, timeBlock,
timeBlock.ownerDocument.defaultView);
- yield onReady;
return yield onSelectionChanged;
}
/**
* Get an instance of the Keyframes component from the timeline.
* @param {AnimationsPanel} panel The panel instance.
* @param {String} propertyName The name of the animated property.
* @return {Keyframes} The Keyframes component instance.
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -14,16 +14,18 @@
--opacity-border-color: var(--theme-highlight-pink);
--opacity-background-color: #df80ff80;
/* The color for animation type 'transform' */
--transform-border-color: var(--theme-graphs-yellow);
--transform-background-color: #d99b2880;
/* The color for other animation type */
--other-border-color: var(--theme-graphs-bluegrey);
--other-background-color: #5e88b080;
+ /* The color for progress indicator */
+ --progress-indicator-color: var(--theme-highlight-gray);
}
.theme-light {
--even-animation-timeline-background-color: rgba(128,128,128,0.03);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
--pause-image: url(chrome://devtools/skin/images/pause.svg);
--rewind-image: url(chrome://devtools/skin/images/rewind.svg);
--play-image: url(chrome://devtools/skin/images/play.svg);
@@ -42,16 +44,18 @@
--opacity-border-color: var(--theme-highlight-pink);
--opacity-background-color: #b82ee580;
/* The color for animation type 'transform' */
--transform-border-color: var(--theme-graphs-orange);
--transform-background-color: #efc05280;
/* The color for other animation type */
--other-border-color: var(--theme-graphs-bluegrey);
--other-background-color: #0072ab80;
+ /* The color for progress indicator */
+ --progress-indicator-color: gray;
}
:root {
/* How high should toolbars be */
--toolbar-height: 20px;
/* How wide should the sidebar be (should be wide enough to contain long
property names like 'border-bottom-right-radius' without ellipsis) */
--timeline-sidebar-width: 200px;
@@ -616,17 +620,16 @@ body {
.keyframes .frame {
position: absolute;
top: 50%;
width: 0;
height: 0;
background-color: inherit;
cursor: pointer;
- z-index: 1;
}
.keyframes .frame::before {
content: "";
display: block;
transform:
translateX(calc(var(--keyframes-marker-size) * -.5))
/* The extra pixel on the Y axis is so that markers are centered on the
@@ -750,8 +753,36 @@ body {
.progress-tick-container .progress-tick:nth-child(3) {
left: 100%;
}
.animated-properties-body .property:last-child {
/* To display animation progress graph clealy when the scroll is bottom. */
padding-bottom: calc(var(--timeline-animation-height) / 2);
}
+
+.animated-properties .progress-indicator-wrapper {
+ pointer-events: none;
+ z-index: 5;
+}
+
+.progress-indicator-wrapper .progress-indicator {
+ position: absolute;
+ pointer-events: none;
+}
+
+.progress-indicator-wrapper .progress-indicator .progress-indicator-shape {
+ position: fixed;
+ width: 0;
+ height: 100vh;
+ border-right: 1px solid var(--progress-indicator-color);
+}
+
+.progress-indicator-wrapper .progress-indicator .progress-indicator-shape::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: -6px;
+ width: 1px;
+ border-top: 5px solid var(--progress-indicator-color);
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+}