diff options
Diffstat (limited to 'devtools/client/inspector/animation/components/keyframes-graph')
9 files changed, 802 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js new file mode 100644 index 0000000000..120b61c73b --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js @@ -0,0 +1,209 @@ +/* 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("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { colorUtils } = require("resource://devtools/shared/css/color.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 }; + +/* Count for linearGradient ID */ +let LINEAR_GRADIENT_ID_COUNT = 0; + +class ColorPath extends ComputedStylePath { + constructor(props) { + super(props); + + this.state = this.propToState(props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState(this.propToState(nextProps)); + } + + getPropertyName() { + return "color"; + } + + getPropertyValue(keyframe) { + return keyframe.value; + } + + propToState({ keyframes, name }) { + const maxObject = { distance: -Number.MAX_VALUE }; + + for (let i = 0; i < keyframes.length - 1; i++) { + const value1 = getRGBA(name, keyframes[i].value); + for (let j = i + 1; j < keyframes.length; j++) { + const value2 = getRGBA(name, keyframes[j].value); + const distance = getRGBADistance(value1, value2); + + if (maxObject.distance >= distance) { + continue; + } + + maxObject.distance = distance; + maxObject.value1 = value1; + maxObject.value2 = value2; + } + } + + const maxDistance = maxObject.distance; + const baseValue = + maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2; + + return { baseValue, maxDistance, name }; + } + + toSegmentValue(computedStyle) { + const { baseValue, maxDistance, name } = this.state; + const value = getRGBA(name, computedStyle); + return getRGBADistance(baseValue, value) / maxDistance; + } + + /** + * Overide parent's method. + */ + renderEasingHint() { + const { easingHintStrokeWidth, graphHeight, keyframes, totalDuration } = + this.props; + + const hints = []; + + for (let i = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const endKeyframe = keyframes[i + 1]; + const startTime = startKeyframe.offset * totalDuration; + const endTime = endKeyframe.offset * totalDuration; + + const g = dom.g( + { + className: "hint", + }, + dom.title({}, startKeyframe.easing), + dom.rect({ + x: startTime, + y: -graphHeight, + height: graphHeight, + width: endTime - startTime, + }), + dom.line({ + x1: startTime, + y1: -graphHeight, + x2: endTime, + y2: -graphHeight, + style: { + "stroke-width": easingHintStrokeWidth, + }, + }) + ); + hints.push(g); + } + + return hints; + } + + /** + * Overide parent's method. + */ + renderPathSegments(segments) { + for (const segment of segments) { + segment.y = 1; + } + + const lastSegment = segments[segments.length - 1]; + const id = `color-property-${LINEAR_GRADIENT_ID_COUNT++}`; + const path = super.renderPathSegments(segments, { fill: `url(#${id})` }); + const linearGradient = dom.linearGradient( + { id }, + segments.map(segment => { + return dom.stop({ + stopColor: segment.computedStyle, + offset: segment.x / lastSegment.x, + }); + }) + ); + + return [path, linearGradient]; + } + + render() { + return dom.g( + { + className: "color-path", + }, + super.renderGraph() + ); + } +} + +/** + * Parse given RGBA string. + * + * @param {String} propertyName + * @param {String} colorString + * e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on. + * @return {Object} + * RGBA {r: r, g: g, b: b, a: a}. + */ +function getRGBA(propertyName, colorString) { + // Special handling for CSS property which can specify the not normal CSS color value. + switch (propertyName) { + case "caret-color": { + // This property can specify "auto" keyword. + if (colorString === "auto") { + return DEFAULT_COLOR; + } + break; + } + case "scrollbar-color": { + // This property can specify "auto", "dark", "light" keywords and multiple colors. + if ( + ["auto", "dark", "light"].includes(colorString) || + colorString.indexOf(" ") > 0 + ) { + return DEFAULT_COLOR; + } + break; + } + } + + const color = new colorUtils.CssColor(colorString); + return color.getRGBATuple(); +} + +/** + * Return the distance from give two RGBA. + * + * @param {Object} rgba1 + * RGBA (format is same to getRGBA) + * @param {Object} rgba2 + * RGBA (format is same to getRGBA) + * @return {Number} + * The range is 0 - 1.0. + */ +function getRGBADistance(rgba1, rgba2) { + const startA = rgba1.a; + const startR = rgba1.r * startA; + const startG = rgba1.g * startA; + const startB = rgba1.b * startA; + const endA = rgba2.a; + const endR = rgba2.r * endA; + 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 + ); +} + +module.exports = ColorPath; 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; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js new file mode 100644 index 0000000000..26e5373f7c --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js @@ -0,0 +1,67 @@ +/* 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("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +class DiscretePath extends ComputedStylePath { + static get propTypes() { + return { + name: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = this.propToState(props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState(this.propToState(nextProps)); + } + + getPropertyName() { + return this.props.name; + } + + getPropertyValue(keyframe) { + return keyframe.value; + } + + propToState({ getComputedStyle, keyframes, name }) { + const discreteValues = []; + + for (const keyframe of keyframes) { + const style = getComputedStyle(name, { [name]: keyframe.value }); + + if (!discreteValues.includes(style)) { + discreteValues.push(style); + } + } + + return { discreteValues }; + } + + toSegmentValue(computedStyle) { + const { discreteValues } = this.state; + return discreteValues.indexOf(computedStyle) / (discreteValues.length - 1); + } + + render() { + return dom.g( + { + className: "discrete-path", + }, + super.renderGraph() + ); + } +} + +module.exports = DiscretePath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js new file mode 100644 index 0000000000..3436366c30 --- /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("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js"); + +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; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js new file mode 100644 index 0000000000..4212fdb88f --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js @@ -0,0 +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"; + +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"); + +class KeyframeMarkerItem extends PureComponent { + static get propTypes() { + return { + keyframe: PropTypes.object.isRequired, + }; + } + + render() { + const { keyframe } = this.props; + + return dom.li({ + className: "keyframe-marker-item", + title: keyframe.value, + style: { + marginInlineStart: `${keyframe.offset * 100}%`, + }, + }); + } +} + +module.exports = KeyframeMarkerItem; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js new file mode 100644 index 0000000000..7fd112f641 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js @@ -0,0 +1,37 @@ +/* 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("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 KeyframeMarkerItem = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js") +); + +class KeyframeMarkerList extends PureComponent { + static get propTypes() { + return { + keyframes: PropTypes.array.isRequired, + }; + } + + render() { + const { keyframes } = this.props; + + return dom.ul( + { + className: "keyframe-marker-list", + }, + keyframes.map(keyframe => KeyframeMarkerItem({ keyframe })) + ); + } +} + +module.exports = KeyframeMarkerList; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js new file mode 100644 index 0000000000..cbab6806d2 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js @@ -0,0 +1,52 @@ +/* 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("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 KeyframeMarkerList = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js") +); +const KeyframesGraphPath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js") +); + +class KeyframesGraph extends PureComponent { + static get propTypes() { + return { + getComputedStyle: PropTypes.func.isRequired, + keyframes: PropTypes.array.isRequired, + name: PropTypes.string.isRequired, + simulateAnimation: PropTypes.func.isRequired, + type: PropTypes.string.isRequired, + }; + } + + render() { + const { getComputedStyle, keyframes, name, simulateAnimation, type } = + this.props; + + return dom.div( + { + className: `keyframes-graph ${name}`, + }, + KeyframesGraphPath({ + getComputedStyle, + keyframes, + name, + simulateAnimation, + type, + }), + KeyframeMarkerList({ keyframes }) + ); + } +} + +module.exports = KeyframesGraph; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js new file mode 100644 index 0000000000..70c2720194 --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js @@ -0,0 +1,111 @@ +/* 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("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 ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js"); + +const ColorPath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js") +); +const DiscretePath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js") +); +const DistancePath = createFactory( + require("resource://devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js") +); + +const { + DEFAULT_EASING_HINT_STROKE_WIDTH, + DEFAULT_GRAPH_HEIGHT, + DEFAULT_KEYFRAMES_GRAPH_DURATION, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +class KeyframesGraphPath extends PureComponent { + static get propTypes() { + return { + getComputedStyle: PropTypes.func.isRequired, + keyframes: PropTypes.array.isRequired, + name: PropTypes.string.isRequired, + simulateAnimation: PropTypes.func.isRequired, + type: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + componentHeight: 0, + componentWidth: 0, + }; + } + + componentDidMount() { + this.updateState(); + } + + getPathComponent(type) { + switch (type) { + case "color": + return ColorPath; + case "discrete": + return DiscretePath; + default: + return DistancePath; + } + } + + updateState() { + const thisEl = ReactDOM.findDOMNode(this); + this.setState({ + componentHeight: thisEl.parentNode.clientHeight, + componentWidth: thisEl.parentNode.clientWidth, + }); + } + + render() { + const { getComputedStyle, keyframes, name, simulateAnimation, type } = + this.props; + const { componentHeight, componentWidth } = this.state; + + if (!componentWidth) { + return dom.svg(); + } + + const pathComponent = this.getPathComponent(type); + const strokeWidthInViewBox = + (DEFAULT_EASING_HINT_STROKE_WIDTH / 2 / componentHeight) * + DEFAULT_GRAPH_HEIGHT; + + return dom.svg( + { + className: "keyframes-graph-path", + preserveAspectRatio: "none", + viewBox: + `0 -${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox} ` + + `${DEFAULT_KEYFRAMES_GRAPH_DURATION} ` + + `${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox * 2}`, + }, + pathComponent({ + componentWidth, + easingHintStrokeWidth: DEFAULT_EASING_HINT_STROKE_WIDTH, + getComputedStyle, + graphHeight: DEFAULT_GRAPH_HEIGHT, + keyframes, + name, + simulateAnimation, + totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION, + }) + ); + } +} + +module.exports = KeyframesGraphPath; diff --git a/devtools/client/inspector/animation/components/keyframes-graph/moz.build b/devtools/client/inspector/animation/components/keyframes-graph/moz.build new file mode 100644 index 0000000000..1ff518e21d --- /dev/null +++ b/devtools/client/inspector/animation/components/keyframes-graph/moz.build @@ -0,0 +1,14 @@ +# 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( + "ColorPath.js", + "ComputedStylePath.js", + "DiscretePath.js", + "DistancePath.js", + "KeyframeMarkerItem.js", + "KeyframeMarkerList.js", + "KeyframesGraph.js", + "KeyframesGraphPath.js", +) |