Bug 1210795 - Part 2: Changes Infinity expression. r=pbro
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Wed, 26 Oct 2016 16:35:58 +0900
changeset 319805 e3f96551347dddaf90cb22c3e886e3de04adeaa5
parent 319804 55ac24f3f8ff2fd06655b23e7f0f92d1a9ca9211
child 319806 4f2a78576e79352669810610f2fde5ff4bec856f
push id20748
push userphilringnalda@gmail.com
push dateFri, 28 Oct 2016 03:39:55 +0000
treeherderfx-team@715360440695 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1210795
milestone52.0a1
Bug 1210795 - Part 2: Changes Infinity expression. r=pbro MozReview-Commit-ID: Bnoy5k388B8
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
devtools/client/themes/animationinspector.css
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -18,22 +18,21 @@ const L10N = new LocalizationHelper("dev
 // smoothly progress if this resolution is not high enough.
 // So, if the difference of animation progress between 2 divisions is more than
 // MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
 // by DURATION_RESOLUTION.
 // DURATION_RESOLUTION shoud be integer and more than 2.
 const DURATION_RESOLUTION = 4;
 // MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
 const MIN_PROGRESS_THRESHOLD = 0.1;
+// Show max 10 iterations for infinite animations
+// to give users a clue that the animation does repeat.
+const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
 // SVG namespace
 const SVG_NS = "http://www.w3.org/2000/svg";
-// ID of mask for fade effect.
-const FADE_MASK_ID = "animationinspector-fade-mask";
-// ID of gradient element.
-const FADE_GRADIENT_ID = "animationinspector-fade-gradient";
 
 /**
  * UI component responsible for displaying a single animation timeline, which
  * basically looks like a rectangle that shows the delay and iterations.
  */
 function AnimationTimeBlock() {
   EventEmitter.decorate(this);
   this.onClick = this.onClick.bind(this);
@@ -70,97 +69,131 @@ AnimationTimeBlock.prototype = {
     // It is positioned according to its delay (divided by the playbackrate),
     // and its width is according to its duration (divided by the playbackrate).
     let {x, iterationW, delayX, delayW, negativeDelayW, endDelayX, endDelayW} =
       TimeScale.getAnimationDimensions(animation);
 
     // background properties for .iterations element
     let backgroundIterations = TimeScale.getIterationsBackgroundData(animation);
 
-    // Displayed total duration
-    const totalDuration = TimeScale.getDuration() * state.playbackRate;
+    // Animation summary graph element.
+    const summaryEl = createNode({
+      parent: this.containerEl,
+      namespace: "http://www.w3.org/2000/svg",
+      nodeType: "svg",
+      attributes: {
+        "class": "summary",
+        "preserveAspectRatio": "none",
+        "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
+      }
+    });
+
+    // Total displayed duration
+    const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();
+
+    // Calculate stroke height in viewBox to display stroke of path.
+    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+
+    // Set viewBox
+    summaryEl.setAttribute("viewBox",
+                           `${ state.delay < 0 ? state.delay : 0 }
+                            -${ 1 + strokeHeightForViewBox }
+                            ${ totalDisplayedDuration }
+                            ${ 1 + strokeHeightForViewBox * 2 }`);
 
     // Get a helper function that returns the path segment of timing-function.
     const segmentHelperFn = getSegmentHelper(state, this.win);
 
     // Minimum segment duration is the duration of one pixel.
-    const minSegmentDuration = totalDuration / this.containerEl.clientWidth;
+    const minSegmentDuration =
+      totalDisplayedDuration / this.containerEl.clientWidth;
     // Minimum progress threshold.
     let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
     // If the easing is step function,
     // minProgressThreshold should be changed by the steps.
     const stepFunction = state.easing.match(/steps\((\d+)/);
     if (stepFunction) {
       minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
     }
 
-    // Calculate stroke height in viewBox to display stroke of path.
-    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
-
-    // Animation summary graph element.
-    const summaryEl = createNode({
-      parent: this.containerEl,
-      namespace: SVG_NS,
-      nodeType: "svg",
-      attributes: {
-        "class": "summary",
-        "viewBox": `${ state.delay < 0 ? state.delay : 0 }
-                    -${ 1 + strokeHeightForViewBox }
-                    ${ totalDuration }
-                    ${ 1 + strokeHeightForViewBox * 2 }`,
-        "preserveAspectRatio": "none",
-        "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%;`,
-      }
-    });
-
-    // Iteration count
-    const iterationCount = state.iterationCount ? state.iterationCount : 1;
-
-    // Append forwards fill-mode.
-    if (state.fill === "both" || state.fill === "forwards") {
-      renderForwardsFill(summaryEl, state, iterationCount,
-                         totalDuration, segmentHelperFn);
-    }
+    // Starting time of main iteration.
+    let mainIterationStartTime = 0;
+    let iterationStart = state.iterationStart;
+    let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
 
     // Append delay.
     if (state.delay > 0) {
       renderDelay(summaryEl, state, segmentHelperFn);
+      mainIterationStartTime = state.delay;
+    } else {
+      const negativeDelayCount = -state.delay / state.duration;
+      // Move to forward the starting point for negative delay.
+      iterationStart += negativeDelayCount;
+      // Consume iteration count by negative delay.
+      if (iterationCount !== Infinity) {
+        iterationCount -= negativeDelayCount;
+      }
     }
 
     // Append 1st section of iterations,
-    // if this animation has decimal iterationStart.
+    // This section is only useful in cases where iterationStart has decimals.
+    // e.g.
+    // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
     const firstSectionCount =
-      state.iterationStart % 1 === 0
-      ? 0 : Math.min(iterationCount, 1) - state.iterationStart % 1;
+      iterationStart % 1 === 0
+      ? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
     if (firstSectionCount) {
-      renderFirstIteration(summaryEl, state, firstSectionCount,
-                           minSegmentDuration, minProgressThreshold,
-                           segmentHelperFn);
+      renderFirstIteration(summaryEl, state, mainIterationStartTime,
+                           firstSectionCount, minSegmentDuration,
+                           minProgressThreshold, segmentHelperFn);
     }
 
-    // Append middle section of iterations.
-    const middleSectionCount =
-      Math.floor(state.iterationCount - firstSectionCount);
-    renderMiddleIterations(summaryEl, state, firstSectionCount,
-                           middleSectionCount, minSegmentDuration,
-                           minProgressThreshold, segmentHelperFn);
+    if (iterationCount === Infinity) {
+      // If the animation repeats infinitely,
+      // we fill the remaining area with iteration paths.
+      renderInfinity(summaryEl, state, mainIterationStartTime,
+                     firstSectionCount, totalDisplayedDuration,
+                     minSegmentDuration, minProgressThreshold, segmentHelperFn);
+    } else {
+      // Otherwise, we show remaining iterations, endDelay and fill.
+
+      // Append forwards fill-mode.
+      if (state.fill === "both" || state.fill === "forwards") {
+        renderForwardsFill(summaryEl, state, mainIterationStartTime,
+                           iterationCount, totalDisplayedDuration,
+                           segmentHelperFn);
+      }
 
-    // Append last section of iterations, if there is remaining iteration.
-    const lastSectionCount =
-      iterationCount - middleSectionCount - firstSectionCount;
-    if (lastSectionCount) {
-      renderLastIteration(summaryEl, state, firstSectionCount,
-                          middleSectionCount, lastSectionCount,
-                          minSegmentDuration, minProgressThreshold,
-                          segmentHelperFn);
-    }
+      // Append middle section of iterations.
+      // e.g.
+      // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+      const middleSectionCount =
+        Math.floor(iterationCount - firstSectionCount);
+      renderMiddleIterations(summaryEl, state, mainIterationStartTime,
+                             firstSectionCount, middleSectionCount,
+                             minSegmentDuration, minProgressThreshold,
+                             segmentHelperFn);
 
-    // Append endDelay.
-    if (state.endDelay > 0) {
-      renderEndDelay(summaryEl, state, iterationCount, segmentHelperFn);
+      // Append last section of iterations, if there is remaining iteration.
+      // e.g.
+      // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
+      const lastSectionCount =
+        iterationCount - middleSectionCount - firstSectionCount;
+      if (lastSectionCount) {
+        renderLastIteration(summaryEl, state, mainIterationStartTime,
+                            firstSectionCount, middleSectionCount,
+                            lastSectionCount, minSegmentDuration,
+                            minProgressThreshold, segmentHelperFn);
+      }
+
+      // Append endDelay.
+      if (state.endDelay > 0) {
+        renderEndDelay(summaryEl, state,
+                       mainIterationStartTime, iterationCount, segmentHelperFn);
+      }
     }
 
     // The animation name is displayed over the iterations.
     // Note that in case of negative delay, it is pushed towards the right so
     // the delay element does not overlap.
     createNode({
       parent: createNode({
         parent: this.containerEl,
@@ -315,125 +348,185 @@ function renderDelay(parentEl, state, ge
   const endSegment = { x: state.delay, y: startSegment.y };
   appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
 }
 
 /**
  * Render first iteration section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
  * @param {function} getSegment - The function of getSegmentHelper.
  */
-function renderFirstIteration(parentEl, state, firstSectionCount,
-                              minSegmentDuration, minProgressThreshold,
-                              getSegment) {
-  const startTime = state.delay;
+function renderFirstIteration(parentEl, state, mainIterationStartTime,
+                              firstSectionCount, minSegmentDuration,
+                              minProgressThreshold, getSegment) {
+  const startTime = mainIterationStartTime;
   const endTime = startTime + firstSectionCount * state.duration;
   const segments =
     createPathSegments(startTime, endTime, minSegmentDuration,
                        minProgressThreshold, getSegment);
   appendPathElement(parentEl, segments, "iteration-path");
 }
 
 /**
  * Render middle iterations section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} middleSectionCount - Iteration count of middle section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
  * @param {function} getSegment - The function of getSegmentHelper.
  */
-function renderMiddleIterations(parentEl, state, firstSectionCount,
-                                middleSectionCount, minSegmentDuration,
-                                minProgressThreshold, getSegment) {
-  const offset = state.delay + firstSectionCount * state.duration;
+function renderMiddleIterations(parentEl, state, mainIterationStartTime,
+                                firstSectionCount, middleSectionCount,
+                                minSegmentDuration, minProgressThreshold,
+                                getSegment) {
+  const offset = mainIterationStartTime + firstSectionCount * state.duration;
   for (let i = 0; i < middleSectionCount; i++) {
     // Get the path segments of each iteration.
     const startTime = offset + i * state.duration;
     const endTime = startTime + state.duration;
     const segments =
       createPathSegments(startTime, endTime, minSegmentDuration,
                          minProgressThreshold, getSegment);
     appendPathElement(parentEl, segments, "iteration-path");
   }
 }
 
 /**
  * Render last iteration section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} middleSectionCount - Iteration count of middle section.
  * @param {Number} lastSectionCount - Iteration count of last section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
  * @param {function} getSegment - The function of getSegmentHelper.
  */
-function renderLastIteration(parentEl, state, firstSectionCount,
-                             middleSectionCount, lastSectionCount,
-                             minSegmentDuration, minProgressThreshold,
-                             getSegment) {
-  const startTime = state.delay + firstSectionCount * state.duration
-                    + middleSectionCount * state.duration;
+function renderLastIteration(parentEl, state, mainIterationStartTime,
+                             firstSectionCount, middleSectionCount,
+                             lastSectionCount, minSegmentDuration,
+                             minProgressThreshold, getSegment) {
+  const startTime = mainIterationStartTime +
+                      (firstSectionCount + middleSectionCount) * state.duration;
   const endTime = startTime + lastSectionCount * state.duration;
   const segments =
     createPathSegments(startTime, endTime, minSegmentDuration,
                        minProgressThreshold, getSegment);
   appendPathElement(parentEl, segments, "iteration-path");
 }
 
 /**
+ * Render Infinity iterations.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderInfinity(parentEl, state, mainIterationStartTime,
+                        firstSectionCount, totalDuration, minSegmentDuration,
+                        minProgressThreshold, getSegment) {
+  // Calculate the number of iterations to display,
+  // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
+  let uncappedInfinityIterationCount =
+    (totalDuration - firstSectionCount * state.duration) / state.duration;
+  // If there is a small floating point error resulting in, e.g. 1.0000001
+  // ceil will give us 2 so round first.
+  uncappedInfinityIterationCount =
+    parseFloat(uncappedInfinityIterationCount.toPrecision(6));
+  const infinityIterationCount =
+    Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
+             Math.ceil(uncappedInfinityIterationCount));
+
+  // Append first full iteration path.
+  const firstStartTime =
+    mainIterationStartTime + firstSectionCount * state.duration;
+  const firstEndTime = firstStartTime + state.duration;
+  const firstSegments =
+    createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
+                       minProgressThreshold, getSegment);
+  appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+
+  // Append other iterations. We can copy first segments.
+  const isAlternate = state.direction.match(/alternate/);
+  for (let i = 1; i < infinityIterationCount; i++) {
+    const startTime = firstStartTime + i * state.duration;
+    let segments;
+    if (isAlternate && i % 2) {
+      // Copy as reverse.
+      segments = firstSegments.map(segment => {
+        return { x: firstEndTime - segment.x + startTime, y: segment.y };
+      });
+    } else {
+      // Copy as is.
+      segments = firstSegments.map(segment => {
+        return { x: segment.x - firstStartTime + startTime, y: segment.y };
+      });
+    }
+    appendPathElement(parentEl, segments, "iteration-path infinity copied");
+  }
+}
+
+/**
  * Render endDelay section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} iterationCount - Whole iteration count.
  * @param {function} getSegment - The function of getSegmentHelper.
  */
-function renderEndDelay(parentEl, state, iterationCount, getSegment) {
-  const startTime = state.delay + iterationCount * state.duration;
+function renderEndDelay(parentEl, state,
+                        mainIterationStartTime, iterationCount, getSegment) {
+  const startTime = mainIterationStartTime + iterationCount * state.duration;
   const startSegment = getSegment(startTime);
   const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
   appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
 }
 
 /**
  * Render forwards fill section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} iterationCount - Whole iteration count.
  * @param {Number} totalDuration - Displayed max duration.
  * @param {function} getSegment - The function of getSegmentHelper.
  */
-function renderForwardsFill(parentEl, state, iterationCount,
-                            totalDuration, getSegment) {
-  const startTime = state.delay + iterationCount * state.duration +
-                    (state.endDelay > 0 ? state.endDelay : 0);
+function renderForwardsFill(parentEl, state, mainIterationStartTime,
+                            iterationCount, totalDuration, getSegment) {
+  const startTime = mainIterationStartTime + iterationCount * state.duration +
+                      (state.endDelay > 0 ? state.endDelay : 0);
   const startSegment = getSegment(startTime);
   const endSegment = { x: totalDuration, y: startSegment.y };
-  const pathEl = appendPathElement(parentEl, [startSegment, endSegment],
-                                   "fill-forwards-path");
-  appendFadeEffect(parentEl, pathEl);
+  appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
 }
 
 /**
  * Get a helper function which returns the segment coord from given time.
  * @param {Object} state - animation state
  * @param {Object} win - window object
  * @return {function} getSegmentHelper
  */
 function getSegmentHelper(state, win) {
   // Create a dummy Animation timing data as the
   // state object we're being passed in.
   const timing = Object.assign({}, state, {
-    iterations: state.iterationCount ? state.iterationCount : 1
+    iterations: state.iterationCount ? state.iterationCount : Infinity
   });
   // Create a dummy Animation with the given timing.
   const dummyAnimation =
     new win.Animation(new win.KeyframeEffect(null, null, timing), null);
   const endTime = dummyAnimation.effect.getComputedTiming().endTime;
   // Return a helper function that, given a time,
   // will calculate the progress through the dummy animation.
   return time => {
@@ -522,72 +615,8 @@ function appendPathElement(parentEl, pat
     attributes: {
       "d": path,
       "class": cls,
       "vector-effect": "non-scaling-stroke",
       "transform": "scale(1, -1)"
     }
   });
 }
-
-/**
- * Append fade effect.
- * @param {Element} parentEl - Parent element of this appended element.
- * @param {Element} el - Append fade effect to this element.
- */
-function appendFadeEffect(parentEl, el) {
-  // We can re-use mask element for fade.
-  // Keep the defs element in SVG element of given parentEl.
-  if (!parentEl.ownerDocument.body.querySelector(`#${ FADE_MASK_ID }`)) {
-    const svgEl = parentEl.closest(".summary");
-    const defsEl = createNode({
-      parent: svgEl,
-      namespace: SVG_NS,
-      nodeType: "defs"
-    });
-    const gradientEl = createNode({
-      parent: defsEl,
-      namespace: SVG_NS,
-      nodeType: "linearGradient",
-      attributes: {
-        "id": FADE_GRADIENT_ID
-      }
-    });
-    createNode({
-      parent: gradientEl,
-      namespace: SVG_NS,
-      nodeType: "stop",
-      attributes: {
-        "offset": 0
-      }
-    });
-    createNode({
-      parent: gradientEl,
-      namespace: SVG_NS,
-      nodeType: "stop",
-      attributes: {
-        "offset": 1
-      }
-    });
-
-    const maskEl = createNode({
-      parent: defsEl,
-      namespace: SVG_NS,
-      nodeType: "mask",
-      attributes: {
-        "id": FADE_MASK_ID,
-        "maskContentUnits": "objectBoundingBox",
-      }
-    });
-    createNode({
-      parent: maskEl,
-      namespace: SVG_NS,
-      nodeType: "rect",
-      attributes: {
-        "y": `${ 1 + svgEl.viewBox.animVal.y }`,
-        "width": "1",
-        "height": `${ svgEl.viewBox.animVal.height }`,
-        "fill": `url(#${ FADE_GRADIENT_ID })`
-      }
-    });
-  }
-  el.setAttribute("mask", `url(#${ FADE_MASK_ID })`);
-}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
@@ -20,23 +20,28 @@ add_task(function* () {
   const timelineComponent = panel.animationsTimelineComponent;
   const timelineEl = timelineComponent.rootWrapperEl;
   let animation = timelineEl.querySelector(".time-block");
   // Get iteration count from summary graph path.
   let iterationCount = getIterationCount(animation);
 
   is(iterationCount, 10,
      "The animation timeline contains the right number of iterations");
+  ok(!animation.querySelector(".infinity"),
+     "The summary graph does not have any elements "
+     + " that have infinity class");
 
   info("Selecting another test node with an infinite animation");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   info("Getting the animation element from the panel again");
   animation = timelineEl.querySelector(".time-block");
   iterationCount = getIterationCount(animation);
 
   is(iterationCount, 1,
-     "The animation timeline contains just one iteration");
+     "The animation timeline contains one iteration");
+  ok(animation.querySelector(".infinity"),
+     "The summary graph has an element that has infinity class");
 });
 
 function getIterationCount(timeblockEl) {
   return timeblockEl.querySelectorAll(".iteration-path").length;
 }
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -351,16 +351,20 @@ body {
   height: 100%;
 }
 
 .animation-timeline .animation .summary path {
   fill: var(--timeline-background-color);
   stroke: var(--timeline-border-color);
 }
 
+.animation-timeline .animation .summary .infinity.copied {
+  opacity: .3;
+}
+
 .animation-timeline .animation .name {
   position: absolute;
   color: var(--theme-selection-color);
   height: 100%;
   display: flex;
   align-items: center;
   padding: 0 2px;
   box-sizing: border-box;
@@ -437,27 +441,16 @@ body {
 .animation-timeline .animation .end-delay.negative {
   /* Negative delays are displayed on top of the animation, so they need a
      right border. Whereas normal delays are displayed just before the
      animation, so there's already the animation's left border that serves as
      a separation. */
   border-width: 1px;
 }
 
-/* Use for fade mask */
-#animationinspector-fade-gradient stop {
-  /* opaque */
-  stop-color: white;
-}
-
-#animationinspector-fade-gradient stop:nth-child(2) {
-  /* transparent */
-  stop-color: black;
-}
-
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 0 4px;
   box-sizing: border-box;
   overflow: hidden;
   text-overflow: ellipsis;