Bug 1210796 - Part 2: Visualize each properties. r=pbro

MozReview-Commit-ID: Hjb1QyOMNZR

--HG--
extra : rebase_source : 8ee1a7249453ed701f40c5f0a8fc0118cee8dd39
This commit is contained in:
Daisuke Akatsuka 2017-04-18 12:15:54 +09:00
Родитель 088af889f0
Коммит 9a2f64b9e1
15 изменённых файлов: 856 добавлений и 65 удалений

Просмотреть файл

@ -100,6 +100,8 @@ var getServerTraits = Task.async(function* (target) {
method: "getProperties" },
{ name: "hasSetWalkerActor", actor: "animations",
method: "setWalkerActor" },
{ name: "hasGetAnimationTypes", actor: "animationplayer",
method: "getAnimationTypes" },
];
let traits = {};

Просмотреть файл

@ -33,7 +33,8 @@ 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;
@ -108,8 +109,9 @@ AnimationDetails.prototype = {
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 {
@ -120,13 +122,18 @@ AnimationDetails.prototype = {
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
});
}
}
@ -135,6 +142,26 @@ AnimationDetails.prototype = {
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();
@ -152,8 +179,8 @@ AnimationDetails.prototype = {
// 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({
@ -196,12 +223,16 @@ AnimationDetails.prototype = {
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) {

Просмотреть файл

@ -7,10 +7,17 @@
"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);
@ -37,7 +44,7 @@ Keyframes.prototype = {
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;
@ -47,6 +54,43 @@ Keyframes.prototype = {
? 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;
@ -79,3 +123,52 @@ Keyframes.prototype = {
});
}
};
/**
* 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 })`;
}

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -12,8 +12,9 @@ DIRS += [
]
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')

Просмотреть файл

@ -381,11 +381,11 @@ function* clickOnAnimation(panel, index, 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];

Просмотреть файл

@ -20,6 +20,9 @@ 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.
@ -57,6 +60,22 @@ function createNode(options) {
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
@ -273,3 +292,19 @@ var TimeScale = {
};
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;

Просмотреть файл

@ -9,7 +9,7 @@
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();
@ -112,22 +112,22 @@ 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");
}

Просмотреть файл

@ -462,7 +462,7 @@ ColorWidget.prototype = {
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();
@ -531,7 +531,7 @@ ColorWidget.prototype = {
}
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];

Просмотреть файл

@ -873,8 +873,7 @@ function parseTimingFunction(value) {
return result;
}
// This is exported for testing.
exports._parseTimingFunction = parseTimingFunction;
exports.parseTimingFunction = parseTimingFunction;
/**
* Removes a class from a node and adds it to another.

Просмотреть файл

@ -194,7 +194,7 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
_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];
},

Просмотреть файл

@ -10,6 +10,15 @@
--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 {
@ -28,6 +37,18 @@
--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;
@ -582,30 +603,22 @@ body {
/* 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 {
@ -619,5 +632,44 @@ body {
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;
}

Просмотреть файл

@ -25,7 +25,7 @@
* /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;
@ -461,12 +461,122 @@ var AnimationPlayerActor = protocol.ActorClassWithSpec(animationPlayerSpec, {
/**
* 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;
}
}
});

Просмотреть файл

@ -141,7 +141,7 @@ CssColor.prototype = {
if (!this.valid) {
return false;
}
return this._getRGBATuple().a !== 1;
return this.getRGBATuple().a !== 1;
},
get valid() {
@ -153,7 +153,7 @@ CssColor.prototype = {
*/
get transparent() {
try {
let tuple = this._getRGBATuple();
let tuple = this.getRGBATuple();
return !(tuple.r || tuple.g || tuple.b || tuple.a);
} catch (e) {
return false;
@ -171,7 +171,7 @@ CssColor.prototype = {
}
try {
let tuple = this._getRGBATuple();
let tuple = this.getRGBATuple();
if (tuple.a !== 1) {
return this.hex;
@ -227,7 +227,7 @@ CssColor.prototype = {
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);
},
@ -238,7 +238,7 @@ CssColor.prototype = {
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");
@ -254,7 +254,7 @@ CssColor.prototype = {
// 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;
@ -269,7 +269,7 @@ CssColor.prototype = {
// 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 + ", " +
@ -301,7 +301,7 @@ CssColor.prototype = {
return this.authored;
}
if (this.hasAlpha) {
let a = this._getRGBATuple().a;
let a = this.getRGBATuple().a;
return this._hsl(a);
}
return this._hsl(1);
@ -401,7 +401,7 @@ CssColor.prototype = {
* 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));
@ -432,7 +432,7 @@ CssColor.prototype = {
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 + ")";
@ -534,7 +534,7 @@ function setAlpha(colorValue, alpha, useCssColor4ColorFunction = false) {
alpha = 1;
}
let { r, g, b } = color._getRGBATuple();
let { r, g, b } = color.getRGBATuple();
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
}

Просмотреть файл

@ -75,6 +75,14 @@ const animationPlayerSpec = generateActorSpec({
response: {
properties: RetVal("array:json")
}
},
getAnimationTypes: {
request: {
propertyNames: Arg(0, "array:string")
},
response: {
animationTypes: RetVal("json")
}
}
}
});
@ -148,4 +156,3 @@ const animationsSpec = generateActorSpec({
});
exports.animationsSpec = animationsSpec;