Bug 1210795 - Part 2: Changes Infinity expression. r=pbro
☠☠ backed out by a256decb1dfa ☠ ☠
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Mon, 17 Oct 2016 16:18:46 +0900
changeset 319329 589975f9a0dbba4f513d24ab0a2da382a578f01a
parent 319328 e95a6c178d8893cdb4f5ea1a8d5e42b1d92b7ba4
child 319330 2227adce7ea6b45baf2b9a486bcfaac6b619a90c
push id30870
push userphilringnalda@gmail.com
push dateWed, 26 Oct 2016 05:04:25 +0000
treeherdermozilla-central@f9f3cc95d728 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1210795
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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,180 @@ 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
+  const infinityIterationCount =
+    Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
+             Math.ceil((totalDuration - firstSectionCount * state.duration)
+               / state.duration));
+
+  // 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 +610,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;