init: function(containerEl) { this.win = containerEl.ownerDocument.defaultView; this.rootWrapperEl = createNode({ parent: containerEl, attributes: { "class": "animation-timeline" } }); this.scrubberEl = createNode({ parent: this.rootWrapperEl, attributes: { "class": "scrubber" } }); this.timeHeaderEl = createNode({ parent: this.rootWrapperEl, attributes: { "class": "time-header" } }); this.timeHeaderEl.addEventListener("mousedown", this.onTimeHeaderMouseDown); this.animationsEl = createNode({ parent: this.rootWrapperEl, nodeType: "ul", attributes: { "class": "animations" } }); },
render: function (animations) { let allRates = this.getAnimationsRates(animations); let hasOneRate = allRates.length === 1; this.selectEl.innerHTML = ""; if (!hasOneRate) { // When the animations displayed have mixed playback rates, we can't // select any of the predefined ones, instead, insert an empty rate. createNode({ parent: this.selectEl, nodeType: "option", attributes: {value: "", selector: "true"}, textContent: "-" }); } for (let rate of this.getAllRates(animations)) { let option = createNode({ parent: this.selectEl, nodeType: "option", attributes: {value: rate}, textContent: L10N.getFormatStr("player.playbackRateLabel", rate) }); // If there's only one rate and this is the option for it, select it. if (hasOneRate && rate === allRates[0]) { option.setAttribute("selected", "true"); } } },
drawTimeBlock: function({state}, el) { let width = el.offsetWidth; // 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). let start = state.previousStartTime || 0; let duration = state.duration; let rate = state.playbackRate; let count = state.iterationCount; let delay = state.delay || 0; let x = TimeScale.startTimeToDistance(start + (delay / rate), width); let w = TimeScale.durationToDistance(duration / rate, width); let iterations = createNode({ parent: el, attributes: { "class": state.type + " iterations" + (count ? "" : " infinite"), // Individual iterations are represented by setting the size of the // repeating linear-gradient. "style": `left:${x}px; width:${w * (count || 1)}px; background-size:${Math.max(w, 2)}px 100%;` } }); // The animation name is displayed over the iterations. // Note that in case of negative delay, we push the name towards the right // so the delay can be shown. createNode({ parent: iterations, attributes: { "class": "name", "title": this.getAnimationTooltipText(state), "style": delay < 0 ? "margin-left:" + TimeScale.durationToDistance(Math.abs(delay), width) + "px" : "" }, textContent: state.name }); // Delay. if (delay) { // Negative delays need to start at 0. let x = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate, width); let w = TimeScale.durationToDistance(Math.abs(delay) / rate, width); createNode({ parent: iterations, attributes: { "class": "delay" + (delay < 0 ? " negative" : ""), "style": `left:-${x}px; width:${w}px;` } }); } }
drawHeaderAndBackground: function() { let width = this.timeHeaderEl.offsetWidth; let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime; let minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width; let intervalLength = findOptimalTimeInterval(minTimeInterval); let intervalWidth = intervalLength * width / animationDuration; drawGraphElementBackground(this.win.document, "time-graduations", width, intervalWidth); // And the time graduation header. this.timeHeaderEl.innerHTML = ""; for (let i = 0; i <= width / intervalWidth; i++) { let pos = 100 * i * intervalWidth / width; createNode({ parent: this.timeHeaderEl, nodeType: "span", attributes: { "class": "time-tick", "style": `left:${pos}%` }, textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos)) }); } }
render: function ({keyframes, propertyName, animation, animationType}) { this.keyframes = keyframes; this.propertyName = propertyName; this.animation = animation; // Create graph element. const graphEl = createSVGNode({ parent: this.keyframesEl, nodeType: "svg", attributes: { "preserveAspectRatio": "none" } }); // This visual is only one iteration, // so we use animation.state.duration as total duration. const totalDuration = animation.state.duration; // Minimum segment duration is the duration of one pixel. const minSegmentDuration = totalDuration / this.containerEl.clientWidth; // Create graph helper to render the animation property graph. const win = this.containerEl.ownerGlobal; const graphHelper = new ProgressGraphHelper(win, propertyName, animationType, keyframes, totalDuration); renderPropertyGraph(graphEl, totalDuration, minSegmentDuration, graphHelper); // 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) { createNode({ parent: this.keyframesEl, attributes: { "class": "frame", "style": `left:${frame.offset * 100}%;`, "data-offset": frame.offset, "data-property": propertyName, "title": frame.value } }); } }
render: function ({keyframes, propertyName, animation}) { this.keyframes = keyframes; this.propertyName = propertyName; this.animation = animation; let iterationStartOffset = animation.state.iterationStart % 1 == 0 ? 0 : 1 - animation.state.iterationStart % 1; this.keyframesEl.classList.add(animation.state.type); for (let frame of this.keyframes) { let offset = frame.offset + iterationStartOffset; createNode({ parent: this.keyframesEl, attributes: { "class": "frame", "style": `left:${offset * 100}%;`, "data-offset": frame.offset, "data-property": propertyName, "title": frame.value } }); } },
init: function (containerEl) { this.containerEl = containerEl; this.keyframesEl = createNode({ parent: this.containerEl, attributes: {"class": "keyframes"} }); },
init: function(containerEl) { this.selectEl = createNode({ parent: containerEl, nodeType: "select", attributes: {"class": "devtools-button"} }); this.selectEl.addEventListener("change", this.onRateChanged); },
init: function (containerEl) { this.containerEl = containerEl; this.keyframesEl = createNode({ parent: this.containerEl, attributes: {"class": "keyframes"} }); this.containerEl.addEventListener("click", this.onClick); },
renderProgressIndicator: function () { // The wrapper represents the area which the indicator is displayable. const progressIndicatorWrapperEl = createNode({ parent: this.containerEl, attributes: { "class": "track-container progress-indicator-wrapper" } }); this.progressIndicatorEl = createNode({ parent: progressIndicatorWrapperEl, attributes: { "class": "progress-indicator" } }); createNode({ parent: this.progressIndicatorEl, attributes: { "class": "progress-indicator-shape" } }); },
init: function (containerEl) { this.selectEl = createNode({ parent: containerEl, nodeType: "select", attributes: { "class": "devtools-button", "title": L10N.getStr("timeline.rateSelectorTooltip") } }); this.selectEl.addEventListener("change", this.onRateChanged); },
renderAnimatedPropertiesHeader: function () { // Add animated property header. const headerEl = createNode({ parent: this.containerEl, attributes: { "class": "animated-properties-header" } }); // Add progress tick container. const progressTickContainerEl = createNode({ parent: this.containerEl, attributes: { "class": "progress-tick-container track-container" } }); // Add label container. const headerLabelContainerEl = createNode({ parent: headerEl, attributes: { "class": "track-container" } }); // Add labels for (let label of [L10N.getFormatStr("detail.propertiesHeader.percentage", 0), L10N.getFormatStr("detail.propertiesHeader.percentage", 50), L10N.getFormatStr("detail.propertiesHeader.percentage", 100)]) { createNode({ parent: progressTickContainerEl, nodeType: "span", attributes: { "class": "progress-tick" } }); createNode({ parent: headerLabelContainerEl, nodeType: "label", attributes: { "class": "header-item" }, textContent: label }); } },
init: function(containerEl) { this.win = containerEl.ownerDocument.defaultView; this.rootWrapperEl = createNode({ parent: containerEl, attributes: { "class": "animation-timeline" } }); let scrubberContainer = createNode({ parent: this.rootWrapperEl, attributes: {"class": "scrubber-wrapper track-container"} }); this.scrubberEl = createNode({ parent: scrubberContainer, attributes: { "class": "scrubber" } }); this.scrubberHandleEl = createNode({ parent: this.scrubberEl, attributes: { "class": "scrubber-handle" } }); this.scrubberHandleEl.addEventListener("mousedown", this.onScrubberMouseDown); this.timeHeaderEl = createNode({ parent: this.rootWrapperEl, attributes: { "class": "time-header track-container" } }); this.timeHeaderEl.addEventListener("mousedown", this.onScrubberMouseDown); this.animationsEl = createNode({ parent: this.rootWrapperEl, nodeType: "ul", attributes: { "class": "animations" } }); this.win.addEventListener("resize", this.onWindowResize); },
/** * 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)" } }); }
drawHeaderAndBackground: function() { let width = this.timeHeaderEl.offsetWidth; let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime); drawGraphElementBackground(this.win.document, "time-graduations", width, scale); // And the time graduation header. this.timeHeaderEl.innerHTML = ""; let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING); for (let i = 0; i < width; i += interval) { createNode({ parent: this.timeHeaderEl, nodeType: "span", attributes: { "class": "time-tick", "style": `left:${i}px` }, textContent: TimeScale.formatTime( TimeScale.distanceToRelativeTime(i, width)) }); } },
init: function(containerEl) { let document = containerEl.ownerDocument; // Init the markup for displaying the target node. this.el = createNode({ parent: containerEl, attributes: { "class": "animation-target" } }); // Icon to select the node in the inspector. this.selectNodeEl = createNode({ parent: this.el, nodeType: "span", attributes: { "class": "node-selector" } }); // Wrapper used for mouseover/out event handling. this.previewEl = createNode({ parent: this.el, nodeType: "span" }); if (!this.options.compact) { this.previewEl.appendChild(document.createTextNode("<")); } // Tag name. this.tagNameEl = createNode({ parent: this.previewEl, nodeType: "span", attributes: { "class": "tag-name theme-fg-color3" } }); // Id attribute container. this.idEl = createNode({ parent: this.previewEl, nodeType: "span" }); if (!this.options.compact) { createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "attribute-name theme-fg-color2" }, textContent: "id" }); this.idEl.appendChild(document.createTextNode("=\"")); } else { createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "theme-fg-color2" }, textContent: "#" }); } createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "attribute-value theme-fg-color6" } }); if (!this.options.compact) { this.idEl.appendChild(document.createTextNode("\"")); } // Class attribute container. this.classEl = createNode({ parent: this.previewEl, nodeType: "span" }); if (!this.options.compact) { createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "attribute-name theme-fg-color2" }, textContent: "class" }); this.classEl.appendChild(document.createTextNode("=\"")); } else { createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "theme-fg-color6" }, textContent: "." }); } createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "attribute-value theme-fg-color6" } }); if (!this.options.compact) { this.classEl.appendChild(document.createTextNode("\"")); this.previewEl.appendChild(document.createTextNode(">")); } // Init events for highlighting and selecting the node. this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); this.selectNodeEl.addEventListener("click", this.onSelectNodeClick); // Start to listen for markupmutation events. this.inspector.on("markupmutation", this.onMarkupMutations); },
render: function(animations, documentCurrentTime) { this.unrender(); this.animations = animations; if (!this.animations.length) { return; } // Loop first to set the time scale for all current animations. for (let {state} of animations) { TimeScale.addAnimation(state); } this.drawHeaderAndBackground(); for (let animation of this.animations) { animation.on("changed", this.onAnimationStateChanged); // Each line contains the target animated node and the animation time // block. let animationEl = createNode({ parent: this.animationsEl, nodeType: "li", attributes: { "class": "animation" } }); // Left sidebar for the animated node. let animatedNodeEl = createNode({ parent: animationEl, attributes: { "class": "target" } }); let timeBlockEl = createNode({ parent: animationEl, attributes: { "class": "time-block" } }); this.drawTimeBlock(animation, timeBlockEl); // Draw the animated node target. let targetNode = new AnimationTargetNode(this.inspector, {compact: true}); targetNode.init(animatedNodeEl); targetNode.render(animation); // Save the targetNode so it can be destroyed later. this.targetNodes.push(targetNode); } // Use the document's current time to position the scrubber (if the server // doesn't provide it, hide the scrubber entirely). // Note that because the currentTime was sent via the protocol, some time // may have gone by since then, and so the scrubber might be a bit late. if (!documentCurrentTime) { this.scrubberEl.style.display = "none"; } else { this.scrubberEl.style.display = "block"; this.startAnimatingScrubber(documentCurrentTime); } },
render: function(animations, documentCurrentTime) { this.unrender(); this.animations = animations; if (!this.animations.length) { return; } // Loop first to set the time scale for all current animations. for (let {state} of animations) { TimeScale.addAnimation(state); } this.drawHeaderAndBackground(); for (let animation of this.animations) { animation.on("changed", this.onAnimationStateChanged); // Each line contains the target animated node and the animation time // block. let animationEl = createNode({ parent: this.animationsEl, nodeType: "li", attributes: { "class": "animation " + animation.state.type + (animation.state.isRunningOnCompositor ? " fast-track" : "") } }); // Right below the line is a hidden-by-default line for displaying the // inline keyframes. let detailsEl = createNode({ parent: this.animationsEl, nodeType: "li", attributes: { "class": "animated-properties" } }); let details = new AnimationDetails(); details.init(detailsEl); details.on("frame-selected", this.onFrameSelected); this.details.push(details); // Left sidebar for the animated node. let animatedNodeEl = createNode({ parent: animationEl, attributes: { "class": "target" } }); // Draw the animated node target. let targetNode = new AnimationTargetNode(this.inspector, {compact: true}); targetNode.init(animatedNodeEl); targetNode.render(animation); this.targetNodes.push(targetNode); // Right-hand part contains the timeline itself (called time-block here). let timeBlockEl = createNode({ parent: animationEl, attributes: { "class": "time-block track-container" } }); // Draw the animation time block. let timeBlock = new AnimationTimeBlock(); timeBlock.init(timeBlockEl); timeBlock.render(animation); this.timeBlocks.push(timeBlock); timeBlock.on("selected", this.onAnimationSelected); } // Use the document's current time to position the scrubber (if the server // doesn't provide it, hide the scrubber entirely). // Note that because the currentTime was sent via the protocol, some time // may have gone by since then, and so the scrubber might be a bit late. if (!documentCurrentTime) { this.scrubberEl.style.display = "none"; } else { this.scrubberEl.style.display = "block"; this.startAnimatingScrubber(this.wasRewound() ? TimeScale.minStartTime : documentCurrentTime); } },
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 }%;` } }); } },
init: function(containerEl) { let document = containerEl.ownerDocument; // Init the markup for displaying the target node. this.el = createNode({ parent: containerEl, attributes: { "class": "animation-target" } }); // Icon to select the node in the inspector. this.highlightNodeEl = createNode({ parent: this.el, nodeType: "span", attributes: { "class": "node-highlighter", "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel") } }); // Wrapper used for mouseover/out event handling. this.previewEl = createNode({ parent: this.el, nodeType: "span", attributes: { "title": L10N.getStr("inspector.nodePreview.selectNodeLabel") } }); if (!this.options.compact) { this.previewEl.appendChild(document.createTextNode("<")); } // Tag name. this.tagNameEl = createNode({ parent: this.previewEl, nodeType: "span", attributes: { "class": "tag-name theme-fg-color3" } }); // Id attribute container. this.idEl = createNode({ parent: this.previewEl, nodeType: "span" }); if (!this.options.compact) { createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "attribute-name theme-fg-color2" }, textContent: "id" }); this.idEl.appendChild(document.createTextNode("=\"")); } else { createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "theme-fg-color6" }, textContent: "#" }); } createNode({ parent: this.idEl, nodeType: "span", attributes: { "class": "attribute-value theme-fg-color6" } }); if (!this.options.compact) { this.idEl.appendChild(document.createTextNode("\"")); } // Class attribute container. this.classEl = createNode({ parent: this.previewEl, nodeType: "span" }); if (!this.options.compact) { createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "attribute-name theme-fg-color2" }, textContent: "class" }); this.classEl.appendChild(document.createTextNode("=\"")); } else { createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "theme-fg-color6" }, textContent: "." }); } createNode({ parent: this.classEl, nodeType: "span", attributes: { "class": "attribute-value theme-fg-color6" } }); if (!this.options.compact) { this.classEl.appendChild(document.createTextNode("\"")); this.previewEl.appendChild(document.createTextNode(">")); } this.startListeners(); },
render: function (animation, tracks) { 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 = createSVGNode({ parent: this.containerEl, 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 }`); // Minimum segment duration is the duration of one pixel. const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth; // Minimum progress threshold for effect timing. const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing); // Render summary graph. // The summary graph is constructed from keyframes's easing and effect timing. const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration); renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration, minEffectProgressThreshold, tracks, graphHelper); if (state.easing !== "linear") { renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration, minEffectProgressThreshold, graphHelper); } graphHelper.destroy(); // The animation name is displayed over the animation. const nameEl = createNode({ parent: this.containerEl, attributes: { "class": "name", "title": this.getTooltipText(state) } }); createSVGNode({ parent: createSVGNode({ parent: nameEl, nodeType: "svg", }), nodeType: "text", attributes: { "y": "50%", "x": "100%", }, 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 }%;` } }); } },
renderAnimatedPropertiesBody: function (animationTypes) { // Add animated property body. const bodyEl = createNode({ parent: this.containerEl, attributes: { "class": "animated-properties-body" } }); // Move unchanged value animation to bottom in the list. const propertyNames = []; const unchangedPropertyNames = []; for (let propertyName in this.tracks) { if (!isUnchangedProperty(this.tracks[propertyName])) { propertyNames.push(propertyName); } else { unchangedPropertyNames.push(propertyName); } } Array.prototype.push.apply(propertyNames, unchangedPropertyNames); for (let propertyName of propertyNames) { let line = createNode({ parent: bodyEl, attributes: {"class": "property"} }); if (unchangedPropertyNames.includes(propertyName)) { line.classList.add("unchanged"); } let {warning, className} = this.getPerfDataForProperty(this.animation, propertyName); createNode({ // text-overflow doesn't work in flex items, so we need a second level // of container to actually have an ellipsis on the name. // See bug 972664. parent: createNode({ parent: line, attributes: {"class": "name"} }), textContent: getCssPropertyName(propertyName), attributes: {"title": warning, "class": className} }); // Add the keyframes diagram for this property. let framesWrapperEl = createNode({ parent: line, attributes: {"class": "track-container"} }); let framesEl = createNode({ parent: framesWrapperEl, attributes: {"class": "frames"} }); let keyframesComponent = new Keyframes(); keyframesComponent.init(framesEl); keyframesComponent.render({ keyframes: this.tracks[propertyName], propertyName: propertyName, animation: this.animation, animationType: animationTypes[propertyName] }); this.keyframeComponents.push(keyframesComponent); } },
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). let {x, iterationW, delayX, delayW, negativeDelayW, endDelayX, endDelayW} = TimeScale.getAnimationDimensions(animation); // background properties for .iterations element let backgroundIterations = TimeScale.getIterationsBackgroundData(animation); createNode({ parent: this.containerEl, attributes: { "class": "iterations" + (state.iterationCount ? "" : " infinite"), // Individual iterations are represented by setting the size of the // repeating linear-gradient. // The background-size, background-position, background-repeat represent // iterationCount and iterationStart. "style": `left:${x}%; width:${iterationW}%; background-size:${backgroundIterations.size}% 100%; background-position:${backgroundIterations.position}% 0; background-repeat:${backgroundIterations.repeat};` } }); // The animation name is displayed over the iterations. // Note that in case of negative delay, it is pushed towards the right so // the delay element does not overlap. createNode({ parent: createNode({ parent: this.containerEl, attributes: { "class": "name", "title": this.getTooltipText(state), // Place the name at the same position as the iterations, but make // space for the negative delay if any. "style": `left:${x + negativeDelayW}%; width:${iterationW - negativeDelayW}%;` }, }), 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" : ""), "style": `left:${delayX}%; width:${delayW}%;` } }); } // endDelay if (state.endDelay) { createNode({ parent: this.containerEl, attributes: { "class": "end-delay" + (state.endDelay < 0 ? " negative" : ""), "style": `left:${endDelayX}%; width:${endDelayW}%;` } }); } },
render: Task.async(function*(animation) { this.unrender(); if (!animation) { return; } this.animation = animation; // We might have been destroyed in the meantime, or the component might // have been re-rendered. if (!this.containerEl || this.animation !== animation) { return; } // Build an element for each animated property track. this.tracks = yield this.getTracks(animation, this.serverTraits); // Useful for tests to know when the keyframes have been retrieved. this.emit("keyframes-retrieved"); for (let propertyName in this.tracks) { let line = createNode({ parent: this.containerEl, attributes: {"class": "property"} }); createNode({ // text-overflow doesn't work in flex items, so we need a second level // of container to actually have an ellipsis on the name. // See bug 972664. parent: createNode({ parent: line, attributes: {"class": "name"}, }), textContent: getCssPropertyName(propertyName) }); // Add the keyframes diagram for this property. let framesWrapperEl = createNode({ parent: line, attributes: {"class": "track-container"} }); let framesEl = createNode({ parent: framesWrapperEl, attributes: {"class": "frames"} }); // Scale the list of keyframes according to the current time scale. let {x, w} = TimeScale.getAnimationDimensions(animation); framesEl.style.left = `${x}%`; framesEl.style.width = `${w}%`; let keyframesComponent = new Keyframes(); keyframesComponent.init(framesEl); keyframesComponent.render({ keyframes: this.tracks[propertyName], propertyName: propertyName, animation: animation }); keyframesComponent.on("frame-selected", this.onFrameSelected); this.keyframeComponents.push(keyframesComponent); } }),
render: function (animation, tracks) { this.unrender(); this.animation = animation; // Animation summary graph element. const summaryEl = createSVGNode({ parent: this.containerEl, nodeType: "svg", attributes: { "class": "summary", "preserveAspectRatio": "none" } }); this.updateSummaryGraphViewBox(summaryEl); const {state} = this.animation; // Total displayed duration const totalDisplayedDuration = this.getTotalDisplayedDuration(); // Minimum segment duration is the duration of one pixel. const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth; // Minimum progress threshold for effect timing. const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing); // Render summary graph. // The summary graph is constructed from keyframes's easing and effect timing. const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration); renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration, minEffectProgressThreshold, tracks, graphHelper); if (state.easing !== "linear") { renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration, minEffectProgressThreshold, graphHelper); } graphHelper.destroy(); // The animation name is displayed over the animation. const nameEl = createNode({ parent: this.containerEl, attributes: { "class": "name", "title": this.getTooltipText(state) } }); createSVGNode({ parent: createSVGNode({ parent: nameEl, nodeType: "svg", }), nodeType: "text", attributes: { "y": "50%", "x": "100%", }, textContent: state.name }); // Delay. if (state.delay) { // Negative delays need to start at 0. const delayEl = createNode({ parent: this.containerEl, attributes: { "class": "delay" + (state.delay < 0 ? " negative" : " positive") + (state.fill === "both" || state.fill === "backwards" ? " fill" : "") } }); this.updateDelayBounds(delayEl); } // endDelay if (state.iterationCount && state.endDelay) { const endDelayEl = createNode({ parent: this.containerEl, attributes: { "class": "end-delay" + (state.endDelay < 0 ? " negative" : " positive") + (state.fill === "both" || state.fill === "forwards" ? " fill" : "") } }); this.updateEndDelayBounds(endDelayEl); } },
render: function(animation) { this.animation = animation; let {state} = this.animation; let width = this.containerEl.offsetWidth; // 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). let start = state.previousStartTime || 0; let duration = state.duration; let rate = state.playbackRate; let count = state.iterationCount; let delay = state.delay || 0; let x = TimeScale.startTimeToDistance(start + (delay / rate), width); let w = TimeScale.durationToDistance(duration / rate, width); let iterationW = w * (count || 1); let delayW = TimeScale.durationToDistance(Math.abs(delay) / rate, width); let iterations = createNode({ parent: this.containerEl, attributes: { "class": state.type + " iterations" + (count ? "" : " infinite"), // Individual iterations are represented by setting the size of the // repeating linear-gradient. "style": `left:${x}px; width:${iterationW}px; background-size:${Math.max(w, 2)}px 100%;` } }); // The animation name is displayed over the iterations. // Note that in case of negative delay, we push the name towards the right // so the delay can be shown. let negativeDelayW = delay < 0 ? delayW : 0; createNode({ parent: iterations, attributes: { "class": "name", "title": this.getTooltipText(state), // Position the fast-track icon with background-position, and make space // for the negative delay with a margin-left. "style": "background-position:" + (iterationW - FAST_TRACK_ICON_SIZE - negativeDelayW) + "px center;margin-left:" + negativeDelayW + "px" }, textContent: state.name }); // Delay. if (delay) { // Negative delays need to start at 0. let delayX = TimeScale.durationToDistance( (delay < 0 ? 0 : delay) / rate, width); createNode({ parent: iterations, attributes: { "class": "delay" + (delay < 0 ? " negative" : ""), "style": `left:-${delayX}px; width:${delayW}px;` } }); } },