summaryrefslogtreecommitdiffstats
path: root/devtools/client/animationinspector/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/animationinspector/utils.js')
-rw-r--r--devtools/client/animationinspector/utils.js275
1 files changed, 275 insertions, 0 deletions
diff --git a/devtools/client/animationinspector/utils.js b/devtools/client/animationinspector/utils.js
new file mode 100644
index 000000000..4b6891ac1
--- /dev/null
+++ b/devtools/client/animationinspector/utils.js
@@ -0,0 +1,275 @@
+/* -*- 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";
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// How many times, maximum, can we 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];
+
+const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
+
+/**
+ * DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ * {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * - textContent {String} Optional text for the node.
+ * - namespace {String} Optional namespace
+ * @return {DOMNode} The newly created node.
+ */
+function createNode(options) {
+ if (!options.parent) {
+ throw new Error("Missing parent DOMNode to create new node");
+ }
+
+ let type = options.nodeType || "div";
+ let node =
+ options.namespace
+ ? options.parent.ownerDocument.createElementNS(options.namespace, type)
+ : options.parent.ownerDocument.createElement(type);
+
+ for (let name in options.attributes || {}) {
+ let value = options.attributes[name];
+ node.setAttribute(name, value);
+ }
+
+ if (options.textContent) {
+ node.textContent = options.textContent;
+ }
+
+ options.parent.appendChild(node);
+ return node;
+}
+
+exports.createNode = createNode;
+
+/**
+ * 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) {
+ let numIters = 0;
+ let multiplier = 1;
+
+ if (!minTimeInterval) {
+ return 0;
+ }
+
+ 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;
+ }
+}
+
+exports.findOptimalTimeInterval = findOptimalTimeInterval;
+
+/**
+ * Format a timestamp (in ms) as a mm:ss.mmm string.
+ * @param {Number} time
+ * @return {String}
+ */
+function formatStopwatchTime(time) {
+ // Format falsy values as 0
+ if (!time) {
+ return "00:00.000";
+ }
+
+ let milliseconds = parseInt(time % 1000, 10);
+ let seconds = parseInt((time / 1000) % 60, 10);
+ let minutes = parseInt((time / (1000 * 60)), 10);
+
+ let pad = (nb, max) => {
+ if (nb < max) {
+ return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
+ }
+ return nb;
+ };
+
+ minutes = pad(minutes, 10);
+ seconds = pad(seconds, 10);
+ milliseconds = pad(milliseconds, 100);
+
+ return `${minutes}:${seconds}.${milliseconds}`;
+}
+
+exports.formatStopwatchTime = formatStopwatchTime;
+
+/**
+ * The TimeScale helper object is used to know which size should something be
+ * displayed with in the animation panel, depending on the animations that are
+ * currently displayed.
+ * If there are 5 animations displayed, and the first one starts at 10000ms and
+ * the last one ends at 20000ms, then this helper can be used to convert any
+ * time in this range to a distance in pixels.
+ *
+ * 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. reset() can be called to start over.
+ */
+var TimeScale = {
+ minStartTime: Infinity,
+ maxEndTime: 0,
+
+ /**
+ * Add a new animation to time scale.
+ * @param {Object} state A PlayerFront.state object.
+ */
+ addAnimation: function (state) {
+ let {previousStartTime, delay, duration, endDelay,
+ iterationCount, playbackRate} = state;
+
+ endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
+ let toRate = v => v / playbackRate;
+ let minZero = v => Math.max(v, 0);
+ let 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.
+ let relevantDelay = delay < 0 ? toRate(delay) : 0;
+ previousStartTime = previousStartTime || 0;
+
+ let startTime = toRate(minZero(delay)) +
+ rateRelativeDuration +
+ endDelay;
+ this.minStartTime = Math.min(
+ this.minStartTime,
+ previousStartTime +
+ relevantDelay +
+ Math.min(startTime, 0)
+ );
+ let length = toRate(delay) +
+ rateRelativeDuration +
+ toRate(minZero(endDelay));
+ let endTime = previousStartTime + length;
+ this.maxEndTime = Math.max(this.maxEndTime, endTime);
+ },
+
+ /**
+ * Reset the current time scale.
+ */
+ reset: function () {
+ this.minStartTime = Infinity;
+ this.maxEndTime = 0;
+ },
+
+ /**
+ * Convert a startTime to a distance in %, in the current time scale.
+ * @param {Number} time
+ * @return {Number}
+ */
+ startTimeToDistance: function (time) {
+ time -= this.minStartTime;
+ return this.durationToDistance(time);
+ },
+
+ /**
+ * Convert a duration to a distance in %, in the current time scale.
+ * @param {Number} time
+ * @return {Number}
+ */
+ durationToDistance: function (duration) {
+ return duration * 100 / this.getDuration();
+ },
+
+ /**
+ * Convert a distance in % to a time, in the current time scale.
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToTime: function (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: function (distance) {
+ let 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: function (time) {
+ // Format in milliseconds if the total duration is short enough.
+ if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
+ return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+ }
+
+ // Otherwise format in seconds.
+ return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
+ },
+
+ getDuration: function () {
+ return this.maxEndTime - this.minStartTime;
+ },
+
+ /**
+ * Given an animation, get the various dimensions (in %) useful to draw the
+ * animation in the timeline.
+ */
+ getAnimationDimensions: function ({state}) {
+ let start = state.previousStartTime || 0;
+ let duration = state.duration;
+ let rate = state.playbackRate;
+ let count = state.iterationCount;
+ let delay = state.delay || 0;
+ let endDelay = state.endDelay || 0;
+
+ // The start position.
+ let x = this.startTimeToDistance(start + (delay / rate));
+ // The width for a single iteration.
+ let w = this.durationToDistance(duration / rate);
+ // The width for all iterations.
+ let iterationW = w * (count || 1);
+ // The start position of the delay.
+ let delayX = delay < 0 ? x : this.startTimeToDistance(start);
+ // The width of the delay.
+ let delayW = this.durationToDistance(Math.abs(delay) / rate);
+ // The width of the delay if it is negative, 0 otherwise.
+ let negativeDelayW = delay < 0 ? delayW : 0;
+ // The width of the endDelay.
+ let endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
+ // The start position of the endDelay.
+ let endDelayX = endDelay < 0 ? x + iterationW - endDelayW
+ : x + iterationW;
+
+ return {x, w, iterationW, delayX, delayW, negativeDelayW,
+ endDelayX, endDelayW};
+ }
+};
+
+exports.TimeScale = TimeScale;