blob: 1d135e488f84b30bf40c7ce5c901c500cfc19b9f [file] [log] [blame]
/*
* 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);
}
};