From 054392da3b7e680a6f75663b9f93ce5e0310a02f Mon Sep 17 00:00:00 2001 From: Daisuke Akatsuka Date: Mon, 25 Sep 2017 08:44:05 +0900 Subject: [PATCH] Bug 1383974 - Part 1: Display easing in keyframes. r=pbro MozReview-Commit-ID: 8pIUMAurfS3 --HG-- extra : rebase_source : d7acb339c012bd67cb1a68975e6934afa995eb5c --- .../components/animation-time-block.js | 21 ++- .../components/keyframes.js | 133 ++++++++++++++++-- .../client/animationinspector/graph-helper.js | 104 +++++++++----- devtools/client/themes/animationinspector.css | 18 +++ 4 files changed, 220 insertions(+), 56 deletions(-) diff --git a/devtools/client/animationinspector/components/animation-time-block.js b/devtools/client/animationinspector/components/animation-time-block.js index 0dbd7340137a..8e4f07e7a5e7 100644 --- a/devtools/client/animationinspector/components/animation-time-block.js +++ b/devtools/client/animationinspector/components/animation-time-block.js @@ -462,7 +462,7 @@ function renderGraph(parentEl, state, totalDisplayedDuration, className, graphHe function renderDelay(parentEl, state, graphHelper) { const startSegment = graphHelper.getSegment(0); const endSegment = { x: state.delay, y: startSegment.y }; - graphHelper.appendPathElement(parentEl, [startSegment, endSegment], "delay-path"); + graphHelper.appendShapePath(parentEl, [startSegment, endSegment], "delay-path"); } /** @@ -478,7 +478,7 @@ function renderFirstIteration(parentEl, state, mainIterationStartTime, const startTime = mainIterationStartTime; const endTime = startTime + firstSectionCount * state.duration; const segments = graphHelper.createPathSegments(startTime, endTime); - graphHelper.appendPathElement(parentEl, segments, "iteration-path"); + graphHelper.appendShapePath(parentEl, segments, "iteration-path"); } /** @@ -499,7 +499,7 @@ function renderMiddleIterations(parentEl, state, mainIterationStartTime, const startTime = offset + i * state.duration; const endTime = startTime + state.duration; const segments = graphHelper.createPathSegments(startTime, endTime); - graphHelper.appendPathElement(parentEl, segments, "iteration-path"); + graphHelper.appendShapePath(parentEl, segments, "iteration-path"); } } @@ -520,7 +520,7 @@ function renderLastIteration(parentEl, state, mainIterationStartTime, (firstSectionCount + middleSectionCount) * state.duration; const endTime = startTime + lastSectionCount * state.duration; const segments = graphHelper.createPathSegments(startTime, endTime); - graphHelper.appendPathElement(parentEl, segments, "iteration-path"); + graphHelper.appendShapePath(parentEl, segments, "iteration-path"); } /** @@ -552,7 +552,7 @@ function renderInfinity(parentEl, state, mainIterationStartTime, const firstEndTime = firstStartTime + state.duration; const firstSegments = graphHelper.createPathSegments(firstStartTime, firstEndTime); - graphHelper.appendPathElement(parentEl, firstSegments, "iteration-path infinity"); + graphHelper.appendShapePath(parentEl, firstSegments, "iteration-path infinity"); // Append other iterations. We can copy first segments. const isAlternate = state.direction.match(/alternate/); @@ -570,7 +570,7 @@ function renderInfinity(parentEl, state, mainIterationStartTime, return { x: segment.x - firstStartTime + startTime, y: segment.y }; }); } - graphHelper.appendPathElement(parentEl, segments, "iteration-path infinity copied"); + graphHelper.appendShapePath(parentEl, segments, "iteration-path infinity copied"); } } @@ -587,7 +587,7 @@ function renderEndDelay(parentEl, state, const startTime = mainIterationStartTime + iterationCount * state.duration; const startSegment = graphHelper.getSegment(startTime); const endSegment = { x: startTime + state.endDelay, y: startSegment.y }; - graphHelper.appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path"); + graphHelper.appendShapePath(parentEl, [startSegment, endSegment], "enddelay-path"); } /** @@ -605,8 +605,7 @@ function renderForwardsFill(parentEl, state, mainIterationStartTime, (state.endDelay > 0 ? state.endDelay : 0); const startSegment = graphHelper.getSegment(startTime); const endSegment = { x: totalDuration, y: startSegment.y }; - graphHelper.appendPathElement(parentEl, [startSegment, endSegment], - "fill-forwards-path"); + graphHelper.appendShapePath(parentEl, [startSegment, endSegment], "fill-forwards-path"); } /** @@ -620,7 +619,7 @@ function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) { const endTime = 0; const segments = graphHelper.createPathSegments(startTime, endTime); - graphHelper.appendPathElement(parentEl, segments, "delay-path negative"); + graphHelper.appendShapePath(parentEl, segments, "delay-path negative"); } /** @@ -633,7 +632,7 @@ function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) { const endTime = state.delay + state.iterationCount * state.duration; const startTime = endTime + state.endDelay; const segments = graphHelper.createPathSegments(startTime, endTime); - graphHelper.appendPathElement(parentEl, segments, "enddelay-path negative"); + graphHelper.appendShapePath(parentEl, segments, "enddelay-path negative"); } /** diff --git a/devtools/client/animationinspector/components/keyframes.js b/devtools/client/animationinspector/components/keyframes.js index 05aff08e9548..fb766f4b2e18 100644 --- a/devtools/client/animationinspector/components/keyframes.js +++ b/devtools/client/animationinspector/components/keyframes.js @@ -9,7 +9,7 @@ const {createNode, createSVGNode} = require("devtools/client/animationinspector/utils"); const {ProgressGraphHelper, getPreferredKeyframesProgressThreshold} = - require("devtools/client/animationinspector/graph-helper.js"); + require("devtools/client/animationinspector/graph-helper.js"); // Counter for linearGradient ID. let LINEAR_GRADIENT_ID_COUNTER = 0; @@ -55,22 +55,14 @@ Keyframes.prototype = { // 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 win = this.containerEl.ownerGlobal; const graphHelper = - new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView, - propertyName, animationType, keyframes, totalDuration); + new ProgressGraphHelper(win, propertyName, animationType, keyframes, totalDuration); renderPropertyGraph(graphEl, totalDuration, minSegmentDuration, getPreferredKeyframesProgressThreshold(keyframes), graphHelper); @@ -78,6 +70,19 @@ Keyframes.prototype = { // Destroy ProgressGraphHelper resources. graphHelper.destroy(); + // Set viewBox which includes invisible stroke width. + // At first, calculate invisible stroke width from maximum width. + // The reason why divide by 2 is that half of stroke width will be invisible + // if we use 0 or 1 for y axis. + const maxStrokeWidth = + win.getComputedStyle(graphEl.querySelector(".keyframes svg .hint")).strokeWidth; + const invisibleStrokeWidthInViewBox = + maxStrokeWidth / 2 / this.containerEl.clientHeight; + graphEl.setAttribute("viewBox", + `0 -${ 1 + invisibleStrokeWidthInViewBox } + ${ totalDuration } + ${ 1 + invisibleStrokeWidthInViewBox * 2 }`); + // Append elements to display keyframe values. this.keyframesEl.classList.add(animation.state.type); for (let frame of this.keyframes) { @@ -110,7 +115,8 @@ function renderPropertyGraph(parentEl, duration, minSegmentDuration, const graphType = graphHelper.getGraphType(); if (graphType !== "color") { - graphHelper.appendPathElement(parentEl, segments, graphType); + graphHelper.appendShapePath(parentEl, segments, graphType); + renderEasingHint(parentEl, segments, graphHelper); return; } @@ -118,7 +124,7 @@ function renderPropertyGraph(parentEl, duration, minSegmentDuration, segments.forEach(segment => { segment.y = 1; }); - const path = graphHelper.appendPathElement(parentEl, segments, graphType); + const path = graphHelper.appendShapePath(parentEl, segments, graphType); const defEl = createSVGNode({ parent: parentEl, nodeType: "def" @@ -142,4 +148,105 @@ function renderPropertyGraph(parentEl, duration, minSegmentDuration, }); }); path.style.fill = `url(#${ id })`; + + renderEasingHintForColor(parentEl, graphHelper); +} + +/** + * Renders the easing hint. + * This method renders an emphasized path over the easing path for a keyframe. + * It appears when hovering over the easing. + * It also renders a tooltip that appears when hovering. + * @param {Element} parentEl - Parent element of this appended path element. + * @param {Array} path segments - [{x: {Number} time, y: {Number} progress}, ...] + * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHelper. + */ +function renderEasingHint(parentEl, segments, helper) { + const keyframes = helper.getKeyframes(); + const duration = helper.getDuration(); + + // Split segments for each keyframe. + for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const startTime = startKeyframe.offset * duration; + const endKeyframe = keyframes[i + 1]; + const endTime = endKeyframe.offset * duration; + + const keyframeSegments = []; + for (; indexOfSegments < segments.length; indexOfSegments++) { + const segment = segments[indexOfSegments]; + if (segment.x < startTime) { + // If previous easings were linear, we need to increment the indexOfSegments. + continue; + } + if (segment.x > endTime) { + indexOfSegments -= 1; + break; + } + keyframeSegments.push(segment); + } + + // If keyframeSegments does not have segment which is at startTime, + // get and set the segment. + if (keyframeSegments[0].x !== startTime) { + keyframeSegments.unshift(helper.getSegment(startTime)); + } + // Also, endTime. + if (keyframeSegments[keyframeSegments.length - 1].x !== endTime) { + keyframeSegments.push(helper.getSegment(endTime)); + } + + // Append easing hint as text and emphasis path. + const gEl = createSVGNode({ + parent: parentEl, + nodeType: "g" + }); + createSVGNode({ + parent: gEl, + nodeType: "title", + textContent: startKeyframe.easing + }); + helper.appendLinePath(gEl, keyframeSegments, `${helper.getGraphType()} hint`); + } +} + +/** + * Render easing hint for properties that are represented by color. + * This method render as text only. + * @param {Element} parentEl - Parent element of this appended path element. + * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper. + */ +function renderEasingHintForColor(parentEl, helper) { + const keyframes = helper.getKeyframes(); + const duration = helper.getDuration(); + + // Split segments for each keyframe. + for (let i = 0; i < keyframes.length - 1; i++) { + const startKeyframe = keyframes[i]; + const startTime = startKeyframe.offset * duration; + const endKeyframe = keyframes[i + 1]; + const endTime = endKeyframe.offset * duration; + + // Append easing hint. + const gEl = createSVGNode({ + parent: parentEl, + nodeType: "g" + }); + createSVGNode({ + parent: gEl, + nodeType: "title", + textContent: startKeyframe.easing + }); + createSVGNode({ + parent: gEl, + nodeType: "rect", + attributes: { + x: startTime, + y: -1, + width: endTime - startTime, + height: 1, + class: "hint", + } + }); + } } diff --git a/devtools/client/animationinspector/graph-helper.js b/devtools/client/animationinspector/graph-helper.js index b0ede10437f6..84af225a6e73 100644 --- a/devtools/client/animationinspector/graph-helper.js +++ b/devtools/client/animationinspector/graph-helper.js @@ -85,6 +85,22 @@ ProgressGraphHelper.prototype = { this.win = null; }, + /** + * Return animation duration. + * @return {Number} duration + */ + getDuration: function () { + return this.animation.effect.timing.duration; + }, + + /** + * Return animation's keyframe. + * @return {Object} keyframe + */ + getKeyframes: function () { + return this.keyframes; + }, + /** * Return graph type. * @return {String} if property is 'opacity' or 'transform', return that value. @@ -248,14 +264,27 @@ ProgressGraphHelper.prototype = { }, /** - * Append path element. + * Append path element as shape. Also, this method appends two segment + * that are {start x, 0} and {end x, 0} to make shape. * @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. */ - appendPathElement: function (parentEl, pathSegments, cls) { - return appendPathElement(parentEl, pathSegments, cls); + appendShapePath: function (parentEl, pathSegments, cls) { + return appendShapePath(parentEl, pathSegments, cls); + }, + + /** + * Append path element as line. + * @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. + */ + appendLinePath: function (parentEl, pathSegments, cls) { + const isClosePathNeeded = false; + return appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded); }, }; @@ -271,7 +300,7 @@ exports.ProgressGraphHelper = ProgressGraphHelper; * setFillMode: * Animation fill-mode (e.g. "none", "backwards", "forwards" or "both") * setClosePathNeeded: - * If true, appendPathElement make the last segment of element to + * If true, appendShapePath make the last segment of element to * "close" segment("Z"). * Therefore, if don't need under-line of graph, please set false. * setOriginalBehavior: @@ -360,7 +389,7 @@ SummaryGraphHelper.prototype = { }, /** - * Set true if need to close path in appendPathElement. + * Set true if need to close path in appendShapePath. * @param {bool} isClosePathNeeded - true: close, false: open. */ setClosePathNeeded: function (isClosePathNeeded) { @@ -411,14 +440,15 @@ SummaryGraphHelper.prototype = { }, /** - * Append path element. + * Append path element as shape. Also, this method appends two segment + * that are {start x, 0} and {end x, 0} to make shape. * @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. */ - appendPathElement: function (parentEl, pathSegments, cls) { - return appendPathElement(parentEl, pathSegments, cls, this.isClosePathNeeded); + appendShapePath: function (parentEl, pathSegments, cls) { + return appendShapePath(parentEl, pathSegments, cls, this.isClosePathNeeded); }, /** @@ -498,40 +528,50 @@ function createPathSegments(startTime, endTime, minSegmentDuration, } /** - * Append path element. + * Append path element as shape. Also, this method appends two segment + * that are {start x, 0} and {end x, 0} to make shape. + * But does not affect given pathSegments. * @param {Element} parentEl - Parent element of this appended path element. * @param {Array} pathSegments - Path segments. Please see createPathSegments. * @param {String} cls - Class name. * @param {bool} isClosePathNeeded - Set true if need to close the path. (default true) * @return {Element} path element. */ -function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded = true) { +function appendShapePath(parentEl, pathSegments, cls, isClosePathNeeded = true) { + const segments = [ + { x: pathSegments[0].x, y: 0 }, + ...pathSegments, + { x: pathSegments[pathSegments.length - 1].x, y: 0 } + ]; + return appendPathElement(parentEl, segments, cls, isClosePathNeeded); +} + +/** + * 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. + * @param {bool} isClosePathNeeded - Set true if need to close the path. + * @return {Element} path element. + */ +function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded) { // 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]; - let createPathFunction; - if (pathSegment.easing.startsWith("steps")) { - createPathFunction = createStepsPathString; - } else if (pathSegment.easing.startsWith("frames")) { - createPathFunction = createFramesPathString; + let currentSegment = pathSegments[0]; + let path = `M${ currentSegment.x },${ currentSegment.y }`; + for (let i = 1; i < pathSegments.length; i++) { + const currentEasing = currentSegment.easing ? currentSegment.easing : "linear"; + const nextSegment = pathSegments[i]; + if (currentEasing === "linear") { + path += createLinePathString(nextSegment); + } else if (currentEasing.startsWith("steps")) { + path += createStepsPathString(currentSegment, nextSegment); + } else if (currentEasing.startsWith("frames")) { + path += createFramesPathString(currentSegment, nextSegment); } else { - createPathFunction = createCubicBezierPathString; + path += createCubicBezierPathString(currentSegment, nextSegment); } - path += createPathFunction(pathSegment, nextPathSegment); + currentSegment = nextSegment; } - path += ` L${ pathSegments[pathSegments.length - 1].x },0`; if (isClosePathNeeded) { path += " Z"; } diff --git a/devtools/client/themes/animationinspector.css b/devtools/client/themes/animationinspector.css index d81b798b137b..fc39e9bda49f 100644 --- a/devtools/client/themes/animationinspector.css +++ b/devtools/client/themes/animationinspector.css @@ -697,6 +697,24 @@ body { height: 100%; } +.keyframes svg .hint { + stroke-opacity: 0; + stroke-linecap: round; + stroke-width: 5; +} + +.keyframes svg path.hint { + fill: none; +} + +.keyframes svg path.hint:hover { + stroke-opacity: 1; +} + +.keyframes svg rect.hint { + fill-opacity: .1; +} + .animation-detail { position: relative; width: 100%;