utils.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
  7. const { LocalizationHelper } = require("devtools/shared/l10n");
  8. const L10N =
  9. new LocalizationHelper("devtools/client/locales/animationinspector.properties");
  10. // How many times, maximum, can we loop before we find the optimal time
  11. // interval in the timeline graph.
  12. const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
  13. // Time graduations should be multiple of one of these number.
  14. const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
  15. const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
  16. /**
  17. * DOM node creation helper function.
  18. * @param {Object} Options to customize the node to be created.
  19. * - nodeType {String} Optional, defaults to "div",
  20. * - attributes {Object} Optional attributes object like
  21. * {attrName1:value1, attrName2: value2, ...}
  22. * - parent {DOMNode} Mandatory node to append the newly created node to.
  23. * - textContent {String} Optional text for the node.
  24. * - namespace {String} Optional namespace
  25. * @return {DOMNode} The newly created node.
  26. */
  27. function createNode(options) {
  28. if (!options.parent) {
  29. throw new Error("Missing parent DOMNode to create new node");
  30. }
  31. let type = options.nodeType || "div";
  32. let node =
  33. options.namespace
  34. ? options.parent.ownerDocument.createElementNS(options.namespace, type)
  35. : options.parent.ownerDocument.createElement(type);
  36. for (let name in options.attributes || {}) {
  37. let value = options.attributes[name];
  38. node.setAttribute(name, value);
  39. }
  40. if (options.textContent) {
  41. node.textContent = options.textContent;
  42. }
  43. options.parent.appendChild(node);
  44. return node;
  45. }
  46. exports.createNode = createNode;
  47. /**
  48. * Find the optimal interval between time graduations in the animation timeline
  49. * graph based on a minimum time interval
  50. * @param {Number} minTimeInterval Minimum time in ms in one interval
  51. * @return {Number} The optimal interval time in ms
  52. */
  53. function findOptimalTimeInterval(minTimeInterval) {
  54. let numIters = 0;
  55. let multiplier = 1;
  56. if (!minTimeInterval) {
  57. return 0;
  58. }
  59. let interval;
  60. while (true) {
  61. for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
  62. interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
  63. if (minTimeInterval <= interval) {
  64. return interval;
  65. }
  66. }
  67. if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
  68. return interval;
  69. }
  70. multiplier *= 10;
  71. }
  72. }
  73. exports.findOptimalTimeInterval = findOptimalTimeInterval;
  74. /**
  75. * Format a timestamp (in ms) as a mm:ss.mmm string.
  76. * @param {Number} time
  77. * @return {String}
  78. */
  79. function formatStopwatchTime(time) {
  80. // Format falsy values as 0
  81. if (!time) {
  82. return "00:00.000";
  83. }
  84. let milliseconds = parseInt(time % 1000, 10);
  85. let seconds = parseInt((time / 1000) % 60, 10);
  86. let minutes = parseInt((time / (1000 * 60)), 10);
  87. let pad = (nb, max) => {
  88. if (nb < max) {
  89. return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
  90. }
  91. return nb;
  92. };
  93. minutes = pad(minutes, 10);
  94. seconds = pad(seconds, 10);
  95. milliseconds = pad(milliseconds, 100);
  96. return `${minutes}:${seconds}.${milliseconds}`;
  97. }
  98. exports.formatStopwatchTime = formatStopwatchTime;
  99. /**
  100. * The TimeScale helper object is used to know which size should something be
  101. * displayed with in the animation panel, depending on the animations that are
  102. * currently displayed.
  103. * If there are 5 animations displayed, and the first one starts at 10000ms and
  104. * the last one ends at 20000ms, then this helper can be used to convert any
  105. * time in this range to a distance in pixels.
  106. *
  107. * For the helper to know how to convert, it needs to know all the animations.
  108. * Whenever a new animation is added to the panel, addAnimation(state) should be
  109. * called. reset() can be called to start over.
  110. */
  111. var TimeScale = {
  112. minStartTime: Infinity,
  113. maxEndTime: 0,
  114. /**
  115. * Add a new animation to time scale.
  116. * @param {Object} state A PlayerFront.state object.
  117. */
  118. addAnimation: function (state) {
  119. let {previousStartTime, delay, duration, endDelay,
  120. iterationCount, playbackRate} = state;
  121. endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
  122. let toRate = v => v / playbackRate;
  123. let minZero = v => Math.max(v, 0);
  124. let rateRelativeDuration =
  125. toRate(duration * (!iterationCount ? 1 : iterationCount));
  126. // Negative-delayed animations have their startTimes set such that we would
  127. // be displaying the delay outside the time window if we didn't take it into
  128. // account here.
  129. let relevantDelay = delay < 0 ? toRate(delay) : 0;
  130. previousStartTime = previousStartTime || 0;
  131. let startTime = toRate(minZero(delay)) +
  132. rateRelativeDuration +
  133. endDelay;
  134. this.minStartTime = Math.min(
  135. this.minStartTime,
  136. previousStartTime +
  137. relevantDelay +
  138. Math.min(startTime, 0)
  139. );
  140. let length = toRate(delay) +
  141. rateRelativeDuration +
  142. toRate(minZero(endDelay));
  143. let endTime = previousStartTime + length;
  144. this.maxEndTime = Math.max(this.maxEndTime, endTime);
  145. },
  146. /**
  147. * Reset the current time scale.
  148. */
  149. reset: function () {
  150. this.minStartTime = Infinity;
  151. this.maxEndTime = 0;
  152. },
  153. /**
  154. * Convert a startTime to a distance in %, in the current time scale.
  155. * @param {Number} time
  156. * @return {Number}
  157. */
  158. startTimeToDistance: function (time) {
  159. time -= this.minStartTime;
  160. return this.durationToDistance(time);
  161. },
  162. /**
  163. * Convert a duration to a distance in %, in the current time scale.
  164. * @param {Number} time
  165. * @return {Number}
  166. */
  167. durationToDistance: function (duration) {
  168. return duration * 100 / this.getDuration();
  169. },
  170. /**
  171. * Convert a distance in % to a time, in the current time scale.
  172. * @param {Number} distance
  173. * @return {Number}
  174. */
  175. distanceToTime: function (distance) {
  176. return this.minStartTime + (this.getDuration() * distance / 100);
  177. },
  178. /**
  179. * Convert a distance in % to a time, in the current time scale.
  180. * The time will be relative to the current minimum start time.
  181. * @param {Number} distance
  182. * @return {Number}
  183. */
  184. distanceToRelativeTime: function (distance) {
  185. let time = this.distanceToTime(distance);
  186. return time - this.minStartTime;
  187. },
  188. /**
  189. * Depending on the time scale, format the given time as milliseconds or
  190. * seconds.
  191. * @param {Number} time
  192. * @return {String} The formatted time string.
  193. */
  194. formatTime: function (time) {
  195. // Format in milliseconds if the total duration is short enough.
  196. if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
  197. return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
  198. }
  199. // Otherwise format in seconds.
  200. return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
  201. },
  202. getDuration: function () {
  203. return this.maxEndTime - this.minStartTime;
  204. },
  205. /**
  206. * Given an animation, get the various dimensions (in %) useful to draw the
  207. * animation in the timeline.
  208. */
  209. getAnimationDimensions: function ({state}) {
  210. let start = state.previousStartTime || 0;
  211. let duration = state.duration;
  212. let rate = state.playbackRate;
  213. let count = state.iterationCount;
  214. let delay = state.delay || 0;
  215. let endDelay = state.endDelay || 0;
  216. // The start position.
  217. let x = this.startTimeToDistance(start + (delay / rate));
  218. // The width for a single iteration.
  219. let w = this.durationToDistance(duration / rate);
  220. // The width for all iterations.
  221. let iterationW = w * (count || 1);
  222. // The start position of the delay.
  223. let delayX = delay < 0 ? x : this.startTimeToDistance(start);
  224. // The width of the delay.
  225. let delayW = this.durationToDistance(Math.abs(delay) / rate);
  226. // The width of the delay if it is negative, 0 otherwise.
  227. let negativeDelayW = delay < 0 ? delayW : 0;
  228. // The width of the endDelay.
  229. let endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
  230. // The start position of the endDelay.
  231. let endDelayX = endDelay < 0 ? x + iterationW - endDelayW
  232. : x + iterationW;
  233. return {x, w, iterationW, delayX, delayW, negativeDelayW,
  234. endDelayX, endDelayW};
  235. }
  236. };
  237. exports.TimeScale = TimeScale;