diff options
Diffstat (limited to 'devtools/client/animationinspector/utils.js')
-rw-r--r-- | devtools/client/animationinspector/utils.js | 275 |
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; |