Bug 1406287 - Part 2: Implement animation time tick and label. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Mon, 13 Nov 2017 17:42:55 +0900
changeset 697015 c86a160ef96269d0b50973995f4dbb7f9d2a1fec
parent 697014 f1e54c2f6496d9be6377de3f44f6b10b1b2b1c6c
child 697016 11b0a5f8ae00f663c641d8da4a2fb535eaa3c8c7
push id88864
push userbmo:dakatsuka@mozilla.com
push dateMon, 13 Nov 2017 08:47:24 +0000
reviewersgl
bugs1406287
milestone58.0a1
Bug 1406287 - Part 2: Implement animation time tick and label. r?gl MozReview-Commit-ID: GlkOal5ClHu
devtools/client/inspector/animation/components/AnimationListContainer.js
devtools/client/inspector/animation/components/AnimationListHeader.js
devtools/client/inspector/animation/components/AnimationTimelineTickItem.js
devtools/client/inspector/animation/components/AnimationTimelineTickList.js
devtools/client/inspector/animation/components/moz.build
devtools/client/inspector/animation/utils/l10n.js
devtools/client/inspector/animation/utils/moz.build
devtools/client/inspector/animation/utils/timescale.js
devtools/client/inspector/animation/utils/utils.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -19,17 +19,21 @@ class AnimationListContainer extends Pur
 
   render() {
     const { animations } = this.props;
 
     return dom.div(
       {
         className: "animation-list-container"
       },
-      AnimationListHeader(),
+      AnimationListHeader(
+        {
+          animations
+        }
+      ),
       AnimationList(
         {
           animations
         }
       )
     );
   }
 }
