| /* |
| * Copyright (C) 2020 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.AnimationContentView = class AnimationContentView extends WI.ContentView |
| { |
| constructor(representedObject) |
| { |
| console.assert(representedObject instanceof WI.Animation); |
| |
| super(representedObject); |
| |
| this._animationTargetDOMNode = null; |
| this._cachedWidth = NaN; |
| |
| this.element.classList.add("animation"); |
| } |
| |
| // Static |
| |
| static get previewHeight() |
| { |
| return 40; |
| } |
| |
| // Public |
| |
| handleRefreshButtonClicked() |
| { |
| this._refreshSubtitle(); |
| } |
| |
| // Protected |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| let headerElement = this.element.appendChild(document.createElement("header")); |
| |
| let titlesContainer = headerElement.appendChild(document.createElement("div")); |
| titlesContainer.className = "titles"; |
| |
| this._titleElement = titlesContainer.appendChild(document.createElement("span")); |
| this._titleElement.className = "title"; |
| |
| this._subtitleElement = titlesContainer.appendChild(document.createElement("span")); |
| this._subtitleElement.className = "subtitle"; |
| |
| let navigationBar = new WI.NavigationBar; |
| |
| let animationTargetButtonNavigationItem = new WI.ButtonNavigationItem("animation-target", WI.UIString("Animation Target"), "Images/Markup.svg", 16, 16); |
| animationTargetButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| WI.addMouseDownContextMenuHandlers(animationTargetButtonNavigationItem.element, this._populateAnimationTargetButtonContextMenu.bind(this)); |
| navigationBar.addNavigationItem(animationTargetButtonNavigationItem); |
| |
| headerElement.append(navigationBar.element); |
| this.addSubview(navigationBar); |
| |
| this._previewContainer = this.element.appendChild(document.createElement("div")); |
| this._previewContainer.className = "preview"; |
| } |
| |
| layout() |
| { |
| super.layout(); |
| |
| this._refreshTitle(); |
| this._refreshSubtitle(); |
| this._refreshPreview(); |
| } |
| |
| sizeDidChange() |
| { |
| super.sizeDidChange(); |
| |
| this._cachedWidth = this.element.realOffsetWidth; |
| } |
| |
| attached() |
| { |
| super.attached(); |
| |
| this.representedObject.addEventListener(WI.Animation.Event.NameChanged, this._handleNameChanged, this); |
| this.representedObject.addEventListener(WI.Animation.Event.EffectChanged, this._handleEffectChanged, this); |
| this.representedObject.addEventListener(WI.Animation.Event.TargetChanged, this._handleTargetChanged, this); |
| } |
| |
| detached() |
| { |
| this.representedObject.removeEventListener(WI.Animation.Event.TargetChanged, this._handleTargetChanged, this); |
| this.representedObject.removeEventListener(WI.Animation.Event.EffectChanged, this._handleEffectChanged, this); |
| this.representedObject.removeEventListener(WI.Animation.Event.NameChanged, this._handleNameChanged, this); |
| |
| super.detached(); |
| } |
| |
| // Private |
| |
| _refreshTitle() |
| { |
| this._titleElement.removeChildren(); |
| |
| let displayName = this.representedObject.displayName; |
| |
| let showIdentifierIfDifferent = (cssName) => { |
| let formatString = ""; |
| let substitutions = []; |
| |
| if (cssName === displayName) |
| formatString = WI.UIString("(%s)"); |
| else { |
| formatString = WI.UIString("%s (%s)"); |
| substitutions.push(displayName); |
| } |
| |
| let cssNameWrapper = document.createElement("code"); |
| cssNameWrapper.textContent = cssName; |
| substitutions.push(cssNameWrapper); |
| |
| String.format(formatString, substitutions, String.standardFormatters, this._titleElement, function(a, b) { |
| a.append(b); |
| return a; |
| }); |
| }; |
| |
| switch (this.representedObject.animationType) { |
| case WI.Animation.Type.WebAnimation: |
| this._titleElement.textContent = this.representedObject.name || WI.UIString("(%s)").format(displayName); |
| break; |
| |
| case WI.Animation.Type.CSSAnimation: |
| showIdentifierIfDifferent(this.representedObject.cssAnimationName); |
| break; |
| |
| case WI.Animation.Type.CSSTransition: |
| showIdentifierIfDifferent(this.representedObject.cssTransitionProperty); |
| break; |
| } |
| } |
| |
| _refreshSubtitle() |
| { |
| this.representedObject.requestEffectTarget((styleable) => { |
| this._animationTargetDOMNode = styleable.node; |
| |
| this._subtitleElement.removeChildren(); |
| if (styleable) |
| this._subtitleElement.appendChild(WI.linkifyStyleable(styleable)); |
| }); |
| } |
| |
| _refreshPreview() |
| { |
| this._previewContainer.removeChildren(); |
| |
| let keyframes = this.representedObject.keyframes; |
| if (!keyframes.length) { |
| let span = this._previewContainer.appendChild(document.createElement("span")); |
| span.textContent = WI.UIString("This animation has no keyframes."); |
| return; |
| } |
| |
| let startDelay = this.representedObject.startDelay || 0; |
| let iterationDuration = this.representedObject.iterationDuration || 0; |
| let endDelay = this.representedObject.endDelay || 0; |
| let totalDuration = startDelay + iterationDuration + endDelay; |
| if (totalDuration === 0) { |
| let span = this._previewContainer.appendChild(document.createElement("span")); |
| span.textContent = WI.UIString("This animation has no duration."); |
| return; |
| } |
| |
| const previewHeight = WI.AnimationContentView.previewHeight; |
| |
| const markerHeadRadius = 4; |
| const markerHeadPadding = 2; |
| |
| // Squeeze the entire preview so that markers aren't cut off. |
| const squeezeXStart = (iterationDuration && startDelay) ? 0 : markerHeadRadius + markerHeadPadding; |
| const squeezeXEnd = (iterationDuration && endDelay) ? 0 : markerHeadRadius + markerHeadPadding; |
| const squeezeYStart = markerHeadRadius + (markerHeadPadding * 2); |
| |
| // Ensure that at least a line is drawn if the output of the timing function is `0`. |
| const adjustEasingHeight = -1; |
| |
| // Move the easing line down to cut off the bottom border. |
| const adjustEasingBottomY = 0.5; |
| |
| |
| let secondsPerPixel = this._cachedWidth / totalDuration; |
| |
| startDelay *= secondsPerPixel; |
| iterationDuration = (iterationDuration * secondsPerPixel) - squeezeXStart - squeezeXEnd; |
| endDelay *= secondsPerPixel; |
| |
| let svg = this._previewContainer.appendChild(createSVGElement("svg")); |
| svg.setAttribute("viewBox", `0 0 ${this._cachedWidth} ${previewHeight}`); |
| |
| function addTitle(parent, title) { |
| let titleElement = parent.appendChild(createSVGElement("title")); |
| titleElement.textContent = title; |
| } |
| |
| if (startDelay) { |
| let startDelayContainer = svg.appendChild(createSVGElement("g")); |
| startDelayContainer.classList.add("delay", "start"); |
| |
| let startDelayLine = startDelayContainer.appendChild(createSVGElement("line")); |
| startDelayLine.setAttribute("y1", (previewHeight + squeezeYStart) / 2); |
| startDelayLine.setAttribute("x2", startDelay); |
| startDelayLine.setAttribute("y2", (previewHeight + squeezeYStart) / 2); |
| |
| let startDelayElement = startDelayContainer.appendChild(createSVGElement("rect")); |
| startDelayElement.setAttribute("width", startDelay); |
| startDelayElement.setAttribute("height", previewHeight); |
| |
| const startDelayTitleFormat = WI.UIString("Start Delay %s", "Web Animation Start Delay Tooltip", "Tooltip for section of graph representing delay before a web animation begins applying styles"); |
| addTitle(startDelayElement, startDelayTitleFormat.format(Number.secondsToString(this.representedObject.startDelay / 1000))); |
| } |
| |
| if (endDelay) { |
| let endDelayContainer = svg.appendChild(createSVGElement("g")); |
| endDelayContainer.setAttribute("transform", `translate(${startDelay + iterationDuration + squeezeXStart}, 0)`); |
| endDelayContainer.classList.add("delay", "end"); |
| |
| let endDelayLine = endDelayContainer.appendChild(createSVGElement("line")); |
| endDelayLine.setAttribute("y1", (previewHeight + squeezeYStart) / 2); |
| endDelayLine.setAttribute("x2", endDelay); |
| endDelayLine.setAttribute("y2", (previewHeight + squeezeYStart) / 2); |
| |
| let endDelayElement = endDelayContainer.appendChild(createSVGElement("rect")); |
| endDelayElement.setAttribute("width", startDelay + iterationDuration + endDelay); |
| endDelayElement.setAttribute("height", previewHeight); |
| |
| const endDelayTitleFormat = WI.UIString("End Delay %s", "Web Animation End Delay Tooltip", "Tooltip for section of graph representing delay after a web animation finishes applying styles"); |
| addTitle(endDelayElement, endDelayTitleFormat.format(Number.secondsToString(this.representedObject.endDelay / 1000))); |
| } |
| |
| if (iterationDuration) { |
| let timingFunction = this.representedObject.timingFunction; |
| |
| let activeDurationContainer = svg.appendChild(createSVGElement("g")); |
| activeDurationContainer.classList.add("active"); |
| activeDurationContainer.setAttribute("transform", `translate(${startDelay + squeezeXStart}, ${squeezeYStart})`); |
| |
| const startY = 0; |
| const endY = previewHeight - squeezeYStart; |
| const height = endY - startY; |
| |
| for (let [keyframeA, keyframeB] of keyframes.adjacencies()) { |
| let startX = iterationDuration * keyframeA.offset; |
| let endX = iterationDuration * keyframeB.offset; |
| let width = endX - startX; |
| |
| let easing = keyframeA.easing || timingFunction; |
| |
| let easingContainer = activeDurationContainer.appendChild(createSVGElement("g")); |
| easingContainer.classList.add("easing"); |
| easingContainer.setAttribute("transform", `translate(${startX}, ${startY})`); |
| |
| let easingPath = easingContainer.appendChild(createSVGElement("path")); |
| |
| let pathSteps = []; |
| if (easing instanceof WI.CubicBezier) { |
| pathSteps.push("C"); |
| pathSteps.push(easing.inPoint.x * width); // x1 |
| pathSteps.push((1 - easing.inPoint.y) * (height + adjustEasingHeight)); // y1 |
| pathSteps.push(easing.outPoint.x * width); // x2 |
| pathSteps.push((1 - easing.outPoint.y) * (height + adjustEasingHeight)); // y2 |
| pathSteps.push(width); // x |
| pathSteps.push(0); // y |
| } else if (easing instanceof WI.StepsFunction) { |
| let goUpFirst = false; |
| let stepStartAdjust = 0; |
| let stepCountAdjust = 0; |
| |
| switch (easing.type) { |
| case WI.StepsFunction.Type.JumpStart: |
| case WI.StepsFunction.Type.Start: |
| goUpFirst = true; |
| break; |
| |
| case WI.StepsFunction.Type.JumpNone: |
| --stepCountAdjust; |
| break; |
| |
| case WI.StepsFunction.Type.JumpBoth: |
| ++stepStartAdjust; |
| ++stepCountAdjust; |
| break; |
| } |
| |
| let stepCount = easing.count + stepCountAdjust; |
| let stepX = width / easing.count; // Always use the defined number of steps to divide the duration. |
| let stepY = (height + adjustEasingHeight) / stepCount; |
| |
| for (let i = stepStartAdjust; i <= stepCount; ++i) { |
| let x = stepX * (i - stepStartAdjust); |
| let y = height + adjustEasingHeight - (stepY * i); |
| |
| if (goUpFirst) { |
| if (i) |
| pathSteps.push("H", x); |
| |
| if (i < stepCount) |
| pathSteps.push("V", y - stepY); |
| } else { |
| pathSteps.push("V", y); |
| |
| if (i < stepCount || stepCountAdjust < 0) |
| pathSteps.push("H", x + stepX); |
| } |
| } |
| } else if (easing instanceof WI.Spring) { |
| let duration = easing.calculateDuration(); |
| for (let i = 0; i < width; i += 1 / window.devicePixelRatio) |
| pathSteps.push("L", i, easing.solve(duration * i / width) * (height + adjustEasingHeight)); |
| } |
| easingPath.setAttribute("d", `M 0 ${height + adjustEasingBottomY} ${pathSteps.join(" ")} V ${height + adjustEasingBottomY} Z`); |
| |
| let titleRect = easingContainer.appendChild(createSVGElement("rect")); |
| titleRect.setAttribute("width", width); |
| titleRect.setAttribute("height", height); |
| |
| addTitle(titleRect, easing.toString()); |
| } |
| |
| for (let keyframe of keyframes) { |
| let x = iterationDuration * keyframe.offset; |
| |
| let keyframeContainer = activeDurationContainer.appendChild(createSVGElement("g")); |
| keyframeContainer.classList.add("keyframe"); |
| keyframeContainer.setAttribute("transform", `translate(${x}, ${startY})`); |
| |
| let keyframeMarkerHead = keyframeContainer.appendChild(createSVGElement("circle")); |
| keyframeMarkerHead.setAttribute("r", markerHeadRadius); |
| |
| let keyframeMarkerLine = keyframeContainer.appendChild(createSVGElement("line")); |
| keyframeMarkerLine.setAttribute("y1", height); |
| |
| let titleRect = keyframeContainer.appendChild(createSVGElement("rect")); |
| titleRect.setAttribute("x", -1 * (markerHeadRadius + markerHeadPadding)); |
| titleRect.setAttribute("y", -1 * squeezeYStart); |
| titleRect.setAttribute("width", (markerHeadRadius + markerHeadPadding) * 2); |
| titleRect.setAttribute("height", height + squeezeYStart); |
| |
| addTitle(titleRect, keyframe.style); |
| } |
| } |
| } |
| |
| _handleNameChanged(event) |
| { |
| if (!this.didInitialLayout) |
| return; |
| |
| this._refreshTitle(); |
| } |
| |
| _handleEffectChanged(event) |
| { |
| this._refreshPreview(); |
| } |
| |
| _handleTargetChanged(event) |
| { |
| this._refreshSubtitle(); |
| } |
| |
| _populateAnimationTargetButtonContextMenu(contextMenu) |
| { |
| contextMenu.appendItem(WI.UIString("Log Animation"), () => { |
| WI.RemoteObject.resolveAnimation(this.representedObject, WI.RuntimeManager.ConsoleObjectGroup, (remoteObject) => { |
| if (!remoteObject) |
| return; |
| |
| const text = WI.UIString("Selected Animation", "Appears as a label when a given web animation is logged to the Console"); |
| const addSpecialUserLogClass = true; |
| WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); |
| }); |
| }); |
| |
| contextMenu.appendSeparator(); |
| |
| if (this._animationTargetDOMNode) |
| WI.appendContextMenuItemsForDOMNode(contextMenu, this._animationTargetDOMNode); |
| } |
| }; |