--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -95,16 +95,18 @@ var getServerTraits = Task.async(functio
{ name: "hasSetCurrentTimes", actor: "animations",
method: "setCurrentTimes" },
{ name: "hasGetFrames", actor: "animationplayer",
method: "getFrames" },
{ name: "hasGetProperties", actor: "animationplayer",
method: "getProperties" },
{ name: "hasSetWalkerActor", actor: "animations",
method: "setWalkerActor" },
+ { name: "hasGetAnimationTypes", actor: "animationplayer",
+ method: "getAnimationTypes" },
];
let traits = {};
for (let {name, actor, method} of config) {
traits[name] = yield target.actorHasMethod(actor, method);
}
return traits;
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -28,17 +28,18 @@ function AnimationDetails(serverTraits)
this.serverTraits = serverTraits;
}
exports.AnimationDetails = AnimationDetails;
AnimationDetails.prototype = {
// These are part of frame objects but are not animated properties. This
// array is used to skip them.
- NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
+ NON_PROPERTIES: ["easing", "composite", "computedOffset",
+ "offset", "simulateComputeValuesFailure"],
init: function (containerEl) {
this.containerEl = containerEl;
},
destroy: function () {
this.unrender();
this.containerEl = null;
@@ -103,43 +104,69 @@ AnimationDetails.prototype = {
*/
if (this.serverTraits.hasGetProperties) {
let properties = yield this.animation.getProperties();
for (let {name, values} of properties) {
if (!tracks[name]) {
tracks[name] = [];
}
- for (let {value, offset} of values) {
- tracks[name].push({value, offset});
+ for (let {value, offset, easing, distance} of values) {
+ distance = distance ? distance : 0;
+ tracks[name].push({value, offset, easing, distance});
}
}
} else {
let frames = yield this.animation.getFrames();
for (let frame of frames) {
for (let name in frame) {
if (this.NON_PROPERTIES.indexOf(name) != -1) {
continue;
}
- if (!tracks[name]) {
- tracks[name] = [];
+ // We have to change to CSS property name
+ // since GetKeyframes returns JS property name.
+ const propertyCSSName = getCssPropertyName(name);
+ if (!tracks[propertyCSSName]) {
+ tracks[propertyCSSName] = [];
}
- tracks[name].push({
+ tracks[propertyCSSName].push({
value: frame[name],
- offset: frame.computedOffset
+ offset: frame.computedOffset,
+ easing: frame.easing,
+ distance: 0
});
}
}
}
return tracks;
}),
+ /**
+ * Get animation types of given CSS property names.
+ * @param {Array} CSS property names.
+ * e.g. ["background-color", "opacity", ...]
+ * @return {Object} Animation type mapped with CSS property name.
+ * e.g. { "background-color": "color", }
+ * "opacity": "float", ... }
+ */
+ getAnimationTypes: Task.async(function* (propertyNames) {
+ if (this.serverTraits.hasGetAnimationTypes) {
+ return yield this.animation.getAnimationTypes(propertyNames);
+ }
+ // Set animation type 'none' since does not support getAnimationTypes.
+ const animationTypes = {};
+ propertyNames.forEach(propertyName => {
+ animationTypes[propertyName] = "none";
+ });
+ return Promise.resolve(animationTypes);
+ }),
+
render: Task.async(function* (animation) {
this.unrender();
if (!animation) {
return;
}
this.animation = animation;
@@ -147,18 +174,18 @@ AnimationDetails.prototype = {
// have been re-rendered.
if (!this.containerEl || this.animation !== animation) {
return;
}
// Build an element for each animated property track.
this.tracks = yield this.getTracks(animation, this.serverTraits);
- // Useful for tests to know when the keyframes have been retrieved.
- this.emit("keyframes-retrieved");
+ // Get animation type for each CSS properties.
+ const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
for (let propertyName in this.tracks) {
let line = createNode({
parent: this.containerEl,
attributes: {"class": "property"}
});
let {warning, className} =
this.getPerfDataForProperty(animation, propertyName);
@@ -191,22 +218,26 @@ AnimationDetails.prototype = {
framesEl.style.left = `${x}%`;
framesEl.style.width = `${w}%`;
let keyframesComponent = new Keyframes();
keyframesComponent.init(framesEl);
keyframesComponent.render({
keyframes: this.tracks[propertyName],
propertyName: propertyName,
- animation: animation
+ animation: animation,
+ animationType: animationTypes[propertyName]
});
keyframesComponent.on("frame-selected", this.onFrameSelected);
-
this.keyframeComponents.push(keyframesComponent);
}
+
+ // Useful for tests to know when rendering of all animation detail UIs
+ // have been completed.
+ this.emit("animation-detail-rendering-completed");
}),
onFrameSelected: function (e, args) {
// Relay the event up, it's needed in parents too.
this.emit(e, args);
}
};
--- a/devtools/client/animationinspector/components/keyframes.js
+++ b/devtools/client/animationinspector/components/keyframes.js
@@ -2,20 +2,27 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 EventEmitter = require("devtools/shared/event-emitter");
-const {createNode} = require("devtools/client/animationinspector/utils");
+const {createNode, createSVGNode} =
+ require("devtools/client/animationinspector/utils");
+const {ProgressGraphHelper, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+ 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.
*/
function Keyframes() {
EventEmitter.decorate(this);
this.onClick = this.onClick.bind(this);
}
exports.Keyframes = Keyframes;
@@ -32,26 +39,63 @@ Keyframes.prototype = {
},
destroy: function () {
this.containerEl.removeEventListener("click", this.onClick);
this.keyframesEl.remove();
this.containerEl = this.keyframesEl = this.animation = null;
},
- render: function ({keyframes, propertyName, animation}) {
+ render: function ({keyframes, propertyName, animation, animationType}) {
this.keyframes = keyframes;
this.propertyName = propertyName;
this.animation = animation;
let iterationStartOffset =
animation.state.iterationStart % 1 == 0
? 0
: 1 - animation.state.iterationStart % 1;
+ // Create graph element.
+ const graphEl = createSVGNode({
+ parent: this.keyframesEl,
+ nodeType: "svg",
+ attributes: {
+ "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 graphHelper =
+ new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
+ propertyName, animationType, keyframes, totalDuration);
+
+ renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
+ DEFAULT_MIN_PROGRESS_THRESHOLD, graphHelper);
+
+ // Destroy ProgressGraphHelper resources.
+ graphHelper.destroy();
+
+ // Append elements to display keyframe values.
this.keyframesEl.classList.add(animation.state.type);
for (let frame of this.keyframes) {
let offset = frame.offset + iterationStartOffset;
createNode({
parent: this.keyframesEl,
attributes: {
"class": "frame",
"style": `left:${offset * 100}%;`,
@@ -74,8 +118,57 @@ Keyframes.prototype = {
animation: this.animation,
propertyName: this.propertyName,
offset: parseFloat(e.target.dataset.offset),
value: e.target.getAttribute("title"),
x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
});
}
};
+
+/**
+ * Render a graph representing the progress of the animation over one iteration.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Number} duration - Duration of one iteration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
+ */
+function renderPropertyGraph(parentEl, duration, minSegmentDuration,
+ minProgressThreshold, graphHelper) {
+ const segments = graphHelper.createPathSegments(0, duration, minSegmentDuration,
+ minProgressThreshold);
+
+ const graphType = graphHelper.getGraphType();
+ if (graphType !== "color") {
+ appendPathElement(parentEl, segments, graphType);
+ return;
+ }
+
+ // Append the color to the path.
+ segments.forEach(segment => {
+ segment.y = 1;
+ });
+ const path = appendPathElement(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",
+ attributes: {
+ "id": id
+ }
+ });
+ segments.forEach(segment => {
+ createSVGNode({
+ parent: linearGradientEl,
+ nodeType: "stop",
+ attributes: {
+ "stop-color": segment.style,
+ "offset": segment.x / duration
+ }
+ });
+ });
+ path.style.fill = `url(#${ id })`;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -0,0 +1,461 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {createSVGNode, getJsPropertyName} =
+ require("devtools/client/animationinspector/utils");
+const {colorUtils} = require("devtools/shared/css/color.js");
+const {parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+// In the createPathSegments function, an animation duration is divided by
+// 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, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
+exports.DEFAULT_MIN_PROGRESS_THRESHOLD = DEFAULT_MIN_PROGRESS_THRESHOLD;
+// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
+// and end bounds when dividing duration in createPathSegments.
+const BOUND_EXCLUDING_TIME = 0.001;
+
+/**
+ * This helper return the segment coordinates and style for property graph,
+ * also return the graph type.
+ * Parameters of constructor are below.
+ * @param {Window} win - window object to animate.
+ * @param {String} propertyCSSName - CSS property name (e.g. background-color).
+ * @param {String} animationType - Animation type of CSS property.
+ * @param {Object} keyframes - AnimationInspector's keyframes object.
+ * @param {float} duration - Duration of animation.
+ */
+function ProgressGraphHelper(win, propertyCSSName, animationType, keyframes, duration) {
+ this.win = win;
+ const doc = this.win.document;
+ this.targetEl = doc.createElement("div");
+ doc.documentElement.appendChild(this.targetEl);
+
+ this.propertyCSSName = propertyCSSName;
+ this.propertyJSName = getJsPropertyName(this.propertyCSSName);
+ this.animationType = animationType;
+
+ // Create keyframe object to make dummy animation.
+ const keyframesObject = keyframes.map(keyframe => {
+ const keyframeObject = Object.assign({}, keyframe);
+ keyframeObject[this.propertyJSName] = keyframe.value;
+ return keyframeObject;
+ });
+
+ // Create effect timing object to make dummy animation.
+ const effectTiming = {
+ duration: duration,
+ fill: "forwards"
+ };
+
+ this.keyframes = keyframesObject;
+ this.devtoolsKeyframes = keyframes;
+ this.animation = this.targetEl.animate(this.keyframes, effectTiming);
+ this.animation.pause();
+ this.valueHelperFunction = this.getValueHelperFunction();
+}
+
+ProgressGraphHelper.prototype = {
+ /**
+ * Destory this object.
+ */
+ destroy: function () {
+ this.targetEl.remove();
+ this.animation.cancel();
+
+ this.targetEl = null;
+ this.animation = null;
+ this.valueHelperFunction = null;
+ this.propertyCSSName = null;
+ this.propertyJSName = null;
+ this.animationType = null;
+ this.keyframes = null;
+ this.win = null;
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * Return a segment in graph by given the time.
+ * @return {Object} Computed result which has follwing values.
+ * - x: x value of graph (float)
+ * - y: y value of graph (float between 0 - 1)
+ * - style: the computed style value of the property at the time
+ */
+ getSegment: function (time) {
+ this.animation.currentTime = time;
+ const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
+ const value = this.valueHelperFunction(style);
+ return { x: time, y: value, style: style };
+ },
+
+ /**
+ * Get a value helper function which calculates the value of Y axis by animation type.
+ * @return {function} ValueHelperFunction returns float value of Y axis
+ * from given progress and style (e.g. rgb(0, 0, 0))
+ */
+ getValueHelperFunction: function () {
+ switch (this.animationType) {
+ case "none": {
+ return () => 1;
+ }
+ case "float": {
+ return this.getFloatValueHelperFunction();
+ }
+ case "coord": {
+ return this.getCoordinateValueHelperFunction();
+ }
+ case "color": {
+ return this.getColorValueHelperFunction();
+ }
+ case "discrete": {
+ return this.getDiscreteValueHelperFunction();
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Return value helper function of animation type 'float'.
+ * @param {Object} keyframes - This object shoud be same as
+ * the parameter of getGraphHelper.
+ * @return {function} ValueHelperFunction returns float value of Y axis
+ * from given float (e.g. 1.0, 0.5 and so on)
+ */
+ getFloatValueHelperFunction: function () {
+ let maxValue = 0;
+ let minValue = Infinity;
+ this.keyframes.forEach(keyframe => {
+ maxValue = Math.max(maxValue, keyframe.value);
+ minValue = Math.min(minValue, keyframe.value);
+ });
+ const distance = maxValue - minValue;
+ return value => {
+ return (value - minValue) / distance;
+ };
+ },
+
+ /**
+ * Return value helper function of animation type 'coord'.
+ * @return {function} ValueHelperFunction returns float value of Y axis
+ * from given style (e.g. 100px)
+ */
+ getCoordinateValueHelperFunction: function () {
+ let maxValue = 0;
+ let minValue = Infinity;
+ for (let i = 0, n = this.keyframes.length; i < n; i++) {
+ if (this.keyframes[i].value.match(/calc/)) {
+ return null;
+ }
+ const value = parseFloat(this.keyframes[i].value);
+ minValue = Math.min(minValue, value);
+ maxValue = Math.max(maxValue, value);
+ }
+ const distance = maxValue - minValue;
+ return value => {
+ return (parseFloat(value) - minValue) / distance;
+ };
+ },
+
+ /**
+ * Return value helper function of animation type 'color'.
+ * @param {Object} keyframes - This object shoud be same as
+ * the parameter of getGraphHelper.
+ * @return {function} ValueHelperFunction returns float value of Y axis
+ * from given color (e.g. rgb(0, 0, 0))
+ */
+ getColorValueHelperFunction: function () {
+ const maxObject = { distance: 0 };
+ for (let i = 0; i < this.keyframes.length - 1; i++) {
+ const value1 = getRGBA(this.keyframes[i].value);
+ for (let j = i + 1; j < this.keyframes.length; j++) {
+ const value2 = getRGBA(this.keyframes[j].value);
+ const distance = getRGBADistance(value1, value2);
+ if (maxObject.distance >= distance) {
+ continue;
+ }
+ maxObject.distance = distance;
+ maxObject.value1 = value1;
+ maxObject.value2 = value2;
+ }
+ }
+ const baseValue =
+ maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
+
+ return value => {
+ const colorValue = getRGBA(value);
+ return getRGBADistance(baseValue, colorValue) / maxObject.distance;
+ };
+ },
+
+ /**
+ * Return value helper function of animation type 'discrete'.
+ * @return {function} ValueHelperFunction returns float value of Y axis
+ * from given style (e.g. center)
+ */
+ getDiscreteValueHelperFunction: function () {
+ const discreteValues = [];
+ this.keyframes.forEach(keyframe => {
+ if (!discreteValues.includes(keyframe.value)) {
+ discreteValues.push(keyframe.value);
+ }
+ });
+ return value => {
+ return discreteValues.indexOf(value) / (discreteValues.length - 1);
+ };
+ },
+
+ /**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @return {Array} path segments -
+ * [{x: {Number} time, y: {Number} progress}, ...]
+ */
+ createPathSegments: function (startTime, endTime,
+ minSegmentDuration, minProgressThreshold) {
+ return !this.valueHelperFunction
+ ? createKeyframesPathSegments(endTime - startTime, this.devtoolsKeyframes)
+ : createPathSegments(startTime, endTime,
+ minSegmentDuration, minProgressThreshold, this);
+ },
+};
+
+exports.ProgressGraphHelper = ProgressGraphHelper;
+
+/**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper
+ * - getSegment(time): Helper function that, given a time,
+ * will calculate the animation progress.
+ * @return {Array} path segments -
+ * [{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ // If the duration is too short, early return.
+ if (endTime - startTime < minSegmentDuration) {
+ return [segmentHelper.getSegment(startTime),
+ segmentHelper.getSegment(endTime)];
+ }
+
+ // Otherwise, start creating segments.
+ let pathSegments = [];
+
+ // Append the segment for the startTime position.
+ const startTimeSegment = segmentHelper.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.
+ const interval = (endTime - startTime) / DURATION_RESOLUTION;
+ for (let index = 1; index <= DURATION_RESOLUTION; index++) {
+ // Create a segment for this interval.
+ const currentSegment =
+ segmentHelper.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).
+ pathSegments = pathSegments.concat(
+ createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
+ currentSegment.x - BOUND_EXCLUDING_TIME,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper));
+ }
+
+ pathSegments.push(currentSegment);
+ previousSegment = currentSegment;
+ }
+
+ return pathSegments;
+}
+exports.createPathSegments = createPathSegments;
+
+/**
+ * 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.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls) {
+ // 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;
+ }
+
+ const nextPathSegment = pathSegments[i + 1];
+ path += pathSegment.easing.startsWith("steps")
+ ? createStepsPathString(pathSegment, nextPathSegment)
+ : createCubicBezierPathString(pathSegment, nextPathSegment);
+ }
+ path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+ // Append and return the path element.
+ return createSVGNode({
+ parent: parentEl,
+ nodeType: "path",
+ attributes: {
+ "d": path,
+ "class": cls,
+ "vector-effect": "non-scaling-stroke",
+ "transform": "scale(1, -1)"
+ }
+ });
+}
+exports.appendPathElement = appendPathElement;
+
+/**
+ * Create the path segments from given keyframes.
+ * @param {Number} duration - Duration of animation.
+ * @param {Object} Keyframes of devtool's format.
+ * @return {Array} path segments -
+ * [{x: {Number} time, y: {Number} distance,
+ * easing: {String} keyframe's easing,
+ * style: {String} keyframe's value}, ...]
+ */
+function createKeyframesPathSegments(duration, keyframes) {
+ return keyframes.map(keyframe => {
+ return {
+ x: keyframe.offset * duration,
+ y: keyframe.distance,
+ easing: keyframe.easing,
+ style: keyframe.value
+ };
+ });
+}
+
+/**
+ * Create a line path string.
+ * @param {Object} segment - e.g. { x: 100, y: 1 }
+ * @return {String} path string - e.g. "L100,1"
+ */
+function createLinePathString(segment) {
+ return ` L${ segment.x },${ segment.y }`;
+}
+
+/**
+ * Create a path string to represents a step function.
+ * @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "steps(2)" }
+ * @param {Object} nextSegment - e.g. { x: 1, y: 1 }
+ * @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
+ */
+function createStepsPathString(currentSegment, nextSegment) {
+ const matches =
+ currentSegment.easing.match(/^steps\((\d+)(,\sstart)?\)/);
+ const stepNumber = parseInt(matches[1], 10);
+ const oneStepX = (nextSegment.x - currentSegment.x) / stepNumber;
+ const oneStepY = (nextSegment.y - currentSegment.y) / stepNumber;
+ const isStepStart = matches[2];
+ const stepOffsetY = isStepStart ? 1 : 0;
+ let path = "";
+ for (let step = 0; step < stepNumber; step++) {
+ const sx = currentSegment.x + step * oneStepX;
+ const ex = sx + oneStepX;
+ const y = currentSegment.y + (step + stepOffsetY) * oneStepY;
+ path += ` L${ sx },${ y } L${ ex },${ y }`;
+ }
+ if (!isStepStart) {
+ path += ` L${ nextSegment.x },${ nextSegment.y }`;
+ }
+ return path;
+}
+
+/**
+ * Create a path string to represents a bezier curve.
+ * @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "ease" }
+ * @param {Object} nextSegment - e.g. { x: 1, y: 1 }
+ * @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
+ */
+function createCubicBezierPathString(currentSegment, nextSegment) {
+ const controlPoints = parseTimingFunction(currentSegment.easing);
+ if (!controlPoints) {
+ // Just return line path string since we could not parse this easing.
+ return createLinePathString(currentSegment);
+ }
+
+ const cp1x = controlPoints[0];
+ const cp1y = controlPoints[1];
+ const cp2x = controlPoints[2];
+ const cp2y = controlPoints[3];
+
+ const diffX = nextSegment.x - currentSegment.x;
+ const diffY = nextSegment.y - currentSegment.y;
+ let path =
+ ` C ${ currentSegment.x + diffX * cp1x } ${ currentSegment.y + diffY * cp1y }`;
+ path += `, ${ currentSegment.x + diffX * cp2x } ${ currentSegment.y + diffY * cp2y }`;
+ path += `, ${ nextSegment.x } ${ nextSegment.y }`;
+ return path;
+}
+
+/**
+ * Parse given RGBA string.
+ * @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(colorString) {
+ 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 {float} distance.
+ */
+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);
+}
--- a/devtools/client/animationinspector/moz.build
+++ b/devtools/client/animationinspector/moz.build
@@ -7,13 +7,14 @@
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DIRS += [
'components'
]
DevToolsModules(
- 'utils.js',
+ 'graph-helper.js',
+ 'utils.js'
)
-with Files('**'):
- BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -376,21 +376,21 @@ function disableHighlighter(toolbox) {
function* clickOnAnimation(panel, index, shouldClose) {
let timeline = panel.animationsTimelineComponent;
// Expect a selection event.
let onSelectionChanged = timeline.once(shouldClose
? "animation-unselected"
: "animation-selected");
- // If we're opening the animation, also wait for the keyframes-retrieved
- // event.
+ // If we're opening the animation, also wait for
+ // the animation-detail-rendering-completed event.
let onReady = shouldClose
? Promise.resolve()
- : timeline.details[index].once("keyframes-retrieved");
+ : timeline.details[index].once("animation-detail-rendering-completed");
info("Click on animation " + index + " in the timeline");
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
EventUtils.sendMouseEvent({type: "click"}, timeBlock,
timeBlock.ownerDocument.defaultView);
yield onReady;
return yield onSelectionChanged;
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -15,16 +15,19 @@ const L10N =
// How many times, maximum, can we loop before we find the optimal time
// interval in the timeline graph.
const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
// Time graduations should be multiple of one of these number.
const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+
/**
* DOM node creation helper function.
* @param {Object} Options to customize the node to be created.
* - nodeType {String} Optional, defaults to "div",
* - attributes {Object} Optional attributes object like
* {attrName1:value1, attrName2: value2, ...}
* - parent {DOMNode} Mandatory node to append the newly created node to.
* - textContent {String} Optional text for the node.
@@ -53,16 +56,32 @@ function createNode(options) {
options.parent.appendChild(node);
return node;
}
exports.createNode = createNode;
/**
+ * SVG DOM node creation helper function.
+ * @param {Object} Options to customize the node to be created.
+ * - nodeType {String} Optional, defaults to "div",
+ * - attributes {Object} Optional attributes object like
+ * {attrName1:value1, attrName2: value2, ...}
+ * - parent {DOMNode} Mandatory node to append the newly created node to.
+ * - textContent {String} Optional text for the node.
+ * @return {DOMNode} The newly created node.
+ */
+function createSVGNode(options) {
+ options.namespace = SVG_NS;
+ return createNode(options);
+}
+exports.createSVGNode = createSVGNode;
+
+/**
* Find the optimal interval between time graduations in the animation timeline
* graph based on a minimum time interval
* @param {Number} minTimeInterval Minimum time in ms in one interval
* @return {Number} The optimal interval time in ms
*/
function findOptimalTimeInterval(minTimeInterval) {
let numIters = 0;
let multiplier = 1;
@@ -268,8 +287,24 @@ var TimeScale = {
: x + iterationW;
return {x, w, iterationW, delayX, delayW, negativeDelayW,
endDelayX, endDelayW};
}
};
exports.TimeScale = TimeScale;
+
+/**
+ * Convert given CSS property name to JavaScript CSS name.
+ * @param {String} 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();
+ });
+}
+exports.getJsPropertyName = getJsPropertyName;
--- a/devtools/client/shared/test/unit/test_cubicBezier.js
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -4,17 +4,17 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests the CubicBezier API in the CubicBezierWidget module
var Cu = Components.utils;
var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+var {CubicBezier, parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
function run_test() {
throwsWhenMissingCoordinates();
throwsWhenIncorrectCoordinates();
convertsStringCoordinates();
coordinatesToStringOutputsAString();
pointGettersReturnPointCoordinatesArrays();
toStringOutputsCubicBezierValue();
@@ -107,32 +107,32 @@ function toStringOutputsCssPresetValues(
c = new CubicBezier([0.42, 0, 0.58, 1]);
do_check_eq(c.toString(), "ease-in-out");
}
function testParseTimingFunction() {
do_print("test parseTimingFunction");
for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
- ok(_parseTimingFunction(test), test);
+ ok(parseTimingFunction(test), test);
}
- ok(!_parseTimingFunction("something"), "non-function token");
- ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
- ok(!_parseTimingFunction("cubic-bezier(something)",
+ ok(!parseTimingFunction("something"), "non-function token");
+ ok(!parseTimingFunction("something()"), "non-cubic-bezier function");
+ ok(!parseTimingFunction("cubic-bezier(something)",
"cubic-bezier with non-numeric argument"));
- ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
+ ok(!parseTimingFunction("cubic-bezier(1,2,3:7)",
"did not see comma"));
- ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
- "did not see close paren"));
- ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
- ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
- deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
+ ok(!parseTimingFunction("cubic-bezier(1,2,3,7:",
+ "did not see close paren"));
+ ok(!parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+ ok(!parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+ deepEqual(parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
"correct invocation");
- deepEqual(_parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
+ deepEqual(parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
[1, 2, 3, 7],
"correct with comments and whitespace");
}
function do_check_throws(cb, info) {
do_print(info);
let hasThrown = false;
--- a/devtools/client/shared/widgets/ColorWidget.js
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -457,17 +457,17 @@ ColorWidget.prototype = {
onHexInputChange: function (event) {
const hex = event.target.value;
const color = new colorUtils.CssColor(hex, true);
if (!color.rgba) {
return;
}
- const { r, g, b, a } = color._getRGBATuple();
+ const { r, g, b, a } = color.getRGBATuple();
this.rgb = [r, g, b, a];
this.updateUI();
this.onChange();
},
onRgbaInputChange: function (event) {
const field = event.target.dataset.id;
const value = event.target.value.toString();
@@ -526,17 +526,17 @@ ColorWidget.prototype = {
hsl[2] = value;
break;
case "a":
hsl[3] = Math.min(value, 1);
break;
}
const cssString = ColorWidget.hslToCssString(hsl[0], hsl[1], hsl[2], hsl[3]);
- const { r, g, b, a } = new colorUtils.CssColor(cssString)._getRGBATuple();
+ const { r, g, b, a } = new colorUtils.CssColor(cssString).getRGBATuple();
this.rgb = [r, g, b, a];
this.hsl = hsl;
this.updateUI();
this.onChange();
},
--- a/devtools/client/shared/widgets/CubicBezierWidget.js
+++ b/devtools/client/shared/widgets/CubicBezierWidget.js
@@ -868,18 +868,17 @@ function parseTimingFunction(value) {
token.text !== (i == 3 ? ")" : ",")) {
return undefined;
}
}
return result;
}
-// This is exported for testing.
-exports._parseTimingFunction = parseTimingFunction;
+exports.parseTimingFunction = parseTimingFunction;
/**
* Removes a class from a node and adds it to another.
* @param {String} className the class to swap
* @param {DOMNode} from the node to remove the class from
* @param {DOMNode} to the node to add the class to
*/
function swapClassName(className, from, to) {
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -189,17 +189,17 @@ SwatchColorPickerTooltip.prototype = Her
_onEyeDropperDone: function () {
this.eyedropperOpen = false;
this.activeSwatch = null;
},
_colorToRgba: function (color) {
color = new colorUtils.CssColor(color, this.cssColor4);
- let rgba = color._getRGBATuple();
+ let rgba = color.getRGBATuple();
return [rgba.r, rgba.g, rgba.b, rgba.a];
},
_toDefaultType: function (color) {
let colorObj = new colorUtils.CssColor(color);
colorObj.setAuthoredUnitFromColor(this._originalColor, this.cssColor4);
return colorObj.toString();
},
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -5,16 +5,25 @@
/* Animation-inspector specific theme variables */
.theme-dark {
--even-animation-timeline-background-color: rgba(255,255,255,0.03);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
--pause-image: url(chrome://devtools/skin/images/pause.svg);
--rewind-image: url(chrome://devtools/skin/images/rewind.svg);
--play-image: url(chrome://devtools/skin/images/play.svg);
+ /* The color for animation type 'opacity' */
+ --opacity-border-color: var(--theme-highlight-pink);
+ --opacity-background-color: #df80ff80;
+ /* The color for animation type 'transform' */
+ --transform-border-color: var(--theme-graphs-yellow);
+ --transform-background-color: #d99b2880;
+ /* The color for other animation type */
+ --other-border-color: var(--theme-graphs-bluegrey);
+ --other-background-color: #5e88b080;
}
.theme-light {
--even-animation-timeline-background-color: rgba(128,128,128,0.03);
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
--pause-image: url(chrome://devtools/skin/images/pause.svg);
--rewind-image: url(chrome://devtools/skin/images/rewind.svg);
--play-image: url(chrome://devtools/skin/images/play.svg);
@@ -23,16 +32,28 @@
.theme-firebug {
--even-animation-timeline-background-color: rgba(128,128,128,0.03);
--command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
--pause-image: url(chrome://devtools/skin/images/firebug/pause.svg);
--rewind-image: url(chrome://devtools/skin/images/firebug/rewind.svg);
--play-image: url(chrome://devtools/skin/images/firebug/play.svg);
}
+.theme-light, .theme-firebug {
+ /* The color for animation type 'opacity' */
+ --opacity-border-color: var(--theme-highlight-pink);
+ --opacity-background-color: #b82ee580;
+ /* The color for animation type 'transform' */
+ --transform-border-color: var(--theme-graphs-orange);
+ --transform-background-color: #efc05280;
+ /* The color for other animation type */
+ --other-border-color: var(--theme-graphs-bluegrey);
+ --other-background-color: #0072ab80;
+}
+
:root {
/* How high should toolbars be */
--toolbar-height: 20px;
/* How wide should the sidebar be (should be wide enough to contain long
property names like 'border-bottom-right-radius' without ellipsis) */
--timeline-sidebar-width: 200px;
/* How high should animations displayed in the timeline be */
--timeline-animation-height: 20px;
@@ -577,47 +598,78 @@ body {
/* Keyframes diagram, displayed below the timeline, inside the animation-details
element. */
.keyframes {
/* Actual keyframe markers are positioned absolutely within this container and
their position is relative to its size (we know the offset of each frame
in percentage) */
- position: relative;
+ position: absolute;
+ left: 0;
+ top: 0;
width: 100%;
- height: 0;
-}
+ height: 100%;
-.keyframes.cssanimation {
- background-color: var(--theme-contrast-background);
-}
-
-.keyframes.csstransition {
- background-color: var(--theme-highlight-blue);
-}
-
-.keyframes.scriptanimation {
- background-color: var(--theme-graphs-green);
}
.keyframes .frame {
position: absolute;
- top: 0;
+ top: 50%;
width: 0;
height: 0;
background-color: inherit;
cursor: pointer;
+ z-index: 1;
}
.keyframes .frame::before {
content: "";
display: block;
transform:
translateX(calc(var(--keyframes-marker-size) * -.5))
/* The extra pixel on the Y axis is so that markers are centered on the
horizontal line in the keyframes diagram. */
translateY(calc(var(--keyframes-marker-size) * -.5 + 1px));
width: var(--keyframes-marker-size);
height: var(--keyframes-marker-size);
border-radius: 100%;
+ border: 1px solid var(--theme-highlight-gray);
background-color: inherit;
}
+
+.keyframes.cssanimation .frame {
+ background-color: var(--theme-contrast-background);
+}
+
+.keyframes.csstransition .frame {
+ background-color: var(--theme-highlight-blue);
+}
+
+.keyframes.scriptanimation .frame {
+ background-color: var(--theme-graphs-green);
+}
+
+.keyframes svg {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+
+.keyframes svg path {
+ fill: var(--other-background-color);
+ stroke: var(--other-border-color);
+}
+
+/* color of path is decided by the animation type */
+.keyframes svg path.opacity {
+ fill: var(--opacity-background-color);
+ stroke: var(--opacity-border-color);
+}
+
+.keyframes svg path.transform {
+ fill: var(--transform-background-color);
+ stroke: var(--transform-border-color);
+}
+
+.keyframes svg path.color {
+ stroke: none;
+}
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -20,17 +20,17 @@
*
* References:
* - WebAnimation spec:
* http://w3c.github.io/web-animations/
* - WebAnimation WebIDL files:
* /dom/webidl/Animation*.webidl
*/
-const {Cu} = require("chrome");
+const {Cu, Ci} = require("chrome");
const promise = require("promise");
const protocol = require("devtools/shared/protocol");
const {Actor} = protocol;
const {animationPlayerSpec, animationsSpec} = require("devtools/shared/specs/animation");
const events = require("sdk/event/core");
// Types of animations.
const ANIMATION_TYPES = {
@@ -456,22 +456,132 @@ var AnimationPlayerActor = protocol.Acto
*/
getFrames: function () {
return this.player.effect.getKeyframes();
},
/**
* Get data about the animated properties of this animation player.
* @return {Array} Returns a list of animated properties.
- * Each property contains a list of values and their offsets
+ * Each property contains a list of values, their offsets and distances.
*/
getProperties: function () {
- return this.player.effect.getProperties().map(property => {
+ const properties = this.player.effect.getProperties().map(property => {
return {name: property.property, values: property.values};
});
+
+ const DOMWindowUtils =
+ this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ // Fill missing keyframe with computed value.
+ for (let property of properties) {
+ let underlyingValue = null;
+ // Check only 0% and 100% keyframes.
+ [0, property.values.length - 1].forEach(index => {
+ const values = property.values[index];
+ if (values.value !== undefined) {
+ return;
+ }
+ if (!underlyingValue) {
+ let pseudo = null;
+ let target = this.player.effect.target;
+ if (target.type) {
+ // This target is a pseudo element.
+ pseudo = target.type;
+ target = target.parentElement;
+ }
+ const value =
+ DOMWindowUtils.getUnanimatedComputedStyle(target, pseudo, property.name);
+ const animationType = DOMWindowUtils.getAnimationTypeForLonghand(property.name);
+ underlyingValue = animationType === "float" ? parseFloat(value, 10) : value;
+ }
+ values.value = underlyingValue;
+ });
+ }
+
+ // Calculate the distance.
+ for (let property of properties) {
+ const propertyName = property.name;
+ const maxObject = { distance: -1 };
+ for (let i = 0; i < property.values.length - 1; i++) {
+ const value1 = property.values[i].value;
+ for (let j = i + 1; j < property.values.length; j++) {
+ const value2 = property.values[j].value;
+ const distance = this.getDistance(this.player.effect.target, propertyName,
+ value1, value2, DOMWindowUtils);
+ if (maxObject.distance >= distance) {
+ continue;
+ }
+ maxObject.distance = distance;
+ maxObject.value1 = value1;
+ maxObject.value2 = value2;
+ }
+ }
+ if (maxObject.distance === 0) {
+ // Distance is zero means that no values change or can't calculate the distance.
+ // In this case, we use the keyframe offset as the distance.
+ property.values.reduce((previous, current) => {
+ // If the current value is same as previous value, use previous distance.
+ current.distance =
+ current.value === previous.value ? previous.distance : current.offset;
+ return current;
+ }, property.values[0]);
+ continue;
+ }
+ const baseValue =
+ maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
+ for (let values of property.values) {
+ const value = values.value;
+ const distance = this.getDistance(this.player.effect.target, propertyName,
+ baseValue, value, DOMWindowUtils);
+ values.distance = distance / maxObject.distance;
+ }
+ }
+ return properties;
+ },
+
+ /**
+ * Get the animation types for a given list of CSS property names.
+ * @param {Array} propertyNames - CSS property names (e.g. background-color)
+ * @return {Object} Returns animation types (e.g. {"background-color": "rgb(0, 0, 0)"}.
+ */
+ getAnimationTypes: function (propertyNames) {
+ const DOMWindowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ const animationTypes = {};
+ for (let propertyName of propertyNames) {
+ animationTypes[propertyName] =
+ DOMWindowUtils.getAnimationTypeForLonghand(propertyName);
+ }
+ return animationTypes;
+ },
+
+ /**
+ * Returns the distance of between value1, value2.
+ * @param {Object} target - dom element
+ * @param {String} propertyName - e.g. transform
+ * @param {String} value1 - e.g. translate(0px)
+ * @param {String} value2 - e.g. translate(10px)
+ * @param {Object} DOMWindowUtils
+ * @param {float} distance
+ */
+ getDistance: function (target, propertyName, value1, value2, DOMWindowUtils) {
+ if (value1 === value2) {
+ return 0;
+ }
+ try {
+ const distance =
+ DOMWindowUtils.computeAnimationDistance(target, propertyName, value1, value2);
+ return distance;
+ } catch (e) {
+ // We can't compute the distance such the 'discrete' animation,
+ // 'auto' keyword and so on.
+ return 0;
+ }
}
});
exports.AnimationPlayerActor = AnimationPlayerActor;
/**
* The Animations actor lists animation players for a given node.
*/
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -136,29 +136,29 @@ CssColor.prototype = {
this._setColorUnitUppercase(color);
}
},
get hasAlpha() {
if (!this.valid) {
return false;
}
- return this._getRGBATuple().a !== 1;
+ return this.getRGBATuple().a !== 1;
},
get valid() {
return isValidCSSColor(this.authored, this.cssColor4);
},
/**
* Return true for all transparent values e.g. rgba(0, 0, 0, 0).
*/
get transparent() {
try {
- let tuple = this._getRGBATuple();
+ let tuple = this.getRGBATuple();
return !(tuple.r || tuple.g || tuple.b || tuple.a);
} catch (e) {
return false;
}
},
get specialValue() {
return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
@@ -166,17 +166,17 @@ CssColor.prototype = {
get name() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
try {
- let tuple = this._getRGBATuple();
+ let tuple = this.getRGBATuple();
if (tuple.a !== 1) {
return this.hex;
}
let {r, g, b} = tuple;
return rgbToColorName(r, g, b);
} catch (e) {
return this.hex;
@@ -222,59 +222,59 @@ CssColor.prototype = {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.hasAlpha) {
return this.longAlphaHex;
}
- let tuple = this._getRGBATuple();
+ let tuple = this.getRGBATuple();
return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) +
(tuple.b << 0)).toString(16).substr(-6);
},
get longAlphaHex() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
- let tuple = this._getRGBATuple();
+ let tuple = this.getRGBATuple();
return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) +
(tuple.b << 0)).toString(16).substr(-6) +
Math.round(tuple.a * 255).toString(16).padEnd(2, "0");
},
get rgb() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (!this.hasAlpha) {
if (this.lowerCased.startsWith("rgb(")) {
// The color is valid and begins with rgb(.
return this.authored;
}
- let tuple = this._getRGBATuple();
+ let tuple = this.getRGBATuple();
return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
}
return this.rgba;
},
get rgba() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.lowerCased.startsWith("rgba(")) {
// The color is valid and begins with rgba(.
return this.authored;
}
- let components = this._getRGBATuple();
+ let components = this.getRGBATuple();
return "rgba(" + components.r + ", " +
components.g + ", " +
components.b + ", " +
components.a + ")";
},
get hsl() {
let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
@@ -296,17 +296,17 @@ CssColor.prototype = {
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
if (this.lowerCased.startsWith("hsla(")) {
// The color is valid and begins with hsla(.
return this.authored;
}
if (this.hasAlpha) {
- let a = this._getRGBATuple().a;
+ let a = this.getRGBATuple().a;
return this._hsl(a);
}
return this._hsl(1);
},
/**
* Check whether the current color value is in the special list e.g.
* transparent or invalid.
@@ -396,17 +396,17 @@ CssColor.prototype = {
return color;
},
/**
* Returns a RGBA 4-Tuple representation of a color or transparent as
* appropriate.
*/
- _getRGBATuple: function () {
+ getRGBATuple: function () {
let tuple = colorToRGBA(this.authored, this.cssColor4);
tuple.a = parseFloat(tuple.a.toFixed(1));
return tuple;
},
/**
@@ -427,17 +427,17 @@ CssColor.prototype = {
},
_hsl: function (maybeAlpha) {
if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
// We can use it as-is.
return this.authored;
}
- let {r, g, b} = this._getRGBATuple();
+ let {r, g, b} = this.getRGBATuple();
let [h, s, l] = rgbToHsl([r, g, b]);
if (maybeAlpha !== undefined) {
return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")";
}
return "hsl(" + h + ", " + s + "%, " + l + "%)";
},
/**
@@ -529,17 +529,17 @@ function setAlpha(colorValue, alpha, use
throw new Error("Invalid color.");
}
// If an invalid alpha valid, just set to 1.
if (!(alpha >= 0 && alpha <= 1)) {
alpha = 1;
}
- let { r, g, b } = color._getRGBATuple();
+ let { r, g, b } = color.getRGBATuple();
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
}
/**
* Given a color, classify its type as one of the possible color
* units, as known by |CssColor.colorUnit|.
*
* @param {String} value
--- a/devtools/shared/specs/animation.js
+++ b/devtools/shared/specs/animation.js
@@ -70,16 +70,24 @@ const animationPlayerSpec = generateActo
frames: RetVal("json")
}
},
getProperties: {
request: {},
response: {
properties: RetVal("array:json")
}
+ },
+ getAnimationTypes: {
+ request: {
+ propertyNames: Arg(0, "array:string")
+ },
+ response: {
+ animationTypes: RetVal("json")
+ }
}
}
});
exports.animationPlayerSpec = animationPlayerSpec;
const animationsSpec = generateActorSpec({
typeName: "animations",
@@ -143,9 +151,8 @@ const animationsSpec = generateActorSpec
rate: Arg(1, "number")
},
response: {}
}
}
});
exports.animationsSpec = animationsSpec;
-