diff options
Diffstat (limited to 'devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js')
-rw-r--r-- | devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js new file mode 100644 index 0000000000..1da5c4da96 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js @@ -0,0 +1,245 @@ +/* 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("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"; + } + // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + return cssPropertyName.replace(/-([a-z])/gi, (str, group) => { + return group.toUpperCase(); + }); +} + +module.exports = ComputedStylePath; |