зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
603e851dab
Коммит
ad1112c3ca
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче