--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -80,77 +80,16 @@ AnimationDetails.prototype = {
} else if (!isRunningOnCompositor && warning != "") {
className = "warning";
}
}
return {className, warning};
},
/**
- * Get a list of the tracks of the animation actor
- * @return {Object} A list of tracks, one per animated property, each
- * with a list of keyframes
- */
- getTracks: Task.async(function* () {
- let tracks = {};
-
- /*
- * getFrames is a AnimationPlayorActor method that returns data about the
- * keyframes of the animation.
- * In FF48, the data it returns change, and will hold only longhand
- * properties ( e.g. borderLeftWidth ), which does not match what we
- * want to display in the animation detail.
- * A new AnimationPlayerActor function, getProperties, is introduced,
- * that returns the animated css properties of the animation and their
- * keyframes values.
- * If the animation actor has the getProperties function, we use it, and if
- * not, we fall back to getFrames, which then returns values we used to
- * handle.
- */
- if (this.serverTraits.hasGetProperties) {
- let properties = yield this.animation.getProperties();
- for (let {name, values} of properties) {
- if (!tracks[name]) {
- tracks[name] = [];
- }
-
- for (let {value, offset, easing, distance} of values) {
- distance = distance ? distance : 0;
- tracks[name].push({value, offset, easing, distance});
- }
- }
- } else {
- let frames = yield this.animation.getFrames();
- for (let frame of frames) {
- for (let name in frame) {
- if (this.NON_PROPERTIES.indexOf(name) != -1) {
- continue;
- }
-
- // We have to change to CSS property name
- // since GetKeyframes returns JS property name.
- const propertyCSSName = getCssPropertyName(name);
- if (!tracks[propertyCSSName]) {
- tracks[propertyCSSName] = [];
- }
-
- tracks[propertyCSSName].push({
- value: frame[name],
- offset: frame.computedOffset,
- easing: frame.easing,
- distance: 0
- });
- }
- }
- }
-
- return tracks;
- }),
-
- /**
* Get animation types of given CSS property names.
* @param {Array} CSS property names.
* e.g. ["background-color", "opacity", ...]
* @return {Object} Animation type mapped with CSS property name.
* e.g. { "background-color": "color", }
* "opacity": "float", ... }
*/
getAnimationTypes: Task.async(function* (propertyNames) {
@@ -160,33 +99,31 @@ AnimationDetails.prototype = {
// Set animation type 'none' since does not support getAnimationTypes.
const animationTypes = {};
propertyNames.forEach(propertyName => {
animationTypes[propertyName] = "none";
});
return Promise.resolve(animationTypes);
}),
- render: Task.async(function* (animation) {
+ render: Task.async(function* (animation, tracks) {
this.unrender();
if (!animation) {
return;
}
this.animation = animation;
+ this.tracks = tracks;
// We might have been destroyed in the meantime, or the component might
// have been re-rendered.
if (!this.containerEl || this.animation !== animation) {
return;
}
- // Build an element for each animated property track.
- this.tracks = yield this.getTracks(animation, this.serverTraits);
-
// Get animation type for each CSS properties.
const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
// Render progress indicator.
this.renderProgressIndicator();
// Render animated properties header.
this.renderAnimatedPropertiesHeader();
// Render animated properties body.
@@ -194,20 +131,16 @@ AnimationDetails.prototype = {
// Create dummy animation to indicate the animation progress.
const timing = Object.assign({}, animation.state, {
iterations: animation.state.iterationCount
? animation.state.iterationCount : Infinity
});
this.dummyAnimation =
new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
-
- // Useful for tests to know when rendering of all animation detail UIs
- // have been completed.
- this.emit("animation-detail-rendering-completed");
}),
renderAnimatedPropertiesHeader: function () {
// Add animated property header.
const headerEl = createNode({
parent: this.containerEl,
attributes: { "class": "animated-properties-header" }
});
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -4,27 +4,31 @@
* 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/animationinspector/utils");
-const {createPathSegments, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+const {SummaryGraphHelper, getPreferredKeyframesProgressThreshold,
+ getPreferredProgressThreshold} =
require("devtools/client/animationinspector/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);
}
@@ -45,17 +49,17 @@ AnimationTimeBlock.prototype = {
},
unrender: function () {
while (this.containerEl.firstChild) {
this.containerEl.firstChild.remove();
}
},
- render: function (animation) {
+ render: function (animation, tracks) {
this.unrender();
this.animation = animation;
let {state} = this.animation;
// Create a container element to hold the delay and iterations.
// It is positioned according to its delay (divided by the playbackrate),
// and its width is according to its duration (divided by the playbackrate).
@@ -81,126 +85,31 @@ AnimationTimeBlock.prototype = {
// Set viewBox
summaryEl.setAttribute("viewBox",
`${ state.delay < 0 ? state.delay : 0 }
-${ 1 + strokeHeightForViewBox }
${ totalDisplayedDuration }
${ 1 + strokeHeightForViewBox * 2 }`);
- // Get a helper function that returns the path segment of timing-function.
- const segmentHelper = getSegmentHelper(state, this.win);
-
// Minimum segment duration is the duration of one pixel.
- const minSegmentDuration =
- totalDisplayedDuration / this.containerEl.clientWidth;
- // Minimum progress threshold.
- let minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
- // If the easing is step function,
- // minProgressThreshold should be changed by the steps.
- const stepFunction = state.easing.match(/steps\((\d+)/);
- if (stepFunction) {
- minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
- }
-
- // Starting time of main iteration.
- let mainIterationStartTime = 0;
- let iterationStart = state.iterationStart;
- let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
-
- // Append delay.
- if (state.delay > 0) {
- renderDelay(summaryEl, state, segmentHelper);
- 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(summaryEl, state, mainIterationStartTime,
- firstSectionCount, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- }
+ const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
+ // Minimum progress threshold for effect timing.
+ const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);
- if (iterationCount === Infinity) {
- // If the animation repeats infinitely,
- // we fill the remaining area with iteration paths.
- renderInfinity(summaryEl, state, mainIterationStartTime,
- firstSectionCount, totalDisplayedDuration,
- minSegmentDuration, minProgressThreshold, segmentHelper);
- } else {
- // Otherwise, we show remaining iterations, endDelay and fill.
-
- // Append forwards fill-mode.
- if (state.fill === "both" || state.fill === "forwards") {
- renderForwardsFill(summaryEl, state, mainIterationStartTime,
- iterationCount, totalDisplayedDuration,
- segmentHelper);
- }
-
- // Append middle section of iterations.
- // e.g.
- // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
- const middleSectionCount =
- Math.floor(iterationCount - firstSectionCount);
- renderMiddleIterations(summaryEl, state, mainIterationStartTime,
- firstSectionCount, middleSectionCount,
- minSegmentDuration, minProgressThreshold,
- segmentHelper);
-
- // 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(summaryEl, state, mainIterationStartTime,
- firstSectionCount, middleSectionCount,
- lastSectionCount, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- }
-
- // Append endDelay.
- if (state.endDelay > 0) {
- renderEndDelay(summaryEl, state,
- mainIterationStartTime, iterationCount, segmentHelper);
- }
+ // 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);
}
-
- // Append negative delay (which overlap the animation).
- if (state.delay < 0) {
- segmentHelper.animation.effect.timing.fill = "both";
- segmentHelper.asOriginalBehavior = false;
- renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- }
- // Append negative endDelay (which overlap the animation).
- if (state.iterationCount && state.endDelay < 0) {
- if (segmentHelper.asOriginalBehavior) {
- segmentHelper.animation.effect.timing.fill = "both";
- segmentHelper.asOriginalBehavior = false;
- }
- renderNegativeEndDelayHiddenProgress(summaryEl, state,
- minSegmentDuration,
- minProgressThreshold,
- segmentHelper);
- }
+ graphHelper.destroy();
// The animation name is displayed over the animation.
createNode({
parent: createNode({
parent: this.containerEl,
attributes: {
"class": "name",
"title": this.getTooltipText(state)
@@ -336,114 +245,246 @@ AnimationTimeBlock.prototype = {
},
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 (let 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} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
-function renderDelay(parentEl, state, segmentHelper) {
- const startSegment = segmentHelper.getSegment(0);
+function renderDelay(parentEl, state, graphHelper) {
+ const startSegment = graphHelper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
- appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderFirstIteration(parentEl, state, mainIterationStartTime,
- firstSectionCount, minSegmentDuration,
- minProgressThreshold, segmentHelper) {
+ firstSectionCount, graphHelper) {
const startTime = mainIterationStartTime;
const endTime = startTime + firstSectionCount * state.duration;
- const segments =
- createPathSegments(startTime, endTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, segments, "iteration-path");
+ const segments = graphHelper.createPathSegments(startTime, endTime);
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderMiddleIterations(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
- minSegmentDuration, minProgressThreshold,
- segmentHelper) {
+ 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 =
- createPathSegments(startTime, endTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, segments, "iteration-path");
+ const segments = graphHelper.createPathSegments(startTime, endTime);
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderLastIteration(parentEl, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
- lastSectionCount, minSegmentDuration,
- minProgressThreshold, segmentHelper) {
+ lastSectionCount, graphHelper) {
const startTime = mainIterationStartTime +
(firstSectionCount + middleSectionCount) * state.duration;
const endTime = startTime + lastSectionCount * state.duration;
- const segments =
- createPathSegments(startTime, endTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, segments, "iteration-path");
+ const segments = graphHelper.createPathSegments(startTime, endTime);
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderInfinity(parentEl, state, mainIterationStartTime,
- firstSectionCount, totalDuration, minSegmentDuration,
- minProgressThreshold, segmentHelper) {
+ 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));
@@ -451,19 +492,18 @@ function renderInfinity(parentEl, state,
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 =
- createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+ graphHelper.createPathSegments(firstStartTime, firstEndTime);
+ graphHelper.appendPathElement(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.
@@ -471,132 +511,106 @@ function renderInfinity(parentEl, state,
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 };
});
}
- appendPathElement(parentEl, segments, "iteration-path infinity copied");
+ graphHelper.appendPathElement(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} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderEndDelay(parentEl, state,
- mainIterationStartTime, iterationCount, segmentHelper) {
+ mainIterationStartTime, iterationCount, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration;
- const startSegment = segmentHelper.getSegment(startTime);
+ const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
- appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+ graphHelper.appendPathElement(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} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
function renderForwardsFill(parentEl, state, mainIterationStartTime,
- iterationCount, totalDuration, segmentHelper) {
+ iterationCount, totalDuration, graphHelper) {
const startTime = mainIterationStartTime + iterationCount * state.duration +
(state.endDelay > 0 ? state.endDelay : 0);
- const startSegment = segmentHelper.getSegment(startTime);
+ const startSegment = graphHelper.getSegment(startTime);
const endSegment = { x: totalDuration, y: startSegment.y };
- appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
-function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
- minProgressThreshold,
- segmentHelper) {
+function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
const startTime = state.delay;
const endTime = 0;
const segments =
- createPathSegments(startTime, endTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, segments, "delay-path negative");
+ graphHelper.createPathSegments(startTime, endTime);
+ graphHelper.appendPathElement(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 {Number} minSegmentDuration - Minimum segment duration.
- * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
*/
-function renderNegativeEndDelayHiddenProgress(parentEl, state,
- minSegmentDuration,
- minProgressThreshold,
- segmentHelper) {
+function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
const endTime = state.delay + state.iterationCount * state.duration;
const startTime = endTime + state.endDelay;
- const segments =
- createPathSegments(startTime, endTime, minSegmentDuration,
- minProgressThreshold, segmentHelper);
- appendPathElement(parentEl, segments, "enddelay-path negative");
+ const segments = graphHelper.createPathSegments(startTime, endTime);
+ graphHelper.appendPathElement(parentEl, segments, "enddelay-path negative");
}
/**
- * Get a helper function which returns the segment coord from given time.
- * @param {Object} state - animation state
- * @param {Object} win - window object
- * @return {Object} A segmentHelper object that has the following properties:
- * - animation: The script animation used to get the progress
- * - endTime: The end time of the animation
- * - asOriginalBehavior: The spec is that the progress of animation is changed
- * if the time of setCurrentTime is during the endDelay.
- * Likewise, in case the time is less than 0.
- * If this flag is true, we prevent the time
- * to make the same animation behavior as the original.
- * - getSegment: Helper function that, given a time,
- * will calculate the progress through the dummy animation.
+ * 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 getSegmentHelper(state, win) {
- // Create a dummy Animation timing data as the
- // state object we're being passed in.
- const timing = Object.assign({}, state, {
- iterations: state.iterationCount ? state.iterationCount : Infinity
- });
-
- // Create a dummy Animation with the given timing.
- const dummyAnimation =
- new win.Animation(new win.KeyframeEffect(null, null, timing), null);
-
- // Returns segment helper object.
- return {
- animation: dummyAnimation,
- endTime: dummyAnimation.effect.getComputedTiming().endTime,
- asOriginalBehavior: true,
- getSegment: function (time) {
- if (this.asOriginalBehavior) {
- // If the given time is less than 0, returned progress is 0.
- if (time < 0) {
- return { x: time, y: 0 };
+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;
}
- // Avoid to apply over endTime.
- this.animation.currentTime = time < this.endTime ? time : this.endTime;
- } else {
- this.animation.currentTime = time;
}
- const progress = this.animation.effect.getComputedTiming().progress;
- return { x: time, y: Math.max(progress, 0) };
+ return true;
+ });
+ if (!exists) {
+ const keyframes = track.map(keyframe => {
+ return { offset: keyframe.offset, easing: keyframe.easing };
+ });
+ result.push(keyframes);
}
- };
+ return result;
+ }, []);
}
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -7,17 +7,18 @@
"use strict";
const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {
createNode,
findOptimalTimeInterval,
getFormattedAnimationTitle,
- TimeScale
+ TimeScale,
+ getCssPropertyName
} = require("devtools/client/animationinspector/utils");
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
@@ -41,16 +42,17 @@ const TIMELINE_BACKGROUND_RESIZE_DEBOUNC
* when this happens, the component emits "current-data-changed" events with the
* new time and state of the timeline.
*
* @param {InspectorPanel} inspector.
* @param {Object} serverTraits The list of server-side capabilities.
*/
function AnimationsTimeline(inspector, serverTraits) {
this.animations = [];
+ this.tracksMap = new WeakMap();
this.targetNodes = [];
this.timeBlocks = [];
this.inspector = inspector;
this.serverTraits = serverTraits;
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
@@ -239,16 +241,17 @@ AnimationsTimeline.prototype = {
this.onScrubberMouseDown);
this.scrubberHandleEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.animationDetailCloseButton.removeEventListener("click",
this.onDetailCloseButtonClick);
this.rootWrapperEl.remove();
this.animations = [];
+ this.tracksMap = null;
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
this.animatedPropertiesEl = null;
this.scrubberEl = null;
this.scrubberHandleEl = null;
this.win = null;
this.inspector = null;
@@ -337,20 +340,20 @@ AnimationsTimeline.prototype = {
this.animatedPropertiesEl.className =
`animated-properties ${ animation.state.type }`;
}
// Select and render.
const selectedAnimationEl = animationEls[index];
selectedAnimationEl.classList.add("selected");
this.animationRootEl.classList.add("animation-detail-visible");
+ // Don't render if the detail displays same animation already.
if (animation !== this.details.animation) {
this.selectedAnimation = animation;
- // Don't render if the detail displays same animation already.
- yield this.details.render(animation);
+ yield this.details.render(animation, this.tracksMap.get(animation));
this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
}
this.onTimelineDataChanged(null, { time: this.currentTime || 0 });
this.emit("animation-selected", animation);
}),
/**
* When move the scrubber to the corresponding position
@@ -424,21 +427,22 @@ AnimationsTimeline.prototype = {
state.propertyState.some(propState => !propState.runningOnCompositor)
? " some-properties"
: " all-properties";
}
return className;
},
- render: function (animations, documentCurrentTime) {
+ render: Task.async(function* (animations, documentCurrentTime) {
this.unrenderButLeaveDetailsComponent();
this.animations = animations;
if (!this.animations.length) {
+ this.emit("animation-timeline-rendering-completed");
return;
}
// Loop first to set the time scale for all current animations.
for (let {state} of animations) {
TimeScale.addAnimation(state);
}
@@ -476,20 +480,22 @@ AnimationsTimeline.prototype = {
let timeBlockEl = createNode({
parent: animationEl,
attributes: {
"class": "time-block track-container"
}
});
// Draw the animation time block.
+ const tracks = yield this.getTracks(animation);
let timeBlock = new AnimationTimeBlock();
timeBlock.init(timeBlockEl);
- timeBlock.render(animation);
+ timeBlock.render(animation, tracks);
this.timeBlocks.push(timeBlock);
+ this.tracksMap.set(animation, tracks);
timeBlock.on("selected", this.onAnimationSelected);
}
// Use the document's current time to position the scrubber (if the server
// doesn't provide it, hide the scrubber entirely).
// Note that because the currentTime was sent via the protocol, some time
// may have gone by since then, and so the scrubber might be a bit late.
@@ -500,32 +506,31 @@ AnimationsTimeline.prototype = {
this.startAnimatingScrubber(this.wasRewound()
? TimeScale.minStartTime
: documentCurrentTime);
}
// To indicate the animation progress in AnimationDetails.
this.on("timeline-data-changed", this.onTimelineDataChanged);
- // Display animation's detail if there is only one animation,
- // or the previously displayed animation is included in timeline list.
if (this.animations.length === 1) {
- this.onAnimationSelected(null, this.animations[0]);
- return;
+ // Display animation's detail if there is only one animation,
+ // even if the detail pane is closing.
+ yield this.onAnimationSelected(null, this.animations[0]);
+ } else if (this.animationRootEl.classList.contains("animation-detail-visible") &&
+ this.animations.indexOf(this.selectedAnimation) >= 0) {
+ // animation's detail displays in case of the previously displayed animation is
+ // included in timeline list and the detail pane is not closing.
+ yield this.onAnimationSelected(null, this.selectedAnimation);
+ } else {
+ // Otherwise, close detail pane.
+ this.onDetailCloseButtonClick();
}
- if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
- // Do nothing since the animation detail pane is closing now.
- return;
- }
- if (this.animations.indexOf(this.selectedAnimation) >= 0) {
- this.onAnimationSelected(null, this.selectedAnimation);
- return;
- }
- this.onDetailCloseButtonClick();
- },
+ this.emit("animation-timeline-rendering-completed");
+ }),
isAtLeastOneAnimationPlaying: function () {
return this.animations.some(({state}) => state.playState === "running");
},
wasRewound: function () {
return !this.isAtLeastOneAnimationPlaying() &&
this.animations.every(({state}) => state.currentTime === 0);
@@ -634,12 +639,77 @@ AnimationsTimeline.prototype = {
onTimelineDataChanged: function (e, { time }) {
this.currentTime = time;
const indicateTime =
TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
this.details.indicateProgress(indicateTime);
},
onDetailCloseButtonClick: function (e) {
+ if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
+ return;
+ }
this.animationRootEl.classList.remove("animation-detail-visible");
this.emit("animation-detail-closed");
- }
+ },
+
+ /**
+ * Get a list of the tracks of the animation actor
+ * @param {Object} animation
+ * @return {Object} A list of tracks, one per animated property, each
+ * with a list of keyframes
+ */
+ getTracks: Task.async(function* (animation) {
+ let tracks = {};
+
+ /*
+ * getFrames is a AnimationPlayorActor method that returns data about the
+ * keyframes of the animation.
+ * In FF48, the data it returns change, and will hold only longhand
+ * properties ( e.g. borderLeftWidth ), which does not match what we
+ * want to display in the animation detail.
+ * A new AnimationPlayerActor function, getProperties, is introduced,
+ * that returns the animated css properties of the animation and their
+ * keyframes values.
+ * If the animation actor has the getProperties function, we use it, and if
+ * not, we fall back to getFrames, which then returns values we used to
+ * handle.
+ */
+ if (this.serverTraits.hasGetProperties) {
+ let properties = yield animation.getProperties();
+ for (let {name, values} of properties) {
+ if (!tracks[name]) {
+ tracks[name] = [];
+ }
+
+ for (let {value, offset, easing, distance} of values) {
+ distance = distance ? distance : 0;
+ tracks[name].push({value, offset, easing, distance});
+ }
+ }
+ } else {
+ let frames = yield animation.getFrames();
+ for (let frame of frames) {
+ for (let name in frame) {
+ if (this.NON_PROPERTIES.indexOf(name) != -1) {
+ continue;
+ }
+
+ // We have to change to CSS property name
+ // since GetKeyframes returns JS property name.
+ const propertyCSSName = getCssPropertyName(name);
+ if (!tracks[propertyCSSName]) {
+ tracks[propertyCSSName] = [];
+ }
+
+ tracks[propertyCSSName].push({
+ value: frame[name],
+ offset: frame.computedOffset,
+ easing: frame.easing,
+ distance: 0
+ });
+ }
+ }
+ }
+
+ return tracks;
+ })
};
--- a/devtools/client/animationinspector/components/keyframes.js
+++ b/devtools/client/animationinspector/components/keyframes.js
@@ -4,17 +4,17 @@
* 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} =
require("devtools/client/animationinspector/utils");
-const {ProgressGraphHelper, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+const {ProgressGraphHelper, appendPathElement, getPreferredKeyframesProgressThreshold} =
require("devtools/client/animationinspector/graph-helper.js");
// Counter for linearGradient ID.
let LINEAR_GRADIENT_ID_COUNTER = 0;
/**
* UI component responsible for displaying a list of keyframes.
* Also, shows a graphical graph for the animation progress of one iteration.
@@ -71,17 +71,17 @@ Keyframes.prototype = {
${ 1 + strokeHeightForViewBox * 2 }`);
// Create graph helper to render the animation property graph.
const graphHelper =
new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
propertyName, animationType, keyframes, totalDuration);
renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
- DEFAULT_MIN_PROGRESS_THRESHOLD, graphHelper);
+ getPreferredKeyframesProgressThreshold(keyframes), graphHelper);
// Destroy ProgressGraphHelper resources.
graphHelper.destroy();
// Append elements to display keyframe values.
this.keyframesEl.classList.add(animation.state.type);
for (let frame of this.keyframes) {
createNode({
--- a/devtools/client/animationinspector/graph-helper.js
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -241,16 +241,195 @@ ProgressGraphHelper.prototype = {
: createPathSegments(startTime, endTime,
minSegmentDuration, minProgressThreshold, this);
},
};
exports.ProgressGraphHelper = ProgressGraphHelper;
/**
+ * This class is used for creating the summary graph in animation-timeline.
+ * The shape of the graph can be changed by using the following methods:
+ * setKeyframes:
+ * If null, the shape is by computed timing progress.
+ * Otherwise, by computed style of 'opacity' to combine effect easing and
+ * keyframe's easing.
+ * setFillMode:
+ * Animation fill-mode (e.g. "none", "backwards", "forwards" or "both")
+ * setClosePathNeeded:
+ * If true, appendPathElement make the last segment of <path> element to
+ * "close" segment("Z").
+ * Therefore, if don't need under-line of graph, please set false.
+ * setOriginalBehavior:
+ * In Animation::SetCurrentTime spec, even if current time of animation is over
+ * the endTime, the progress is changed. Likewise, in case the time is less than 0.
+ * If set true, prevent the time to make the same animation behavior as the original.
+ * setMinProgressThreshold:
+ * SummaryGraphHelper searches and creates the summary graph until the progress
+ * distance is less than this minProgressThreshold.
+ * So, while setting a low threshold produces a smooth graph,
+ * it will have an effect on performance.
+ * @param {Object} win - window object.
+ * @param {Object} state - animation state.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ */
+function SummaryGraphHelper(win, state, minSegmentDuration) {
+ this.win = win;
+ const doc = this.win.document;
+ this.targetEl = doc.createElement("div");
+ doc.documentElement.appendChild(this.targetEl);
+
+ const effectTiming = Object.assign({}, state, {
+ iterations: state.iterationCount ? state.iterationCount : Infinity
+ });
+ this.animation = this.targetEl.animate(null, effectTiming);
+ this.animation.pause();
+ this.endTime = this.animation.effect.getComputedTiming().endTime;
+
+ this.minSegmentDuration = minSegmentDuration;
+ this.minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+}
+
+SummaryGraphHelper.prototype = {
+ /**
+ * Destory this object.
+ */
+ destroy: function () {
+ this.animation.cancel();
+ this.targetEl.remove();
+ this.targetEl = null;
+ this.animation = null;
+ this.win = null;
+ },
+
+ /*
+ * Set keyframes to shape graph by computed style. This method creates new keyframe
+ * object using only offset and easing of given keyframes.
+ * Also, allows null value. In case of null, this graph helper shapes graph using
+ * computed timing progress.
+ * @param {Object} keyframes - Should have offset and easing, or null.
+ */
+ setKeyframes: function (keyframes) {
+ let frames = null;
+ if (keyframes) {
+ // Create new keyframes for opacity as computed style.
+ frames = keyframes.map(keyframe => {
+ return {
+ opacity: keyframe.offset,
+ offset: keyframe.offset,
+ easing: keyframe.easing
+ };
+ });
+ }
+ this.animation.effect.setKeyframes(frames);
+ this.hasFrames = !!frames;
+ },
+
+ /*
+ * Set animation behavior.
+ * In Animation::SetCurrentTime spec, even if current time of animation is over
+ * endTime, the progress is changed. Likewise, in case the time is less than 0.
+ * If set true, we prevent the time to make the same animation behavior as the original.
+ * @param {bool} isOriginalBehavior - true: original behavior
+ * false: according to spec.
+ */
+ setOriginalBehavior: function (isOriginalBehavior) {
+ this.isOriginalBehavior = isOriginalBehavior;
+ },
+
+ /**
+ * Set animation fill mode.
+ * @param {String} fill - "both", "forwards", "backwards" or "both"
+ */
+ setFillMode: function (fill) {
+ this.animation.effect.timing.fill = fill;
+ },
+
+ /**
+ * Set true if need to close path in appendPathElement.
+ * @param {bool} isClosePathNeeded - true: close, false: open.
+ */
+ setClosePathNeeded: function (isClosePathNeeded) {
+ this.isClosePathNeeded = isClosePathNeeded;
+ },
+
+ /**
+ * SummaryGraphHelper searches and creates the summary graph untill the progress
+ * distance is less than this minProgressThreshold.
+ */
+ setMinProgressThreshold: function (minProgressThreshold) {
+ this.minProgressThreshold = minProgressThreshold;
+ },
+
+ /**
+ * Return a segment in graph by given the time.
+ * @return {Object} Computed result which has follwing values.
+ * - x: x value of graph (float)
+ * - y: y value of graph (float between 0 - 1)
+ */
+ getSegment: function (time) {
+ if (this.isOriginalBehavior) {
+ // If the given time is less than 0, returned progress is 0.
+ if (time < 0) {
+ return { x: time, y: 0 };
+ }
+ // Avoid to apply over endTime.
+ this.animation.currentTime = time < this.endTime ? time : this.endTime;
+ } else {
+ this.animation.currentTime = time;
+ }
+ const value = this.hasFrames ? this.getOpacityValue() : this.getProgressValue();
+ return { x: time, y: value };
+ },
+
+ /**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @return {Array} path segments -
+ * [{x: {Number} time, y: {Number} progress}, ...]
+ */
+ createPathSegments: function (startTime, endTime) {
+ return createPathSegments(startTime, endTime,
+ this.minSegmentDuration, this.minProgressThreshold, this);
+ },
+
+ /**
+ * Append path element.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} pathSegments - Path segments. Please see createPathSegments.
+ * @param {String} cls - Class name.
+ * @return {Element} path element.
+ */
+ appendPathElement: function (parentEl, pathSegments, cls) {
+ return appendPathElement(parentEl, pathSegments, cls, this.isClosePathNeeded);
+ },
+
+ /**
+ * Return current computed timing progress of the animation.
+ * @return {float} computed timing progress as float value of Y axis.
+ */
+ getProgressValue: function () {
+ return Math.max(this.animation.effect.getComputedTiming().progress, 0);
+ },
+
+ /**
+ * Return current computed 'opacity' value of the element which is animating.
+ * @return {float} computed timing progress as float value of Y axis.
+ */
+ getOpacityValue: function () {
+ return this.win.getComputedStyle(this.targetEl).opacity;
+ }
+};
+
+exports.SummaryGraphHelper = SummaryGraphHelper;
+
+/**
* Create the path segments from given parameters.
* @param {Number} startTime - Starting time of animation.
* @param {Number} endTime - Ending time of animation.
* @param {Number} minSegmentDuration - Minimum segment duration.
* @param {Number} minProgressThreshold - Minimum progress threshold.
* @param {Object} segmentHelper
* - getSegment(time): Helper function that, given a time,
* will calculate the animation progress.
@@ -303,19 +482,20 @@ function createPathSegments(startTime, e
}
exports.createPathSegments = createPathSegments;
/**
* Append path element.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
* @param {String} cls - Class name.
+ * @param {bool} isClosePathNeeded - Set true if need to close the path. (default true)
* @return {Element} path element.
*/
-function appendPathElement(parentEl, pathSegments, cls) {
+function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded = true) {
// Create path string.
let path = `M${ pathSegments[0].x },0`;
for (let i = 0; i < pathSegments.length; i++) {
const pathSegment = pathSegments[i];
if (!pathSegment.easing || pathSegment.easing === "linear") {
path += createLinePathString(pathSegment);
continue;
}
@@ -325,17 +505,20 @@ function appendPathElement(parentEl, pat
break;
}
const nextPathSegment = pathSegments[i + 1];
path += pathSegment.easing.startsWith("steps")
? createStepsPathString(pathSegment, nextPathSegment)
: createCubicBezierPathString(pathSegment, nextPathSegment);
}
- path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+ path += ` L${ pathSegments[pathSegments.length - 1].x },0`;
+ if (isClosePathNeeded) {
+ path += " Z";
+ }
// Append and return the path element.
return createSVGNode({
parent: parentEl,
nodeType: "path",
attributes: {
"d": path,
"class": cls,
"vector-effect": "non-scaling-stroke",
@@ -454,8 +637,47 @@ function getRGBADistance(rgba1, rgba2) {
const endG = rgba2.g * endA;
const endB = rgba2.b * endA;
const diffA = startA - endA;
const diffR = startR - endR;
const diffG = startG - endG;
const diffB = startB - endB;
return Math.sqrt(diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB);
}
+
+/**
+ * Return preferred progress threshold for given keyframes.
+ * See the documentation of DURATION_RESOLUTION and DEFAULT_MIN_PROGRESS_THRESHOLD
+ * for more information regarding this.
+ * @param {Array} keyframes - keyframes
+ * @return {float} - preferred progress threshold.
+ */
+function getPreferredKeyframesProgressThreshold(keyframes) {
+ let minProgressTreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const keyframe = keyframes[i];
+ if (!keyframe.easing) {
+ continue;
+ }
+ let keyframeProgressThreshold = getPreferredProgressThreshold(keyframe.easing);
+ if (keyframeProgressThreshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
+ // We should consider the keyframe's duration.
+ keyframeProgressThreshold *=
+ (keyframes[i + 1].offset - keyframe.offset);
+ }
+ minProgressTreshold = Math.min(keyframeProgressThreshold, minProgressTreshold);
+ }
+ return minProgressTreshold;
+}
+exports.getPreferredKeyframesProgressThreshold = getPreferredKeyframesProgressThreshold;
+
+/**
+ * Return preferred progress threshold to render summary graph.
+ * @param {String} - easing e.g. steps(2), linear and so on.
+ * @return {float} - preferred threshold.
+ */
+function getPreferredProgressThreshold(easing) {
+ const stepFunction = easing.match(/steps\((\d+)/);
+ return stepFunction
+ ? 1 / (parseInt(stepFunction[1], 10) + 1)
+ : DEFAULT_MIN_PROGRESS_THRESHOLD;
+}
+exports.getPreferredProgressThreshold = getPreferredProgressThreshold;
--- a/devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
@@ -6,20 +6,20 @@
// Test for animations that have different starting time.
// We should check progress indicator working well even if the start time is not zero.
// Also, check that there is no duplication display.
add_task(function* () {
yield addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
const { panel } = yield openAnimationInspector();
- yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target2");
- yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target3");
- yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target4");
- yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target5");
+ yield setStyle(null, panel, "animation", "anim 100s", "#target2");
+ yield setStyle(null, panel, "animation", "anim 100s", "#target3");
+ yield setStyle(null, panel, "animation", "anim 100s", "#target4");
+ yield setStyle(null, panel, "animation", "anim 100s", "#target5");
const timelineComponent = panel.animationsTimelineComponent;
const detailsComponent = timelineComponent.details;
const headers =
detailsComponent.containerEl.querySelectorAll(".animated-properties-header");
is(headers.length, 1, "There should be only one header in the details panel");
// Check indicator.
@@ -37,14 +37,8 @@ add_task(function* () {
"The progress indicator position should be 50% at half time of animation");
detailsComponent.indicateProgress(startTime + 99 * 1000);
is(progressIndicatorEl.style.left, "99%",
"The progress indicator position should be 99% at 99s");
detailsComponent.indicateProgress(startTime + 100 * 1000);
is(progressIndicatorEl.style.left, "0%",
"The progress indicator position should be 0% at end of animation");
});
-
-function* setStyleAndWaitForAnimationSelecting(panel, name, value, selector) {
- const onSelecting = waitForAnimationSelecting(panel);
- yield setStyle(null, panel, name, value, selector);
- yield onSelecting;
-}
--- a/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
+++ b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
@@ -34,10 +34,11 @@ function* startNewAnimation(controller,
info("Add a new animation to the page and check the time again");
let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
selector: ".still",
attributeName: "class",
attributeValue: "ball still short"
});
yield onPlayerAdded;
+ yield waitForAnimationTimelineRendering(panel);
yield waitForAllAnimationTargets(panel);
}
--- a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
+++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -12,17 +12,19 @@
add_task(function* () {
yield addTab(URL_ROOT + "doc_negative_animation.html");
let {controller, panel} = yield openAnimationInspector();
info("Wait until all animations have been added " +
"(they're added with setTimeout)");
while (controller.animationPlayers.length < 3) {
yield controller.once(controller.PLAYERS_UPDATED_EVENT);
+ yield waitForAnimationTimelineRendering(panel);
}
+
yield waitForAllAnimationTargets(panel);
is(panel.animationsTimelineComponent.animations.length, 3,
"The timeline shows 3 animations too");
// Reduce the known nodeFronts to a set to make them unique.
let nodeFronts = new Set(panel.animationsTimelineComponent
.targetNodes.map(n => n.previewer.nodeFront));
--- a/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
@@ -17,35 +17,32 @@ add_task(function* () {
assertAnimationsDisplayed(panel, 0);
info("Start an animation on the node");
yield changeElementAndWait({
selector: ".still",
attributeName: "class",
attributeValue: "ball animated"
- }, panel, inspector, true);
+ }, panel, inspector);
assertAnimationsDisplayed(panel, 1);
info("Remove the animation class on the node");
yield changeElementAndWait({
selector: ".ball.animated",
attributeName: "class",
attributeValue: "ball still"
- }, panel, inspector, false);
+ }, panel, inspector);
assertAnimationsDisplayed(panel, 0);
});
-function* changeElementAndWait(options, panel, inspector, isDetailDisplayed) {
+function* changeElementAndWait(options, panel, inspector) {
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let onInspectorUpdated = inspector.once("inspector-updated");
- let onDetailRendered = isDetailDisplayed
- ? panel.animationsTimelineComponent
- .details.once("animation-detail-rendering-completed")
- : Promise.resolve();
yield executeInContent("devtools:test:setAttribute", options);
yield promise.all([onInspectorUpdated, onPanelUpdated,
- waitForAllAnimationTargets(panel), onDetailRendered]);
+ waitForAllAnimationTargets(panel),
+ waitForAnimationTimelineRendering(panel)]);
}
--- a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
+++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
@@ -21,25 +21,25 @@ add_task(function* () {
let {playTimelineButtonEl} = panel;
// ensure the focus is on the animation panel
window.focus();
info("Simulate spacebar stroke and check playResume button" +
" is in paused state");
- // sending the key will lead to a UI_UPDATE_EVENT
- let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ // sending the key will lead to render animation timeline
+ let onUpdated = waitForAnimationTimelineRendering(panel);
EventUtils.sendKey("SPACE", window);
yield onUpdated;
ok(playTimelineButtonEl.classList.contains("paused"),
"The play/resume button is in its paused state");
info("Simulate spacebar stroke and check playResume button" +
" is in playing state");
- // sending the key will lead to a UI_UPDATE_EVENT
- onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ // sending the key will lead to render animation timeline
+ onUpdated = waitForAnimationTimelineRendering(panel);
EventUtils.sendKey("SPACE", window);
yield onUpdated;
ok(!playTimelineButtonEl.classList.contains("paused"),
"The play/resume button is in its play state again");
});
--- a/devtools/client/animationinspector/test/browser_animation_timeline_short_duration.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_short_duration.js
@@ -73,17 +73,22 @@ function checkSummaryGraph(el, state, is
+ thirdLastPathSeg.x);
// The test animation is not 'forwards' fill-mode.
// Therefore, the y of second last path segment will be 0.
const secondLastPathSeg =
pathSegList.getItem(pathSegList.numberOfItems - 3);
is(secondLastPathSeg.x, endX,
`The x of second last segment should be ${ endX }`);
- is(secondLastPathSeg.y, 0, "The y of second last segment should be 0");
+ // We use computed style of 'opacity' to create summary graph.
+ // So, if currentTime is same to the duration, although progress is null opacity is 1.
+ const expectedY =
+ state.iterationCount && expectedIterationCount === index + 1 ? 1 : 0;
+ is(secondLastPathSeg.y, expectedY,
+ `The y of second last segment should be ${ expectedY }`);
const lastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
is(lastPathSeg.x, endX, `The x of last segment should be ${ endX }`);
is(lastPathSeg.y, 0, "The y of last segment should be 0");
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
`The actual last segment should be close path`);
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -89,20 +89,18 @@ var selectNodeAndWaitForAnimations = Tas
function* (data, inspector, reason = "test") {
yield selectNode(data, inspector, reason);
// We want to make sure the rest of the test waits for the animations to
// be properly displayed (wait for all target DOM nodes to be previewed).
let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
yield waitForAllAnimationTargets(AnimationsPanel);
- if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
- // Wait for selecting the animation since there is only one animation.
- yield waitForAnimationSelecting(AnimationsPanel);
- }
+ // Wait for animation timeline rendering.
+ yield waitForAnimationTimelineRendering(AnimationsPanel);
}
);
/**
* Check if there are the expected number of animations being displayed in the
* panel right now.
* @param {AnimationsPanel} panel
* @param {Number} nbAnimations The expected number of animations.
@@ -158,21 +156,18 @@ var openAnimationInspector = Task.async(
} else {
yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
}
// Make sure we wait for all animations to be loaded (especially their target
// nodes to be lazily displayed). This is safe to do even if there are no
// animations displayed.
yield waitForAllAnimationTargets(AnimationsPanel);
-
- if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
- // Wait for selecting the animation since there is only one animation.
- yield waitForAnimationSelecting(AnimationsPanel);
- }
+ // Also, wait for timeline redering.
+ yield waitForAnimationTimelineRendering(AnimationsPanel);
return {
toolbox: toolbox,
inspector: inspector,
controller: AnimationsController,
panel: AnimationsPanel,
window: win
};
@@ -296,49 +291,54 @@ function* assertScrubberMoving(panel, is
/**
* Click the play/pause button in the timeline toolbar and wait for animations
* to update.
* @param {AnimationsPanel} panel
*/
function* clickTimelinePlayPauseButton(panel) {
let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
let btn = panel.playTimelineButtonEl;
let win = btn.ownerDocument.defaultView;
EventUtils.sendMouseEvent({type: "click"}, btn, win);
yield onUiUpdated;
+ yield onAnimationTimelineRendered;
yield waitForAllAnimationTargets(panel);
}
/**
* Click the rewind button in the timeline toolbar and wait for animations to
* update.
* @param {AnimationsPanel} panel
*/
function* clickTimelineRewindButton(panel) {
let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
let btn = panel.rewindTimelineButtonEl;
let win = btn.ownerDocument.defaultView;
EventUtils.sendMouseEvent({type: "click"}, btn, win);
yield onUiUpdated;
+ yield onAnimationTimelineRendered;
yield waitForAllAnimationTargets(panel);
}
/**
* Select a rate inside the playback rate selector in the timeline toolbar and
* wait for animations to update.
* @param {AnimationsPanel} panel
* @param {Number} rate The new rate value to be selected
*/
function* changeTimelinePlaybackRate(panel, rate) {
let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+ const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
let select = panel.rateSelectorEl.firstChild;
let win = select.ownerDocument.defaultView;
// Get the right option.
let option = [...select.options].filter(o => o.value === rate + "")[0];
if (!option) {
ok(false,
@@ -347,16 +347,17 @@ function* changeTimelinePlaybackRate(pan
return;
}
// Simulate the right events to select the option in the drop-down.
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
yield onUiUpdated;
+ yield onAnimationTimelineRendered;
yield waitForAllAnimationTargets(panel);
// Simulate a mousemove outside of the rate selector area to avoid subsequent
// tests from failing because of unwanted mouseover events.
EventUtils.synthesizeMouseAtCenter(
win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
}
@@ -364,16 +365,28 @@ function* changeTimelinePlaybackRate(pan
* Wait for animation selecting.
* @param {AnimationsPanel} panel
*/
function* waitForAnimationSelecting(panel) {
yield panel.animationsTimelineComponent.once("animation-selected");
}
/**
+ * Wait for rendering animation timeline.
+ * @param {AnimationsPanel} panel
+ */
+function* waitForAnimationTimelineRendering(panel) {
+ const ready =
+ panel.animationsTimelineComponent.animations.length === 0
+ ? Promise.resolve()
+ : panel.animationsTimelineComponent.once("animation-timeline-rendering-completed");
+ yield ready;
+}
+
+/**
+ * Click the timeline header to update the animation current time.
+ * @param {AnimationsPanel} panel
+ * @param {Number} x position rate on timeline header.
+ * This method calculates
+ * `position * offsetWidth + offsetLeft of timeline header`
+ * as the clientX of MouseEvent.
+ * This parameter should be from 0.0 to 1.0.
+ */
@@ -474,9 +487,11 @@ function* setStyle(animation, panel, nam
propertyName: name,
propertyValue: value
});
yield onAnimationChanged;
// Also wait for the target node previews to be loaded if the panel got
// refreshed as a result of this animation mutation.
yield waitForAllAnimationTargets(panel);
+ // And wait for animation timeline rendering.
+ yield waitForAnimationTimelineRendering(panel);
}
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -397,19 +397,24 @@ body {
/* Animation summary graph */
.animation-timeline .animation .summary {
position: absolute;
width: 100%;
height: 100%;
}
-.animation-timeline .animation .summary path {
+.animation-timeline .animation .summary .effect-easing path {
+ fill: none;
+ stroke: var(--timeline-border-color);
+ stroke-dasharray: 2px 2px;
+}
+
+.animation-timeline .animation .summary .keyframes-easing path {
fill: var(--timeline-background-color);
- stroke: var(--timeline-border-color);
}
.animation-timeline .animation .summary .infinity.copied {
opacity: .3;
}
.animation-timeline .animation .summary path.delay-path.negative,
.animation-timeline .animation .summary path.enddelay-path.negative {