summaryrefslogtreecommitdiffstats
path: root/devtools/client/animationinspector/components/animation-time-block.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/animationinspector/components/animation-time-block.js')
-rw-r--r--devtools/client/animationinspector/components/animation-time-block.js719
1 files changed, 719 insertions, 0 deletions
diff --git a/devtools/client/animationinspector/components/animation-time-block.js b/devtools/client/animationinspector/components/animation-time-block.js
new file mode 100644
index 000000000..51392da8f
--- /dev/null
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -0,0 +1,719 @@
+/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
+const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
+// 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
+// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const MIN_PROGRESS_THRESHOLD = 0.1;
+// Show max 10 iterations for infinite animations
+// to give users a clue that the animation does repeat.
+const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+
+/**
+ * UI component responsible for displaying a single animation timeline, which
+ * basically looks like a rectangle that shows the delay and iterations.
+ */
+function AnimationTimeBlock() {
+ EventEmitter.decorate(this);
+ this.onClick = this.onClick.bind(this);
+}
+
+exports.AnimationTimeBlock = AnimationTimeBlock;
+
+AnimationTimeBlock.prototype = {
+ init: function (containerEl) {
+ this.containerEl = containerEl;
+ this.containerEl.addEventListener("click", this.onClick);
+ },
+
+ destroy: function () {
+ this.containerEl.removeEventListener("click", this.onClick);
+ this.unrender();
+ this.containerEl = null;
+ this.animation = null;
+ },
+
+ unrender: function () {
+ while (this.containerEl.firstChild) {
+ this.containerEl.firstChild.remove();
+ }
+ },
+
+ render: function (animation) {
+ this.unrender();
+
+ this.animation = animation;
+ let {state} = this.animation;
+
+ // Create a container element to hold the delay and iterations.
+ // It is positioned according to its delay (divided by the playbackrate),
+ // and its width is according to its duration (divided by the playbackrate).
+ const {x, delayX, delayW, endDelayX, endDelayW} =
+ TimeScale.getAnimationDimensions(animation);
+
+ // Animation summary graph element.
+ const summaryEl = createNode({
+ parent: this.containerEl,
+ namespace: "http://www.w3.org/2000/svg",
+ nodeType: "svg",
+ attributes: {
+ "class": "summary",
+ "preserveAspectRatio": "none",
+ "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
+ }
+ });
+
+ // Total displayed duration
+ const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();
+
+ // Calculate stroke height in viewBox to display stroke of path.
+ const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+
+ // Set viewBox
+ summaryEl.setAttribute("viewBox",
+ `${ state.delay < 0 ? state.delay : 0 }
+ -${ 1 + strokeHeightForViewBox }
+ ${ totalDisplayedDuration }
+ ${ 1 + strokeHeightForViewBox * 2 }`);
+
+ // Get a helper function that returns the path segment of timing-function.
+ const segmentHelper = getSegmentHelper(state, this.win);
+
+ // Minimum segment duration is the duration of one pixel.
+ const minSegmentDuration =
+ totalDisplayedDuration / this.containerEl.clientWidth;
+ // Minimum progress threshold.
+ let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
+ // If the easing is step function,
+ // minProgressThreshold should be changed by the steps.
+ const stepFunction = state.easing.match(/steps\((\d+)/);
+ if (stepFunction) {
+ minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
+ }
+
+ // Starting time of main iteration.
+ let mainIterationStartTime = 0;
+ let iterationStart = state.iterationStart;
+ let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
+
+ // Append delay.
+ if (state.delay > 0) {
+ renderDelay(summaryEl, state, segmentHelper);
+ 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) {
+ renderFirstIteration(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+
+ if (iterationCount === Infinity) {
+ // If the animation repeats infinitely,
+ // we fill the remaining area with iteration paths.
+ renderInfinity(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, totalDisplayedDuration,
+ minSegmentDuration, minProgressThreshold, segmentHelper);
+ } else {
+ // Otherwise, we show remaining iterations, endDelay and fill.
+
+ // Append forwards fill-mode.
+ if (state.fill === "both" || state.fill === "forwards") {
+ renderForwardsFill(summaryEl, state, mainIterationStartTime,
+ iterationCount, totalDisplayedDuration,
+ segmentHelper);
+ }
+
+ // Append middle section of iterations.
+ // e.g.
+ // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+ const middleSectionCount =
+ Math.floor(iterationCount - firstSectionCount);
+ renderMiddleIterations(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper);
+
+ // 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) {
+ renderLastIteration(summaryEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ lastSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+
+ // Append endDelay.
+ if (state.endDelay > 0) {
+ renderEndDelay(summaryEl, state,
+ mainIterationStartTime, iterationCount, segmentHelper);
+ }
+ }
+
+ // Append negative delay (which overlap the animation).
+ if (state.delay < 0) {
+ segmentHelper.animation.effect.timing.fill = "both";
+ segmentHelper.asOriginalBehavior = false;
+ renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ }
+ // Append negative endDelay (which overlap the animation).
+ if (state.iterationCount && state.endDelay < 0) {
+ if (segmentHelper.asOriginalBehavior) {
+ segmentHelper.animation.effect.timing.fill = "both";
+ segmentHelper.asOriginalBehavior = false;
+ }
+ renderNegativeEndDelayHiddenProgress(summaryEl, state,
+ minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper);
+ }
+
+ // The animation name is displayed over the animation.
+ createNode({
+ parent: createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "name",
+ "title": this.getTooltipText(state)
+ },
+ }),
+ textContent: state.name
+ });
+
+ // Delay.
+ if (state.delay) {
+ // Negative delays need to start at 0.
+ createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "delay"
+ + (state.delay < 0 ? " negative" : " positive")
+ + (state.fill === "both" ||
+ state.fill === "backwards" ? " fill" : ""),
+ "style": `left:${ delayX }%; width:${ delayW }%;`
+ }
+ });
+ }
+
+ // endDelay
+ if (state.iterationCount && state.endDelay) {
+ createNode({
+ parent: this.containerEl,
+ attributes: {
+ "class": "end-delay"
+ + (state.endDelay < 0 ? " negative" : " positive")
+ + (state.fill === "both" ||
+ state.fill === "forwards" ? " fill" : ""),
+ "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
+ }
+ });
+ }
+ },
+
+ getTooltipText: function (state) {
+ let getTime = time => L10N.getFormatStr("player.timeLabel",
+ L10N.numberWithDecimals(time / 1000, 2));
+
+ let text = "";
+
+ // Adding the name.
+ text += getFormattedAnimationTitle({state});
+ text += "\n";
+
+ // Adding the delay.
+ if (state.delay) {
+ text += L10N.getStr("player.animationDelayLabel") + " ";
+ text += getTime(state.delay);
+ text += "\n";
+ }
+
+ // Adding the duration.
+ text += L10N.getStr("player.animationDurationLabel") + " ";
+ text += getTime(state.duration);
+ text += "\n";
+
+ // Adding the endDelay.
+ if (state.endDelay) {
+ text += L10N.getStr("player.animationEndDelayLabel") + " ";
+ text += getTime(state.endDelay);
+ text += "\n";
+ }
+
+ // Adding the iteration count (the infinite symbol, or an integer).
+ if (state.iterationCount !== 1) {
+ text += L10N.getStr("player.animationIterationCountLabel") + " ";
+ text += state.iterationCount ||
+ L10N.getStr("player.infiniteIterationCountText");
+ text += "\n";
+ }
+
+ // Adding the iteration start.
+ if (state.iterationStart !== 0) {
+ let iterationStartTime = state.iterationStart * state.duration / 1000;
+ text += L10N.getFormatStr("player.animationIterationStartLabel",
+ state.iterationStart,
+ L10N.numberWithDecimals(iterationStartTime, 2));
+ text += "\n";
+ }
+
+ // Adding the easing.
+ if (state.easing) {
+ text += L10N.getStr("player.animationEasingLabel") + " ";
+ text += state.easing;
+ text += "\n";
+ }
+
+ // Adding the fill mode.
+ if (state.fill) {
+ text += L10N.getStr("player.animationFillLabel") + " ";
+ text += state.fill;
+ text += "\n";
+ }
+
+ // Adding the direction mode.
+ if (state.direction) {
+ text += L10N.getStr("player.animationDirectionLabel") + " ";
+ text += state.direction;
+ text += "\n";
+ }
+
+ // Adding the playback rate if it's different than 1.
+ if (state.playbackRate !== 1) {
+ text += L10N.getStr("player.animationRateLabel") + " ";
+ text += state.playbackRate;
+ text += "\n";
+ }
+
+ // Adding a note that the animation is running on the compositor thread if
+ // needed.
+ if (state.propertyState) {
+ if (state.propertyState
+ .every(propState => propState.runningOnCompositor)) {
+ text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
+ } else if (state.propertyState
+ .some(propState => propState.runningOnCompositor)) {
+ text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
+ }
+ } else if (state.isRunningOnCompositor) {
+ text += L10N.getStr("player.runningOnCompositorTooltip");
+ }
+
+ return text;
+ },
+
+ onClick: function (e) {
+ e.stopPropagation();
+ this.emit("selected", this.animation);
+ },
+
+ get win() {
+ return this.containerEl.ownerDocument.defaultView;
+ }
+};
+
+/**
+ * Get a formatted title for this animation. This will be either:
+ * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
+ * "some-name : Script Animation", or "Script Animation", depending
+ * if the server provides the type, what type it is and if the animation
+ * has a name
+ * @param {AnimationPlayerFront} animation
+ */
+function getFormattedAnimationTitle({state}) {
+ // Older servers don't send a type, and only know about
+ // CSSAnimations and CSSTransitions, so it's safe to use
+ // just the name.
+ if (!state.type) {
+ return state.name;
+ }
+
+ // Script-generated animations may not have a name.
+ if (state.type === "scriptanimation" && !state.name) {
+ return L10N.getStr("timeline.scriptanimation.unnamedLabel");
+ }
+
+ return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
+}
+
+/**
+ * Render delay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderDelay(parentEl, state, segmentHelper) {
+ const startSegment = segmentHelper.getSegment(0);
+ const endSegment = { x: state.delay, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+}
+
+/**
+ * Render first iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderFirstIteration(parentEl, state, mainIterationStartTime,
+ firstSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ const startTime = mainIterationStartTime;
+ const endTime = startTime + firstSectionCount * state.duration;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render middle iterations section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @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} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderMiddleIterations(parentEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper) {
+ 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 =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+ }
+}
+
+/**
+ * Render last iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @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 {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderLastIteration(parentEl, state, mainIterationStartTime,
+ firstSectionCount, middleSectionCount,
+ lastSectionCount, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ const startTime = mainIterationStartTime +
+ (firstSectionCount + middleSectionCount) * state.duration;
+ const endTime = startTime + lastSectionCount * state.duration;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render Infinity iterations.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderInfinity(parentEl, state, mainIterationStartTime,
+ firstSectionCount, totalDuration, minSegmentDuration,
+ minProgressThreshold, segmentHelper) {
+ // Calculate the number of iterations to display,
+ // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
+ let uncappedInfinityIterationCount =
+ (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 =
+ createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+
+ // 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 };
+ });
+ }
+ appendPathElement(parentEl, segments, "iteration-path infinity copied");
+ }
+}
+
+/**
+ * Render endDelay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderEndDelay(parentEl, state,
+ mainIterationStartTime, iterationCount, segmentHelper) {
+ const startTime = mainIterationStartTime + iterationCount * state.duration;
+ const startSegment = segmentHelper.getSegment(startTime);
+ const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+}
+
+/**
+ * Render forwards fill section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderForwardsFill(parentEl, state, mainIterationStartTime,
+ iterationCount, totalDuration, segmentHelper) {
+ const startTime = mainIterationStartTime + iterationCount * state.duration +
+ (state.endDelay > 0 ? state.endDelay : 0);
+ const startSegment = segmentHelper.getSegment(startTime);
+ const endSegment = { x: totalDuration, y: startSegment.y };
+ appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
+}
+
+/**
+ * Render hidden progress of negative delay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper) {
+ const startTime = state.delay;
+ const endTime = 0;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "delay-path negative");
+}
+
+/**
+ * Render hidden progress of negative endDelay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeEndDelayHiddenProgress(parentEl, state,
+ minSegmentDuration,
+ minProgressThreshold,
+ segmentHelper) {
+ const endTime = state.delay + state.iterationCount * state.duration;
+ const startTime = endTime + state.endDelay;
+ const segments =
+ createPathSegments(startTime, endTime, minSegmentDuration,
+ minProgressThreshold, segmentHelper);
+ appendPathElement(parentEl, segments, "enddelay-path negative");
+}
+
+/**
+ * Get a helper function which returns the segment coord from given time.
+ * @param {Object} state - animation state
+ * @param {Object} win - window object
+ * @return {Object} A segmentHelper object that has the following properties:
+ * - animation: The script animation used to get the progress
+ * - endTime: The end time of the animation
+ * - asOriginalBehavior: The spec is that the progress of animation is changed
+ * if the time of setCurrentTime is during the endDelay.
+ * Likewise, in case the time is less than 0.
+ * If this flag is true, we prevent the time
+ * to make the same animation behavior as the original.
+ * - getSegment: Helper function that, given a time,
+ * will calculate the progress through the dummy animation.
+ */
+function getSegmentHelper(state, win) {
+ // Create a dummy Animation timing data as the
+ // state object we're being passed in.
+ const timing = Object.assign({}, state, {
+ iterations: state.iterationCount ? state.iterationCount : Infinity
+ });
+
+ // Create a dummy Animation with the given timing.
+ const dummyAnimation =
+ new win.Animation(new win.KeyframeEffect(null, null, timing), null);
+
+ // Returns segment helper object.
+ return {
+ animation: dummyAnimation,
+ endTime: dummyAnimation.effect.getComputedTiming().endTime,
+ asOriginalBehavior: true,
+ getSegment: function (time) {
+ if (this.asOriginalBehavior) {
+ // If the given time is less than 0, returned progress is 0.
+ if (time < 0) {
+ return { x: time, y: 0 };
+ }
+ // Avoid to apply over endTime.
+ this.animation.currentTime = time < this.endTime ? time : this.endTime;
+ } else {
+ this.animation.currentTime = time;
+ }
+ const progress = this.animation.effect.getComputedTiming().progress;
+ return { x: time, y: Math.max(progress, 0) };
+ }
+ };
+}
+
+/**
+ * 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 - The object of getSegmentHelper.
+ * @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 1ms).
+ pathSegments = pathSegments.concat(
+ createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
+ minSegmentDuration, minProgressThreshold,
+ segmentHelper));
+ }
+
+ pathSegments.push(currentSegment);
+ previousSegment = currentSegment;
+ }
+
+ return pathSegments;
+}
+
+/**
+ * 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`;
+ pathSegments.forEach(pathSegment => {
+ path += ` L${ pathSegment.x },${ pathSegment.y }`;
+ });
+ path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+ // Append and return the path element.
+ return createNode({
+ parent: parentEl,
+ namespace: SVG_NS,
+ nodeType: "path",
+ attributes: {
+ "d": path,
+ "class": cls,
+ "vector-effect": "non-scaling-stroke",
+ "transform": "scale(1, -1)"
+ }
+ });
+}