--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -457,33 +457,33 @@ function renderGraph(parentEl, state, to
* 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.appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+ 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.appendPathElement(parentEl, segments, "iteration-path");
+ 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.
@@ -494,17 +494,17 @@ function renderMiddleIterations(parentEl
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.appendPathElement(parentEl, segments, "iteration-path");
+ 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.
@@ -515,17 +515,17 @@ function renderMiddleIterations(parentEl
*/
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.appendPathElement(parentEl, segments, "iteration-path");
+ 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.
@@ -547,17 +547,17 @@ function renderInfinity(parentEl, state,
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.appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+ 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.
@@ -565,34 +565,34 @@ 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 };
});
}
- graphHelper.appendPathElement(parentEl, segments, "iteration-path infinity copied");
+ 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.appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+ 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.
@@ -600,45 +600,44 @@ function renderEndDelay(parentEl, state,
* @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.appendPathElement(parentEl, [startSegment, endSegment],
- "fill-forwards-path");
+ 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.appendPathElement(parentEl, segments, "delay-path negative");
+ 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.appendPathElement(parentEl, segments, "enddelay-path negative");
+ 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.
*/
--- 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 {createNode, createSVGNode} =
require("devtools/client/animationinspector/utils");
const {ProgressGraphHelper, getPreferredKeyframesProgressThreshold} =
- require("devtools/client/animationinspector/graph-helper.js");
+ 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.
*/
@@ -50,39 +50,44 @@ Keyframes.prototype = {
"preserveAspectRatio": "none"
}
});
// This visual is only one iteration,
// so we use animation.state.duration as total duration.
const totalDuration = animation.state.duration;
- // Calculate stroke height in viewBox to display stroke of path.
- const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
// Minimum segment duration is the duration of one pixel.
const minSegmentDuration =
totalDuration / this.containerEl.clientWidth;
- // Set viewBox.
- graphEl.setAttribute("viewBox",
- `0 -${ 1 + strokeHeightForViewBox }
- ${ totalDuration }
- ${ 1 + strokeHeightForViewBox * 2 }`);
-
// Create graph helper to render the animation property graph.
+ const win = this.containerEl.ownerGlobal;
const graphHelper =
- new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
- propertyName, animationType, keyframes, totalDuration);
+ new ProgressGraphHelper(win, propertyName, animationType, keyframes, totalDuration);
renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
getPreferredKeyframesProgressThreshold(keyframes), graphHelper);
// Destroy ProgressGraphHelper resources.
graphHelper.destroy();
+ // Set viewBox which includes invisible stroke width.
+ // At first, calculate invisible stroke width from maximum width.
+ // The reason why divide by 2 is that half of stroke width will be invisible
+ // if we use 0 or 1 for y axis.
+ const maxStrokeWidth =
+ win.getComputedStyle(graphEl.querySelector(".keyframes svg .hint")).strokeWidth;
+ const invisibleStrokeWidthInViewBox =
+ maxStrokeWidth / 2 / this.containerEl.clientHeight;
+ graphEl.setAttribute("viewBox",
+ `0 -${ 1 + invisibleStrokeWidthInViewBox }
+ ${ totalDuration }
+ ${ 1 + invisibleStrokeWidthInViewBox * 2 }`);
+
// Append elements to display keyframe values.
this.keyframesEl.classList.add(animation.state.type);
for (let frame of this.keyframes) {
createNode({
parent: this.keyframesEl,
attributes: {
"class": "frame",
"style": `left:${frame.offset * 100}%;`,
@@ -105,25 +110,26 @@ Keyframes.prototype = {
*/
function renderPropertyGraph(parentEl, duration, minSegmentDuration,
minProgressThreshold, graphHelper) {
const segments = graphHelper.createPathSegments(0, duration, minSegmentDuration,
minProgressThreshold);
const graphType = graphHelper.getGraphType();
if (graphType !== "color") {
- graphHelper.appendPathElement(parentEl, segments, graphType);
+ graphHelper.appendShapePath(parentEl, segments, graphType);
+ renderEasingHint(parentEl, segments, graphHelper);
return;
}
// Append the color to the path.
segments.forEach(segment => {
segment.y = 1;
});
- const path = graphHelper.appendPathElement(parentEl, segments, graphType);
+ const path = graphHelper.appendShapePath(parentEl, segments, graphType);
const defEl = createSVGNode({
parent: parentEl,
nodeType: "def"
});
const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
const linearGradientEl = createSVGNode({
parent: defEl,
nodeType: "linearGradient",
@@ -137,9 +143,110 @@ function renderPropertyGraph(parentEl, d
nodeType: "stop",
attributes: {
"stop-color": segment.style,
"offset": segment.x / duration
}
});
});
path.style.fill = `url(#${ id })`;
+
+ renderEasingHintForColor(parentEl, graphHelper);
}
+
+/**
+ * Renders the easing hint.
+ * This method renders an emphasized path over the easing path for a keyframe.
+ * It appears when hovering over the easing.
+ * It also renders a tooltip that appears when hovering.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} path segments - [{x: {Number} time, y: {Number} progress}, ...]
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHelper.
+ */
+function renderEasingHint(parentEl, segments, helper) {
+ const keyframes = helper.getKeyframes();
+ const duration = helper.getDuration();
+
+ // Split segments for each keyframe.
+ for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
+ const startKeyframe = keyframes[i];
+ const startTime = startKeyframe.offset * duration;
+ const endKeyframe = keyframes[i + 1];
+ const endTime = endKeyframe.offset * duration;
+
+ const keyframeSegments = [];
+ for (; indexOfSegments < segments.length; indexOfSegments++) {
+ const segment = segments[indexOfSegments];
+ if (segment.x < startTime) {
+ // If previous easings were linear, we need to increment the indexOfSegments.
+ continue;
+ }
+ if (segment.x > endTime) {
+ indexOfSegments -= 1;
+ break;
+ }
+ keyframeSegments.push(segment);
+ }
+
+ // If keyframeSegments does not have segment which is at startTime,
+ // get and set the segment.
+ if (keyframeSegments[0].x !== startTime) {
+ keyframeSegments.unshift(helper.getSegment(startTime));
+ }
+ // Also, endTime.
+ if (keyframeSegments[keyframeSegments.length - 1].x !== endTime) {
+ keyframeSegments.push(helper.getSegment(endTime));
+ }
+
+ // Append easing hint as text and emphasis path.
+ const gEl = createSVGNode({
+ parent: parentEl,
+ nodeType: "g"
+ });
+ createSVGNode({
+ parent: gEl,
+ nodeType: "title",
+ textContent: startKeyframe.easing
+ });
+ helper.appendLinePath(gEl, keyframeSegments, `${helper.getGraphType()} hint`);
+ }
+}
+
+/**
+ * Render easing hint for properties that are represented by color.
+ * This method render as text only.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
+ */
+function renderEasingHintForColor(parentEl, helper) {
+ const keyframes = helper.getKeyframes();
+ const duration = helper.getDuration();
+
+ // Split segments for each keyframe.
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const startKeyframe = keyframes[i];
+ const startTime = startKeyframe.offset * duration;
+ const endKeyframe = keyframes[i + 1];
+ const endTime = endKeyframe.offset * duration;
+
+ // Append easing hint.
+ const gEl = createSVGNode({
+ parent: parentEl,
+ nodeType: "g"
+ });
+ createSVGNode({
+ parent: gEl,
+ nodeType: "title",
+ textContent: startKeyframe.easing
+ });
+ createSVGNode({
+ parent: gEl,
+ nodeType: "rect",
+ attributes: {
+ x: startTime,
+ y: -1,
+ width: endTime - startTime,
+ height: 1,
+ class: "hint",
+ }
+ });
+ }
+}
--- a/devtools/client/animationinspector/graph-helper.js
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -81,16 +81,32 @@ ProgressGraphHelper.prototype = {
this.propertyCSSName = null;
this.propertyJSName = null;
this.animationType = null;
this.keyframes = null;
this.win = null;
},
/**
+ * Return animation duration.
+ * @return {Number} duration
+ */
+ getDuration: function () {
+ return this.animation.effect.timing.duration;
+ },
+
+ /**
+ * Return animation's keyframe.
+ * @return {Object} keyframe
+ */
+ getKeyframes: function () {
+ return this.keyframes;
+ },
+
+ /**
* Return graph type.
* @return {String} if property is 'opacity' or 'transform', return that value.
* Otherwise, return given animation type in constructor.
*/
getGraphType: function () {
return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
? this.propertyJSName : this.animationType;
},
@@ -243,40 +259,53 @@ ProgressGraphHelper.prototype = {
minSegmentDuration, minProgressThreshold) {
return !this.valueHelperFunction
? createKeyframesPathSegments(endTime - startTime, this.devtoolsKeyframes)
: createPathSegments(startTime, endTime,
minSegmentDuration, minProgressThreshold, this);
},
/**
- * Append path element.
+ * Append path element as shape. Also, this method appends two segment
+ * that are {start x, 0} and {end x, 0} to make shape.
* @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);
+ appendShapePath: function (parentEl, pathSegments, cls) {
+ return appendShapePath(parentEl, pathSegments, cls);
+ },
+
+ /**
+ * Append path element as line.
+ * @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.
+ */
+ appendLinePath: function (parentEl, pathSegments, cls) {
+ const isClosePathNeeded = false;
+ return appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded);
},
};
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
+ * If true, appendShapePath 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
@@ -355,17 +384,17 @@ SummaryGraphHelper.prototype = {
* 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.
+ * Set true if need to close path in appendShapePath.
* @param {bool} isClosePathNeeded - true: close, false: open.
*/
setClosePathNeeded: function (isClosePathNeeded) {
this.isClosePathNeeded = isClosePathNeeded;
},
/**
* SummaryGraphHelper searches and creates the summary graph untill the progress
@@ -406,24 +435,25 @@ SummaryGraphHelper.prototype = {
* [{x: {Number} time, y: {Number} progress}, ...]
*/
createPathSegments: function (startTime, endTime) {
return createPathSegments(startTime, endTime,
this.minSegmentDuration, this.minProgressThreshold, this);
},
/**
- * Append path element.
+ * Append path element as shape. Also, this method appends two segment
+ * that are {start x, 0} and {end x, 0} to make shape.
* @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);
+ appendShapePath: function (parentEl, pathSegments, cls) {
+ return appendShapePath(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);
@@ -493,50 +523,60 @@ function createPathSegments(startTime, e
pathSegments.push(currentSegment);
previousSegment = currentSegment;
}
return pathSegments;
}
/**
- * Append path element.
+ * Append path element as shape. Also, this method appends two segment
+ * that are {start x, 0} and {end x, 0} to make shape.
+ * But does not affect given pathSegments.
* @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, isClosePathNeeded = true) {
+function appendShapePath(parentEl, pathSegments, cls, isClosePathNeeded = true) {
+ const segments = [
+ { x: pathSegments[0].x, y: 0 },
+ ...pathSegments,
+ { x: pathSegments[pathSegments.length - 1].x, y: 0 }
+ ];
+ return appendPathElement(parentEl, segments, cls, isClosePathNeeded);
+}
+
+/**
+ * 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.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded) {
// 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;
- }
-
- if (i + 1 === pathSegments.length) {
- // We already create steps or cubic-bezier path string in previous.
- break;
+ let currentSegment = pathSegments[0];
+ let path = `M${ currentSegment.x },${ currentSegment.y }`;
+ for (let i = 1; i < pathSegments.length; i++) {
+ const currentEasing = currentSegment.easing ? currentSegment.easing : "linear";
+ const nextSegment = pathSegments[i];
+ if (currentEasing === "linear") {
+ path += createLinePathString(nextSegment);
+ } else if (currentEasing.startsWith("steps")) {
+ path += createStepsPathString(currentSegment, nextSegment);
+ } else if (currentEasing.startsWith("frames")) {
+ path += createFramesPathString(currentSegment, nextSegment);
+ } else {
+ path += createCubicBezierPathString(currentSegment, nextSegment);
}
-
- const nextPathSegment = pathSegments[i + 1];
- let createPathFunction;
- if (pathSegment.easing.startsWith("steps")) {
- createPathFunction = createStepsPathString;
- } else if (pathSegment.easing.startsWith("frames")) {
- createPathFunction = createFramesPathString;
- } else {
- createPathFunction = createCubicBezierPathString;
- }
- path += createPathFunction(pathSegment, nextPathSegment);
+ currentSegment = nextSegment;
}
- 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: {
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -692,16 +692,34 @@ body {
stroke: var(--transform-border-color);
}
.keyframes svg path.color {
stroke: none;
height: 100%;
}
+.keyframes svg .hint {
+ stroke-opacity: 0;
+ stroke-linecap: round;
+ stroke-width: 5;
+}
+
+.keyframes svg path.hint {
+ fill: none;
+}
+
+.keyframes svg path.hint:hover {
+ stroke-opacity: 1;
+}
+
+.keyframes svg rect.hint {
+ fill-opacity: .1;
+}
+
.animation-detail {
position: relative;
width: 100%;
background-color: var(--theme-body-background);
z-index: 5;
}
.animation-detail .animation-detail-header {