Bug 1406285 - Part 5: Implement computed timing graph of summary graph. r=gl

MozReview-Commit-ID: C91ORqTRSfj

--HG--
extra : rebase_source : fb721a287e7fa8177b8f602f55ccc972dd739b15
This commit is contained in:
Daisuke Akatsuka 2018-01-18 12:42:31 +09:00
Родитель 603e851dab
Коммит ad1112c3ca
14 изменённых файлов: 755 добавлений и 7 удалений

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

@ -18,10 +18,12 @@ const { updateSidebarSize } = require("./actions/sidebar");
const { isAllAnimationEqual } = require("./utils/utils");
class AnimationInspector {
constructor(inspector) {
constructor(inspector, win) {
this.inspector = inspector;
this.win = win;
this.getNodeFromActor = this.getNodeFromActor.bind(this);
this.simulateAnimation = this.simulateAnimation.bind(this);
this.toggleElementPicker = this.toggleElementPicker.bind(this);
this.update = this.update.bind(this);
this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
@ -48,6 +50,7 @@ class AnimationInspector {
const {
emit: emitEventForTest,
getNodeFromActor,
simulateAnimation,
toggleElementPicker,
} = this;
@ -67,6 +70,7 @@ class AnimationInspector {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
toggleElementPicker,
}
)
@ -87,7 +91,18 @@ class AnimationInspector {
this.inspector.toolbox.off("picker-started", this.onElementPickerStarted);
this.inspector.toolbox.off("picker-stopped", this.onElementPickerStopped);
if (this.simulatedAnimation) {
this.simulatedAnimation.cancel();
this.simulatedAnimation = null;
}
if (this.simulatedElement) {
this.simulatedElement.remove();
this.simulatedElement = null;
}
this.inspector = null;
this.win = null;
}
/**
@ -139,6 +154,48 @@ class AnimationInspector {
this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
}
/**
* Returns simulatable animation by given parameters.
* The returned animation is implementing Animation interface of Web Animation API.
* https://drafts.csswg.org/web-animations/#the-animation-interface
*
* @param {Array} keyframes
* e.g. [{ opacity: 0 }, { opacity: 1 }]
* @param {Object} effectTiming
* e.g. { duration: 1000, fill: "both" }
* @param {Boolean} isElementNeeded
* true: create animation with an element.
* If want to know computed value of the element, turn on.
* false: create animation without an element,
* If need to know only timing progress.
* @return {Animation}
* https://drafts.csswg.org/web-animations/#the-animation-interface
*/
simulateAnimation(keyframes, effectTiming, isElementNeeded) {
let targetEl = null;
if (isElementNeeded) {
if (!this.simulatedElement) {
this.simulatedElement = this.win.document.createElement("div");
this.win.document.documentElement.appendChild(this.simulatedElement);
} else {
// Reset styles.
this.simulatedElement.style.cssText = "";
}
targetEl = this.simulatedElement;
}
if (!this.simulatedAnimation) {
this.simulatedAnimation = new this.win.Animation();
}
this.simulatedAnimation.effect =
new this.win.KeyframeEffect(targetEl, keyframes, effectTiming);
return this.simulatedAnimation;
}
toggleElementPicker() {
this.inspector.toolbox.highlighterUtils.togglePicker();
}

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

@ -20,6 +20,7 @@ class AnimationItem extends PureComponent {
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
@ -32,6 +33,7 @@ class AnimationItem extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
timeScale,
} = this.props;
@ -52,6 +54,7 @@ class AnimationItem extends PureComponent {
SummaryGraph(
{
animation,
simulateAnimation,
timeScale,
}
)

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

@ -19,6 +19,7 @@ class AnimationList extends PureComponent {
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
@ -31,6 +32,7 @@ class AnimationList extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
timeScale,
} = this.props;
@ -47,6 +49,7 @@ class AnimationList extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
timeScale,
}
)

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

@ -23,6 +23,7 @@ class AnimationListContainer extends PureComponent {
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
};
}
@ -34,6 +35,7 @@ class AnimationListContainer extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
} = this.props;
const timeScale = new TimeScale(animations);
@ -54,6 +56,7 @@ class AnimationListContainer extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
timeScale,
}
)

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

