fune/devtools/client/inspector/animation-old/components/animation-time-block.js
Julian Descottes 640fe52298 Bug 1454696 - Run eslint --fix for prefer-const;r=yulia
MozReview-Commit-ID: F6xUXCgdRE4

--HG--
extra : rebase_source : 65de1b0aba412d9044b5196115f74276caa058f2
2018-06-01 12:36:09 +02:00

677 lines
25 KiB
JavaScript

/* -*- 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";
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode, createSVGNode, TimeScale, getFormattedAnimationTitle} =
require("devtools/client/inspector/animation-old/utils");
const {SummaryGraphHelper, getPreferredKeyframesProgressThreshold,
getPreferredProgressThreshold} =
require("devtools/client/inspector/animation-old/graph-helper");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// Show max 10 iterations for infinite animations
// to give users a clue that the animation does repeat.
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
// Minimum opacity for semitransparent fill color for keyframes's easing graph.
const MIN_KEYFRAMES_EASING_OPACITY = .5;
/**
* 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);
}
exports.AnimationTimeBlock = AnimationTimeBlock;
AnimationTimeBlock.prototype = {
init: function(containerEl) {
this.containerEl = containerEl;
this.containerEl.addEventListener("click", this.onClick);
},
destroy: function() {
this.containerEl.removeEventListener("click", this.onClick);
this.unrender();
this.containerEl = null;
this.animation = null;
},
unrender: function() {
while (this.containerEl.firstChild) {
this.containerEl.firstChild.remove();
}
},
render: function(animation, tracks) {
this.unrender();
this.animation = animation;
// Animation summary graph element.
const summaryEl = createSVGNode({
parent: this.containerEl,
nodeType: "svg",
attributes: {
"class": "summary",
"preserveAspectRatio": "none"
}
});
this.updateSummaryGraphViewBox(summaryEl);
const {state} = this.animation;
// Total displayed duration
const totalDisplayedDuration = this.getTotalDisplayedDuration();
// Minimum segment duration is the duration of one pixel.
const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
// Minimum progress threshold for effect timing.
const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);
// Render summary graph.
// The summary graph is constructed from keyframes's easing and effect timing.
const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration);
renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration,
minEffectProgressThreshold, tracks, graphHelper);
if (state.easing !== "linear") {
renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration,
minEffectProgressThreshold, graphHelper);
}
graphHelper.destroy();
// The animation name is displayed over the animation.
const nameEl = createNode({
parent: this.containerEl,
attributes: {
"class": "name",
"title": this.getTooltipText(state)
}
});
createSVGNode({
parent: createSVGNode({
parent: nameEl,
nodeType: "svg",
}),
nodeType: "text",
attributes: {
"y": "50%",
"x": "100%",
},
textContent: state.name
});
// Delay.
if (state.delay) {
// Negative delays need to start at 0.
const delayEl = createNode({
parent: this.containerEl,
attributes: {
"class": "delay"
+ (state.delay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "backwards" ? " fill" : "")
}
});
this.updateDelayBounds(delayEl);
}
// endDelay
if (state.iterationCount && state.endDelay) {
const endDelayEl = createNode({
parent: this.containerEl,
attributes: {
"class": "end-delay"
+ (state.endDelay < 0 ? " negative" : " positive")
+ (state.fill === "both" ||
state.fill === "forwards" ? " fill" : "")
}
});
this.updateEndDelayBounds(endDelayEl);
}
},
/**
* Update animation and updating its DOM accordingly.
* Unlike 'render' method, this method does not generate any elements, but update
* the bounds of DOM.
* @param {Object} animation
*/
update: function(animation) {
this.animation = animation;
this.updateSummaryGraphViewBox(this.containerEl.querySelector(".summary"));
const delayEl = this.containerEl.querySelector(".delay");
if (delayEl) {
this.updateDelayBounds(delayEl);
}
const endDelayEl = this.containerEl.querySelector(".end-delay");
if (endDelayEl) {
this.updateEndDelayBounds(endDelayEl);
}
},
/**
* Update viewBox and style of SVG element for summary graph to fit to latest
* TimeScale.
* @param {Element} summaryEl - SVG element for summary graph.
*/
updateSummaryGraphViewBox: function(summaryEl) {
const {x, delayW} = TimeScale.getAnimationDimensions(this.animation);
const totalDisplayedDuration = this.getTotalDisplayedDuration();
const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
const {state} = this.animation;
summaryEl.setAttribute("viewBox",
`${state.delay < 0 ? state.delay : 0} ` +
`-${1 + strokeHeightForViewBox} ` +
`${totalDisplayedDuration} ` +
`${1 + strokeHeightForViewBox * 2}`);
summaryEl.setAttribute("style", `left: ${ x - (state.delay > 0 ? delayW : 0) }%`);
},
/**
* Update bounds of element which represents delay to fit to latest TimeScale.
* @param {Element} delayEl - which represents delay.
*/
updateDelayBounds: function(delayEl) {
const {delayX, delayW} = TimeScale.getAnimationDimensions(this.animation);
delayEl.style.left = `${ delayX }%`;
delayEl.style.width = `${ delayW }%`;
},
/**
* Update bounds of element which represents endDelay to fit to latest TimeScale.
* @param {Element} endDelayEl - which represents endDelay.
*/
updateEndDelayBounds: function(endDelayEl) {
const {endDelayX, endDelayW} = TimeScale.getAnimationDimensions(this.animation);
endDelayEl.style.left = `${ endDelayX }%`;
endDelayEl.style.width = `${ endDelayW }%`;
},
getTotalDisplayedDuration: function() {
return this.animation.state.playbackRate * TimeScale.getDuration();
},
getTooltipText: function(state) {
const getTime = time => L10N.getFormatStr("player.timeLabel",
L10N.numberWithDecimals(time / 1000, 2));
let text = "";
// Adding the name.
text += getFormattedAnimationTitle({state});
text += "\n";
// Adding the delay.
if (state.delay) {
text += L10N.getStr("player.animationDelayLabel") + " ";
text += getTime(state.delay);
text += "\n";
}
// Adding the duration.
text += L10N.getStr("player.animationDurationLabel") + " ";
text += getTime(state.duration);
text += "\n";
// Adding the endDelay.
if (state.endDelay) {
text += L10N.getStr("player.animationEndDelayLabel") + " ";
text += getTime(state.endDelay);
text += "\n";
}
// Adding the iteration count (the infinite symbol, or an integer).
if (state.iterationCount !== 1) {
text += L10N.getStr("player.animationIterationCountLabel") + " ";
text += state.iterationCount ||
L10N.getStr("player.infiniteIterationCountText");
text += "\n";
}
// Adding the iteration start.
if (state.iterationStart !== 0) {
const iterationStartTime = state.iterationStart * state.duration / 1000;
text += L10N.getFormatStr("player.animationIterationStartLabel",
state.iterationStart,
L10N.numberWithDecimals(iterationStartTime, 2));
text += "\n";
}
// Adding the easing if it is not "linear".
if (state.easing && state.easing !== "linear") {
text += L10N.getStr("player.animationOverallEasingLabel") + " ";
text += state.easing;
text += "\n";
}
// Adding the fill mode.
if (state.fill) {
text += L10N.getStr("player.animationFillLabel") + " ";
text += state.fill;
text += "\n";
}
// Adding the direction mode if it is not "normal".
if (state.direction && state.direction !== "normal") {
text += L10N.getStr("player.animationDirectionLabel") + " ";
text += state.direction;
text += "\n";
}
// Adding the playback rate if it's different than 1.
if (state.playbackRate !== 1) {
text += L10N.getStr("player.animationRateLabel") + " ";
text += state.playbackRate;
text += "\n";
}
// Adding the animation-timing-function
// if it is not "ease" which is default value for CSS Animations.
if (state.animationTimingFunction && state.animationTimingFunction !== "ease") {
text += L10N.getStr("player.animationTimingFunctionLabel") + " ";
text += state.animationTimingFunction;
text += "\n";
}
// Adding a note that the animation is running on the compositor thread if
// needed.
if (state.propertyState) {
if (state.propertyState
.every(propState => propState.runningOnCompositor)) {
text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
} else if (state.propertyState
.some(propState => propState.runningOnCompositor)) {
text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
}
} else if (state.isRunningOnCompositor) {
text += L10N.getStr("player.runningOnCompositorTooltip");
}
return text;
},
onClick: function(e) {
e.stopPropagation();
this.emit("selected", this.animation);
},
get win() {
return this.containerEl.ownerDocument.defaultView;
}
};
/**
* Render keyframes's easing graph.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderKeyframesEasingGraph(parentEl, state, totalDisplayedDuration,
minEffectProgressThreshold, tracks, graphHelper) {
const keyframesList = getOffsetAndEasingOnlyKeyframesList(tracks);
const keyframeEasingOpacity = Math.max(1 / keyframesList.length,
MIN_KEYFRAMES_EASING_OPACITY);
for (const keyframes of keyframesList) {
const minProgressTreshold =
Math.min(minEffectProgressThreshold,
getPreferredKeyframesProgressThreshold(keyframes));
graphHelper.setMinProgressThreshold(minProgressTreshold);
graphHelper.setKeyframes(keyframes);
graphHelper.setClosePathNeeded(true);
const element = renderGraph(parentEl, state, totalDisplayedDuration,
"keyframes-easing", graphHelper);
element.style.opacity = keyframeEasingOpacity;
}
}
/**
* Render effect easing graph.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderEffectEasingGraph(parentEl, state, totalDisplayedDuration,
minEffectProgressThreshold, graphHelper) {
graphHelper.setMinProgressThreshold(minEffectProgressThreshold);
graphHelper.setKeyframes(null);
graphHelper.setClosePathNeeded(false);
renderGraph(parentEl, state, totalDisplayedDuration, "effect-easing", graphHelper);
}
/**
* Render a graph of given parameters.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {float} totalDisplayedDuration - Displayable total duration.
* @param {String} className - Class name for graph element.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderGraph(parentEl, state, totalDisplayedDuration, className, graphHelper) {
const graphEl = createSVGNode({
parent: parentEl,
nodeType: "g",
attributes: {
"class": className,
}
});
// Starting time of main iteration.
let mainIterationStartTime = 0;
let iterationStart = state.iterationStart;
let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
graphHelper.setFillMode(state.fill);
graphHelper.setOriginalBehavior(true);
// Append delay.
if (state.delay > 0) {
renderDelay(graphEl, state, graphHelper);
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,
// 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 =
iterationStart % 1 === 0
? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
if (firstSectionCount) {
renderFirstIteration(graphEl, state, mainIterationStartTime,
firstSectionCount, graphHelper);
}
if (iterationCount === Infinity) {
// If the animation repeats infinitely,
// we fill the remaining area with iteration paths.
renderInfinity(graphEl, state, mainIterationStartTime,
firstSectionCount, totalDisplayedDuration, graphHelper);
} else {
// Otherwise, we show remaining iterations, endDelay and fill.
// Append forwards fill-mode.
if (state.fill === "both" || state.fill === "forwards") {
renderForwardsFill(graphEl, state, mainIterationStartTime,
iterationCount, totalDisplayedDuration, graphHelper);
}
// Append middle section of iterations.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
const middleSectionCount =
Math.floor(iterationCount - firstSectionCount);
renderMiddleIterations(graphEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount, graphHelper);
// 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(graphEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, graphHelper);
}
// Append endDelay.
if (state.endDelay > 0) {
renderEndDelay(graphEl, state,
mainIterationStartTime, iterationCount, graphHelper);
}
}
// Append negative delay (which overlap the animation).
if (state.delay < 0) {
graphHelper.setFillMode("both");
graphHelper.setOriginalBehavior(false);
renderNegativeDelayHiddenProgress(graphEl, state, graphHelper);
}
// Append negative endDelay (which overlap the animation).
if (state.iterationCount && state.endDelay < 0) {
graphHelper.setFillMode("both");
graphHelper.setOriginalBehavior(false);
renderNegativeEndDelayHiddenProgress(graphEl, state, graphHelper);
}
return graphEl;
}
/**
* Render delay section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderDelay(parentEl, state, graphHelper) {
const startSegment = graphHelper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderFirstIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, graphHelper) {
const startTime = mainIterationStartTime;
const endTime = startTime + firstSectionCount * state.duration;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderMiddleIterations(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
graphHelper) {
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 = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderLastIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, graphHelper) {
const startTime = mainIterationStartTime +
(firstSectionCount + middleSectionCount) * state.duration;
const endTime = startTime + lastSectionCount * state.duration;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderInfinity(parentEl, state, mainIterationStartTime,
firstSectionCount, totalDuration, graphHelper) {
// Calculate the number of iterations to display,
// with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
let uncappedInfinityIterationCount =
(totalDuration - firstSectionCount * state.duration) / state.duration;
// If there is a small floating point error resulting in, e.g. 1.0000001
// ceil will give us 2 so round first.
uncappedInfinityIterationCount =
parseFloat(uncappedInfinityIterationCount.toPrecision(6));
const infinityIterationCount =
Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
Math.ceil(uncappedInfinityIterationCount));
// Append first full iteration path.
const firstStartTime =
mainIterationStartTime + firstSectionCount * state.duration;
const firstEndTime = firstStartTime + state.duration;
const firstSegments =
graphHelper.createPathSegments(firstStartTime, firstEndTime);
graphHelper.appendShapePath(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 };
});
}
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderEndDelay(parentEl, state,
mainIterationStartTime, iterationCount, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration;
const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
*/
function renderForwardsFill(parentEl, state, mainIterationStartTime,
iterationCount, totalDuration, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration +
(state.endDelay > 0 ? state.endDelay : 0);
const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: totalDuration, y: startSegment.y };
graphHelper.appendShapePath(parentEl, [startSegment, endSegment], "fill-forwards-path");
}
/**
* Render hidden progress of negative delay.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
const startTime = state.delay;
const endTime = 0;
const segments =
graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(parentEl, segments, "delay-path negative");
}
/**
* Render hidden progress of negative endDelay.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
const endTime = state.delay + state.iterationCount * state.duration;
const startTime = endTime + state.endDelay;
const segments = graphHelper.createPathSegments(startTime, endTime);
graphHelper.appendShapePath(parentEl, segments, "enddelay-path negative");
}
/**
* Create new keyframes object which has only offset and easing.
* Also, the returned value has no duplication.
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
* @return {Array} keyframes list.
*/
function getOffsetAndEasingOnlyKeyframesList(tracks) {
return Object.keys(tracks).reduce((result, name) => {
const track = tracks[name];
const exists = result.find(keyframes => {
if (track.length !== keyframes.length) {
return false;
}
for (let i = 0; i < track.length; i++) {
const keyframe1 = track[i];
const keyframe2 = keyframes[i];
if (keyframe1.offset !== keyframe2.offset ||
keyframe1.easing !== keyframe2.easing) {
return false;
}
}
return true;
});
if (!exists) {
const keyframes = track.map(keyframe => {
return { offset: keyframe.offset, easing: keyframe.easing };
});
result.push(keyframes);
}
return result;
}, []);
}