Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"use strict";
const {
PureComponent,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const {
createPathSegments,
DEFAULT_DURATION_RESOLUTION,
getPreferredProgressThresholdByKeyframes,
toPathString,
} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js");
/*
* 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,
easingHintStrokeWidth: PropTypes.number.isRequired,
graphHeight: PropTypes.number.isRequired,
keyframes: PropTypes.array.isRequired,
simulateAnimation: PropTypes.func.isRequired,
totalDuration: PropTypes.number.isRequired,
};
}
/**
* Return an array containing the path segments between the given start and
* end keyframe values.
*
* @param {Object} startKeyframe
* Starting keyframe.
* @param {Object} endKeyframe
* Ending keyframe.
* @return {Array}
* Array of path segment.
* [{x: {Number} time, y: {Number} segment value}, ...]
*/
getPathSegments(startKeyframe, endKeyframe) {
const { componentWidth, simulateAnimation, totalDuration } = this.props;
const propertyName = this.getPropertyName();
const offsetDistance = endKeyframe.offset - startKeyframe.offset;
const duration = offsetDistance * totalDuration;
const keyframes = [startKeyframe, endKeyframe].map((keyframe, index) => {
return {
offset: index,
easing: keyframe.easing,
[getJsPropertyName(propertyName)]: this.getPropertyValue(keyframe),
};
});
const effect = {
duration,
fill: "forwards",
};
const simulatedAnimation = simulateAnimation(keyframes, effect, true);
if (!simulatedAnimation) {
return null;
}
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 = startKeyframe.offset * totalDuration;
for (const segment of segments) {
segment.x += offset;
}
return segments;
}
/**
* Render easing hint from given path segments.
*
* @param {Array} segments
* Path segments.
* @return {Element}
* Element which represents easing hint.
*/
renderEasingHint(segments) {
const { easingHintStrokeWidth, keyframes, totalDuration } = this.props;
const hints = [];
for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
const startKeyframe = keyframes[i];
const endKeyframe = keyframes[i + 1];
const endTime = endKeyframe.offset * totalDuration;
const hintSegments = [];
for (; indexOfSegments < segments.length; indexOfSegments++) {
const segment = segments[indexOfSegments];
hintSegments.push(segment);
if (startKeyframe.offset === endKeyframe.offset) {
hintSegments.push(segments[++indexOfSegments]);
break;
} else if (segment.x === endTime) {
break;
}
}
const g = dom.g(
{
className: "hint",
},
dom.title({}, startKeyframe.easing),
dom.path({
d:
`M${hintSegments[0].x},${hintSegments[0].y} ` +
toPathString(hintSegments),
style: {
"stroke-width": easingHintStrokeWidth,
},
})
);
hints.push(g);
}
return hints;
}
/**
* Render graph. This method returns React dom.
*
* @return {Element}
*/
renderGraph() {
const { keyframes } = this.props;
const segments = [];
for (let i = 0; i < keyframes.length - 1; i++) {
const startKeyframe = keyframes[i];
const endKeyframe = keyframes[i + 1];
const keyframesSegments = this.getPathSegments(
startKeyframe,
endKeyframe
);
if (!keyframesSegments) {
return null;
}
segments.push(...keyframesSegments);
}
return [this.renderPathSegments(segments), this.renderEasingHint(segments)];
}
/**
* Return react dom fron given path segments.
*
* @param {Array} segments
* @param {Object} style
* @return {Element}
*/
renderPathSegments(segments, style) {
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, style });
}
}
/**
* Convert given CSS property name to JavaScript CSS name.
*
* @param {String} cssPropertyName
* CSS property name (e.g. background-color).
* @return {String}
* JavaScript CSS property name (e.g. backgroundColor).
*/
function getJsPropertyName(cssPropertyName) {
if (cssPropertyName == "float") {
return "cssFloat";
}
return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
return group.toUpperCase();
});
}
module.exports = ComputedStylePath;