@ -21,6 +21,7 @@ class App extends PureComponent {
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
toggleElementPicker: PropTypes.func.isRequired,
};
}
@ -37,6 +38,7 @@ class App extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
toggleElementPicker,
} = this.props;
@ -53,6 +55,7 @@ class App extends PureComponent {
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
simulateAnimation,
}
)
:

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

@ -4,22 +4,90 @@
"use strict";
const { PureComponent } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
class ComputedTimingPath extends PureComponent {
const { SummaryGraphHelper, toPathString } = require("../../utils/graph-helper");
const TimingPath = require("./TimingPath");
class ComputedTimingPath extends TimingPath {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
durationPerPixel: PropTypes.number.isRequired,
keyframes: PropTypes.object.isRequired,
totalDisplayedDuration: PropTypes.number.isRequired,
opacity: PropTypes.number.isRequired,
simulateAnimation: PropTypes.func.isRequired,
totalDuration: PropTypes.number.isRequired,
};
}
render() {
return dom.g({});
const {
animation,
durationPerPixel,
keyframes,
opacity,
simulateAnimation,
totalDuration,
} = this.props;
const { state } = animation;
const effectTiming = Object.assign({}, state, {
iterations: state.iterationCount ? state.iterationCount : Infinity
});
// Create new keyframes for opacity as computed style.
// The reason why we use computed value instead of computed timing progress is to
// include the easing in keyframes as well. Although the computed timing progress
// is not affected by the easing in keyframes at all, computed value reflects that.
const frames = keyframes.map(keyframe => {
return {
opacity: keyframe.offset,
offset: keyframe.offset,
easing: keyframe.easing
};
});
const simulatedAnimation = simulateAnimation(frames, effectTiming, true);
const simulatedElement = simulatedAnimation.effect.target;
const win = simulatedElement.ownerGlobal;
const endTime = simulatedAnimation.effect.getComputedTiming().endTime;
// Set the underlying opacity to zero so that if we sample the animation's output
// during the delay phase and it is not filling backwards, we get zero.
simulatedElement.style.opacity = 0;
const getValueFunc = time => {
if (time < 0) {
return { x: time, y: 0 };
}
simulatedAnimation.currentTime = time < endTime ? time : endTime;
return win.getComputedStyle(simulatedElement).opacity;
};
const toPathStringFunc = segments => {
const firstSegment = segments[0];
let pathString = `M${ firstSegment.x },0 `;
pathString += toPathString(segments);
const lastSegment = segments[segments.length - 1];
pathString += `L${ lastSegment.x },0 Z`;
return pathString;
};
const helper = new SummaryGraphHelper(state, keyframes,
totalDuration, durationPerPixel,
getValueFunc, toPathStringFunc);
const offset = state.previousStartTime ? state.previousStartTime : 0;
return dom.g(
{
className: "animation-computed-timing-path",
style: { opacity },
transform: `translate(${ offset })`
},
super.renderGraph(state, helper)
);
}
}

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

