Bug 1210796 - Part 9: Add progress indicator. r=pbro draft
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Tue, 18 Apr 2017 12:15:56 +0900
changeset 564122 6b6c0ecd2bf056b6a411a78fea912a4022fa4ed5
parent 564121 767ce7f132b549bb2bc5d0b0b3f60d29c36f0bf9
child 564123 38bf08f87bb0414c211099d1b4e85092baefd9ae
push id54524
push userbmo:dakatsuka@mozilla.com
push dateTue, 18 Apr 2017 09:24:06 +0000
reviewerspbro
bugs1210796
milestone55.0a1
Bug 1210796 - Part 9: Add progress indicator. r=pbro MozReview-Commit-ID: GRcj1tFIKZB
devtools/client/animationinspector/components/animation-details.js
devtools/client/animationinspector/components/animation-timeline.js
devtools/client/animationinspector/test/head.js
devtools/client/themes/animationinspector.css
--- 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;
+}