--- a/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
@@ -10,35 +10,43 @@ const PropTypes = require("devtools/clie
const AnimatedPropertyName = createFactory(require("./AnimatedPropertyName"));
const KeyframesGraph = createFactory(require("./keyframes-graph/KeyframesGraph"));
class AnimatedPropertyItem extends PureComponent {
static get propTypes() {
return {
property: PropTypes.string.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
state: PropTypes.object.isRequired,
values: PropTypes.array.isRequired,
};
}
render() {
const {
property,
+ simulateAnimation,
state,
+ values,
} = this.props;
return dom.li(
{
className: "animated-property-item"
},
AnimatedPropertyName(
{
property,
state,
}
),
- KeyframesGraph()
+ KeyframesGraph(
+ {
+ simulateAnimation,
+ values,
+ }
+ )
);
}
}
module.exports = AnimatedPropertyItem;
--- a/devtools/client/inspector/animation/components/AnimatedPropertyList.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyList.js
@@ -11,16 +11,17 @@ const PropTypes = require("devtools/clie
const AnimatedPropertyItem = createFactory(require("./AnimatedPropertyItem"));
class AnimatedPropertyList extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
animatedPropertyMap: null
@@ -55,31 +56,37 @@ class AnimatedPropertyList extends PureC
const animatedPropertyMap = await getAnimatedPropertyMap(animation);
this.setState({ animatedPropertyMap });
emitEventForTest("animation-keyframes-rendered");
}
render() {
- const { animatedPropertyMap } = this.state;
+ const {
+ simulateAnimation,
+ } = this.props;
+ const {
+ animatedPropertyMap,
+ } = this.state;
if (!animatedPropertyMap) {
return null;
}
return dom.ul(
{
className: "animated-property-list"
},
[...animatedPropertyMap.entries()].map(([property, values]) => {
const state = this.getPropertyState(property);
return AnimatedPropertyItem(
{
property,
+ simulateAnimation,
state,
values,
}
);
})
);
}
}
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
@@ -12,35 +12,38 @@ const AnimatedPropertyList = createFacto
const AnimatedPropertyListHeader = createFactory(require("./AnimatedPropertyListHeader"));
class AnimatedPropertyListContainer extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
};
}
render() {
const {
animation,
emitEventForTest,
getAnimatedPropertyMap,
+ simulateAnimation,
} = this.props;
return dom.div(
{
className: `animated-property-list-container ${ animation.state.type }`
},
AnimatedPropertyListHeader(),
AnimatedPropertyList(
{
animation,
emitEventForTest,
getAnimatedPropertyMap,
+ simulateAnimation,
}
)
);
}
}
module.exports = AnimatedPropertyListContainer;
--- a/devtools/client/inspector/animation/components/AnimationDetailContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
@@ -15,25 +15,27 @@ const AnimatedPropertyListContainer =
class AnimationDetailContainer extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
setDetailVisibility: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
};
}
render() {
const {
animation,
emitEventForTest,
getAnimatedPropertyMap,
setDetailVisibility,
+ simulateAnimation,
} = this.props;
return dom.div(
{
className: "animation-detail-container"
},
animation ?
AnimationDetailHeader(
@@ -45,16 +47,17 @@ class AnimationDetailContainer extends P
:
null,
animation ?
AnimatedPropertyListContainer(
{
animation,
emitEventForTest,
getAnimatedPropertyMap,
+ simulateAnimation,
}
)
:
null
);
}
}
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -60,16 +60,17 @@ class App extends PureComponent {
animations.length ?
SplitBox({
className: "animation-container-splitter",
endPanel: AnimationDetailContainer(
{
emitEventForTest,
getAnimatedPropertyMap,
setDetailVisibility,
+ simulateAnimation,
}
),
endPanelControl: true,
initialHeight: "50%",
splitterSize: 1,
startPanel: AnimationListContainer(
{
animations,
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
@@ -0,0 +1,153 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const {
+ createPathSegments,
+ DEFAULT_DURATION_RESOLUTION,
+ getPreferredProgressThresholdByKeyframes,
+ toPathString,
+} = require("../../utils/graph-helper");
+
+/*
+ * This class is an abstraction for computed style path of keyframes.
+ * Subclass of this should implement the following methods:
+ *
+ * getPropertyName()
+ * Returns property name which will be animated.
+ * @return {String}
+ * e.g. opacity
+ *
+ * getPropertyValue(keyframe)
+ * Returns value which uses as animated keyframe value from given parameter.
+ * @param {Object} keyframe
+ * @return {String||Number}
+ * e.g. 0
+ *
+ * toSegmentValue(computedStyle)
+ * Convert computed style to segment value of graph.
+ * @param {String||Number}
+ * e.g. 0
+ * @return {Number}
+ * e.g. 0 (should be 0 - 1.0)
+ */
+class ComputedStylePath extends PureComponent {
+ static get propTypes() {
+ return {
+ componentWidth: PropTypes.number.isRequired,
+ graphHeight: PropTypes.number.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ totalDuration: PropTypes.number.isRequired,
+ values: PropTypes.array.isRequired,
+ };
+ }
+
+ /**
+ * Return an array containing the path segments between the given start and
+ * end keyframe values.
+ *
+ * @param {Object} startValue
+ * Starting keyframe.
+ * @param {Object} startValue
+ * Ending keyframe.
+ * @return {Array}
+ * Array of path segment.
+ * [{x: {Number} time, y: {Number} segment value}, ...]
+ */
+ getPathSegments(startValue, endValue) {
+ const {
+ componentWidth,
+ simulateAnimation,
+ totalDuration,
+ } = this.props;
+
+ const propertyName = this.getPropertyName();
+ const offsetDistance = endValue.offset - startValue.offset;
+ const duration = offsetDistance * totalDuration;
+
+ const keyframes = [startValue, endValue].map((keyframe, index) => {
+ return {
+ offset: index,
+ easing: keyframe.easing,
+ [propertyName]: this.getPropertyValue(keyframe),
+ };
+ });
+ const effect = {
+ duration,
+ fill: "forwards",
+ };
+ const simulatedAnimation = simulateAnimation(keyframes, effect, true);
+ const simulatedElement = simulatedAnimation.effect.target;
+ const win = simulatedElement.ownerGlobal;
+ const threshold = getPreferredProgressThresholdByKeyframes(keyframes);
+
+ const getSegment = time => {
+ simulatedAnimation.currentTime = time;
+ const computedStyle =
+ win.getComputedStyle(simulatedElement).getPropertyValue(propertyName);
+
+ return {
+ computedStyle,
+ x: time,
+ y: this.toSegmentValue(computedStyle),
+ };
+ };
+
+ const segments = createPathSegments(0, duration, duration / componentWidth, threshold,
+ DEFAULT_DURATION_RESOLUTION, getSegment);
+ const offset = startValue.offset * totalDuration;
+
+ for (const segment of segments) {
+ segment.x += offset;
+ }
+
+ return segments;
+ }
+
+ /**
+ * Render graph. This method returns React dom.
+ *
+ * @return {Element}
+ */
+ renderGraph() {
+ const { values } = this.props;
+
+ const segments = [];
+
+ for (let i = 0; i < values.length - 1; i++) {
+ const startValue = values[i];
+ const endValue = values[i + 1];
+ segments.push(...this.getPathSegments(startValue, endValue));
+ }
+
+ return this.renderPathSegments(segments);
+ }
+
+ /**
+ * Return react dom fron given path segments.
+ *
+ * @param {Array} segments
+ * @return {Element}
+ */
+ renderPathSegments(segments) {
+ const { graphHeight } = this.props;
+
+ for (const segment of segments) {
+ segment.y *= graphHeight;
+ }
+
+ let d = `M${ segments[0].x },0 `;
+ d += toPathString(segments);
+ d += `L${ segments[segments.length - 1].x },0 Z`;
+
+ return dom.path({ d });
+ }
+}
+
+module.exports = ComputedStylePath;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js
@@ -0,0 +1,34 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const ComputedStylePath = require("./ComputedStylePath");
+
+class DistancePath extends ComputedStylePath {
+ getPropertyName() {
+ return "opacity";
+ }
+
+ getPropertyValue(keyframe) {
+ return keyframe.distance;
+ }
+
+ toSegmentValue(computedStyle) {
+ return computedStyle;
+ }
+
+ render() {
+ return dom.g(
+ {
+ className: "distance-path",
+ },
+ super.renderGraph()
+ );
+ }
+}
+
+module.exports = DistancePath;
--- a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
@@ -1,20 +1,41 @@
/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const KeyframesGraphPath = createFactory(require("./KeyframesGraphPath"));
class KeyframesGraph extends PureComponent {
+ static get propTypes() {
+ return {
+ simulateAnimation: PropTypes.func.isRequired,
+ values: PropTypes.array.isRequired,
+ };
+ }
+
render() {
+ const {
+ simulateAnimation,
+ values,
+ } = this.props;
+
return dom.div(
{
- className: "keyframes-graph"
- }
+ className: "keyframes-graph",
+ },
+ KeyframesGraphPath(
+ {
+ simulateAnimation,
+ values,
+ }
+ )
);
}
}
module.exports = KeyframesGraph;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
@@ -0,0 +1,75 @@
+/* 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 { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const DistancePath = createFactory(require("./DistancePath"));
+
+const {
+ DEFAULT_GRAPH_HEIGHT,
+ DEFAULT_KEYFRAMES_GRAPH_DURATION,
+} = require("../../utils/graph-helper");
+
+class KeyframesGraphPath extends PureComponent {
+ static get propTypes() {
+ return {
+ simulateAnimation: PropTypes.func.isRequired,
+ values: PropTypes.array.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ componentWidth: 0,
+ };
+ }
+
+ componentDidMount() {
+ this.updateState();
+ }
+
+ updateState() {
+ const thisEl = ReactDOM.findDOMNode(this);
+ this.setState({ componentWidth: thisEl.parentNode.clientWidth });
+ }
+
+ render() {
+ const {
+ simulateAnimation,
+ values,
+ } = this.props;
+ const { componentWidth } = this.state;
+
+ if (!componentWidth) {
+ return dom.svg();
+ }
+
+ return dom.svg(
+ {
+ className: "keyframes-graph-path",
+ preserveAspectRatio: "none",
+ viewBox: `0 -${ DEFAULT_GRAPH_HEIGHT } `
+ + `${ DEFAULT_KEYFRAMES_GRAPH_DURATION } ${ DEFAULT_GRAPH_HEIGHT }`,
+ },
+ DistancePath(
+ {
+ componentWidth,
+ graphHeight: DEFAULT_GRAPH_HEIGHT,
+ simulateAnimation,
+ totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION,
+ values,
+ }
+ )
+ );
+ }
+}
+
+module.exports = KeyframesGraphPath;
--- a/devtools/client/inspector/animation/components/keyframes-graph/moz.build
+++ b/devtools/client/inspector/animation/components/keyframes-graph/moz.build
@@ -1,7 +1,10 @@
# 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/.
DevToolsModules(
+ 'ComputedStylePath.js',
+ 'DistancePath.js',
'KeyframesGraph.js',
+ 'KeyframesGraphPath.js',
)
--- a/devtools/client/inspector/animation/utils/graph-helper.js
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -1,31 +1,33 @@
/* 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";
// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
-// and end bounds when dividing duration in createPathSegments.
+// and end bounds when dividing duration in createPathSegments.
const BOUND_EXCLUDING_TIME = 0.001;
// We define default graph height since if the height of viewport in SVG is
// too small (e.g. 1), vector-effect may not be able to calculate correctly.
const DEFAULT_GRAPH_HEIGHT = 100;
+// Default animation duration for keyframes graph.
+const DEFAULT_KEYFRAMES_GRAPH_DURATION = 1000;
// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
// In the createPathSegments function, an animation duration is divided by
-// DURATION_RESOLUTION in order to draw the way the animation progresses.
+// DEFAULT_DURATION_RESOLUTION in order to draw the way the animation progresses.
// But depending on the timing-function, we may be not able to make the graph
// smoothly progress if this resolution is not high enough.
// So, if the difference of animation progress between 2 divisions is more than
// DEFAULT_MIN_PROGRESS_THRESHOLD * DEFAULT_GRAPH_HEIGHT, then createPathSegments
-// re-divides by DURATION_RESOLUTION.
-// DURATION_RESOLUTION shoud be integer and more than 2.
-const DURATION_RESOLUTION = 4;
+// re-divides by DEFAULT_DURATION_RESOLUTION.
+// DEFAULT_DURATION_RESOLUTION shoud be integer and more than 2.
+const DEFAULT_DURATION_RESOLUTION = 4;
/**
* The helper class for creating summary graph.
*/
class SummaryGraphHelper {
/**
* Constructor.
*
@@ -117,34 +119,34 @@ function createPathSegments(startTime, e
let pathSegments = [];
// Append the segment for the startTime position.
const startTimeSegment = getSegment(startTime);
pathSegments.push(startTimeSegment);
let previousSegment = startTimeSegment;
// Split the duration in equal intervals, and iterate over them.
- // See the definition of DURATION_RESOLUTION for more information about this.
+ // See the definition of DEFAULT_DURATION_RESOLUTION for more information about this.
const interval = (endTime - startTime) / resolution;
for (let index = 1; index <= resolution; index++) {
// Create a segment for this interval.
const currentSegment = getSegment(startTime + index * interval);
// If the distance between the Y coordinate (the animation's progress) of
// the previous segment and the Y coordinate of the current segment is too
// large, then recurse with a smaller duration to get more details
// in the graph.
if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
// Divide the current interval (excluding start and end bounds
// by adding/subtracting BOUND_EXCLUDING_TIME).
const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME;
const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME;
const segments =
createPathSegments(nextStartTime, nextEndTime, minSegmentDuration,
- minProgressThreshold, DURATION_RESOLUTION, getSegment);
+ minProgressThreshold, DEFAULT_DURATION_RESOLUTION, getSegment);
pathSegments = pathSegments.concat(segments);
}
pathSegments.push(currentSegment);
previousSegment = currentSegment;
}
return pathSegments;
@@ -156,20 +158,20 @@ function createPathSegments(startTime, e
*
* @param {Array} keyframes
* Array of keyframe.
* @return {Number}
* Preferred duration resolution.
*/
function getPreferredDurationResolution(keyframes) {
if (!keyframes) {
- return DURATION_RESOLUTION;
+ return DEFAULT_DURATION_RESOLUTION;
}
- let durationResolution = DURATION_RESOLUTION;
+ let durationResolution = DEFAULT_DURATION_RESOLUTION;
let previousOffset = 0;
for (let keyframe of keyframes) {
if (previousOffset && previousOffset != keyframe.offset) {
const interval = keyframe.offset - previousOffset;
durationResolution = Math.max(durationResolution, Math.ceil(1 / interval));
}
previousOffset = keyframe.offset;
}
@@ -194,16 +196,33 @@ function getPreferredProgressThreshold(s
if ((stepsOrFrames = getStepsOrFramesCount(state.easing))) {
threshold = Math.min(threshold, (1 / (stepsOrFrames + 1)));
}
if (!keyframes) {
return threshold;
}
+ threshold = Math.min(threshold, getPreferredProgressThresholdByKeyframes(keyframes));
+
+ return threshold;
+}
+
+/**
+ * Return preferred progress threshold by keyframes.
+ *
+ * @param {Array} keyframes
+ * Array of keyframe.
+ * @return {float}
+ * Preferred threshold.
+ */
+function getPreferredProgressThresholdByKeyframes(keyframes) {
+ let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+ let stepsOrFrames;
+
for (let i = 0; i < keyframes.length - 1; i++) {
const keyframe = keyframes[i];
if (!keyframe.easing) {
continue;
}
if ((stepsOrFrames = getStepsOrFramesCount(keyframe.easing))) {
@@ -234,11 +253,16 @@ function getStepsOrFramesCount(easing) {
function toPathString(segments) {
let pathString = "";
segments.forEach(segment => {
pathString += `L${ segment.x },${ segment.y } `;
});
return pathString;
}
-module.exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
+exports.createPathSegments = createPathSegments;
+exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION;
+exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
+exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION;
+exports.getPreferredProgressThresholdByKeyframes =
+ getPreferredProgressThresholdByKeyframes;
exports.SummaryGraphHelper = SummaryGraphHelper;
exports.toPathString = toPathString;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -355,19 +355,31 @@
.animated-property-name.warning span {
text-decoration: underline dotted;
}
/* Keyframes Graph */
.keyframes-graph {
height: 100%;
+ padding-top: 5px;
width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
}
+.keyframes-graph-path {
+ height: 100%;
+ width: 100%;
+}
+
+.keyframes-graph-path path {
+ fill: lime;
+ vector-effect: non-scaling-stroke;
+ transform: scale(1, -1);
+}
+
/* No Animation Panel */
.animation-error-message {
overflow: auto;
}
.animation-error-message > p {
white-space: pre;
}