@ -14,6 +14,7 @@ class SummaryGraph extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
@ -21,6 +22,7 @@ class SummaryGraph extends PureComponent {
render() {
const {
animation,
simulateAnimation,
timeScale,
} = this.props;
@ -31,6 +33,7 @@ class SummaryGraph extends PureComponent {
SummaryGraphPath(
{
animation,
simulateAnimation,
timeScale,
}
)

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

@ -10,11 +10,14 @@ const dom = require("devtools/client/shared/vendor/react-dom-factories");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const ComputedTimingPath = createFactory(require("./ComputedTimingPath"));
// Minimum opacity for semitransparent fill color for keyframes's easing graph.
const MIN_KEYFRAMES_EASING_OPACITY = 0.5;
class SummaryGraphPath extends PureComponent {
static get propTypes() {
return {
animation: PropTypes.object.isRequired,
simulateAnimation: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
};
}
@ -146,6 +149,7 @@ class SummaryGraphPath extends PureComponent {
const {
animation,
simulateAnimation,
timeScale,
} = this.props;
@ -153,6 +157,7 @@ class SummaryGraphPath extends PureComponent {
const startTime = timeScale.minStartTime;
const keyframesList =
this.getOffsetAndEasingOnlyKeyframes(animation.animatedPropertyMap);
const opacity = Math.max(1 / keyframesList.length, MIN_KEYFRAMES_EASING_OPACITY);
return dom.svg(
{
@ -166,6 +171,8 @@ class SummaryGraphPath extends PureComponent {
animation,
durationPerPixel,
keyframes,
opacity,
simulateAnimation,
totalDuration,
}
)

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

@ -0,0 +1,349 @@
/* 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("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
// Show max 10 iterations for infinite animations
// to give users a clue that the animation does repeat.
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
class TimingPath extends PureComponent {
/**
* Render a graph of given parameters and return as <path> element list.
*
* @param {Object} state
* State of animation.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
* @return {Array}
* list of <path> element.
*/
renderGraph(state, helper) {
// Starting time of main iteration.
let mainIterationStartTime = 0;
let iterationStart = state.iterationStart;
let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
const pathList = [];
// Append delay.
if (state.delay > 0) {
this.renderDelay(pathList, state, helper);
mainIterationStartTime = state.delay;
} else {
const negativeDelayCount = -state.delay / state.duration;
// Move to forward the starting point for negative delay.
iterationStart += negativeDelayCount;
// Consume iteration count by negative delay.
if (iterationCount !== Infinity) {
iterationCount -= negativeDelayCount;
}
}
// Append 1st section of iterations,
// This section is only useful in cases where iterationStart has decimals.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
const firstSectionCount = iterationStart % 1 === 0
? 0
: Math.min(iterationCount, 1) - iterationStart % 1;
if (firstSectionCount) {
this.renderFirstIteration(pathList, state,
mainIterationStartTime, firstSectionCount, helper);
}
if (iterationCount === Infinity) {
// If the animation repeats infinitely,
// we fill the remaining area with iteration paths.
this.renderInfinity(pathList, state,
mainIterationStartTime, firstSectionCount, helper);
} else {
// Otherwise, we show remaining iterations, endDelay and fill.
// Append forwards fill-mode.
if (state.fill === "both" || state.fill === "forwards") {
this.renderForwardsFill(pathList, state,
mainIterationStartTime, iterationCount, helper);
}
// Append middle section of iterations.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
const middleSectionCount = Math.floor(iterationCount - firstSectionCount);
this.renderMiddleIterations(pathList, state, mainIterationStartTime,
firstSectionCount, middleSectionCount, helper);
// Append last section of iterations, if there is remaining iteration.
// e.g.
// if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
const lastSectionCount = iterationCount - middleSectionCount - firstSectionCount;
if (lastSectionCount) {
this.renderLastIteration(pathList, state, mainIterationStartTime,
firstSectionCount, middleSectionCount,
lastSectionCount, helper);
}
// Append endDelay.
if (state.endDelay > 0) {
this.renderEndDelay(pathList, state,
mainIterationStartTime, iterationCount, helper);
}
}
return pathList;
}
/**
* Render 'delay' part in animation and add a <path> element to given pathList.
*
* @param {Array} pathList
* Add rendered <path> element to this array.
* @param {Object} state
* State of animation.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderDelay(pathList, state, helper) {
const startSegment = helper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
const segments = [startSegment, endSegment];
pathList.push(
dom.path(
{
className: "animation-delay-path",
d: helper.toPathString(segments),
}
)
);
}
/**
* Render 1st section of iterations and add a <path> element to given pathList.
* This section is only useful in cases where iterationStart has decimals.
*
* @param {Array} pathList
* Add rendered <path> element to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Start time of main iteration.
* @param {Number} firstSectionCount
* Iteration count of first section.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderFirstIteration(pathList, state, mainIterationStartTime,
firstSectionCount, helper) {
const startTime = mainIterationStartTime;
const endTime = startTime + firstSectionCount * state.duration;
const segments = helper.createPathSegments(startTime, endTime);
pathList.push(
dom.path(
{
className: "animation-iteration-path",
d: helper.toPathString(segments),
}
)
);
}
/**
* Render middle iterations and add <path> elements to given pathList.
*
* @param {Array} pathList
* Add rendered <path> elements to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Starting time of main iteration.
* @param {Number} firstSectionCount
* Iteration count of first section.
* @param {Number} middleSectionCount
* Iteration count of middle section.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderMiddleIterations(pathList, state, mainIterationStartTime,
firstSectionCount, middleSectionCount, helper) {
const offset = mainIterationStartTime + firstSectionCount * state.duration;
for (let i = 0; i < middleSectionCount; i++) {
// Get the path segments of each iteration.
const startTime = offset + i * state.duration;
const endTime = startTime + state.duration;
const segments = helper.createPathSegments(startTime, endTime);
pathList.push(
dom.path(
{
className: "animation-iteration-path",
d: helper.toPathString(segments),
}
)
);
}
}
/**
* Render last section of iterations and add a <path> element to given pathList.
* This section is only useful in cases where iterationStart has decimals.
*
* @param {Array} pathList
* Add rendered <path> elements to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Starting time of main iteration.
* @param {Number} firstSectionCount
* Iteration count of first section.
* @param {Number} middleSectionCount
* Iteration count of middle section.
* @param {Number} lastSectionCount
* Iteration count of last section.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderLastIteration(pathList, state, mainIterationStartTime, firstSectionCount,
middleSectionCount, lastSectionCount, helper) {
const startTime = mainIterationStartTime
+ (firstSectionCount + middleSectionCount) * state.duration;
const endTime = startTime + lastSectionCount * state.duration;
const segments = helper.createPathSegments(startTime, endTime);
pathList.push(
dom.path(
{
className: "animation-iteration-path",
d: helper.toPathString(segments),
}
)
);
}
/**
* Render infinity iterations and add <path> elements to given pathList.
*
* @param {Array} pathList
* Add rendered <path> elements to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Starting time of main iteration.
* @param {Number} firstSectionCount
* Iteration count of first section.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderInfinity(pathList, state, mainIterationStartTime, firstSectionCount, helper) {
// Calculate the number of iterations to display,
// with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
let uncappedInfinityIterationCount =
(helper.totalDuration - firstSectionCount * state.duration) / state.duration;
// If there is a small floating point error resulting in, e.g. 1.0000001
// ceil will give us 2 so round first.
uncappedInfinityIterationCount =
parseFloat(uncappedInfinityIterationCount.toPrecision(6));
const infinityIterationCount = Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
Math.ceil(uncappedInfinityIterationCount));
// Append first full iteration path.
const firstStartTime =
mainIterationStartTime + firstSectionCount * state.duration;
const firstEndTime = firstStartTime + state.duration;
const firstSegments = helper.createPathSegments(firstStartTime, firstEndTime);
pathList.push(
dom.path(
{
className: "animation-iteration-path",
d: helper.toPathString(firstSegments),
}
)
);
// Append other iterations. We can copy first segments.
const isAlternate = state.direction.match(/alternate/);
for (let i = 1; i < infinityIterationCount; i++) {
const startTime = firstStartTime + i * state.duration;
let segments;
if (isAlternate && i % 2) {
// Copy as reverse.
segments = firstSegments.map(segment => {
return { x: firstEndTime - segment.x + startTime, y: segment.y };
});
} else {
// Copy as is.
segments = firstSegments.map(segment => {
return { x: segment.x - firstStartTime + startTime, y: segment.y };
});
}
pathList.push(
dom.path(
{
className: "animation-iteration-path infinity",
d: helper.toPathString(segments),
}
)
);
}
}
/**
* Render 'endDelay' part in animation and add a <path> element to given pathList.
*
* @param {Array} pathList
* Add rendered <path> element to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Starting time of main iteration.
* @param {Number} iterationCount
* Iteration count of whole animation.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderEndDelay(pathList, state, mainIterationStartTime, iterationCount, helper) {
const startTime = mainIterationStartTime + iterationCount * state.duration;
const startSegment = helper.getSegment(startTime);
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
pathList.push(
dom.path(
{
className: "animation-enddelay-path",
d: helper.toPathString([startSegment, endSegment]),
}
)
);
}
/**
* Render 'fill' for forwards part in animation and
* add a <path> element to given pathList.
*
* @param {Array} pathList
* Add rendered <path> element to this array.
* @param {Object} state
* State of animation.
* @param {Number} mainIterationStartTime
* Starting time of main iteration.
* @param {Number} iterationCount
* Iteration count of whole animation.
* @param {SummaryGraphHelper} helper
* Instance of SummaryGraphHelper.
*/
renderForwardsFill(pathList, state, mainIterationStartTime, iterationCount, helper) {
const startTime = mainIterationStartTime + iterationCount * state.duration
+ (state.endDelay > 0 ? state.endDelay : 0);
const startSegment = helper.getSegment(startTime);
const endSegment = { x: helper.totalDuration, y: startSegment.y };
pathList.push(
dom.path(
{
className: "animation-fill-forwards-path",
d: helper.toPathString([startSegment, endSegment]),
}
)
);
}
}
module.exports = TimingPath;

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

@ -5,5 +5,6 @@
DevToolsModules(
'ComputedTimingPath.js',
'SummaryGraph.js',
'SummaryGraphPath.js'
'SummaryGraphPath.js',
'TimingPath.js'
)

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

@ -0,0 +1,239 @@
/* 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";
// 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;
// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
// 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;
/**
* The helper class for creating summary graph.
*/
class SummaryGraphHelper {
/**
* Constructor.
*
* @param {Object} state
* State of animation.
* @param {Array} keyframes
* Array of keyframe.
* @param {Number} totalDuration
* Total displayable duration.
* @param {Number} minSegmentDuration
* Minimum segment duration.
* @param {Function} getValueFunc
* Which returns graph value of given time.
* The function should return a number value between 0 - 1.
* e.g. time => { return 1.0 };
* @param {Function} toPathStringFunc
* Which returns a path string for 'd' attribute for <path> from given segments.
*/
constructor(state, keyframes, totalDuration, minSegmentDuration,
getValueFunc, toPathStringFunc) {
this.totalDuration = totalDuration;
this.minSegmentDuration = minSegmentDuration;
this.minProgressThreshold = getPreferredProgressThreshold(state, keyframes);
this.durationResolution = getPreferredDurationResolution(keyframes);
this.getValue = getValueFunc;
this.toPathString = toPathStringFunc;
this.getSegment = this.getSegment.bind(this);
}
/**
* Create the path segments from given parameters.
*
* @param {Number} startTime
* Starting time of animation.
* @param {Number} endTime
* Ending time of animation.
* @return {Array}
* Array of path segment.
* e.g.[{x: {Number} time, y: {Number} progress}, ...]
*/
createPathSegments(startTime, endTime) {
return createPathSegments(startTime, endTime,
this.minSegmentDuration, this.minProgressThreshold,
this.durationResolution, this.getSegment);
}
/**
* Return a coordinate as a graph segment at given time.
*
* @param {Number} time
* @return {Object}
* { x: Number, y: Number }
*/
getSegment(time) {
const value = this.getValue(time);
return { x: time, y: value };
}
}
/**
* 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 {Number} resolution
* Duration resolution for first time.
* @param {Function} getSegment
* A function that calculate the graph segment.
* @return {Array}
* Array of path segment.
* e.g.[{x: {Number} time, y: {Number} progress}, ...]
*/
function createPathSegments(startTime, endTime, minSegmentDuration,
minProgressThreshold, resolution, getSegment) {
// If the duration is too short, early return.
if (endTime - startTime < minSegmentDuration) {
return [getSegment(startTime), getSegment(endTime)];
}
// Otherwise, start creating segments.
let pathSegments = [];
// Append the segment for the startTime position.
const startTimeSegment = 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) / resolution;
for (let index = 1; index <= resolution; index++) {
// Create a segment for this interval.
const currentSegment = 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).
const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME;
const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME;
const segments =
createPathSegments(nextStartTime, nextEndTime, minSegmentDuration,
minProgressThreshold, DURATION_RESOLUTION, getSegment);
pathSegments = pathSegments.concat(segments);
}
pathSegments.push(currentSegment);
previousSegment = currentSegment;
}
return pathSegments;
}
/**
* Return preferred duration resolution.
* This corresponds to narrow interval keyframe offset.
*
* @param {Array} keyframes
* Array of keyframe.
* @return {Number}
* Preferred duration resolution.
*/
function getPreferredDurationResolution(keyframes) {
if (!keyframes) {
return DURATION_RESOLUTION;
}
let durationResolution = DURATION_RESOLUTION;
let previousOffset = 0;
for (let keyframe of keyframes) {
if (previousOffset && previousOffset != keyframe.offset) {
const interval = keyframe.offset - previousOffset;
durationResolution = Math.max(durationResolution, Math.ceil(1 / interval));
}
previousOffset = keyframe.offset;
}
return durationResolution;
}
/**
* Return preferred progress threshold to render summary graph.
*
* @param {Object} state
* State of animation.
* @param {Array} keyframes
* Array of keyframe.
* @return {float}
* Preferred threshold.
*/
function getPreferredProgressThreshold(state, keyframes) {
let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
let stepsOrFrames;
if ((stepsOrFrames = getStepsOrFramesCount(state.easing))) {
threshold = Math.min(threshold, (1 / (stepsOrFrames + 1)));
}
if (!keyframes) {
return threshold;
}
for (let i = 0; i < keyframes.length - 1; i++) {
const keyframe = keyframes[i];
if (!keyframe.easing) {
continue;
}
if ((stepsOrFrames = getStepsOrFramesCount(keyframe.easing))) {
const nextKeyframe = keyframes[i + 1];
threshold =
Math.min(threshold,
1 / (stepsOrFrames + 1) * (nextKeyframe.offset - keyframe.offset));
}
}
return threshold;
}
function getStepsOrFramesCount(easing) {
const stepsOrFramesFunction = easing.match(/(steps|frames)\((\d+)/);
return stepsOrFramesFunction ? parseInt(stepsOrFramesFunction[2], 10) : 0;
}
/**
* Return path string for 'd' attribute for <path> from given segments.
*
* @param {Array} segments
* e.g. [{ x: 100, y: 0 }, { x: 200, y: 1 }]
* @return {String}
* Path string.
* e.g. "L100,0 L200,1"
*/
function toPathString(segments) {
let pathString = "";
segments.forEach(segment => {
pathString += `L${ segment.x },${ segment.y } `;
});
return pathString;
}
exports.SummaryGraphHelper = SummaryGraphHelper;
exports.toPathString = toPathString;

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

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'graph-helper.js',
'l10n.js',
'timescale.js',
'utils.js',

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

@ -807,7 +807,7 @@ Inspector.prototype = {
panel: () => {
const AnimationInspector =
this.browserRequire("devtools/client/inspector/animation/animation");
this.animationinspector = new AnimationInspector(this);
this.animationinspector = new AnimationInspector(this, this.panelWin);
return this.animationinspector.provider;
}
},

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

@ -72,6 +72,7 @@
/* Summary Graph */
.animation-summary-graph {
height: 100%;
padding-top: 5px;
width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
}
@ -80,6 +81,16 @@
width: 100%;
}
.animation-computed-timing-path path {
fill: lime;
vector-effect: non-scaling-stroke;
transform: scale(1, -1);
}
.animation-computed-timing-path path.infinity:nth-child(n+2) {
opacity: 0.3;
}
/* No Animation Panel */
.animation-error-message {
overflow: auto;