--- a/devtools/client/inspector/animation/components/AnimationListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimationListHeader.js
@@ -1,23 +1,35 @@
 /* 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 AnimationTimelineTickList = createFactory(require("./AnimationTimelineTickList"));
 
 class AnimationListHeader extends PureComponent {
+  static get propTypes() {
+    return {
+      animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+    };
+  }
+
   render() {
+    const { animations } = this.props;
+
     return dom.div(
       {
         className: "animation-list-header devtools-toolbar"
       },
-      AnimationTimelineTickList()
+      AnimationTimelineTickList(
+        {
+          animations
+        }
+      )
     );
   }
 }
 
 module.exports = AnimationListHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTimelineTickItem.js
@@ -0,0 +1,31 @@
+/* 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 propTypes() {
+    return {
+      position: PropTypes.number.isRequired,
+      timeTickLabel: PropTypes.string.isRequired,
+    };
+  }
+
+  render() {
+    const { position, timeTickLabel } = this.props;
+
+    return dom.div(
+      {
+        className: "animation-timeline-tick-item",
+        style: { left: `${ position }%` }
+      },
+      timeTickLabel
+    );
+  }
+}
+
+module.exports = AnimationTimeTickItem;
--- a/devtools/client/inspector/animation/components/AnimationTimelineTickList.js
+++ b/devtools/client/inspector/animation/components/AnimationTimelineTickList.js
@@ -1,20 +1,73 @@
 /* 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 AnimationTimelineTickItem = createFactory(require("./AnimationTimelineTickItem"));
+
+const TimeScale = require("../utils/timescale");
+const { findOptimalTimeInterval } = require("../utils/utils");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
 
 class AnimationTimelineTickList extends PureComponent {
+  static get propTypes() {
+    return {
+      animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      tickList: [],
+    };
+  }
+
+  componentDidMount() {
+    this.updateTickList();
+  }
+
+  updateTickList() {
+    const { animations } = this.props;
+    const timeScale = new TimeScale(animations);
+    const tickListEl = ReactDOM.findDOMNode(this);
+    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 tickCount = width / intervalWidth;
+    const intervalPositionPercentage = 100 * intervalWidth / width;
+
+    const tickList = [];
+    for (let i = 0; i <= tickCount; i++) {
+      const position = i * intervalPositionPercentage;
+      const timeTickLabel =
+        timeScale.formatTime(timeScale.distanceToRelativeTime(position));
+      tickList.push({ position, timeTickLabel });
+    }
+
+    this.setState({ tickList });
+  }
+
   render() {
+    const { tickList } = this.state;
+
     return dom.div(
       {
         className: "animation-timeline-tick-list"
-      }
+      },
+      tickList.map(tickItem => AnimationTimelineTickItem(tickItem))
     );
   }
 }
 
 module.exports = AnimationTimelineTickList;
--- 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',
+    'AnimationTimelineTickItem.js',
     'AnimationTimelineTickList.js',
     'App.js',
     'NoAnimationPanel.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/l10n.js
@@ -0,0 +1,13 @@
+/* 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");
+const L10N =
+  new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+module.exports = {
+  getFormatStr: (...args) => L10N.getFormatStr(...args),
+};
--- 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(
-    'utils.js'
+    'l10n.js',
+    'timescale.js',
+    'utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -0,0 +1,121 @@
+/* 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 { getFormatStr } = require("./l10n");
+
+// If total duration for all animations is eqaul to or less than
+// TIME_FORMAT_MAX_DURATION_IN_MS, the text which expresses time is in milliseconds,
+// and seconds otherwise. Use in formatTime function.
+const TIME_FORMAT_MAX_DURATION_IN_MS = 4000;
+
+/**
+ * TimeScale object holds the total duration, start time and end time information for all
+ * animations which should be displayed, and is used to calculate the displayed area for
+ * each animation.
+ *
+ * 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;
+    for (const animation of animations) {
+      this.addAnimation(animation.state);
+    }
+  }
+
+  /**
+   * Add a new animation to time scale.
+   *
+   * @param {Object} state
+   *                 A PlayerFront.state object.
+   */
+  addAnimation(state) {
+    let {
+      delay,
+      duration,
+      endDelay = 0,
+      iterationCount,
+      playbackRate,
+      previousStartTime,
+    } = state;
+
+    const toRate = v => v / playbackRate;
+    const minZero = v => Math.max(v, 0);
+    const 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.
+    const relevantDelay = delay < 0 ? toRate(delay) : 0;
+    previousStartTime = previousStartTime || 0;
+
+    const startTime = toRate(minZero(delay)) +
+                      rateRelativeDuration +
+                      endDelay;
+    this.minStartTime = Math.min(
+      this.minStartTime,
+      previousStartTime +
+      relevantDelay +
+      Math.min(startTime, 0)
+    );
+    const length = toRate(delay) + rateRelativeDuration + toRate(minZero(endDelay));
+    const 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() <= TIME_FORMAT_MAX_DURATION_IN_MS) {
+      return getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+    }
+
+    // Otherwise format in seconds.
+    return 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,54 @@
 /* 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";
 
+// The maximum number of times we can 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) {
@@ -16,16 +56,17 @@ function isAllTimingEffectEqual(animatio
     return false;
   }
 
   for (let i = 0; i < animationsA.length; i++) {
     if (!isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
       return false;
     }
   }
+
   return true;
 }
 
 /**
  * Check the equality given states as effect timing.
  *
  * @param {Object} state of animation.
  * @param {Object} same to avobe.
@@ -37,10 +78,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.isAllTimingEffectEqual = isAllTimingEffectEqual;
-module.exports.isTimingEffectEqual = isTimingEffectEqual;
+exports.findOptimalTimeInterval = findOptimalTimeInterval;
+exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
+exports.isTimingEffectEqual = isTimingEffectEqual;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -1,55 +1,64 @@
 /* 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/. */
 
 /* Animation-inspector specific theme variables */
 
 :root {
-  --animation-even-background-color: rgba(0,0,0,0.05);
+  --animation-even-background-color: rgba(0, 0, 0, 0.05);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
 }
 
 :root.theme-dark {
-  --animation-even-background-color: rgba(255,255,255,0.05);
+  --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 */
 .animation-list-header {
   display: flex;
   justify-content: flex-end;
+  padding: 0;
 }
 
+/* Animation Timeline Tick List */
 .animation-timeline-tick-list {
   margin-right: 10px;
+  position: relative;
   width: calc(100% - 210px);
 }
 
-/* Settings for animations element */
+.animation-timeline-tick-item {
+  border-left: 0.5px solid rgba(128, 136, 144, .5);
+  height: 100vh;
+  position: absolute;
+}
+
+/* Animation List */
 .animation-list {
   list-style-type: none;
   margin-top: 0;
   padding: 0;
 }
 
-/* Settings for each animation element */
+/* Animation Item */
 .animation-item {
   height: 30px;
 }
 
 .animation-item:nth-child(2n+1) {
   background-color: var(--animation-even-background-color);
 }
 
-/* Settings for no animation message */
+/* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }