forked from mirrors/gecko-dev
Merge mozilla-central to autoland. a=merge CLOSED TREE
This commit is contained in:
commit
3fc5bc9ad5
725 changed files with 58262 additions and 40743 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -138,7 +138,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.6.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
@ -829,7 +829,7 @@ dependencies = [
|
|||
name = "geckodriver"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.12.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
@ -1876,7 +1876,7 @@ dependencies = [
|
|||
name = "rsdparsa"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.66 (git+https://github.com/servo/serde?branch=deserialize_from_enums8)",
|
||||
]
|
||||
|
|
@ -2615,7 +2615,7 @@ dependencies = [
|
|||
name = "webdriver"
|
||||
version = "0.36.0"
|
||||
dependencies = [
|
||||
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"http 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
@ -2840,7 +2840,7 @@ dependencies = [
|
|||
"checksum atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb2dcb6e6d35f20276943cc04bb98e538b348d525a04ac79c10021561d202f21"
|
||||
"checksum atty 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d0fd4c0631f06448cc45a6bbb3b710ebb7ff8ccb96a0800c994afe23a70d5df2"
|
||||
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||
"checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9"
|
||||
"checksum base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "85415d2594767338a74a30c1d370b2f3262ec1b4ed2d7bba5b3faf4de40467d9"
|
||||
"checksum binary-space-partition 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "88ceb0d16c4fd0e42876e298d7d3ce3780dd9ebdcbe4199816a32c77e08597ff"
|
||||
"checksum bincode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bda13183df33055cbb84b847becce220d392df502ebe7a4a78d7021771ed94d0"
|
||||
"checksum bindgen 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eac4ed5f2de9efc3c87cb722468fa49d0763e98f999d539bfc5e452c13d85c91"
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ support-files =
|
|||
!/accessible/tests/mochitest/*.js
|
||||
|
||||
[browser_aria_owns.js]
|
||||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
skip-if = true || (verify && !debug && (os == 'linux')) #Bug 1445513
|
||||
|
|
|
|||
|
|
@ -63,10 +63,6 @@ let whitelist = [
|
|||
intermittent: true,
|
||||
errorMessage: /Property contained reference to invalid variable.*background/i,
|
||||
isFromDevTools: true},
|
||||
{sourceName: /devtools\/skin\/animationinspector\.css$/i,
|
||||
intermittent: true,
|
||||
errorMessage: /Property contained reference to invalid variable.*color/i,
|
||||
isFromDevTools: true},
|
||||
];
|
||||
|
||||
if (!Services.prefs.getBoolPref("layout.css.xul-box-display-values.content.enabled")) {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ skip-if = !e10s || !crashreporter # the tab's process is killed during the test.
|
|||
[browser_ext_sessions_forgetClosedTab.js]
|
||||
[browser_ext_sessions_forgetClosedWindow.js]
|
||||
[browser_ext_sessions_getRecentlyClosed.js]
|
||||
skip-if = (debug && os == 'linux') # Bug 1377641
|
||||
[browser_ext_sessions_getRecentlyClosed_private.js]
|
||||
[browser_ext_sessions_getRecentlyClosed_tabs.js]
|
||||
[browser_ext_sessions_restore.js]
|
||||
|
|
|
|||
|
|
@ -1416,8 +1416,8 @@ add_old_configure_assignment('MOZ_LTO_LDFLAGS', lto.ldflags)
|
|||
js_option('--enable-address-sanitizer', help='Enable Address Sanitizer')
|
||||
|
||||
|
||||
@depends_if('--enable-address-sanitizer')
|
||||
def asan(value):
|
||||
@depends_if('--enable-address-sanitizer', '--help')
|
||||
def asan(value, _):
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
This folder contains the code for the current animation inspector. It is currently being rewritten in React in https://bugzilla.mozilla.org/show_bug.cgi?id=1399830.
|
||||
|
||||
The new version of the animation inspector can be found in devtools/inspector/animation.
|
||||
|
|
@ -1,399 +0,0 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
/* animation-panel.js is loaded in the same scope but we don't use
|
||||
import-globals-from to avoid infinite loops since animation-panel.js already
|
||||
imports globals from animation-controller.js */
|
||||
/* globals AnimationsPanel */
|
||||
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { loader, require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
|
||||
loader.lazyRequireGetter(this, "promise");
|
||||
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
|
||||
loader.lazyRequireGetter(this, "AnimationsFront", "devtools/shared/fronts/animation", true);
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// Global toolbox/inspector, set when startup is called.
|
||||
var gToolbox, gInspector;
|
||||
|
||||
/**
|
||||
* Startup the animationinspector controller and view, called by the sidebar
|
||||
* widget when loading/unloading the iframe into the tab.
|
||||
*/
|
||||
var startup = async function(inspector) {
|
||||
gInspector = inspector;
|
||||
gToolbox = inspector.toolbox;
|
||||
|
||||
// Don't assume that AnimationsPanel is defined here, it's in another file.
|
||||
if (!typeof AnimationsPanel === "undefined") {
|
||||
throw new Error("AnimationsPanel was not loaded in the " +
|
||||
"animationinspector window");
|
||||
}
|
||||
|
||||
// Startup first initalizes the controller and then the panel, in sequence.
|
||||
// If you want to know when everything's ready, do:
|
||||
// AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED)
|
||||
await AnimationsController.initialize();
|
||||
await AnimationsPanel.initialize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shutdown the animationinspector controller and view, called by the sidebar
|
||||
* widget when loading/unloading the iframe into the tab.
|
||||
*/
|
||||
var shutdown = async function() {
|
||||
await AnimationsController.destroy();
|
||||
// Don't assume that AnimationsPanel is defined here, it's in another file.
|
||||
if (typeof AnimationsPanel !== "undefined") {
|
||||
await AnimationsPanel.destroy();
|
||||
}
|
||||
gToolbox = gInspector = null;
|
||||
};
|
||||
|
||||
// This is what makes the sidebar widget able to load/unload the panel.
|
||||
function setPanel(panel) {
|
||||
return startup(panel).catch(console.error);
|
||||
}
|
||||
function destroy() {
|
||||
return shutdown().catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the server-side capabilities (traits) so the UI knows whether or not
|
||||
* features should be enabled/disabled.
|
||||
* @param {Target} target The current toolbox target.
|
||||
* @return {Object} An object with boolean properties.
|
||||
*/
|
||||
var getServerTraits = async function(target) {
|
||||
const config = [
|
||||
{ name: "hasToggleAll", actor: "animations",
|
||||
method: "toggleAll" },
|
||||
{ name: "hasToggleSeveral", actor: "animations",
|
||||
method: "toggleSeveral" },
|
||||
{ name: "hasSetCurrentTime", actor: "animationplayer",
|
||||
method: "setCurrentTime" },
|
||||
{ name: "hasMutationEvents", actor: "animations",
|
||||
method: "stopAnimationPlayerUpdates" },
|
||||
{ name: "hasSetPlaybackRate", actor: "animationplayer",
|
||||
method: "setPlaybackRate" },
|
||||
{ name: "hasSetPlaybackRates", actor: "animations",
|
||||
method: "setPlaybackRates" },
|
||||
{ name: "hasTargetNode", actor: "domwalker",
|
||||
method: "getNodeFromActor" },
|
||||
{ name: "hasSetCurrentTimes", actor: "animations",
|
||||
method: "setCurrentTimes" },
|
||||
{ name: "hasGetFrames", actor: "animationplayer",
|
||||
method: "getFrames" },
|
||||
{ name: "hasGetProperties", actor: "animationplayer",
|
||||
method: "getProperties" },
|
||||
{ name: "hasSetWalkerActor", actor: "animations",
|
||||
method: "setWalkerActor" },
|
||||
{ name: "hasGetAnimationTypes", actor: "animationplayer",
|
||||
method: "getAnimationTypes" },
|
||||
];
|
||||
|
||||
const traits = {};
|
||||
for (const {name, actor, method} of config) {
|
||||
traits[name] = await target.actorHasMethod(actor, method);
|
||||
}
|
||||
|
||||
return traits;
|
||||
};
|
||||
|
||||
/**
|
||||
* The animationinspector controller's job is to retrieve AnimationPlayerFronts
|
||||
* from the server. It is also responsible for keeping the list of players up to
|
||||
* date when the node selection changes in the inspector, as well as making sure
|
||||
* no updates are done when the animationinspector sidebar panel is not visible.
|
||||
*
|
||||
* AnimationPlayerFronts are available in AnimationsController.animationPlayers.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
* onPlayers);
|
||||
* function onPlayers() {
|
||||
* for (let player of AnimationsController.animationPlayers) {
|
||||
* // do something with player
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
var AnimationsController = {
|
||||
PLAYERS_UPDATED_EVENT: "players-updated",
|
||||
ALL_ANIMATIONS_TOGGLED_EVENT: "all-animations-toggled",
|
||||
|
||||
async initialize() {
|
||||
if (this.initialized) {
|
||||
await this.initialized;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolver;
|
||||
this.initialized = new Promise(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
|
||||
this.onNewNodeFront = this.onNewNodeFront.bind(this);
|
||||
this.onAnimationMutations = this.onAnimationMutations.bind(this);
|
||||
|
||||
const target = gInspector.target;
|
||||
this.animationsFront = new AnimationsFront(target.client, target.form);
|
||||
|
||||
// Expose actor capabilities.
|
||||
this.traits = await getServerTraits(target);
|
||||
|
||||
if (this.destroyed) {
|
||||
console.warn("Could not fully initialize the AnimationsController");
|
||||
return;
|
||||
}
|
||||
|
||||
// Let the AnimationsActor know what WalkerActor we're using. This will
|
||||
// come in handy later to return references to DOM Nodes.
|
||||
if (this.traits.hasSetWalkerActor) {
|
||||
await this.animationsFront.setWalkerActor(gInspector.walker);
|
||||
}
|
||||
|
||||
this.startListeners();
|
||||
await this.onNewNodeFront();
|
||||
|
||||
resolver();
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
await this.destroyed;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolver;
|
||||
this.destroyed = new Promise(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
this.stopListeners();
|
||||
this.destroyAnimationPlayers();
|
||||
this.nodeFront = null;
|
||||
|
||||
if (this.animationsFront) {
|
||||
this.animationsFront.destroy();
|
||||
this.animationsFront = null;
|
||||
}
|
||||
resolver();
|
||||
},
|
||||
|
||||
startListeners: function() {
|
||||
// Re-create the list of players when a new node is selected, except if the
|
||||
// sidebar isn't visible.
|
||||
gInspector.selection.on("new-node-front", this.onNewNodeFront);
|
||||
gInspector.sidebar.on("select", this.onPanelVisibilityChange);
|
||||
gToolbox.on("select", this.onPanelVisibilityChange);
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
gInspector.selection.off("new-node-front", this.onNewNodeFront);
|
||||
gInspector.sidebar.off("select", this.onPanelVisibilityChange);
|
||||
gToolbox.off("select", this.onPanelVisibilityChange);
|
||||
if (this.isListeningToMutations) {
|
||||
this.animationsFront.off("mutations", this.onAnimationMutations);
|
||||
}
|
||||
},
|
||||
|
||||
isPanelVisible: function() {
|
||||
return gToolbox.currentToolId === "inspector" &&
|
||||
gInspector.sidebar &&
|
||||
gInspector.sidebar.getCurrentTabID() == "animationinspector";
|
||||
},
|
||||
|
||||
async onPanelVisibilityChange() {
|
||||
if (this.isPanelVisible()) {
|
||||
this.onNewNodeFront();
|
||||
}
|
||||
},
|
||||
|
||||
async onNewNodeFront() {
|
||||
// Ignore if the panel isn't visible.
|
||||
// Or the node selection hasn't changed and no animation mutations event occurs during
|
||||
// hidden.
|
||||
if (!this.isPanelVisible() || (this.nodeFront === gInspector.selection.nodeFront &&
|
||||
!this.mutationsDetectedWhileHidden)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mutationsDetectedWhileHidden = false;
|
||||
|
||||
this.nodeFront = gInspector.selection.nodeFront;
|
||||
const done = gInspector.updating("animationscontroller");
|
||||
|
||||
if (!gInspector.selection.isConnected() ||
|
||||
!gInspector.selection.isElementNode()) {
|
||||
this.destroyAnimationPlayers();
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.refreshAnimationPlayers(this.nodeFront);
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
|
||||
|
||||
done();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle (pause/play) all animations in the current target.
|
||||
*/
|
||||
toggleAll: function() {
|
||||
if (!this.traits.hasToggleAll) {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
return this.animationsFront.toggleAll()
|
||||
.then(() => this.emit(this.ALL_ANIMATIONS_TOGGLED_EVENT, this))
|
||||
.catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Similar to toggleAll except that it only plays/pauses the currently known
|
||||
* animations (those listed in this.animationPlayers).
|
||||
* @param {Boolean} shouldPause True if the animations should be paused, false
|
||||
* if they should be played.
|
||||
* @return {Promise} Resolves when the playState has been changed.
|
||||
*/
|
||||
async toggleCurrentAnimations(shouldPause) {
|
||||
if (this.traits.hasToggleSeveral) {
|
||||
await this.animationsFront.toggleSeveral(this.animationPlayers,
|
||||
shouldPause);
|
||||
} else {
|
||||
// Fall back to pausing/playing the players one by one, which is bound to
|
||||
// introduce some de-synchronization.
|
||||
for (const player of this.animationPlayers) {
|
||||
if (shouldPause) {
|
||||
await player.pause();
|
||||
} else {
|
||||
await player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set all known animations' currentTimes to the provided time.
|
||||
* @param {Number} time.
|
||||
* @param {Boolean} shouldPause Should the animations be paused too.
|
||||
* @return {Promise} Resolves when the current time has been set.
|
||||
*/
|
||||
async setCurrentTimeAll(time, shouldPause) {
|
||||
if (this.traits.hasSetCurrentTimes) {
|
||||
await this.animationsFront.setCurrentTimes(this.animationPlayers, time,
|
||||
shouldPause);
|
||||
} else {
|
||||
// Fall back to pausing and setting the current time on each player, one
|
||||
// by one, which is bound to introduce some de-synchronization.
|
||||
for (const animation of this.animationPlayers) {
|
||||
if (shouldPause) {
|
||||
await animation.pause();
|
||||
}
|
||||
await animation.setCurrentTime(time);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set all known animations' playback rates to the provided rate.
|
||||
* @param {Number} rate.
|
||||
* @return {Promise} Resolves when the rate has been set.
|
||||
*/
|
||||
async setPlaybackRateAll(rate) {
|
||||
if (this.traits.hasSetPlaybackRates) {
|
||||
// If the backend can set all playback rates at the same time, use that.
|
||||
await this.animationsFront.setPlaybackRates(this.animationPlayers, rate);
|
||||
} else if (this.traits.hasSetPlaybackRate) {
|
||||
// Otherwise, fall back to setting each rate individually.
|
||||
for (const animation of this.animationPlayers) {
|
||||
await animation.setPlaybackRate(rate);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// AnimationPlayerFront objects are managed by this controller. They are
|
||||
// retrieved when refreshAnimationPlayers is called, stored in the
|
||||
// animationPlayers array, and destroyed when refreshAnimationPlayers is
|
||||
// called again.
|
||||
animationPlayers: [],
|
||||
|
||||
async refreshAnimationPlayers(nodeFront) {
|
||||
this.destroyAnimationPlayers();
|
||||
|
||||
this.animationPlayers = await this.animationsFront
|
||||
.getAnimationPlayersForNode(nodeFront);
|
||||
|
||||
// Start listening for animation mutations only after the first method call
|
||||
// otherwise events won't be sent.
|
||||
if (!this.isListeningToMutations && this.traits.hasMutationEvents) {
|
||||
this.animationsFront.on("mutations", this.onAnimationMutations);
|
||||
this.isListeningToMutations = true;
|
||||
}
|
||||
},
|
||||
|
||||
onAnimationMutations: function(changes) {
|
||||
// Insert new players into this.animationPlayers when new animations are
|
||||
// added.
|
||||
for (const {type, player} of changes) {
|
||||
if (type === "added") {
|
||||
this.animationPlayers.push(player);
|
||||
}
|
||||
|
||||
if (type === "removed") {
|
||||
const index = this.animationPlayers.indexOf(player);
|
||||
this.animationPlayers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isPanelVisible()) {
|
||||
// Let the UI know the list has been updated.
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
|
||||
} else {
|
||||
// Avoid updating the UI while the panel is hidden.
|
||||
// This avoids unnecessary work.
|
||||
this.mutationsDetectedWhileHidden = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the latest known current time of document.timeline.
|
||||
* This value is sent along with all AnimationPlayerActors' states, but it
|
||||
* isn't updated after that, so this function loops over all know animations
|
||||
* to find the highest value.
|
||||
* @return {Number|Boolean} False is returned if this server version doesn't
|
||||
* provide document's current time.
|
||||
*/
|
||||
get documentCurrentTime() {
|
||||
let time = 0;
|
||||
for (const {state} of this.animationPlayers) {
|
||||
if (!state.documentCurrentTime) {
|
||||
return false;
|
||||
}
|
||||
time = Math.max(time, state.documentCurrentTime);
|
||||
}
|
||||
return time;
|
||||
},
|
||||
|
||||
destroyAnimationPlayers: function() {
|
||||
this.animationPlayers = [];
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.decorate(AnimationsController);
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/SplitBox.css"/>
|
||||
<script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"/>
|
||||
</head>
|
||||
<body class="theme-sidebar" role="application" empty="true">
|
||||
<div id="global-toolbar" class="theme-toolbar">
|
||||
<span id="all-animations-label" class="label"></span>
|
||||
<button id="toggle-all" class="devtools-button pause-button"></button>
|
||||
</div>
|
||||
<div id="timeline-toolbar" class="theme-toolbar">
|
||||
<button id="rewind-timeline" class="devtools-button"></button>
|
||||
<button id="pause-resume-timeline" class="devtools-button pause-button paused"></button>
|
||||
<span id="timeline-rate" class="devtools-button"></span>
|
||||
<span id="timeline-current-time" class="label"></span>
|
||||
</div>
|
||||
<div id="players"></div>
|
||||
<div id="error-message" class="devtools-sidepanel-no-result">
|
||||
<p id="error-type"></p>
|
||||
<p id="error-hint"></p>
|
||||
<button id="element-picker" data-standalone="true" class="devtools-button"></button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
/* eslint-disable */
|
||||
var isInChrome = window.location.href.includes("chrome:");
|
||||
if (isInChrome) {
|
||||
var exports = {};
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
|
||||
}
|
||||
</script>
|
||||
<script type="application/javascript" src="animation-controller.js"></script>
|
||||
<script type="application/javascript" src="animation-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,348 +0,0 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
/* import-globals-from animation-controller.js */
|
||||
/* globals document */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {AnimationsTimeline} = require("devtools/client/inspector/animation-old/components/animation-timeline");
|
||||
const {RateSelector} = require("devtools/client/inspector/animation-old/components/rate-selector");
|
||||
const {formatStopwatchTime} = require("devtools/client/inspector/animation-old/utils");
|
||||
const {KeyCodes} = require("devtools/client/shared/keycodes");
|
||||
|
||||
var $ = (selector, target = document) => target.querySelector(selector);
|
||||
|
||||
/**
|
||||
* The main animations panel UI.
|
||||
*/
|
||||
var AnimationsPanel = {
|
||||
UI_UPDATED_EVENT: "ui-updated",
|
||||
PANEL_INITIALIZED: "panel-initialized",
|
||||
|
||||
async initialize() {
|
||||
if (AnimationsController.destroyed) {
|
||||
console.warn("Could not initialize the animation-panel, controller " +
|
||||
"was destroyed");
|
||||
return;
|
||||
}
|
||||
if (this.initialized) {
|
||||
await this.initialized;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolver;
|
||||
this.initialized = new Promise(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
this.playersEl = $("#players");
|
||||
this.errorMessageEl = $("#error-message");
|
||||
this.pickerButtonEl = $("#element-picker");
|
||||
this.toggleAllButtonEl = $("#toggle-all");
|
||||
this.playTimelineButtonEl = $("#pause-resume-timeline");
|
||||
this.rewindTimelineButtonEl = $("#rewind-timeline");
|
||||
this.timelineCurrentTimeEl = $("#timeline-current-time");
|
||||
this.rateSelectorEl = $("#timeline-rate");
|
||||
|
||||
this.rewindTimelineButtonEl.setAttribute("title",
|
||||
L10N.getStr("timeline.rewindButtonTooltip"));
|
||||
|
||||
$("#all-animations-label").textContent = L10N.getStr("panel.allAnimations");
|
||||
|
||||
// If the server doesn't support toggling all animations at once, hide the
|
||||
// whole global toolbar.
|
||||
if (!AnimationsController.traits.hasToggleAll) {
|
||||
$("#global-toolbar").style.display = "none";
|
||||
}
|
||||
|
||||
// Binding functions that need to be called in scope.
|
||||
for (const functionName of [
|
||||
"onKeyDown", "onPickerStarted",
|
||||
"onPickerStopped", "refreshAnimationsUI", "onToggleAllClicked",
|
||||
"onTabNavigated", "onTimelineDataChanged", "onTimelinePlayClicked",
|
||||
"onTimelineRewindClicked", "onRateChanged"]) {
|
||||
this[functionName] = this[functionName].bind(this);
|
||||
}
|
||||
const hUtils = gToolbox.highlighterUtils;
|
||||
this.togglePicker = hUtils.togglePicker.bind(hUtils);
|
||||
|
||||
this.animationsTimelineComponent = new AnimationsTimeline(gInspector,
|
||||
AnimationsController.traits);
|
||||
this.animationsTimelineComponent.init(this.playersEl);
|
||||
|
||||
if (AnimationsController.traits.hasSetPlaybackRate) {
|
||||
this.rateSelectorComponent = new RateSelector();
|
||||
this.rateSelectorComponent.init(this.rateSelectorEl);
|
||||
}
|
||||
|
||||
this.startListeners();
|
||||
|
||||
await this.refreshAnimationsUI();
|
||||
|
||||
resolver();
|
||||
this.emit(this.PANEL_INITIALIZED);
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
await this.destroyed;
|
||||
return;
|
||||
}
|
||||
|
||||
let resolver;
|
||||
this.destroyed = new Promise(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
this.stopListeners();
|
||||
|
||||
this.animationsTimelineComponent.destroy();
|
||||
this.animationsTimelineComponent = null;
|
||||
|
||||
if (this.rateSelectorComponent) {
|
||||
this.rateSelectorComponent.destroy();
|
||||
this.rateSelectorComponent = null;
|
||||
}
|
||||
|
||||
this.playersEl = this.errorMessageEl = null;
|
||||
this.toggleAllButtonEl = this.pickerButtonEl = null;
|
||||
this.playTimelineButtonEl = this.rewindTimelineButtonEl = null;
|
||||
this.timelineCurrentTimeEl = this.rateSelectorEl = null;
|
||||
|
||||
resolver();
|
||||
},
|
||||
|
||||
startListeners: function() {
|
||||
AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.refreshAnimationsUI);
|
||||
|
||||
this.pickerButtonEl.addEventListener("click", this.togglePicker);
|
||||
gToolbox.on("picker-started", this.onPickerStarted);
|
||||
gToolbox.on("picker-stopped", this.onPickerStopped);
|
||||
|
||||
this.toggleAllButtonEl.addEventListener("click", this.onToggleAllClicked);
|
||||
this.playTimelineButtonEl.addEventListener(
|
||||
"click", this.onTimelinePlayClicked);
|
||||
this.rewindTimelineButtonEl.addEventListener(
|
||||
"click", this.onTimelineRewindClicked);
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
|
||||
gToolbox.target.on("navigate", this.onTabNavigated);
|
||||
|
||||
this.animationsTimelineComponent.on("timeline-data-changed",
|
||||
this.onTimelineDataChanged);
|
||||
|
||||
if (this.rateSelectorComponent) {
|
||||
this.rateSelectorComponent.on("rate-changed", this.onRateChanged);
|
||||
}
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.refreshAnimationsUI);
|
||||
|
||||
this.pickerButtonEl.removeEventListener("click", this.togglePicker);
|
||||
gToolbox.off("picker-started", this.onPickerStarted);
|
||||
gToolbox.off("picker-stopped", this.onPickerStopped);
|
||||
|
||||
this.toggleAllButtonEl.removeEventListener("click",
|
||||
this.onToggleAllClicked);
|
||||
this.playTimelineButtonEl.removeEventListener("click",
|
||||
this.onTimelinePlayClicked);
|
||||
this.rewindTimelineButtonEl.removeEventListener("click",
|
||||
this.onTimelineRewindClicked);
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
|
||||
gToolbox.target.off("navigate", this.onTabNavigated);
|
||||
|
||||
this.animationsTimelineComponent.off("timeline-data-changed",
|
||||
this.onTimelineDataChanged);
|
||||
|
||||
if (this.rateSelectorComponent) {
|
||||
this.rateSelectorComponent.off("rate-changed", this.onRateChanged);
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(event) {
|
||||
// If the space key is pressed, it should toggle the play state of
|
||||
// the animations displayed in the panel, or of all the animations on
|
||||
// the page if the selected node does not have any animation on it.
|
||||
if (event.keyCode === KeyCodes.DOM_VK_SPACE) {
|
||||
if (AnimationsController.animationPlayers.length > 0) {
|
||||
this.playPauseTimeline().catch(console.error);
|
||||
} else {
|
||||
this.toggleAll().catch(console.error);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
togglePlayers: function(isVisible) {
|
||||
if (isVisible) {
|
||||
document.body.removeAttribute("empty");
|
||||
document.body.setAttribute("timeline", "true");
|
||||
} else {
|
||||
document.body.setAttribute("empty", "true");
|
||||
document.body.removeAttribute("timeline");
|
||||
$("#error-type").textContent = L10N.getStr("panel.invalidElementSelected");
|
||||
$("#error-hint").textContent = L10N.getStr("panel.selectElement");
|
||||
}
|
||||
},
|
||||
|
||||
onPickerStarted: function() {
|
||||
this.pickerButtonEl.classList.add("checked");
|
||||
},
|
||||
|
||||
onPickerStopped: function() {
|
||||
this.pickerButtonEl.classList.remove("checked");
|
||||
},
|
||||
|
||||
onToggleAllClicked: function() {
|
||||
this.toggleAll().catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle (pause/play) all animations in the current target
|
||||
* and update the UI the toggleAll button.
|
||||
*/
|
||||
async toggleAll() {
|
||||
this.toggleAllButtonEl.classList.toggle("paused");
|
||||
await AnimationsController.toggleAll();
|
||||
},
|
||||
|
||||
onTimelinePlayClicked: function() {
|
||||
this.playPauseTimeline().catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Depending on the state of the timeline either pause or play the animations
|
||||
* displayed in it.
|
||||
* If the animations are finished, this will play them from the start again.
|
||||
* If the animations are playing, this will pause them.
|
||||
* If the animations are paused, this will resume them.
|
||||
*
|
||||
* @return {Promise} Resolves when the playState is changed and the UI
|
||||
* is refreshed
|
||||
*/
|
||||
playPauseTimeline: function() {
|
||||
return AnimationsController
|
||||
.toggleCurrentAnimations(this.timelineData.isMoving)
|
||||
.then(() => this.refreshAnimationsStateAndUI());
|
||||
},
|
||||
|
||||
onTimelineRewindClicked: function() {
|
||||
this.rewindTimeline().catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the startTime of all current animations shown in the timeline and
|
||||
* pause them.
|
||||
*
|
||||
* @return {Promise} Resolves when currentTime is set and the UI is refreshed
|
||||
*/
|
||||
rewindTimeline: function() {
|
||||
return AnimationsController
|
||||
.setCurrentTimeAll(0, true)
|
||||
.then(() => this.refreshAnimationsStateAndUI());
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the playback rate of all current animations shown in the timeline to
|
||||
* the value of this.rateSelectorEl.
|
||||
*/
|
||||
onRateChanged: function(rate) {
|
||||
AnimationsController.setPlaybackRateAll(rate)
|
||||
.then(() => this.refreshAnimationsStateAndUI())
|
||||
.catch(console.error);
|
||||
},
|
||||
|
||||
onTabNavigated: function() {
|
||||
this.toggleAllButtonEl.classList.remove("paused");
|
||||
},
|
||||
|
||||
onTimelineDataChanged: function(data) {
|
||||
this.timelineData = data;
|
||||
const {isMoving, isUserDrag, time} = data;
|
||||
|
||||
this.playTimelineButtonEl.classList.toggle("paused", !isMoving);
|
||||
|
||||
const l10nPlayProperty = isMoving ? "timeline.resumedButtonTooltip" :
|
||||
"timeline.pausedButtonTooltip";
|
||||
|
||||
this.playTimelineButtonEl.setAttribute("title",
|
||||
L10N.getStr(l10nPlayProperty));
|
||||
|
||||
// If the timeline data changed as a result of the user dragging the
|
||||
// scrubber, then pause all animations and set their currentTimes.
|
||||
// (Note that we want server-side requests to be sequenced, so we only do
|
||||
// this after the previous currentTime setting was done).
|
||||
if (isUserDrag && !this.setCurrentTimeAllPromise) {
|
||||
this.setCurrentTimeAllPromise =
|
||||
AnimationsController.setCurrentTimeAll(time, true)
|
||||
.catch(console.error)
|
||||
.then(() => {
|
||||
this.setCurrentTimeAllPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
this.displayTimelineCurrentTime();
|
||||
},
|
||||
|
||||
displayTimelineCurrentTime: function() {
|
||||
const {time} = this.timelineData;
|
||||
this.timelineCurrentTimeEl.textContent = formatStopwatchTime(time);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make sure all known animations have their states up to date (which is
|
||||
* useful after the playState or currentTime has been changed and in case the
|
||||
* animations aren't auto-refreshing), and then refresh the UI.
|
||||
*/
|
||||
async refreshAnimationsStateAndUI() {
|
||||
for (const player of AnimationsController.animationPlayers) {
|
||||
await player.refreshState();
|
||||
}
|
||||
await this.refreshAnimationsUI();
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the list of animations UI. This will empty the panel and re-render
|
||||
* the various components again.
|
||||
*/
|
||||
async refreshAnimationsUI() {
|
||||
// Empty the whole panel first.
|
||||
this.togglePlayers(true);
|
||||
|
||||
// Re-render the timeline component.
|
||||
this.animationsTimelineComponent.render(
|
||||
AnimationsController.animationPlayers,
|
||||
AnimationsController.documentCurrentTime);
|
||||
|
||||
// Re-render the rate selector component.
|
||||
if (this.rateSelectorComponent) {
|
||||
this.rateSelectorComponent.render(AnimationsController.animationPlayers);
|
||||
}
|
||||
|
||||
// If there are no players to show, show the error message instead and
|
||||
// return.
|
||||
if (!AnimationsController.animationPlayers.length) {
|
||||
this.togglePlayers(false);
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.decorate(AnimationsPanel);
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
/* -*- 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 {createNode, getCssPropertyName} =
|
||||
require("devtools/client/inspector/animation-old/utils");
|
||||
const {Keyframes} = require("devtools/client/inspector/animation-old/components/keyframes");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying detailed information for a given
|
||||
* animation.
|
||||
* This includes information about timing, easing, keyframes, animated
|
||||
* properties.
|
||||
*
|
||||
* @param {Object} serverTraits The list of server-side capabilities.
|
||||
*/
|
||||
function AnimationDetails(serverTraits) {
|
||||
this.keyframeComponents = [];
|
||||
this.serverTraits = serverTraits;
|
||||
}
|
||||
|
||||
exports.AnimationDetails = AnimationDetails;
|
||||
|
||||
AnimationDetails.prototype = {
|
||||
// These are part of frame objects but are not animated properties. This
|
||||
// array is used to skip them.
|
||||
NON_PROPERTIES: ["easing", "composite", "computedOffset",
|
||||
"offset", "simulateComputeValuesFailure"],
|
||||
|
||||
init: function(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.unrender();
|
||||
this.containerEl = null;
|
||||
this.serverTraits = null;
|
||||
this.progressIndicatorEl = null;
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
for (const component of this.keyframeComponents) {
|
||||
component.destroy();
|
||||
}
|
||||
this.keyframeComponents = [];
|
||||
|
||||
while (this.containerEl.firstChild) {
|
||||
this.containerEl.firstChild.remove();
|
||||
}
|
||||
},
|
||||
|
||||
getPerfDataForProperty: function(animation, propertyName) {
|
||||
let warning = "";
|
||||
let className = "";
|
||||
if (animation.state.propertyState) {
|
||||
let isRunningOnCompositor;
|
||||
for (const propState of animation.state.propertyState) {
|
||||
if (propState.property == propertyName) {
|
||||
isRunningOnCompositor = propState.runningOnCompositor;
|
||||
if (typeof propState.warning != "undefined") {
|
||||
warning = propState.warning;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isRunningOnCompositor && warning == "") {
|
||||
className = "oncompositor";
|
||||
} else if (!isRunningOnCompositor && warning != "") {
|
||||
className = "warning";
|
||||
}
|
||||
}
|
||||
return {className, warning};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get animation types of given CSS property names.
|
||||
* @param {Array} CSS property names.
|
||||
* e.g. ["background-color", "opacity", ...]
|
||||
* @return {Object} Animation type mapped with CSS property name.
|
||||
* e.g. { "background-color": "color", }
|
||||
* "opacity": "float", ... }
|
||||
*/
|
||||
async getAnimationTypes(propertyNames) {
|
||||
if (this.serverTraits.hasGetAnimationTypes) {
|
||||
return this.animation.getAnimationTypes(propertyNames);
|
||||
}
|
||||
// Set animation type 'none' since does not support getAnimationTypes.
|
||||
const animationTypes = {};
|
||||
propertyNames.forEach(propertyName => {
|
||||
animationTypes[propertyName] = "none";
|
||||
});
|
||||
return Promise.resolve(animationTypes);
|
||||
},
|
||||
|
||||
async render(animation, tracks) {
|
||||
this.unrender();
|
||||
|
||||
if (!animation) {
|
||||
return;
|
||||
}
|
||||
this.animation = animation;
|
||||
this.tracks = tracks;
|
||||
|
||||
// We might have been destroyed in the meantime, or the component might
|
||||
// have been re-rendered.
|
||||
if (!this.containerEl || this.animation !== animation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get animation type for each CSS properties.
|
||||
const animationTypes = await this.getAnimationTypes(Object.keys(this.tracks));
|
||||
|
||||
// Render progress indicator.
|
||||
this.renderProgressIndicator();
|
||||
// Render animated properties header.
|
||||
this.renderAnimatedPropertiesHeader();
|
||||
// Render animated properties body.
|
||||
this.renderAnimatedPropertiesBody(animationTypes);
|
||||
|
||||
// Create dummy animation to indicate the animation progress.
|
||||
const timing = Object.assign({}, animation.state, {
|
||||
iterations: animation.state.iterationCount
|
||||
? animation.state.iterationCount : Infinity
|
||||
});
|
||||
this.dummyAnimation =
|
||||
new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
|
||||
},
|
||||
|
||||
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 (const 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
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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 (const propertyName in this.tracks) {
|
||||
if (!isUnchangedProperty(this.tracks[propertyName])) {
|
||||
propertyNames.push(propertyName);
|
||||
} else {
|
||||
unchangedPropertyNames.push(propertyName);
|
||||
}
|
||||
}
|
||||
Array.prototype.push.apply(propertyNames, unchangedPropertyNames);
|
||||
|
||||
for (const propertyName of propertyNames) {
|
||||
const line = createNode({
|
||||
parent: bodyEl,
|
||||
attributes: {"class": "property"}
|
||||
});
|
||||
if (unchangedPropertyNames.includes(propertyName)) {
|
||||
line.classList.add("unchanged");
|
||||
}
|
||||
const {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.
|
||||
const framesWrapperEl = createNode({
|
||||
parent: line,
|
||||
attributes: {"class": "track-container"}
|
||||
});
|
||||
|
||||
const framesEl = createNode({
|
||||
parent: framesWrapperEl,
|
||||
attributes: {"class": "frames"}
|
||||
});
|
||||
|
||||
const keyframesComponent = new Keyframes();
|
||||
keyframesComponent.init(framesEl);
|
||||
keyframesComponent.render({
|
||||
keyframes: this.tracks[propertyName],
|
||||
propertyName: propertyName,
|
||||
animation: this.animation,
|
||||
animationType: animationTypes[propertyName]
|
||||
});
|
||||
this.keyframeComponents.push(keyframesComponent);
|
||||
}
|
||||
},
|
||||
|
||||
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"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
indicateProgress: function(time) {
|
||||
if (!this.progressIndicatorEl) {
|
||||
// Not displayed yet.
|
||||
return;
|
||||
}
|
||||
const startTime = this.animation.state.previousStartTime || 0;
|
||||
this.dummyAnimation.currentTime =
|
||||
(time - startTime) * this.animation.state.playbackRate;
|
||||
this.progressIndicatorEl.style.left =
|
||||
`${ this.dummyAnimation.effect.getComputedTiming().progress * 100 }%`;
|
||||
},
|
||||
|
||||
get win() {
|
||||
return this.containerEl.ownerDocument.defaultView;
|
||||
}
|
||||
};
|
||||
|
||||
function isUnchangedProperty(values) {
|
||||
const firstValue = values[0].value;
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
if (values[i].value !== firstValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
/* -*- 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 {DomNodePreview} = require("devtools/client/inspector/shared/dom-node-preview");
|
||||
|
||||
// Map dom node fronts by animation fronts so we don't have to get them from the
|
||||
// walker every time the timeline is refreshed.
|
||||
var nodeFronts = new WeakMap();
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a preview of the target dom node of
|
||||
* a given animation.
|
||||
* Accepts the same parameters as the DomNodePreview component. See
|
||||
* devtools/client/inspector/shared/dom-node-preview.js for documentation.
|
||||
*/
|
||||
function AnimationTargetNode(inspector, options) {
|
||||
this.inspector = inspector;
|
||||
this.previewer = new DomNodePreview(inspector, options);
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.AnimationTargetNode = AnimationTargetNode;
|
||||
|
||||
AnimationTargetNode.prototype = {
|
||||
init: function(containerEl) {
|
||||
this.previewer.init(containerEl);
|
||||
this.isDestroyed = false;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.previewer.destroy();
|
||||
this.inspector = null;
|
||||
this.isDestroyed = true;
|
||||
},
|
||||
|
||||
async render(playerFront) {
|
||||
// Get the nodeFront from the cache if it was stored previously.
|
||||
let nodeFront = nodeFronts.get(playerFront);
|
||||
|
||||
// Try and get it from the playerFront directly next.
|
||||
if (!nodeFront) {
|
||||
nodeFront = playerFront.animationTargetNodeFront;
|
||||
}
|
||||
|
||||
// Finally, get it from the walkerActor if it wasn't found.
|
||||
if (!nodeFront) {
|
||||
try {
|
||||
nodeFront = await this.inspector.walker.getNodeFromActor(
|
||||
playerFront.actorID, ["node"]);
|
||||
} catch (e) {
|
||||
// If an error occured while getting the nodeFront and if it can't be
|
||||
// attributed to the panel having been destroyed in the meantime, this
|
||||
// error needs to be logged and render needs to stop.
|
||||
if (!this.isDestroyed) {
|
||||
console.error(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In all cases, if by now the panel doesn't exist anymore, we need to
|
||||
// stop rendering too.
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the nodeFront to the cache.
|
||||
nodeFronts.set(playerFront, nodeFront);
|
||||
|
||||
this.previewer.render(nodeFront);
|
||||
this.emit("target-retrieved");
|
||||
}
|
||||
};
|
||||
|
|
@ -1,677 +0,0 @@
|
|||
/* -*- 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, createSVGNode, TimeScale, getFormattedAnimationTitle} =
|
||||
require("devtools/client/inspector/animation-old/utils");
|
||||
const {SummaryGraphHelper, getPreferredKeyframesProgressThreshold,
|
||||
getPreferredProgressThreshold} =
|
||||
require("devtools/client/inspector/animation-old/graph-helper");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// Show max 10 iterations for infinite animations
|
||||
// to give users a clue that the animation does repeat.
|
||||
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
|
||||
|
||||
// Minimum opacity for semitransparent fill color for keyframes's easing graph.
|
||||
const MIN_KEYFRAMES_EASING_OPACITY = .5;
|
||||
|
||||
/**
|
||||
* 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, 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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update animation and updating its DOM accordingly.
|
||||
* Unlike 'render' method, this method does not generate any elements, but update
|
||||
* the bounds of DOM.
|
||||
* @param {Object} animation
|
||||
*/
|
||||
update: function(animation) {
|
||||
this.animation = animation;
|
||||
this.updateSummaryGraphViewBox(this.containerEl.querySelector(".summary"));
|
||||
const delayEl = this.containerEl.querySelector(".delay");
|
||||
if (delayEl) {
|
||||
this.updateDelayBounds(delayEl);
|
||||
}
|
||||
const endDelayEl = this.containerEl.querySelector(".end-delay");
|
||||
if (endDelayEl) {
|
||||
this.updateEndDelayBounds(endDelayEl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update viewBox and style of SVG element for summary graph to fit to latest
|
||||
* TimeScale.
|
||||
* @param {Element} summaryEl - SVG element for summary graph.
|
||||
*/
|
||||
updateSummaryGraphViewBox: function(summaryEl) {
|
||||
const {x, delayW} = TimeScale.getAnimationDimensions(this.animation);
|
||||
const totalDisplayedDuration = this.getTotalDisplayedDuration();
|
||||
const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
|
||||
const {state} = this.animation;
|
||||
summaryEl.setAttribute("viewBox",
|
||||
`${state.delay < 0 ? state.delay : 0} ` +
|
||||
`-${1 + strokeHeightForViewBox} ` +
|
||||
`${totalDisplayedDuration} ` +
|
||||
`${1 + strokeHeightForViewBox * 2}`);
|
||||
summaryEl.setAttribute("style", `left: ${ x - (state.delay > 0 ? delayW : 0) }%`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update bounds of element which represents delay to fit to latest TimeScale.
|
||||
* @param {Element} delayEl - which represents delay.
|
||||
*/
|
||||
updateDelayBounds: function(delayEl) {
|
||||
const {delayX, delayW} = TimeScale.getAnimationDimensions(this.animation);
|
||||
delayEl.style.left = `${ delayX }%`;
|
||||
delayEl.style.width = `${ delayW }%`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update bounds of element which represents endDelay to fit to latest TimeScale.
|
||||
* @param {Element} endDelayEl - which represents endDelay.
|
||||
*/
|
||||
updateEndDelayBounds: function(endDelayEl) {
|
||||
const {endDelayX, endDelayW} = TimeScale.getAnimationDimensions(this.animation);
|
||||
endDelayEl.style.left = `${ endDelayX }%`;
|
||||
endDelayEl.style.width = `${ endDelayW }%`;
|
||||
},
|
||||
|
||||
getTotalDisplayedDuration: function() {
|
||||
return this.animation.state.playbackRate * TimeScale.getDuration();
|
||||
},
|
||||
|
||||
getTooltipText: function(state) {
|
||||
const 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) {
|
||||
const iterationStartTime = state.iterationStart * state.duration / 1000;
|
||||
text += L10N.getFormatStr("player.animationIterationStartLabel",
|
||||
state.iterationStart,
|
||||
L10N.numberWithDecimals(iterationStartTime, 2));
|
||||
text += "\n";
|
||||
}
|
||||
|
||||
// Adding the easing if it is not "linear".
|
||||
if (state.easing && state.easing !== "linear") {
|
||||
text += L10N.getStr("player.animationOverallEasingLabel") + " ";
|
||||
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 it is not "normal".
|
||||
if (state.direction && state.direction !== "normal") {
|
||||
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 the animation-timing-function
|
||||
// if it is not "ease" which is default value for CSS Animations.
|
||||
if (state.animationTimingFunction && state.animationTimingFunction !== "ease") {
|
||||
text += L10N.getStr("player.animationTimingFunctionLabel") + " ";
|
||||
text += state.animationTimingFunction;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render keyframes's easing graph.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Object} state - State of animation.
|
||||
* @param {float} totalDisplayedDuration - Displayable total duration.
|
||||
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
|
||||
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
|
||||
* @param {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderKeyframesEasingGraph(parentEl, state, totalDisplayedDuration,
|
||||
minEffectProgressThreshold, tracks, graphHelper) {
|
||||
const keyframesList = getOffsetAndEasingOnlyKeyframesList(tracks);
|
||||
const keyframeEasingOpacity = Math.max(1 / keyframesList.length,
|
||||
MIN_KEYFRAMES_EASING_OPACITY);
|
||||
for (const keyframes of keyframesList) {
|
||||
const minProgressTreshold =
|
||||
Math.min(minEffectProgressThreshold,
|
||||
getPreferredKeyframesProgressThreshold(keyframes));
|
||||
graphHelper.setMinProgressThreshold(minProgressTreshold);
|
||||
graphHelper.setKeyframes(keyframes);
|
||||
graphHelper.setClosePathNeeded(true);
|
||||
const element = renderGraph(parentEl, state, totalDisplayedDuration,
|
||||
"keyframes-easing", graphHelper);
|
||||
element.style.opacity = keyframeEasingOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render effect easing graph.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Object} state - State of animation.
|
||||
* @param {float} totalDisplayedDuration - Displayable total duration.
|
||||
* @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
|
||||
* @param {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderEffectEasingGraph(parentEl, state, totalDisplayedDuration,
|
||||
minEffectProgressThreshold, graphHelper) {
|
||||
graphHelper.setMinProgressThreshold(minEffectProgressThreshold);
|
||||
graphHelper.setKeyframes(null);
|
||||
graphHelper.setClosePathNeeded(false);
|
||||
renderGraph(parentEl, state, totalDisplayedDuration, "effect-easing", graphHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a graph of given parameters.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Object} state - State of animation.
|
||||
* @param {float} totalDisplayedDuration - Displayable total duration.
|
||||
* @param {String} className - Class name for graph element.
|
||||
* @param {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderGraph(parentEl, state, totalDisplayedDuration, className, graphHelper) {
|
||||
const graphEl = createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "g",
|
||||
attributes: {
|
||||
"class": className,
|
||||
}
|
||||
});
|
||||
|
||||
// Starting time of main iteration.
|
||||
let mainIterationStartTime = 0;
|
||||
let iterationStart = state.iterationStart;
|
||||
let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
|
||||
|
||||
graphHelper.setFillMode(state.fill);
|
||||
graphHelper.setOriginalBehavior(true);
|
||||
|
||||
// Append delay.
|
||||
if (state.delay > 0) {
|
||||
renderDelay(graphEl, state, graphHelper);
|
||||
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(graphEl, state, mainIterationStartTime,
|
||||
firstSectionCount, graphHelper);
|
||||
}
|
||||
|
||||
if (iterationCount === Infinity) {
|
||||
// If the animation repeats infinitely,
|
||||
// we fill the remaining area with iteration paths.
|
||||
renderInfinity(graphEl, state, mainIterationStartTime,
|
||||
firstSectionCount, totalDisplayedDuration, graphHelper);
|
||||
} else {
|
||||
// Otherwise, we show remaining iterations, endDelay and fill.
|
||||
|
||||
// Append forwards fill-mode.
|
||||
if (state.fill === "both" || state.fill === "forwards") {
|
||||
renderForwardsFill(graphEl, state, mainIterationStartTime,
|
||||
iterationCount, totalDisplayedDuration, graphHelper);
|
||||
}
|
||||
|
||||
// Append middle section of iterations.
|
||||
// e.g.
|
||||
// if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
|
||||
const middleSectionCount =
|
||||
Math.floor(iterationCount - firstSectionCount);
|
||||
renderMiddleIterations(graphEl, state, mainIterationStartTime,
|
||||
firstSectionCount, middleSectionCount, graphHelper);
|
||||
|
||||
// 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(graphEl, state, mainIterationStartTime,
|
||||
firstSectionCount, middleSectionCount,
|
||||
lastSectionCount, graphHelper);
|
||||
}
|
||||
|
||||
// Append endDelay.
|
||||
if (state.endDelay > 0) {
|
||||
renderEndDelay(graphEl, state,
|
||||
mainIterationStartTime, iterationCount, graphHelper);
|
||||
}
|
||||
}
|
||||
|
||||
// Append negative delay (which overlap the animation).
|
||||
if (state.delay < 0) {
|
||||
graphHelper.setFillMode("both");
|
||||
graphHelper.setOriginalBehavior(false);
|
||||
renderNegativeDelayHiddenProgress(graphEl, state, graphHelper);
|
||||
}
|
||||
// Append negative endDelay (which overlap the animation).
|
||||
if (state.iterationCount && state.endDelay < 0) {
|
||||
graphHelper.setFillMode("both");
|
||||
graphHelper.setOriginalBehavior(false);
|
||||
renderNegativeEndDelayHiddenProgress(graphEl, state, graphHelper);
|
||||
}
|
||||
|
||||
return graphEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render delay section.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Object} state - State of animation.
|
||||
* @param {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderDelay(parentEl, state, graphHelper) {
|
||||
const startSegment = graphHelper.getSegment(0);
|
||||
const endSegment = { x: state.delay, y: startSegment.y };
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderFirstIteration(parentEl, state, mainIterationStartTime,
|
||||
firstSectionCount, graphHelper) {
|
||||
const startTime = mainIterationStartTime;
|
||||
const endTime = startTime + firstSectionCount * state.duration;
|
||||
const segments = graphHelper.createPathSegments(startTime, endTime);
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderMiddleIterations(parentEl, state, mainIterationStartTime,
|
||||
firstSectionCount, middleSectionCount,
|
||||
graphHelper) {
|
||||
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 = graphHelper.createPathSegments(startTime, endTime);
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderLastIteration(parentEl, state, mainIterationStartTime,
|
||||
firstSectionCount, middleSectionCount,
|
||||
lastSectionCount, graphHelper) {
|
||||
const startTime = mainIterationStartTime +
|
||||
(firstSectionCount + middleSectionCount) * state.duration;
|
||||
const endTime = startTime + lastSectionCount * state.duration;
|
||||
const segments = graphHelper.createPathSegments(startTime, endTime);
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderInfinity(parentEl, state, mainIterationStartTime,
|
||||
firstSectionCount, totalDuration, graphHelper) {
|
||||
// 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 =
|
||||
graphHelper.createPathSegments(firstStartTime, firstEndTime);
|
||||
graphHelper.appendShapePath(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 };
|
||||
});
|
||||
}
|
||||
graphHelper.appendShapePath(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} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderEndDelay(parentEl, state,
|
||||
mainIterationStartTime, iterationCount, graphHelper) {
|
||||
const startTime = mainIterationStartTime + iterationCount * state.duration;
|
||||
const startSegment = graphHelper.getSegment(startTime);
|
||||
const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
|
||||
graphHelper.appendShapePath(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} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderForwardsFill(parentEl, state, mainIterationStartTime,
|
||||
iterationCount, totalDuration, graphHelper) {
|
||||
const startTime = mainIterationStartTime + iterationCount * state.duration +
|
||||
(state.endDelay > 0 ? state.endDelay : 0);
|
||||
const startSegment = graphHelper.getSegment(startTime);
|
||||
const endSegment = { x: totalDuration, y: startSegment.y };
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
|
||||
const startTime = state.delay;
|
||||
const endTime = 0;
|
||||
const segments =
|
||||
graphHelper.createPathSegments(startTime, endTime);
|
||||
graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
|
||||
*/
|
||||
function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
|
||||
const endTime = state.delay + state.iterationCount * state.duration;
|
||||
const startTime = endTime + state.endDelay;
|
||||
const segments = graphHelper.createPathSegments(startTime, endTime);
|
||||
graphHelper.appendShapePath(parentEl, segments, "enddelay-path negative");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new keyframes object which has only offset and easing.
|
||||
* Also, the returned value has no duplication.
|
||||
* @param {Object} tracks - The value of AnimationsTimeline.getTracks().
|
||||
* @return {Array} keyframes list.
|
||||
*/
|
||||
function getOffsetAndEasingOnlyKeyframesList(tracks) {
|
||||
return Object.keys(tracks).reduce((result, name) => {
|
||||
const track = tracks[name];
|
||||
const exists = result.find(keyframes => {
|
||||
if (track.length !== keyframes.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < track.length; i++) {
|
||||
const keyframe1 = track[i];
|
||||
const keyframe2 = keyframes[i];
|
||||
if (keyframe1.offset !== keyframe2.offset ||
|
||||
keyframe1.easing !== keyframe2.easing) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!exists) {
|
||||
const keyframes = track.map(keyframe => {
|
||||
return { offset: keyframe.offset, easing: keyframe.easing };
|
||||
});
|
||||
result.push(keyframes);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
|
@ -1,806 +0,0 @@
|
|||
/* -*- 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,
|
||||
findOptimalTimeInterval,
|
||||
getFormattedAnimationTitle,
|
||||
TimeScale,
|
||||
getCssPropertyName
|
||||
} = require("devtools/client/inspector/animation-old/utils");
|
||||
const { AnimationDetails } = require("devtools/client/inspector/animation-old/components/animation-details");
|
||||
const { AnimationTargetNode } = require("devtools/client/inspector/animation-old/components/animation-target-node");
|
||||
const { AnimationTimeBlock } = require("devtools/client/inspector/animation-old/components/animation-time-block");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// The minimum spacing between 2 time graduation headers in the timeline (px).
|
||||
const TIME_GRADUATION_MIN_SPACING = 40;
|
||||
// When the container window is resized, the timeline background gets refreshed,
|
||||
// but only after a timer, and the timer is reset if the window is continuously
|
||||
// resized.
|
||||
const TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER = 50;
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a timeline for animations.
|
||||
* The timeline is essentially a graph with time along the x axis and animations
|
||||
* along the y axis.
|
||||
* The time is represented with a graduation header at the top and a current
|
||||
* time play head.
|
||||
* Animations are organized by lines, with a left margin containing the preview
|
||||
* of the target DOM element the animation applies to.
|
||||
* The current time play head can be moved by clicking/dragging in the header.
|
||||
* when this happens, the component emits "current-data-changed" events with the
|
||||
* new time and state of the timeline.
|
||||
*
|
||||
* @param {InspectorPanel} inspector.
|
||||
* @param {Object} serverTraits The list of server-side capabilities.
|
||||
*/
|
||||
function AnimationsTimeline(inspector, serverTraits) {
|
||||
this.animations = [];
|
||||
this.componentsMap = {};
|
||||
this.inspector = inspector;
|
||||
this.serverTraits = serverTraits;
|
||||
|
||||
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
|
||||
this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
|
||||
this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
|
||||
this.onScrubberMouseOut = this.onScrubberMouseOut.bind(this);
|
||||
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
|
||||
this.onAnimationSelected = this.onAnimationSelected.bind(this);
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
this.onTimelineDataChanged = this.onTimelineDataChanged.bind(this);
|
||||
this.onDetailCloseButtonClick = this.onDetailCloseButtonClick.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.AnimationsTimeline = AnimationsTimeline;
|
||||
|
||||
AnimationsTimeline.prototype = {
|
||||
init: function(containerEl) {
|
||||
this.win = containerEl.ownerDocument.defaultView;
|
||||
this.rootWrapperEl = containerEl;
|
||||
|
||||
this.setupSplitBox();
|
||||
this.setupAnimationTimeline();
|
||||
this.setupAnimationDetail();
|
||||
|
||||
this.win.addEventListener("resize",
|
||||
this.onWindowResize);
|
||||
},
|
||||
|
||||
setupSplitBox: function() {
|
||||
const browserRequire = this.win.BrowserLoader({
|
||||
window: this.win,
|
||||
useOnlyShared: true
|
||||
}).require;
|
||||
|
||||
const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
|
||||
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
|
||||
const SplitBox = createFactory(
|
||||
browserRequire("devtools/client/shared/components/splitter/SplitBox"));
|
||||
|
||||
const splitter = SplitBox({
|
||||
className: "animation-root",
|
||||
splitterSize: 1,
|
||||
initialHeight: "50%",
|
||||
endPanelControl: true,
|
||||
startPanel: dom.div({
|
||||
className: "animation-timeline"
|
||||
}),
|
||||
endPanel: dom.div({
|
||||
className: "animation-detail"
|
||||
}),
|
||||
vert: false
|
||||
});
|
||||
|
||||
ReactDOM.render(splitter, this.rootWrapperEl);
|
||||
|
||||
this.animationRootEl = this.rootWrapperEl.querySelector(".animation-root");
|
||||
},
|
||||
|
||||
setupAnimationTimeline: function() {
|
||||
const animationTimelineEl = this.rootWrapperEl.querySelector(".animation-timeline");
|
||||
|
||||
const scrubberContainer = createNode({
|
||||
parent: animationTimelineEl,
|
||||
attributes: {"class": "scrubber-wrapper"}
|
||||
});
|
||||
|
||||
this.scrubberEl = createNode({
|
||||
parent: scrubberContainer,
|
||||
attributes: {
|
||||
"class": "scrubber"
|
||||
}
|
||||
});
|
||||
|
||||
this.scrubberHandleEl = createNode({
|
||||
parent: this.scrubberEl,
|
||||
attributes: {
|
||||
"class": "scrubber-handle"
|
||||
}
|
||||
});
|
||||
createNode({
|
||||
parent: this.scrubberHandleEl,
|
||||
attributes: {
|
||||
"class": "scrubber-line"
|
||||
}
|
||||
});
|
||||
this.scrubberHandleEl.addEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
|
||||
this.headerWrapper = createNode({
|
||||
parent: animationTimelineEl,
|
||||
attributes: {
|
||||
"class": "header-wrapper"
|
||||
}
|
||||
});
|
||||
|
||||
this.timeHeaderEl = createNode({
|
||||
parent: this.headerWrapper,
|
||||
attributes: {
|
||||
"class": "time-header track-container"
|
||||
}
|
||||
});
|
||||
|
||||
this.timeHeaderEl.addEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
|
||||
this.timeTickEl = createNode({
|
||||
parent: animationTimelineEl,
|
||||
attributes: {
|
||||
"class": "time-body track-container"
|
||||
}
|
||||
});
|
||||
|
||||
this.animationsEl = createNode({
|
||||
parent: animationTimelineEl,
|
||||
nodeType: "ul",
|
||||
attributes: {
|
||||
"class": "animations devtools-monospace"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupAnimationDetail: function() {
|
||||
const animationDetailEl = this.rootWrapperEl.querySelector(".animation-detail");
|
||||
|
||||
const animationDetailHeaderEl = createNode({
|
||||
parent: animationDetailEl,
|
||||
attributes: {
|
||||
"class": "animation-detail-header"
|
||||
}
|
||||
});
|
||||
|
||||
const headerTitleEl = createNode({
|
||||
parent: animationDetailHeaderEl,
|
||||
attributes: {
|
||||
"class": "devtools-toolbar"
|
||||
}
|
||||
});
|
||||
|
||||
createNode({
|
||||
parent: headerTitleEl,
|
||||
textContent: L10N.getStr("detail.headerTitle")
|
||||
});
|
||||
|
||||
this.animationAnimationNameEl = createNode({
|
||||
parent: headerTitleEl
|
||||
});
|
||||
|
||||
this.animationDetailCloseButton = createNode({
|
||||
parent: headerTitleEl,
|
||||
nodeType: "button",
|
||||
attributes: {
|
||||
"class": "devtools-button",
|
||||
title: L10N.getStr("detail.header.closeLabel"),
|
||||
}
|
||||
});
|
||||
this.animationDetailCloseButton.addEventListener("click",
|
||||
this.onDetailCloseButtonClick);
|
||||
|
||||
const animationDetailBodyEl = createNode({
|
||||
parent: animationDetailEl,
|
||||
attributes: {
|
||||
"class": "animation-detail-body"
|
||||
}
|
||||
});
|
||||
|
||||
this.animatedPropertiesEl = createNode({
|
||||
parent: animationDetailBodyEl,
|
||||
attributes: {
|
||||
"class": "animated-properties"
|
||||
}
|
||||
});
|
||||
|
||||
this.details = new AnimationDetails(this.serverTraits);
|
||||
this.details.init(this.animatedPropertiesEl);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.stopAnimatingScrubber();
|
||||
this.unrender();
|
||||
this.details.destroy();
|
||||
|
||||
this.win.removeEventListener("resize",
|
||||
this.onWindowResize);
|
||||
this.timeHeaderEl.removeEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
this.scrubberHandleEl.removeEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
this.animationDetailCloseButton.removeEventListener("click",
|
||||
this.onDetailCloseButtonClick);
|
||||
|
||||
this.rootWrapperEl.remove();
|
||||
this.animations = [];
|
||||
this.componentsMap = null;
|
||||
this.rootWrapperEl = null;
|
||||
this.timeHeaderEl = null;
|
||||
this.animationsEl = null;
|
||||
this.animatedPropertiesEl = null;
|
||||
this.scrubberEl = null;
|
||||
this.scrubberHandleEl = null;
|
||||
this.win = null;
|
||||
this.inspector = null;
|
||||
this.serverTraits = null;
|
||||
this.animationDetailEl = null;
|
||||
this.animationAnimationNameEl = null;
|
||||
this.animatedPropertiesEl = null;
|
||||
this.animationDetailCloseButton = null;
|
||||
this.animationRootEl = null;
|
||||
this.selectedAnimation = null;
|
||||
|
||||
this.isDestroyed = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy all sub-components that have been created and stored on this instance.
|
||||
*/
|
||||
destroyAllSubComponents: function() {
|
||||
for (const actorID in this.componentsMap) {
|
||||
this.destroySubComponents(actorID);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy sub-components which related to given actor id.
|
||||
* @param {String} actor id
|
||||
*/
|
||||
destroySubComponents: function(actorID) {
|
||||
const components = this.componentsMap[actorID];
|
||||
components.timeBlock.destroy();
|
||||
components.targetNode.destroy();
|
||||
components.animationEl.remove();
|
||||
delete components.state;
|
||||
delete components.tracks;
|
||||
delete this.componentsMap[actorID];
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
for (const animation of this.animations) {
|
||||
animation.off("changed", this.onAnimationStateChanged);
|
||||
}
|
||||
this.stopAnimatingScrubber();
|
||||
TimeScale.reset();
|
||||
this.destroyAllSubComponents();
|
||||
this.animationsEl.innerHTML = "";
|
||||
this.off("timeline-data-changed", this.onTimelineDataChanged);
|
||||
this.details.unrender();
|
||||
},
|
||||
|
||||
onWindowResize: function() {
|
||||
// Don't do anything if the root element has a width of 0
|
||||
if (this.rootWrapperEl.offsetWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.windowResizeTimer) {
|
||||
this.win.clearTimeout(this.windowResizeTimer);
|
||||
}
|
||||
|
||||
this.windowResizeTimer = this.win.setTimeout(() => {
|
||||
this.drawHeaderAndBackground();
|
||||
}, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
|
||||
},
|
||||
|
||||
async onAnimationSelected(animation) {
|
||||
const index = this.animations.indexOf(animation);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unselect an animation which was selected.
|
||||
const animationEls = this.rootWrapperEl.querySelectorAll(".animation");
|
||||
for (let i = 0; i < animationEls.length; i++) {
|
||||
const animationEl = animationEls[i];
|
||||
if (!animationEl.classList.contains("selected")) {
|
||||
continue;
|
||||
}
|
||||
if (i === index) {
|
||||
if (this.animationRootEl.classList.contains("animation-detail-visible")) {
|
||||
// Already the animation is selected.
|
||||
this.emit("animation-already-selected", this.animations[i]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
animationEl.classList.remove("selected");
|
||||
this.emit("animation-unselected", this.animations[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Add class of animation type to animatedPropertiesEl to display the compositor sign.
|
||||
if (!this.animatedPropertiesEl.classList.contains(animation.state.type)) {
|
||||
this.animatedPropertiesEl.className =
|
||||
`animated-properties ${ animation.state.type }`;
|
||||
}
|
||||
|
||||
// Select and render.
|
||||
const selectedAnimationEl = animationEls[index];
|
||||
selectedAnimationEl.classList.add("selected");
|
||||
this.animationRootEl.classList.add("animation-detail-visible");
|
||||
// Don't render if the detail displays same animation already.
|
||||
if (animation !== this.details.animation) {
|
||||
this.selectedAnimation = animation;
|
||||
await this.details.render(animation, this.componentsMap[animation.actorID].tracks);
|
||||
this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
|
||||
}
|
||||
this.onTimelineDataChanged({ time: this.currentTime || 0 });
|
||||
this.emit("animation-selected", animation);
|
||||
},
|
||||
|
||||
/**
|
||||
* When move the scrubber to the corresponding position
|
||||
*/
|
||||
onScrubberMouseDown: function(e) {
|
||||
this.moveScrubberTo(e.pageX);
|
||||
this.win.addEventListener("mouseup", this.onScrubberMouseUp);
|
||||
this.win.addEventListener("mouseout", this.onScrubberMouseOut);
|
||||
this.win.addEventListener("mousemove", this.onScrubberMouseMove);
|
||||
|
||||
// Prevent text selection while dragging.
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
onScrubberMouseUp: function() {
|
||||
this.cancelTimeHeaderDragging();
|
||||
},
|
||||
|
||||
onScrubberMouseOut: function(e) {
|
||||
// Check that mouseout happened on the window itself, and if yes, cancel
|
||||
// the dragging.
|
||||
if (!this.win.document.contains(e.relatedTarget)) {
|
||||
this.cancelTimeHeaderDragging();
|
||||
}
|
||||
},
|
||||
|
||||
cancelTimeHeaderDragging: function() {
|
||||
this.win.removeEventListener("mouseup", this.onScrubberMouseUp);
|
||||
this.win.removeEventListener("mouseout", this.onScrubberMouseOut);
|
||||
this.win.removeEventListener("mousemove", this.onScrubberMouseMove);
|
||||
},
|
||||
|
||||
onScrubberMouseMove: function(e) {
|
||||
this.moveScrubberTo(e.pageX);
|
||||
},
|
||||
|
||||
moveScrubberTo: function(pageX, noOffset) {
|
||||
this.stopAnimatingScrubber();
|
||||
|
||||
// The offset needs to be in % and relative to the timeline's area (so we
|
||||
// subtract the scrubber's left offset, which is equal to the sidebar's
|
||||
// width).
|
||||
let offset = pageX;
|
||||
if (!noOffset) {
|
||||
offset -= this.timeHeaderEl.offsetLeft;
|
||||
}
|
||||
offset = offset * 100 / this.timeHeaderEl.offsetWidth;
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
this.scrubberEl.style.left = offset + "%";
|
||||
|
||||
const time = TimeScale.distanceToRelativeTime(offset);
|
||||
|
||||
this.emit("timeline-data-changed", {
|
||||
isPaused: true,
|
||||
isMoving: false,
|
||||
isUserDrag: true,
|
||||
time: time
|
||||
});
|
||||
},
|
||||
|
||||
getCompositorStatusClassName: function(state) {
|
||||
let className = state.isRunningOnCompositor
|
||||
? " fast-track"
|
||||
: "";
|
||||
|
||||
if (state.isRunningOnCompositor && state.propertyState) {
|
||||
className +=
|
||||
state.propertyState.some(propState => !propState.runningOnCompositor)
|
||||
? " some-properties"
|
||||
: " all-properties";
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
|
||||
async render(animations, documentCurrentTime) {
|
||||
this.animations = animations;
|
||||
|
||||
// Destroy components which are no longer existed in given animations.
|
||||
for (const animation of this.animations) {
|
||||
if (this.componentsMap[animation.actorID]) {
|
||||
this.componentsMap[animation.actorID].needToLeave = true;
|
||||
}
|
||||
}
|
||||
for (const actorID in this.componentsMap) {
|
||||
const components = this.componentsMap[actorID];
|
||||
if (components.needToLeave) {
|
||||
delete components.needToLeave;
|
||||
} else {
|
||||
this.destroySubComponents(actorID);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.animations.length) {
|
||||
this.emit("animation-timeline-rendering-completed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop to set the time scale for all current animations.
|
||||
TimeScale.reset();
|
||||
for (const {state} of animations) {
|
||||
TimeScale.addAnimation(state);
|
||||
}
|
||||
|
||||
this.drawHeaderAndBackground();
|
||||
|
||||
for (const animation of this.animations) {
|
||||
animation.on("changed", this.onAnimationStateChanged);
|
||||
|
||||
const tracks = await this.getTracks(animation);
|
||||
// If we're destroyed by now, just give up.
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.componentsMap[animation.actorID]) {
|
||||
// Update animation UI using existent components.
|
||||
this.updateAnimation(animation, tracks, this.componentsMap[animation.actorID]);
|
||||
} else {
|
||||
// Render animation UI as new element.
|
||||
const animationEl = createNode({
|
||||
parent: this.animationsEl,
|
||||
nodeType: "li",
|
||||
});
|
||||
this.renderAnimation(animation, tracks, animationEl);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// To indicate the animation progress in AnimationDetails.
|
||||
this.on("timeline-data-changed", this.onTimelineDataChanged);
|
||||
|
||||
if (this.animations.length === 1) {
|
||||
// Display animation's detail if there is only one animation,
|
||||
// even if the detail pane is closing.
|
||||
await this.onAnimationSelected(this.animations[0]);
|
||||
} else if (this.animationRootEl.classList.contains("animation-detail-visible") &&
|
||||
this.animations.includes(this.selectedAnimation)) {
|
||||
// animation's detail displays in case of the previously displayed animation is
|
||||
// included in timeline list and the detail pane is not closing.
|
||||
await this.onAnimationSelected(this.selectedAnimation);
|
||||
} else {
|
||||
// Otherwise, close detail pane.
|
||||
this.onDetailCloseButtonClick();
|
||||
}
|
||||
this.emit("animation-timeline-rendering-completed");
|
||||
},
|
||||
|
||||
updateAnimation: function(animation, tracks, existentComponents) {
|
||||
// If keyframes (tracks) and effect timing (state) are not changed, we update the
|
||||
// view box only.
|
||||
// As an exception, if iterationCount reprensents Infinity, we need to re-render
|
||||
// the shape along new time scale.
|
||||
// FIXME: To avoid re-rendering even Infinity, we need to change the
|
||||
// representation for Infinity.
|
||||
if (animation.state.iterationCount &&
|
||||
areTimingEffectsEqual(existentComponents.state, animation.state) &&
|
||||
existentComponents.tracks.toString() === tracks.toString()) {
|
||||
// Update timeBlock.
|
||||
existentComponents.timeBlock.update(animation);
|
||||
} else {
|
||||
// Destroy previous components.
|
||||
existentComponents.timeBlock.destroy();
|
||||
existentComponents.targetNode.destroy();
|
||||
// Remove children to re-use.
|
||||
existentComponents.animationEl.innerHTML = "";
|
||||
// Re-render animation using existent animationEl.
|
||||
this.renderAnimation(animation, tracks, existentComponents.animationEl);
|
||||
}
|
||||
},
|
||||
|
||||
renderAnimation: function(animation, tracks, animationEl) {
|
||||
animationEl.setAttribute("class",
|
||||
"animation " + animation.state.type +
|
||||
this.getCompositorStatusClassName(animation.state));
|
||||
|
||||
// Left sidebar for the animated node.
|
||||
const animatedNodeEl = createNode({
|
||||
parent: animationEl,
|
||||
attributes: {
|
||||
"class": "target"
|
||||
}
|
||||
});
|
||||
|
||||
// Draw the animated node target.
|
||||
const targetNode = new AnimationTargetNode(this.inspector, {compact: true});
|
||||
targetNode.init(animatedNodeEl);
|
||||
targetNode.render(animation);
|
||||
|
||||
// Right-hand part contains the timeline itself (called time-block here).
|
||||
const timeBlockEl = createNode({
|
||||
parent: animationEl,
|
||||
attributes: {
|
||||
"class": "time-block track-container"
|
||||
}
|
||||
});
|
||||
|
||||
// Draw the animation time block.
|
||||
const timeBlock = new AnimationTimeBlock();
|
||||
timeBlock.init(timeBlockEl);
|
||||
timeBlock.render(animation, tracks);
|
||||
timeBlock.on("selected", this.onAnimationSelected);
|
||||
|
||||
this.componentsMap[animation.actorID] = {
|
||||
animationEl, targetNode, timeBlock, tracks, state: animation.state
|
||||
};
|
||||
},
|
||||
|
||||
isAtLeastOneAnimationPlaying: function() {
|
||||
return this.animations.some(({state}) => state.playState === "running");
|
||||
},
|
||||
|
||||
wasRewound: function() {
|
||||
return !this.isAtLeastOneAnimationPlaying() &&
|
||||
this.animations.every(({state}) => state.currentTime === 0);
|
||||
},
|
||||
|
||||
hasInfiniteAnimations: function() {
|
||||
return this.animations.some(({state}) => !state.iterationCount);
|
||||
},
|
||||
|
||||
startAnimatingScrubber: function(time) {
|
||||
const isOutOfBounds = time < TimeScale.minStartTime ||
|
||||
time > TimeScale.maxEndTime;
|
||||
const isAllPaused = !this.isAtLeastOneAnimationPlaying();
|
||||
const hasInfinite = this.hasInfiniteAnimations();
|
||||
|
||||
let x = TimeScale.startTimeToDistance(time);
|
||||
if (x > 100 && !hasInfinite) {
|
||||
x = 100;
|
||||
}
|
||||
this.scrubberEl.style.left = x + "%";
|
||||
|
||||
// Only stop the scrubber if it's out of bounds or all animations have been
|
||||
// paused, but not if at least an animation is infinite.
|
||||
if (isAllPaused || (isOutOfBounds && !hasInfinite)) {
|
||||
this.stopAnimatingScrubber();
|
||||
this.emit("timeline-data-changed", {
|
||||
isPaused: !this.isAtLeastOneAnimationPlaying(),
|
||||
isMoving: false,
|
||||
isUserDrag: false,
|
||||
time: TimeScale.distanceToRelativeTime(x)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("timeline-data-changed", {
|
||||
isPaused: false,
|
||||
isMoving: true,
|
||||
isUserDrag: false,
|
||||
time: TimeScale.distanceToRelativeTime(x)
|
||||
});
|
||||
|
||||
const now = this.win.performance.now();
|
||||
this.rafID = this.win.requestAnimationFrame(() => {
|
||||
if (!this.rafID) {
|
||||
// In case the scrubber was stopped in the meantime.
|
||||
return;
|
||||
}
|
||||
this.startAnimatingScrubber(time + this.win.performance.now() - now);
|
||||
});
|
||||
},
|
||||
|
||||
stopAnimatingScrubber: function() {
|
||||
if (this.rafID) {
|
||||
this.win.cancelAnimationFrame(this.rafID);
|
||||
this.rafID = null;
|
||||
}
|
||||
},
|
||||
|
||||
onAnimationStateChanged: function() {
|
||||
// For now, simply re-render the component. The animation front's state has
|
||||
// already been updated.
|
||||
this.render(this.animations);
|
||||
},
|
||||
|
||||
drawHeaderAndBackground: function() {
|
||||
const width = this.timeHeaderEl.offsetWidth;
|
||||
const animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
|
||||
const minTimeInterval = TIME_GRADUATION_MIN_SPACING *
|
||||
animationDuration / width;
|
||||
const intervalLength = findOptimalTimeInterval(minTimeInterval);
|
||||
const intervalWidth = intervalLength * width / animationDuration;
|
||||
|
||||
// And the time graduation header.
|
||||
this.timeHeaderEl.innerHTML = "";
|
||||
this.timeTickEl.innerHTML = "";
|
||||
|
||||
for (let i = 0; i <= width / intervalWidth; i++) {
|
||||
const pos = 100 * i * intervalWidth / width;
|
||||
|
||||
// This element is the header of time tick for displaying animation
|
||||
// duration time.
|
||||
createNode({
|
||||
parent: this.timeHeaderEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "header-item",
|
||||
"style": `left:${pos}%`
|
||||
},
|
||||
textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos))
|
||||
});
|
||||
|
||||
// This element is displayed as a vertical line separator corresponding
|
||||
// the header of time tick for indicating time slice for animation
|
||||
// iterations.
|
||||
createNode({
|
||||
parent: this.timeTickEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "time-tick",
|
||||
"style": `left:${pos}%`
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onTimelineDataChanged: function({ time }) {
|
||||
this.currentTime = time;
|
||||
const indicateTime =
|
||||
TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
|
||||
this.details.indicateProgress(indicateTime);
|
||||
},
|
||||
|
||||
onDetailCloseButtonClick: function(e) {
|
||||
if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
|
||||
return;
|
||||
}
|
||||
this.animationRootEl.classList.remove("animation-detail-visible");
|
||||
this.emit("animation-detail-closed");
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a list of the tracks of the animation actor
|
||||
* @param {Object} animation
|
||||
* @return {Object} A list of tracks, one per animated property, each
|
||||
* with a list of keyframes
|
||||
*/
|
||||
async getTracks(animation) {
|
||||
const tracks = {};
|
||||
|
||||
/*
|
||||
* getFrames is a AnimationPlayorActor method that returns data about the
|
||||
* keyframes of the animation.
|
||||
* In FF48, the data it returns change, and will hold only longhand
|
||||
* properties ( e.g. borderLeftWidth ), which does not match what we
|
||||
* want to display in the animation detail.
|
||||
* A new AnimationPlayerActor function, getProperties, is introduced,
|
||||
* that returns the animated css properties of the animation and their
|
||||
* keyframes values.
|
||||
* If the animation actor has the getProperties function, we use it, and if
|
||||
* not, we fall back to getFrames, which then returns values we used to
|
||||
* handle.
|
||||
*/
|
||||
if (this.serverTraits.hasGetProperties) {
|
||||
let properties = [];
|
||||
try {
|
||||
properties = await animation.getProperties();
|
||||
} catch (e) {
|
||||
// Expected if we've already been destroyed in the meantime.
|
||||
if (!this.isDestroyed) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, values} of properties) {
|
||||
if (!tracks[name]) {
|
||||
tracks[name] = [];
|
||||
}
|
||||
|
||||
for (let {value, offset, easing, distance} of values) {
|
||||
distance = distance ? distance : 0;
|
||||
offset = parseFloat(offset.toFixed(3));
|
||||
tracks[name].push({value, offset, easing, distance});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let frames = [];
|
||||
try {
|
||||
frames = await animation.getFrames();
|
||||
} catch (e) {
|
||||
// Expected if we've already been destroyed in the meantime.
|
||||
if (!this.isDestroyed) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
for (const frame of frames) {
|
||||
for (const name in frame) {
|
||||
if (this.NON_PROPERTIES.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have to change to CSS property name
|
||||
// since GetKeyframes returns JS property name.
|
||||
const propertyCSSName = getCssPropertyName(name);
|
||||
if (!tracks[propertyCSSName]) {
|
||||
tracks[propertyCSSName] = [];
|
||||
}
|
||||
|
||||
tracks[propertyCSSName].push({
|
||||
value: frame[name],
|
||||
offset: parseFloat(frame.computedOffset.toFixed(3)),
|
||||
easing: frame.easing,
|
||||
distance: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the equality given states as effect timing.
|
||||
* @param {Object} state of animation.
|
||||
* @param {Object} same to avobe.
|
||||
* @return {boolean} true: same effect timing
|
||||
*/
|
||||
function areTimingEffectsEqual(stateA, stateB) {
|
||||
for (const property of ["playbackRate", "duration", "delay", "endDelay",
|
||||
"iterationCount", "iterationStart", "easing",
|
||||
"fill", "direction"]) {
|
||||
if (stateA[property] !== stateB[property]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
/* -*- 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 {createNode, createSVGNode} =
|
||||
require("devtools/client/inspector/animation-old/utils");
|
||||
const { ProgressGraphHelper, } =
|
||||
require("devtools/client/inspector/animation-old/graph-helper.js");
|
||||
|
||||
// Counter for linearGradient ID.
|
||||
let LINEAR_GRADIENT_ID_COUNTER = 0;
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a list of keyframes.
|
||||
* Also, shows a graphical graph for the animation progress of one iteration.
|
||||
*/
|
||||
function Keyframes() {}
|
||||
|
||||
exports.Keyframes = Keyframes;
|
||||
|
||||
Keyframes.prototype = {
|
||||
init: function(containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
|
||||
this.keyframesEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {"class": "keyframes"}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.keyframesEl.remove();
|
||||
this.containerEl = this.keyframesEl = this.animation = null;
|
||||
},
|
||||
|
||||
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 (const 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 a graph representing the progress of the animation over one iteration.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Number} duration - Duration of one iteration.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
|
||||
*/
|
||||
function renderPropertyGraph(parentEl, duration, minSegmentDuration, graphHelper) {
|
||||
const segments = graphHelper.createPathSegments(duration, minSegmentDuration);
|
||||
|
||||
const graphType = graphHelper.getGraphType();
|
||||
if (graphType !== "color") {
|
||||
graphHelper.appendShapePath(parentEl, segments, graphType);
|
||||
renderEasingHint(parentEl, segments, graphHelper);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the color to the path.
|
||||
segments.forEach(segment => {
|
||||
segment.y = 1;
|
||||
});
|
||||
const path = graphHelper.appendShapePath(parentEl, segments, graphType);
|
||||
const defEl = createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "def"
|
||||
});
|
||||
const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
|
||||
const linearGradientEl = createSVGNode({
|
||||
parent: defEl,
|
||||
nodeType: "linearGradient",
|
||||
attributes: {
|
||||
"id": id
|
||||
}
|
||||
});
|
||||
segments.forEach(segment => {
|
||||
createSVGNode({
|
||||
parent: linearGradientEl,
|
||||
nodeType: "stop",
|
||||
attributes: {
|
||||
"stop-color": segment.style,
|
||||
"offset": segment.x / duration
|
||||
}
|
||||
});
|
||||
});
|
||||
path.style.fill = `url(#${ id })`;
|
||||
|
||||
renderEasingHintForColor(parentEl, graphHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the easing hint.
|
||||
* This method renders an emphasized path over the easing path for a keyframe.
|
||||
* It appears when hovering over the easing.
|
||||
* It also renders a tooltip that appears when hovering.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} path segments - [{x: {Number} time, y: {Number} progress}, ...]
|
||||
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHelper.
|
||||
*/
|
||||
function renderEasingHint(parentEl, segments, helper) {
|
||||
const keyframes = helper.getKeyframes();
|
||||
const duration = helper.getDuration();
|
||||
|
||||
// Split segments for each keyframe.
|
||||
for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
|
||||
const startKeyframe = keyframes[i];
|
||||
const endKeyframe = keyframes[i + 1];
|
||||
const endTime = endKeyframe.offset * duration;
|
||||
const keyframeSegments = [];
|
||||
|
||||
for (; indexOfSegments < segments.length; indexOfSegments++) {
|
||||
const segment = segments[indexOfSegments];
|
||||
keyframeSegments.push(segment);
|
||||
|
||||
if (startKeyframe.offset === endKeyframe.offset) {
|
||||
keyframeSegments.push(segments[++indexOfSegments]);
|
||||
break;
|
||||
} else if (segment.x === endTime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Append easing hint as text and emphasis path.
|
||||
const gEl = createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "g"
|
||||
});
|
||||
createSVGNode({
|
||||
parent: gEl,
|
||||
nodeType: "title",
|
||||
textContent: startKeyframe.easing
|
||||
});
|
||||
helper.appendLinePath(gEl, keyframeSegments, `${helper.getGraphType()} hint`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render easing hint for properties that are represented by color.
|
||||
* This method render as text only.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
|
||||
*/
|
||||
function renderEasingHintForColor(parentEl, helper) {
|
||||
const keyframes = helper.getKeyframes();
|
||||
const duration = helper.getDuration();
|
||||
|
||||
// Split segments for each keyframe.
|
||||
for (let i = 0; i < keyframes.length - 1; i++) {
|
||||
const startKeyframe = keyframes[i];
|
||||
const startTime = startKeyframe.offset * duration;
|
||||
const endKeyframe = keyframes[i + 1];
|
||||
const endTime = endKeyframe.offset * duration;
|
||||
|
||||
// Append easing hint.
|
||||
const gEl = createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "g"
|
||||
});
|
||||
createSVGNode({
|
||||
parent: gEl,
|
||||
nodeType: "title",
|
||||
textContent: startKeyframe.easing
|
||||
});
|
||||
createSVGNode({
|
||||
parent: gEl,
|
||||
nodeType: "rect",
|
||||
attributes: {
|
||||
x: startTime,
|
||||
y: -1,
|
||||
width: endTime - startTime,
|
||||
height: 1,
|
||||
class: "hint",
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'animation-details.js',
|
||||
'animation-target-node.js',
|
||||
'animation-time-block.js',
|
||||
'animation-timeline.js',
|
||||
'keyframes.js',
|
||||
'rate-selector.js'
|
||||
)
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/* -*- 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} = require("devtools/client/inspector/animation-old/utils");
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// List of playback rate presets displayed in the timeline toolbar.
|
||||
const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a playback rate selector UI.
|
||||
* The rendering logic is such that a predefined list of rates is generated.
|
||||
* If *all* animations passed to render share the same rate, then that rate is
|
||||
* selected in the <select> element, otherwise, the empty value is selected.
|
||||
* If the rate that all animations share isn't part of the list of predefined
|
||||
* rates, than that rate is added to the list.
|
||||
*/
|
||||
function RateSelector() {
|
||||
this.onRateChanged = this.onRateChanged.bind(this);
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.RateSelector = RateSelector;
|
||||
|
||||
RateSelector.prototype = {
|
||||
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);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.selectEl.removeEventListener("change", this.onRateChanged);
|
||||
this.selectEl.remove();
|
||||
this.selectEl = null;
|
||||
},
|
||||
|
||||
getAnimationsRates: function(animations) {
|
||||
return sortedUnique(animations.map(a => a.state.playbackRate));
|
||||
},
|
||||
|
||||
getAllRates: function(animations) {
|
||||
const animationsRates = this.getAnimationsRates(animations);
|
||||
if (animationsRates.length > 1) {
|
||||
return PLAYBACK_RATES;
|
||||
}
|
||||
|
||||
return sortedUnique(PLAYBACK_RATES.concat(animationsRates));
|
||||
},
|
||||
|
||||
render: function(animations) {
|
||||
const allRates = this.getAnimationsRates(animations);
|
||||
const 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 (const rate of this.getAllRates(animations)) {
|
||||
const 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");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onRateChanged: function() {
|
||||
const rate = parseFloat(this.selectEl.value);
|
||||
if (!isNaN(rate)) {
|
||||
this.emit("rate-changed", rate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sortedUnique = arr => [...new Set(arr)].sort((a, b) => a > b);
|
||||
|
|
@ -1,816 +0,0 @@
|
|||
/* -*- 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 {createSVGNode, getJsPropertyName} =
|
||||
require("devtools/client/inspector/animation-old/utils");
|
||||
const {colorUtils} = require("devtools/shared/css/color.js");
|
||||
const {parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
|
||||
|
||||
// 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
|
||||
// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
|
||||
// by DURATION_RESOLUTION.
|
||||
// DURATION_RESOLUTION shoud be integer and more than 2.
|
||||
const DURATION_RESOLUTION = 4;
|
||||
// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
|
||||
const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
|
||||
exports.DEFAULT_MIN_PROGRESS_THRESHOLD = DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
|
||||
// and end bounds when dividing duration in createPathSegments.
|
||||
const BOUND_EXCLUDING_TIME = 0.001;
|
||||
|
||||
/**
|
||||
* This helper return the segment coordinates and style for property graph,
|
||||
* also return the graph type.
|
||||
* Parameters of constructor are below.
|
||||
* @param {Window} win - window object to animate.
|
||||
* @param {String} propertyCSSName - CSS property name (e.g. background-color).
|
||||
* @param {String} animationType - Animation type of CSS property.
|
||||
* @param {Object} keyframes - AnimationInspector's keyframes object.
|
||||
* @param {float} duration - Duration of animation.
|
||||
*/
|
||||
function ProgressGraphHelper(win, propertyCSSName, animationType, keyframes, duration) {
|
||||
this.win = win;
|
||||
const doc = this.win.document;
|
||||
this.targetEl = doc.createElement("div");
|
||||
doc.documentElement.appendChild(this.targetEl);
|
||||
|
||||
this.propertyCSSName = propertyCSSName;
|
||||
this.propertyJSName = getJsPropertyName(this.propertyCSSName);
|
||||
this.animationType = animationType;
|
||||
|
||||
// Create keyframe object to make dummy animation.
|
||||
const keyframesObject = keyframes.map(keyframe => {
|
||||
const keyframeObject = Object.assign({}, keyframe);
|
||||
keyframeObject[this.propertyJSName] = keyframe.value;
|
||||
return keyframeObject;
|
||||
});
|
||||
|
||||
// Create effect timing object to make dummy animation.
|
||||
const effectTiming = {
|
||||
duration: duration,
|
||||
fill: "forwards"
|
||||
};
|
||||
|
||||
this.keyframes = keyframesObject;
|
||||
this.devtoolsKeyframes = keyframes;
|
||||
this.valueHelperFunction = this.getValueHelperFunction();
|
||||
this.animation = this.targetEl.animate(this.keyframes, effectTiming);
|
||||
this.animation.pause();
|
||||
}
|
||||
|
||||
ProgressGraphHelper.prototype = {
|
||||
/**
|
||||
* Destory this object.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.targetEl.remove();
|
||||
this.animation.cancel();
|
||||
|
||||
this.targetEl = null;
|
||||
this.animation = null;
|
||||
this.valueHelperFunction = null;
|
||||
this.propertyCSSName = null;
|
||||
this.propertyJSName = null;
|
||||
this.animationType = null;
|
||||
this.keyframes = null;
|
||||
this.win = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return animation duration.
|
||||
* @return {Number} duration
|
||||
*/
|
||||
getDuration: function() {
|
||||
return this.animation.effect.getComputedTiming().duration;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return animation's keyframe.
|
||||
* @return {Object} keyframe
|
||||
*/
|
||||
getKeyframes: function() {
|
||||
return this.keyframes;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return graph type.
|
||||
* @return {String} if property is 'opacity' or 'transform', return that value.
|
||||
* Otherwise, return given animation type in constructor.
|
||||
*/
|
||||
getGraphType: function() {
|
||||
return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
|
||||
? this.propertyJSName : this.animationType;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a segment in graph by given the time.
|
||||
* @return {Object} Computed result which has follwing values.
|
||||
* - x: x value of graph (float)
|
||||
* - y: y value of graph (float between 0 - 1)
|
||||
* - style: the computed style value of the property at the time
|
||||
*/
|
||||
getSegment: function(time) {
|
||||
this.animation.currentTime = time;
|
||||
const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
|
||||
const value = this.valueHelperFunction(style);
|
||||
return { x: time, y: value, style: style };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a value helper function which calculates the value of Y axis by animation type.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given progress and style (e.g. rgb(0, 0, 0))
|
||||
*/
|
||||
getValueHelperFunction: function() {
|
||||
switch (this.animationType) {
|
||||
case "none": {
|
||||
return () => 1;
|
||||
}
|
||||
case "float": {
|
||||
return this.getFloatValueHelperFunction();
|
||||
}
|
||||
case "coord": {
|
||||
return this.getCoordinateValueHelperFunction();
|
||||
}
|
||||
case "color": {
|
||||
return this.getColorValueHelperFunction();
|
||||
}
|
||||
case "discrete": {
|
||||
return this.getDiscreteValueHelperFunction();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'float'.
|
||||
* @param {Object} keyframes - This object shoud be same as
|
||||
* the parameter of getGraphHelper.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given float (e.g. 1.0, 0.5 and so on)
|
||||
*/
|
||||
getFloatValueHelperFunction: function() {
|
||||
let maxValue = 0;
|
||||
let minValue = Infinity;
|
||||
this.keyframes.forEach(keyframe => {
|
||||
maxValue = Math.max(maxValue, keyframe.value);
|
||||
minValue = Math.min(minValue, keyframe.value);
|
||||
});
|
||||
const distance = maxValue - minValue;
|
||||
return value => {
|
||||
return (value - minValue) / distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'coord'.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given style (e.g. 100px)
|
||||
*/
|
||||
getCoordinateValueHelperFunction: function() {
|
||||
let maxValue = 0;
|
||||
let minValue = Infinity;
|
||||
for (let i = 0, n = this.keyframes.length; i < n; i++) {
|
||||
if (this.keyframes[i].value.match(/calc/)) {
|
||||
return null;
|
||||
}
|
||||
const value = parseFloat(this.keyframes[i].value);
|
||||
minValue = Math.min(minValue, value);
|
||||
maxValue = Math.max(maxValue, value);
|
||||
}
|
||||
const distance = maxValue - minValue;
|
||||
return value => {
|
||||
return (parseFloat(value) - minValue) / distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'color'.
|
||||
* @param {Object} keyframes - This object shoud be same as
|
||||
* the parameter of getGraphHelper.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given color (e.g. rgb(0, 0, 0))
|
||||
*/
|
||||
getColorValueHelperFunction: function() {
|
||||
const maxObject = { distance: 0 };
|
||||
for (let i = 0; i < this.keyframes.length - 1; i++) {
|
||||
const value1 = getRGBA(this.keyframes[i].value);
|
||||
for (let j = i + 1; j < this.keyframes.length; j++) {
|
||||
const value2 = getRGBA(this.keyframes[j].value);
|
||||
const distance = getRGBADistance(value1, value2);
|
||||
if (maxObject.distance >= distance) {
|
||||
continue;
|
||||
}
|
||||
maxObject.distance = distance;
|
||||
maxObject.value1 = value1;
|
||||
maxObject.value2 = value2;
|
||||
}
|
||||
}
|
||||
const baseValue =
|
||||
maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
|
||||
|
||||
return value => {
|
||||
const colorValue = getRGBA(value);
|
||||
return getRGBADistance(baseValue, colorValue) / maxObject.distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'discrete'.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given style (e.g. center)
|
||||
*/
|
||||
getDiscreteValueHelperFunction: function() {
|
||||
const discreteValues = [];
|
||||
this.keyframes.forEach(keyframe => {
|
||||
// Set style once since the computed value may differ from specified keyframe value.
|
||||
this.targetEl.style[this.propertyJSName] = keyframe.value;
|
||||
const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
|
||||
if (!discreteValues.includes(style)) {
|
||||
discreteValues.push(style);
|
||||
}
|
||||
});
|
||||
this.targetEl.style[this.propertyJSName] = "unset";
|
||||
|
||||
return value => {
|
||||
return discreteValues.indexOf(value) / (discreteValues.length - 1);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the path segments from given parameters.
|
||||
*
|
||||
* @param {Number} duration - Duration of animation.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {Number} minProgressThreshold - Minimum progress threshold.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
createPathSegments: function(duration, minSegmentDuration, minProgressThreshold) {
|
||||
if (!this.valueHelperFunction) {
|
||||
return createKeyframesPathSegments(duration, this.devtoolsKeyframes);
|
||||
}
|
||||
|
||||
const segments = [];
|
||||
|
||||
for (let i = 0; i < this.devtoolsKeyframes.length - 1; i++) {
|
||||
const startKeyframe = this.devtoolsKeyframes[i];
|
||||
const endKeyframe = this.devtoolsKeyframes[i + 1];
|
||||
const startTime = startKeyframe.offset * duration;
|
||||
const endTime = endKeyframe.offset * duration;
|
||||
|
||||
if (startKeyframe.offset === endKeyframe.offset) {
|
||||
const startSegment = this.getSegment(startTime - BOUND_EXCLUDING_TIME);
|
||||
startSegment.x = startTime;
|
||||
const endSegment = this.getSegment(endTime);
|
||||
segments.push(startSegment, endSegment);
|
||||
} else {
|
||||
let threshold = getPreferredProgressThreshold(startKeyframe.easing);
|
||||
|
||||
if (threshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
|
||||
// We should consider the keyframe's duration.
|
||||
threshold *= (endKeyframe.offset - startKeyframe.offset);
|
||||
}
|
||||
|
||||
segments.push(...createPathSegments(startTime, endTime - BOUND_EXCLUDING_TIME,
|
||||
minSegmentDuration, threshold, this));
|
||||
}
|
||||
}
|
||||
|
||||
const lastKeyframe = this.devtoolsKeyframes[this.devtoolsKeyframes.length - 1];
|
||||
const lastTime = lastKeyframe.offset * duration;
|
||||
segments.push(this.getSegment(lastTime));
|
||||
return segments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Append path element as shape. Also, this method appends two segment
|
||||
* that are {start x, 0} and {end x, 0} to make shape.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
appendShapePath: function(parentEl, pathSegments, cls) {
|
||||
return appendShapePath(parentEl, pathSegments, cls);
|
||||
},
|
||||
|
||||
/**
|
||||
* Append path element as line.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
appendLinePath: function(parentEl, pathSegments, cls) {
|
||||
const isClosePathNeeded = false;
|
||||
return appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded);
|
||||
},
|
||||
};
|
||||
|
||||
exports.ProgressGraphHelper = ProgressGraphHelper;
|
||||
|
||||
/**
|
||||
* This class is used for creating the summary graph in animation-timeline.
|
||||
* The shape of the graph can be changed by using the following methods:
|
||||
* setKeyframes:
|
||||
* If null, the shape is by computed timing progress.
|
||||
* Otherwise, by computed style of 'opacity' to combine effect easing and
|
||||
* keyframe's easing.
|
||||
* setFillMode:
|
||||
* Animation fill-mode (e.g. "none", "backwards", "forwards" or "both")
|
||||
* setClosePathNeeded:
|
||||
* If true, appendShapePath make the last segment of <path> element to
|
||||
* "close" segment("Z").
|
||||
* Therefore, if don't need under-line of graph, please set false.
|
||||
* setOriginalBehavior:
|
||||
* In Animation::SetCurrentTime spec, even if current time of animation is over
|
||||
* the endTime, the progress is changed. Likewise, in case the time is less than 0.
|
||||
* If set true, prevent the time to make the same animation behavior as the original.
|
||||
* setMinProgressThreshold:
|
||||
* SummaryGraphHelper searches and creates the summary graph until the progress
|
||||
* distance is less than this minProgressThreshold.
|
||||
* So, while setting a low threshold produces a smooth graph,
|
||||
* it will have an effect on performance.
|
||||
* @param {Object} win - window object.
|
||||
* @param {Object} state - animation state.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
*/
|
||||
function SummaryGraphHelper(win, state, minSegmentDuration) {
|
||||
this.win = win;
|
||||
const doc = this.win.document;
|
||||
this.targetEl = doc.createElement("div");
|
||||
doc.documentElement.appendChild(this.targetEl);
|
||||
|
||||
const effectTiming = Object.assign({}, state, {
|
||||
iterations: state.iterationCount ? state.iterationCount : Infinity
|
||||
});
|
||||
this.animation = this.targetEl.animate(null, effectTiming);
|
||||
this.animation.pause();
|
||||
this.endTime = this.animation.effect.getComputedTiming().endTime;
|
||||
|
||||
this.minSegmentDuration = minSegmentDuration;
|
||||
this.minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
}
|
||||
|
||||
SummaryGraphHelper.prototype = {
|
||||
/**
|
||||
* Destory this object.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.animation.cancel();
|
||||
this.targetEl.remove();
|
||||
this.targetEl = null;
|
||||
this.animation = null;
|
||||
this.win = null;
|
||||
},
|
||||
|
||||
/*
|
||||
* Set keyframes to shape graph by computed style. This method creates new keyframe
|
||||
* object using only offset and easing of given keyframes.
|
||||
* Also, allows null value. In case of null, this graph helper shapes graph using
|
||||
* computed timing progress.
|
||||
* @param {Object} keyframes - Should have offset and easing, or null.
|
||||
*/
|
||||
setKeyframes: function(keyframes) {
|
||||
let frames = null;
|
||||
// We need to change the duration resolution in case of interval of keyframes offset
|
||||
// was narrow.
|
||||
let durationResolution = DURATION_RESOLUTION;
|
||||
|
||||
if (keyframes) {
|
||||
let previousOffset = 0;
|
||||
|
||||
// Create new keyframes for opacity as computed style.
|
||||
// The reason why we use computed value instead of computed timing progress is to
|
||||
// include the easing in keyframes as well. Although the computed timing progress
|
||||
// is not affected by the easing in keyframes at all, computed value reflects that.
|
||||
frames = keyframes.map(keyframe => {
|
||||
if (previousOffset && previousOffset != keyframe.offset) {
|
||||
const interval = keyframe.offset - previousOffset;
|
||||
durationResolution = Math.max(durationResolution, Math.ceil(1 / interval));
|
||||
}
|
||||
previousOffset = keyframe.offset;
|
||||
|
||||
return {
|
||||
opacity: keyframe.offset,
|
||||
offset: keyframe.offset,
|
||||
easing: keyframe.easing
|
||||
};
|
||||
});
|
||||
|
||||
// Set the underlying opacity to zero so that if we sample the animation's output
|
||||
// during the delay phase and it is not filling backwards, we get zero.
|
||||
this.targetEl.style.opacity = 0;
|
||||
}
|
||||
|
||||
this.durationResolution = durationResolution;
|
||||
this.animation.effect.setKeyframes(frames);
|
||||
this.hasFrames = !!frames;
|
||||
},
|
||||
|
||||
/*
|
||||
* Set animation behavior.
|
||||
* In Animation::SetCurrentTime spec, even if current time of animation is over
|
||||
* endTime, the progress is changed. Likewise, in case the time is less than 0.
|
||||
* If set true, we prevent the time to make the same animation behavior as the original.
|
||||
* @param {bool} isOriginalBehavior - true: original behavior
|
||||
* false: according to spec.
|
||||
*/
|
||||
setOriginalBehavior: function(isOriginalBehavior) {
|
||||
this.isOriginalBehavior = isOriginalBehavior;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set animation fill mode.
|
||||
* @param {String} fill - "both", "forwards", "backwards" or "both"
|
||||
*/
|
||||
setFillMode: function(fill) {
|
||||
this.animation.effect.updateTiming({ fill });
|
||||
},
|
||||
|
||||
/**
|
||||
* Set true if need to close path in appendShapePath.
|
||||
* @param {bool} isClosePathNeeded - true: close, false: open.
|
||||
*/
|
||||
setClosePathNeeded: function(isClosePathNeeded) {
|
||||
this.isClosePathNeeded = isClosePathNeeded;
|
||||
},
|
||||
|
||||
/**
|
||||
* SummaryGraphHelper searches and creates the summary graph untill the progress
|
||||
* distance is less than this minProgressThreshold.
|
||||
*/
|
||||
setMinProgressThreshold: function(minProgressThreshold) {
|
||||
this.minProgressThreshold = minProgressThreshold;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a segment in graph by given the time.
|
||||
* @return {Object} Computed result which has follwing values.
|
||||
* - x: x value of graph (float)
|
||||
* - y: y value of graph (float between 0 - 1)
|
||||
*/
|
||||
getSegment: function(time) {
|
||||
if (this.isOriginalBehavior) {
|
||||
// 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 value = this.hasFrames ? this.getOpacityValue() : this.getProgressValue();
|
||||
return { x: time, y: value };
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the path segments from given parameters.
|
||||
* @param {Number} startTime - Starting time of animation.
|
||||
* @param {Number} endTime - Ending time of animation.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
createPathSegments: function(startTime, endTime) {
|
||||
return createPathSegments(startTime, endTime, this.minSegmentDuration,
|
||||
this.minProgressThreshold, this, this.durationResolution);
|
||||
},
|
||||
|
||||
/**
|
||||
* Append path element as shape. Also, this method appends two segment
|
||||
* that are {start x, 0} and {end x, 0} to make shape.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
appendShapePath: function(parentEl, pathSegments, cls) {
|
||||
return appendShapePath(parentEl, pathSegments, cls, this.isClosePathNeeded);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return current computed timing progress of the animation.
|
||||
* @return {float} computed timing progress as float value of Y axis.
|
||||
*/
|
||||
getProgressValue: function() {
|
||||
return Math.max(this.animation.effect.getComputedTiming().progress, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return current computed 'opacity' value of the element which is animating.
|
||||
* @return {float} computed timing progress as float value of Y axis.
|
||||
*/
|
||||
getOpacityValue: function() {
|
||||
return this.win.getComputedStyle(this.targetEl).opacity;
|
||||
}
|
||||
};
|
||||
|
||||
exports.SummaryGraphHelper = SummaryGraphHelper;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Number} resolution - Duration resolution for first time.
|
||||
* If null, use DURATION_RESOLUTION.
|
||||
* - getSegment(time): Helper function that, given a time,
|
||||
* will calculate the animation progress.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
function createPathSegments(startTime, endTime, minSegmentDuration,
|
||||
minProgressThreshold, segmentHelper,
|
||||
resolution = DURATION_RESOLUTION) {
|
||||
// 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) / resolution;
|
||||
for (let index = 1; index <= 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 BOUND_EXCLUDING_TIME).
|
||||
pathSegments = pathSegments.concat(
|
||||
createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
|
||||
currentSegment.x - BOUND_EXCLUDING_TIME,
|
||||
minSegmentDuration, minProgressThreshold,
|
||||
segmentHelper));
|
||||
}
|
||||
|
||||
pathSegments.push(currentSegment);
|
||||
previousSegment = currentSegment;
|
||||
}
|
||||
|
||||
return pathSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append path element as shape. Also, this method appends two segment
|
||||
* that are {start x, 0} and {end x, 0} to make shape.
|
||||
* But does not affect given pathSegments.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @param {bool} isClosePathNeeded - Set true if need to close the path. (default true)
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
function appendShapePath(parentEl, pathSegments, cls, isClosePathNeeded = true) {
|
||||
const segments = [
|
||||
{ x: pathSegments[0].x, y: 0 },
|
||||
...pathSegments,
|
||||
{ x: pathSegments[pathSegments.length - 1].x, y: 0 }
|
||||
];
|
||||
return appendPathElement(parentEl, segments, cls, isClosePathNeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append path element.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @param {bool} isClosePathNeeded - Set true if need to close the path.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded) {
|
||||
// Create path string.
|
||||
let currentSegment = pathSegments[0];
|
||||
let path = `M${ currentSegment.x },${ currentSegment.y }`;
|
||||
for (let i = 1; i < pathSegments.length; i++) {
|
||||
const currentEasing = currentSegment.easing ? currentSegment.easing : "linear";
|
||||
const nextSegment = pathSegments[i];
|
||||
if (currentEasing === "linear") {
|
||||
path += createLinePathString(nextSegment);
|
||||
} else if (currentEasing.startsWith("steps")) {
|
||||
path += createStepsPathString(currentSegment, nextSegment);
|
||||
} else if (currentEasing.startsWith("frames")) {
|
||||
path += createFramesPathString(currentSegment, nextSegment);
|
||||
} else {
|
||||
path += createCubicBezierPathString(currentSegment, nextSegment);
|
||||
}
|
||||
currentSegment = nextSegment;
|
||||
}
|
||||
if (isClosePathNeeded) {
|
||||
path += " Z";
|
||||
}
|
||||
// Append and return the path element.
|
||||
return createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "path",
|
||||
attributes: {
|
||||
"d": path,
|
||||
"class": cls,
|
||||
"vector-effect": "non-scaling-stroke",
|
||||
"transform": "scale(1, -1)"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the path segments from given keyframes.
|
||||
* @param {Number} duration - Duration of animation.
|
||||
* @param {Object} Keyframes of devtool's format.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} distance,
|
||||
* easing: {String} keyframe's easing,
|
||||
* style: {String} keyframe's value}, ...]
|
||||
*/
|
||||
function createKeyframesPathSegments(duration, keyframes) {
|
||||
return keyframes.map(keyframe => {
|
||||
return {
|
||||
x: keyframe.offset * duration,
|
||||
y: keyframe.distance,
|
||||
easing: keyframe.easing,
|
||||
style: keyframe.value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a line path string.
|
||||
* @param {Object} segment - e.g. { x: 100, y: 1 }
|
||||
* @return {String} path string - e.g. "L100,1"
|
||||
*/
|
||||
function createLinePathString(segment) {
|
||||
return ` L${ segment.x },${ segment.y }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a path string to represents a step function.
|
||||
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "steps(2)" }
|
||||
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
|
||||
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
|
||||
*/
|
||||
function createStepsPathString(currentSegment, nextSegment) {
|
||||
const matches =
|
||||
currentSegment.easing.match(/^steps\((\d+)(,\sstart)?\)/);
|
||||
const stepNumber = parseInt(matches[1], 10);
|
||||
const oneStepX = (nextSegment.x - currentSegment.x) / stepNumber;
|
||||
const oneStepY = (nextSegment.y - currentSegment.y) / stepNumber;
|
||||
const isStepStart = matches[2];
|
||||
const stepOffsetY = isStepStart ? 1 : 0;
|
||||
let path = "";
|
||||
for (let step = 0; step < stepNumber; step++) {
|
||||
const sx = currentSegment.x + step * oneStepX;
|
||||
const ex = sx + oneStepX;
|
||||
const y = currentSegment.y + (step + stepOffsetY) * oneStepY;
|
||||
path += ` L${ sx },${ y } L${ ex },${ y }`;
|
||||
}
|
||||
if (!isStepStart) {
|
||||
path += ` L${ nextSegment.x },${ nextSegment.y }`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a path string to represents a frames function.
|
||||
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "frames(2)" }
|
||||
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
|
||||
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
|
||||
*/
|
||||
function createFramesPathString(currentSegment, nextSegment) {
|
||||
const matches =
|
||||
currentSegment.easing.match(/^frames\((\d+)\)/);
|
||||
const framesNumber = parseInt(matches[1], 10);
|
||||
const oneFrameX = (nextSegment.x - currentSegment.x) / framesNumber;
|
||||
const oneFrameY = (nextSegment.y - currentSegment.y) / (framesNumber - 1);
|
||||
let path = "";
|
||||
for (let frame = 0; frame < framesNumber; frame++) {
|
||||
const sx = currentSegment.x + frame * oneFrameX;
|
||||
const ex = sx + oneFrameX;
|
||||
const y = currentSegment.y + frame * oneFrameY;
|
||||
path += ` L${ sx },${ y } L${ ex },${ y }`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a path string to represents a bezier curve.
|
||||
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "ease" }
|
||||
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
|
||||
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
|
||||
*/
|
||||
function createCubicBezierPathString(currentSegment, nextSegment) {
|
||||
const controlPoints = parseTimingFunction(currentSegment.easing);
|
||||
if (!controlPoints) {
|
||||
// Just return line path string since we could not parse this easing.
|
||||
return createLinePathString(currentSegment);
|
||||
}
|
||||
|
||||
const cp1x = controlPoints[0];
|
||||
const cp1y = controlPoints[1];
|
||||
const cp2x = controlPoints[2];
|
||||
const cp2y = controlPoints[3];
|
||||
|
||||
const diffX = nextSegment.x - currentSegment.x;
|
||||
const diffY = nextSegment.y - currentSegment.y;
|
||||
let path =
|
||||
` C ${ currentSegment.x + diffX * cp1x } ${ currentSegment.y + diffY * cp1y }`;
|
||||
path += `, ${ currentSegment.x + diffX * cp2x } ${ currentSegment.y + diffY * cp2y }`;
|
||||
path += `, ${ nextSegment.x } ${ nextSegment.y }`;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse given RGBA string.
|
||||
* @param {String} colorString - e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on.
|
||||
* @return {Object} RGBA {r: r, g: g, b: b, a: a}.
|
||||
*/
|
||||
function getRGBA(colorString) {
|
||||
const color = new colorUtils.CssColor(colorString);
|
||||
return color.getRGBATuple();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the distance from give two RGBA.
|
||||
* @param {Object} rgba1 - RGBA (format is same to getRGBA)
|
||||
* @param {Object} rgba2 - RGBA (format is same to getRGBA)
|
||||
* @return {float} distance.
|
||||
*/
|
||||
function getRGBADistance(rgba1, rgba2) {
|
||||
const startA = rgba1.a;
|
||||
const startR = rgba1.r * startA;
|
||||
const startG = rgba1.g * startA;
|
||||
const startB = rgba1.b * startA;
|
||||
const endA = rgba2.a;
|
||||
const endR = rgba2.r * endA;
|
||||
const endG = rgba2.g * endA;
|
||||
const endB = rgba2.b * endA;
|
||||
const diffA = startA - endA;
|
||||
const diffR = startR - endR;
|
||||
const diffG = startG - endG;
|
||||
const diffB = startB - endB;
|
||||
return Math.sqrt(diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return preferred progress threshold for given keyframes.
|
||||
* See the documentation of DURATION_RESOLUTION and DEFAULT_MIN_PROGRESS_THRESHOLD
|
||||
* for more information regarding this.
|
||||
* @param {Array} keyframes - keyframes
|
||||
* @return {float} - preferred progress threshold.
|
||||
*/
|
||||
function getPreferredKeyframesProgressThreshold(keyframes) {
|
||||
let minProgressTreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
for (let i = 0; i < keyframes.length - 1; i++) {
|
||||
const keyframe = keyframes[i];
|
||||
if (!keyframe.easing) {
|
||||
continue;
|
||||
}
|
||||
let keyframeProgressThreshold = getPreferredProgressThreshold(keyframe.easing);
|
||||
if (keyframeProgressThreshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
|
||||
// We should consider the keyframe's duration.
|
||||
keyframeProgressThreshold *=
|
||||
(keyframes[i + 1].offset - keyframe.offset);
|
||||
}
|
||||
minProgressTreshold = Math.min(keyframeProgressThreshold, minProgressTreshold);
|
||||
}
|
||||
return minProgressTreshold;
|
||||
}
|
||||
exports.getPreferredKeyframesProgressThreshold = getPreferredKeyframesProgressThreshold;
|
||||
|
||||
/**
|
||||
* Return preferred progress threshold to render summary graph.
|
||||
* @param {String} - easing e.g. steps(2), linear and so on.
|
||||
* @return {float} - preferred threshold.
|
||||
*/
|
||||
function getPreferredProgressThreshold(easing) {
|
||||
const stepOrFramesFunction = easing.match(/(steps|frames)\((\d+)/);
|
||||
return stepOrFramesFunction
|
||||
? 1 / (parseInt(stepOrFramesFunction[2], 10) + 1)
|
||||
: DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
}
|
||||
exports.getPreferredProgressThreshold = getPreferredProgressThreshold;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
||||
|
||||
DIRS += [
|
||||
'components'
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'graph-helper.js',
|
||||
'utils.js'
|
||||
)
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('DevTools', 'Animation Inspector')
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
[DEFAULT]
|
||||
tags = devtools
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
doc_add_animation.html
|
||||
doc_body_animation.html
|
||||
doc_delayed_starttime_animations.html
|
||||
doc_end_delay.html
|
||||
doc_frame_script.js
|
||||
doc_keyframes.html
|
||||
doc_modify_playbackRate.html
|
||||
doc_negative_animation.html
|
||||
doc_pseudo_elements.html
|
||||
doc_script_animation.html
|
||||
doc_short_duration_animation.html
|
||||
doc_simple_animation.html
|
||||
doc_multiple_animation_types.html
|
||||
doc_multiple_easings.html
|
||||
doc_multiple_property_types.html
|
||||
doc_timing_combination_animation.html
|
||||
head.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/frame-script-utils.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
!/devtools/client/shared/test/test-actor-registry.js
|
||||
!/devtools/client/shared/test/test-actor.js
|
||||
|
||||
[browser_animation_animated_properties_displayed.js]
|
||||
[browser_animation_animated_properties_for_delayed_starttime_animations.js]
|
||||
[browser_animation_animated_properties_path.js]
|
||||
[browser_animation_animated_properties_progress_indicator.js]
|
||||
[browser_animation_click_selects_animation.js]
|
||||
[browser_animation_controller_exposes_document_currentTime.js]
|
||||
[browser_animation_detail_displayed.js]
|
||||
skip-if = os == "linux" && !debug # Bug 1234567
|
||||
[browser_animation_detail_easings.js]
|
||||
[browser_animation_empty_on_invalid_nodes.js]
|
||||
[browser_animation_keyframe_markers.js]
|
||||
[browser_animation_mutations_with_same_names.js]
|
||||
skip-if = true # Bug 1447710
|
||||
[browser_animation_panel_exists.js]
|
||||
[browser_animation_participate_in_inspector_update.js]
|
||||
[browser_animation_playerFronts_are_refreshed.js]
|
||||
[browser_animation_playerWidgets_appear_on_panel_init.js]
|
||||
[browser_animation_playerWidgets_target_nodes.js]
|
||||
[browser_animation_pseudo_elements.js]
|
||||
[browser_animation_refresh_on_added_animation.js]
|
||||
[browser_animation_refresh_on_removed_animation.js]
|
||||
skip-if = os == "linux" && !debug # Bug 1227792
|
||||
[browser_animation_refresh_when_active.js]
|
||||
[browser_animation_refresh_when_active_after_mutations.js]
|
||||
[browser_animation_running_on_compositor.js]
|
||||
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
|
||||
[browser_animation_shows_player_on_valid_node.js]
|
||||
[browser_animation_spacebar_toggles_animations.js]
|
||||
[browser_animation_spacebar_toggles_node_animations.js]
|
||||
[browser_animation_summarygraph_for_multiple_easings.js]
|
||||
[browser_animation_target_highlight_select.js]
|
||||
[browser_animation_target_highlighter_lock.js]
|
||||
skip-if = verify
|
||||
[browser_animation_timeline_add_animation.js]
|
||||
[browser_animation_timeline_currentTime.js]
|
||||
[browser_animation_timeline_header.js]
|
||||
[browser_animation_timeline_iterationStart.js]
|
||||
[browser_animation_timeline_pause_button_01.js]
|
||||
[browser_animation_timeline_pause_button_02.js]
|
||||
skip-if = (verify && debug && (os == 'linux'))
|
||||
[browser_animation_timeline_pause_button_03.js]
|
||||
[browser_animation_timeline_rate_selector.js]
|
||||
skip-if = (os == "win" && ccov) # Bug 1444211
|
||||
[browser_animation_timeline_rewind_button.js]
|
||||
[browser_animation_timeline_scrubber_exists.js]
|
||||
[browser_animation_timeline_scrubber_movable.js]
|
||||
[browser_animation_timeline_scrubber_moves.js]
|
||||
[browser_animation_timeline_setCurrentTime.js]
|
||||
[browser_animation_timeline_short_duration.js]
|
||||
[browser_animation_timeline_shows_delay.js]
|
||||
[browser_animation_timeline_shows_endDelay.js]
|
||||
[browser_animation_timeline_shows_iterations.js]
|
||||
[browser_animation_timeline_shows_name_label.js]
|
||||
[browser_animation_timeline_shows_time_info.js]
|
||||
[browser_animation_timeline_takes_rate_into_account.js]
|
||||
[browser_animation_timeline_ui.js]
|
||||
[browser_animation_toggle_button_resets_on_navigate.js]
|
||||
[browser_animation_toggle_button_toggles_animations.js]
|
||||
[browser_animation_toolbar_exists.js]
|
||||
[browser_animation_ui_updates_when_animation_data_changes.js]
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const LAYOUT_ERRORS_L10N =
|
||||
new LocalizationHelper("toolkit/locales/layout_errors.properties");
|
||||
|
||||
// Test that when an animation is selected, its list of animated properties is
|
||||
// displayed below it.
|
||||
|
||||
const EXPECTED_PROPERTIES = [
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"filter",
|
||||
"height",
|
||||
"transform",
|
||||
"width",
|
||||
// Unchanged value properties
|
||||
"background-attachment",
|
||||
"background-clip",
|
||||
"background-color",
|
||||
"background-image",
|
||||
"background-origin",
|
||||
"background-position-x",
|
||||
"background-position-y",
|
||||
"background-repeat",
|
||||
"background-size"
|
||||
].sort();
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_keyframes.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const propertiesList = timeline.rootWrapperEl
|
||||
.querySelector(".animated-properties");
|
||||
|
||||
// doc_keyframes.html has only one animation,
|
||||
// so the propertiesList shoud be shown.
|
||||
ok(isNodeVisible(propertiesList),
|
||||
"The list of properties panel shoud be shown");
|
||||
|
||||
ok(propertiesList.querySelectorAll(".property").length,
|
||||
"The list of properties panel actually contains properties");
|
||||
ok(hasExpectedProperties(propertiesList),
|
||||
"The list of properties panel contains the right properties");
|
||||
ok(hasExpectedWarnings(propertiesList),
|
||||
"The list of properties panel contains the right warnings");
|
||||
|
||||
info("Click same animation again");
|
||||
await clickOnAnimation(panel, 0, true);
|
||||
ok(isNodeVisible(propertiesList),
|
||||
"The list of properties panel keeps");
|
||||
});
|
||||
|
||||
function hasExpectedProperties(containerEl) {
|
||||
const names = [...containerEl.querySelectorAll(".property .name")]
|
||||
.map(n => n.textContent)
|
||||
.sort();
|
||||
|
||||
if (names.length !== EXPECTED_PROPERTIES.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (names[i] !== EXPECTED_PROPERTIES[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasExpectedWarnings(containerEl) {
|
||||
const warnings = [...containerEl.querySelectorAll(".warning")];
|
||||
for (const warning of warnings) {
|
||||
const warningID =
|
||||
"CompositorAnimationWarningTransformWithSyncGeometricAnimations";
|
||||
if (warning.getAttribute("title") == LAYOUT_ERRORS_L10N.getStr(warningID)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test for animations that have different starting time.
|
||||
// We should check progress indicator working well even if the start time is not zero.
|
||||
// Also, check that there is no duplication display.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
|
||||
const { panel } = await openAnimationInspector();
|
||||
await setStyle(null, panel, "animation", "anim 100s", "#target2");
|
||||
await setStyle(null, panel, "animation", "anim 100s", "#target3");
|
||||
await setStyle(null, panel, "animation", "anim 100s", "#target4");
|
||||
await setStyle(null, panel, "animation", "anim 100s", "#target5");
|
||||
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailsComponent = timelineComponent.details;
|
||||
const headers =
|
||||
detailsComponent.containerEl.querySelectorAll(".animated-properties-header");
|
||||
is(headers.length, 1, "There should be only one header in the details panel");
|
||||
|
||||
// Check indicator.
|
||||
await clickOnAnimation(panel, 1);
|
||||
const progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
const startTime = detailsComponent.animation.state.previousStartTime;
|
||||
detailsComponent.indicateProgress(0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at 0ms");
|
||||
detailsComponent.indicateProgress(startTime);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at start time");
|
||||
detailsComponent.indicateProgress(startTime + 50 * 1000);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The progress indicator position should be 50% at half time of animation");
|
||||
detailsComponent.indicateProgress(startTime + 99 * 1000);
|
||||
is(progressIndicatorEl.style.left, "99%",
|
||||
"The progress indicator position should be 99% at 99s");
|
||||
detailsComponent.indicateProgress(startTime + 100 * 1000);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at end of animation");
|
||||
});
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check animated properties's graph.
|
||||
// The graph constructs from SVG, also uses path (for shape), linearGradient,
|
||||
// stop (for color) element and so on.
|
||||
// We test followings.
|
||||
// 1. class name - which represents the animation type.
|
||||
// 2. coordinates of the path - x is time, y is graph y value which should be 0 - 1.
|
||||
// The path of animation types 'color', 'coord', 'opacity' or 'discrete' are created by
|
||||
// createPathSegments. Other types are created by createKeyframesPathSegments.
|
||||
// 3. color - animation type 'color' has linearGradient element.
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"background-repeat": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1, color: "rgb(0, 255, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(255, 0, 0)" }
|
||||
]
|
||||
},
|
||||
"background-repeat": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 500, y: 1, color: "rgb(0, 0, 255)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"background-repeat": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 249.999, y: 0 },
|
||||
{ x: 250, y: 1 },
|
||||
{ x: 749.999, y: 1 },
|
||||
{ x: 750, y: 0 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 249.999, y: 0 },
|
||||
{ x: 250, y: 1 },
|
||||
{ x: 749.999, y: 1 },
|
||||
{ x: 750, y: 0 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 499.999, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 500, y: 1, color: "rgb(128, 128, 0)" },
|
||||
{ x: 999.999, y: 1, color: "rgb(128, 128, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"background-repeat": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 1000, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 999.999, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 999.999, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 1000, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 250, y: 0.25 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 750, y: 0.75 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 199, y: 0 },
|
||||
{ x: 200, y: 0.25 },
|
||||
{ x: 399, y: 0.25 },
|
||||
{ x: 400, y: 0.5 },
|
||||
{ x: 599, y: 0.5 },
|
||||
{ x: 600, y: 0.75 },
|
||||
{ x: 799, y: 0.75 },
|
||||
{ x: 800, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 1 },
|
||||
{ x: 110, y: 1 },
|
||||
{ x: 114.9, y: 1 },
|
||||
{ x: 115, y: 0.5 },
|
||||
{ x: 129.9, y: 0.5 },
|
||||
{ x: 130, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 250, y: 1 },
|
||||
{ x: 499, y: 1 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 500, y: 0 },
|
||||
{ x: 750, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailEl = timelineComponent.details.containerEl;
|
||||
const hasClosePath = true;
|
||||
|
||||
for (let i = 0; i < TEST_CASES.length; i++) {
|
||||
info(`Click to select the animation[${ i }]`);
|
||||
await clickOnAnimation(panel, i);
|
||||
const timeBlock = getAnimationTimeBlocks(panel)[0];
|
||||
const state = timeBlock.animation.state;
|
||||
const properties = TEST_CASES[i];
|
||||
for (const property in properties) {
|
||||
const testcase = properties[property];
|
||||
info(`Test path of ${ property }`);
|
||||
const className = testcase.expectedClass;
|
||||
const pathEl = detailEl.querySelector(`path.${ className }`);
|
||||
ok(pathEl, `Path element with class '${ className }' should exis`);
|
||||
assertPathSegments(pathEl, state.duration, hasClosePath, testcase.expectedValues);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test progress indicator in animated properties.
|
||||
// Since this indicator works with the timeline, after selecting each animation,
|
||||
// click the timeline header to change the current time and check the change.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const { panel } = await openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailsComponent = timelineComponent.details;
|
||||
|
||||
info("Click to select the animation");
|
||||
await clickOnAnimation(panel, 0);
|
||||
let progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
ok(progressIndicatorEl, "The progress indicator should be exist");
|
||||
await clickOnTimelineHeader(panel, 0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 0ms");
|
||||
await clickOnTimelineHeader(panel, 0.5);
|
||||
approximate(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be "
|
||||
+ "approximately 50% at 500ms");
|
||||
await clickOnTimelineHeader(panel, 1);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% at 1000ms");
|
||||
|
||||
info("Click to select the steps animation");
|
||||
await clickOnAnimation(panel, 4);
|
||||
// Re-get progressIndicatorEl since this element re-create
|
||||
// in case of select the animation.
|
||||
progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
// Use indicateProgess directly from here since
|
||||
// MouseEvent.clientX may not be able to indicate finely
|
||||
// in case of the width of header element * xPositionRate has a fraction.
|
||||
detailsComponent.indicateProgress(499);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 0ms");
|
||||
detailsComponent.indicateProgress(499);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 499ms");
|
||||
detailsComponent.indicateProgress(500);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% at 500ms");
|
||||
detailsComponent.indicateProgress(999);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% at 999ms");
|
||||
await clickOnTimelineHeader(panel, 1);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% at 1000ms");
|
||||
|
||||
info("Change the playback rate");
|
||||
await changeTimelinePlaybackRate(panel, 2);
|
||||
await clickOnAnimation(panel, 0);
|
||||
progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
await clickOnTimelineHeader(panel, 0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% "
|
||||
+ "at 0ms and playback rate 2");
|
||||
detailsComponent.indicateProgress(250);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% "
|
||||
+ "at 250ms and playback rate 2");
|
||||
detailsComponent.indicateProgress(500);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% "
|
||||
+ "at 500ms and playback rate 2");
|
||||
|
||||
info("Check the progress indicator position after select another animation");
|
||||
await changeTimelinePlaybackRate(panel, 1);
|
||||
await clickOnTimelineHeader(panel, 0.5);
|
||||
const originalIndicatorPosition = progressIndicatorEl.style.left;
|
||||
await clickOnAnimation(panel, 1);
|
||||
is(progressIndicatorEl.style.left, originalIndicatorPosition,
|
||||
"The animation time should be continued even if another animation selects");
|
||||
});
|
||||
|
||||
function approximate(percentageString1, percentageString2, message) {
|
||||
const val1 = Math.round(parseFloat(percentageString1));
|
||||
const val2 = Math.round(parseFloat(percentageString2));
|
||||
is(val1, val2, message);
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that animations displayed in the timeline can be selected by clicking
|
||||
// them, and that this emits the right events and adds the right classes.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
const selected = timeline.rootWrapperEl.querySelectorAll(".animation.selected");
|
||||
ok(!selected.length, "There are no animations selected by default");
|
||||
|
||||
info("Click on the first animation, expect the right event and right class");
|
||||
const animation0 = await clickOnAnimation(panel, 0);
|
||||
is(animation0, timeline.animations[0],
|
||||
"The selected event was emitted with the right animation");
|
||||
ok(isTimeBlockSelected(timeline, 0),
|
||||
"The time block has the right selected class");
|
||||
|
||||
info("Click on the second animation, expect it to be selected too");
|
||||
const animation1 = await clickOnAnimation(panel, 1);
|
||||
is(animation1, timeline.animations[1],
|
||||
"The selected event was emitted with the right animation");
|
||||
ok(isTimeBlockSelected(timeline, 1),
|
||||
"The second time block has the right selected class");
|
||||
ok(!isTimeBlockSelected(timeline, 0),
|
||||
"The first time block has been unselected");
|
||||
|
||||
info("Click again on the first animation and check if it unselects");
|
||||
await clickOnAnimation(panel, 0);
|
||||
ok(isTimeBlockSelected(timeline, 0),
|
||||
"The time block has the right selected class again");
|
||||
ok(!isTimeBlockSelected(timeline, 1),
|
||||
"The second time block has been unselected");
|
||||
});
|
||||
|
||||
function isTimeBlockSelected(timeline, index) {
|
||||
const animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
|
||||
return animation.classList.contains("selected");
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the controller provides the document.timeline currentTime (at least
|
||||
// the last known version since new animations were added).
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel, controller} = await openAnimationInspector();
|
||||
|
||||
ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
|
||||
checkDocumentTimeIsCorrect(controller);
|
||||
const time1 = controller.documentCurrentTime;
|
||||
|
||||
await startNewAnimation(controller, panel);
|
||||
checkDocumentTimeIsCorrect(controller);
|
||||
const time2 = controller.documentCurrentTime;
|
||||
ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
|
||||
});
|
||||
|
||||
function checkDocumentTimeIsCorrect(controller) {
|
||||
let time = 0;
|
||||
for (const {state} of controller.animationPlayers) {
|
||||
time = Math.max(time, state.documentCurrentTime);
|
||||
}
|
||||
is(controller.documentCurrentTime, time,
|
||||
"The documentCurrentTime is correct");
|
||||
}
|
||||
|
||||
async function startNewAnimation(controller, panel) {
|
||||
info("Add a new animation to the page and check the time again");
|
||||
const onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
|
||||
await executeInContent("devtools:test:setAttribute", {
|
||||
selector: ".still",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball still short"
|
||||
});
|
||||
|
||||
await onPlayerAdded;
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the behavior of animation-detail container.
|
||||
// We test following cases.
|
||||
// 1. Existance of animation-detail element.
|
||||
// 2. Hidden at first if multiple animations were displayed.
|
||||
// 3. Display after click on an animation.
|
||||
// 4. Display from first time if displayed animation is only one.
|
||||
// 5. Close the animation-detail element by clicking on close button.
|
||||
// 6. Stay selected animation even if refresh all UI.
|
||||
// 7. Close the animation-detail element again and click selected animation again.
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const { panel, inspector } = await openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const animationDetailEl =
|
||||
timelineComponent.rootWrapperEl.querySelector(".animation-detail");
|
||||
const splitboxControlledEl =
|
||||
timelineComponent.rootWrapperEl.querySelector(".controlled");
|
||||
|
||||
// 1. Existance of animation-detail element.
|
||||
ok(animationDetailEl, "The animation-detail element should exist");
|
||||
|
||||
// 2. Hidden at first if multiple animations were displayed.
|
||||
const win = timelineComponent.rootWrapperEl.ownerGlobal;
|
||||
is(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"The animation-detail element should be hidden at first "
|
||||
+ "if multiple animations were displayed");
|
||||
|
||||
// 3. Display after click on an animation.
|
||||
await clickOnAnimation(panel, 0);
|
||||
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"The animation-detail element should be displayed after clicked on an animation");
|
||||
|
||||
// 4. Display from first time if displayed animation is only one.
|
||||
await selectNodeAndWaitForAnimations("#target1", inspector);
|
||||
ok(animationDetailEl.querySelector(".property"),
|
||||
"The property in animation-detail element should be displayed");
|
||||
|
||||
// 5. Close the animation-detail element by clicking on close button.
|
||||
const previousHeight = animationDetailEl.offsetHeight;
|
||||
await clickCloseButtonForDetailPanel(timelineComponent, animationDetailEl);
|
||||
is(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"animation-detail element should not display");
|
||||
|
||||
// Select another animation.
|
||||
await selectNodeAndWaitForAnimations("#target2", inspector);
|
||||
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"animation-detail element should display");
|
||||
is(animationDetailEl.offsetHeight, previousHeight,
|
||||
"The height of animation-detail should keep the height");
|
||||
|
||||
// 6. Stay selected animation even if refresh all UI.
|
||||
await selectNodeAndWaitForAnimations("#target1", inspector);
|
||||
await clickTimelineRewindButton(panel);
|
||||
ok(animationDetailEl.querySelector(".property"),
|
||||
"The property in animation-detail element should stay as is");
|
||||
|
||||
// 7. Close the animation-detail element again and click selected animation again.
|
||||
await clickCloseButtonForDetailPanel(timelineComponent, animationDetailEl);
|
||||
await clickOnAnimation(panel, 0);
|
||||
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"animation-detail element should display again");
|
||||
});
|
||||
|
||||
/**
|
||||
* Click close button for animation-detail panel.
|
||||
*
|
||||
* @param {AnimationTimeline} AnimationTimeline component
|
||||
* @param {DOMNode} animation-detail element
|
||||
* @return {Promise} which wait for close the detail pane
|
||||
*/
|
||||
async function clickCloseButtonForDetailPanel(timeline, element) {
|
||||
const button = element.querySelector(".animation-detail-header button");
|
||||
const onclosed = timeline.once("animation-detail-closed");
|
||||
EventUtils.sendMouseEvent({type: "click"}, button, element.ownerDocument.defaultView);
|
||||
return onclosed;
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
// This is a test for displaying the easing of keyframes.
|
||||
// Checks easing text which is displayed as popup and
|
||||
// the path which emphasises by mouseover.
|
||||
|
||||
const TEST_CASES = {
|
||||
"no-easing": {
|
||||
opacity: {
|
||||
expectedValues: ["linear"],
|
||||
}
|
||||
},
|
||||
"effect-easing": {
|
||||
opacity: {
|
||||
expectedValues: ["linear"],
|
||||
}
|
||||
},
|
||||
"keyframe-easing": {
|
||||
opacity: {
|
||||
expectedValues: ["steps(2)"],
|
||||
}
|
||||
},
|
||||
"both-easing": {
|
||||
opacity: {
|
||||
expectedValues: ["steps(2)"],
|
||||
}
|
||||
},
|
||||
"many-keyframes": {
|
||||
backgroundColor: {
|
||||
selector: "rect",
|
||||
expectedValues: ["steps(2)", "ease-out"],
|
||||
noEmphasisPath: true,
|
||||
},
|
||||
opacity: {
|
||||
expectedValues: ["steps(2)", "ease-in", "linear", "ease-out"],
|
||||
}
|
||||
},
|
||||
"css-animations": {
|
||||
opacity: {
|
||||
expectedValues: ["ease", "ease"],
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_easings.html");
|
||||
const { panel } = await openAnimationInspector();
|
||||
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const timeBlocks = getAnimationTimeBlocks(panel);
|
||||
for (let i = 0; i < timeBlocks.length; i++) {
|
||||
await clickOnAnimation(panel, i);
|
||||
|
||||
const detailComponent = timelineComponent.details;
|
||||
const detailEl = detailComponent.containerEl;
|
||||
const state = detailComponent.animation.state;
|
||||
|
||||
const testcase = TEST_CASES[state.name];
|
||||
if (!testcase) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const testProperty in testcase) {
|
||||
const testIdentity = `"${ testProperty }" of "${ state.name }"`;
|
||||
info(`Test for ${ testIdentity }`);
|
||||
|
||||
const testdata = testcase[testProperty];
|
||||
const selector = testdata.selector ? testdata.selector : `.${testProperty}`;
|
||||
const hintEls = detailEl.querySelectorAll(`${ selector }.hint`);
|
||||
const expectedValues = testdata.expectedValues;
|
||||
is(hintEls.length, expectedValues.length,
|
||||
`Length of hints for ${ testIdentity } should be ${expectedValues.length}`);
|
||||
|
||||
for (let j = 0; j < hintEls.length; j++) {
|
||||
const hintEl = hintEls[j];
|
||||
const expectedValue = expectedValues[j];
|
||||
|
||||
info("Test easing text");
|
||||
const gEl = hintEl.closest("g");
|
||||
ok(gEl, `<g> element for ${ testIdentity } should exists`);
|
||||
const titleEl = gEl.querySelector("title");
|
||||
ok(titleEl, `<title> element for ${ testIdentity } should exists`);
|
||||
is(titleEl.textContent, expectedValue,
|
||||
`textContent of <title> for ${ testIdentity } should be ${ expectedValue }`);
|
||||
|
||||
info("Test emphasis path");
|
||||
// Scroll to show the hintEl since the element may be out of displayed area.
|
||||
hintEl.scrollIntoView(false);
|
||||
const win = hintEl.ownerGlobal;
|
||||
|
||||
// Mouse is over the hintEl. Ideally this would use EventUtils, but mouseover
|
||||
// on the path element is flaky, so let's trust that hovering works and force
|
||||
// on a hover class to get the same styles.
|
||||
hintEl.classList.add("hover");
|
||||
if (testdata.noEmphasisPath) {
|
||||
is(win.getComputedStyle(hintEl).strokeOpacity, 0,
|
||||
`stroke-opacity of hintEl for ${ testIdentity } should be 0 ` +
|
||||
`even while mouse is over the element`);
|
||||
} else {
|
||||
is(win.getComputedStyle(hintEl).strokeOpacity, 1,
|
||||
`stroke-opacity of hintEl for ${ testIdentity } should be 1 ` +
|
||||
`while mouse is over the element`);
|
||||
}
|
||||
// Mouse out once from hintEl.
|
||||
hintEl.classList.remove("hover");
|
||||
is(win.getComputedStyle(hintEl).strokeOpacity, 0,
|
||||
`stroke-opacity of hintEl for ${ testIdentity } should be 0 ` +
|
||||
`while mouse is out from the element`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the panel shows no animation data for invalid or not animated nodes
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel, window} = await openAnimationInspector();
|
||||
const {document} = window;
|
||||
|
||||
info("Select node .still and check that the panel is empty");
|
||||
const stillNode = await getNodeFront(".still", inspector);
|
||||
await selectNodeAndWaitForAnimations(stillNode, inspector);
|
||||
|
||||
is(panel.animationsTimelineComponent.animations.length, 0,
|
||||
"No animation players stored in the timeline component for a still node");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
|
||||
"No animation displayed in the timeline component for a still node");
|
||||
is(document.querySelector("#error-type").textContent,
|
||||
ANIMATION_L10N.getStr("panel.invalidElementSelected"),
|
||||
"The correct error message is displayed");
|
||||
|
||||
info("Select the comment text node and check that the panel is empty");
|
||||
const commentNode = await inspector.walker.previousSibling(stillNode);
|
||||
await selectNodeAndWaitForAnimations(commentNode, inspector);
|
||||
|
||||
is(panel.animationsTimelineComponent.animations.length, 0,
|
||||
"No animation players stored in the timeline component for a text node");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
|
||||
"No animation displayed in the timeline component for a text node");
|
||||
is(document.querySelector("#error-type").textContent,
|
||||
ANIMATION_L10N.getStr("panel.invalidElementSelected"),
|
||||
"The correct error message is displayed");
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that when an animation is selected and its list of properties is shown,
|
||||
// there are keyframes markers next to each property being animated.
|
||||
|
||||
const EXPECTED_PROPERTIES = [
|
||||
"backgroundColor",
|
||||
"backgroundPosition",
|
||||
"backgroundSize",
|
||||
"borderBottomLeftRadius",
|
||||
"borderBottomRightRadius",
|
||||
"borderTopLeftRadius",
|
||||
"borderTopRightRadius",
|
||||
"filter",
|
||||
"height",
|
||||
"transform",
|
||||
"width"
|
||||
];
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_keyframes.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
// doc_keyframes.html has only one animation.
|
||||
// So we don't need to click the animation since already the animation detail shown.
|
||||
|
||||
ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
|
||||
"There are container elements for displaying keyframes");
|
||||
|
||||
const data = await getExpectedKeyframesData(timeline.animations[0]);
|
||||
for (const propertyName in data) {
|
||||
info("Check the keyframe markers for " + propertyName);
|
||||
const widthMarkerSelector = ".frame[data-property=" + propertyName + "]";
|
||||
const markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector);
|
||||
|
||||
is(markers.length, data[propertyName].length,
|
||||
"The right number of keyframes was found for " + propertyName);
|
||||
|
||||
const offsets = [...markers].map(m => parseFloat(m.dataset.offset));
|
||||
const values = [...markers].map(m => m.dataset.value);
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
is(markers[i].dataset.offset, offsets[i],
|
||||
"Marker " + i + " for " + propertyName + " has the right offset");
|
||||
is(markers[i].dataset.value, values[i],
|
||||
"Marker " + i + " for " + propertyName + " has the right value");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function getExpectedKeyframesData(animation) {
|
||||
// We're testing the UI state here, so it's fine to get the list of expected
|
||||
// properties from the animation actor.
|
||||
const properties = await animation.getProperties();
|
||||
const data = {};
|
||||
|
||||
for (const expectedProperty of EXPECTED_PROPERTIES) {
|
||||
data[expectedProperty] = [];
|
||||
for (const {name, values} of properties) {
|
||||
if (name !== expectedProperty) {
|
||||
continue;
|
||||
}
|
||||
for (const {offset, value} of values) {
|
||||
data[expectedProperty].push({offset, value});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that when animations are added later (through animation mutations) and
|
||||
// if these animations have the same names, then all of them are still being
|
||||
// displayed (which should be true as long as these animations apply to
|
||||
// different nodes).
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_negative_animation.html");
|
||||
const {controller, panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
const areTracksReady = () => timeline.animations.every(a => {
|
||||
return timeline.componentsMap[a.actorID];
|
||||
});
|
||||
|
||||
// We need to wait for all tracks to be ready, cause this is an async part of the init
|
||||
// of the panel.
|
||||
while (controller.animationPlayers.length < 3 || !areTracksReady()) {
|
||||
await waitForAnimationTimelineRendering(panel);
|
||||
}
|
||||
|
||||
// Same for animation targets, they're retrieved asynchronously.
|
||||
await waitForAllAnimationTargets(panel);
|
||||
|
||||
is(panel.animationsTimelineComponent.animations.length, 3,
|
||||
"The timeline shows 3 animations too");
|
||||
|
||||
// Reduce the known nodeFronts to a set to make them unique.
|
||||
const nodeFronts =
|
||||
new Set(getAnimationTargetNodes(panel).map(n => n.previewer.nodeFront));
|
||||
is(nodeFronts.size, 3, "The animations are applied to 3 different node fronts");
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the animation panel sidebar exists
|
||||
|
||||
add_task(async function() {
|
||||
await addTab("data:text/html;charset=utf-8,welcome to the animation panel");
|
||||
const {panel, controller} = await openAnimationInspector();
|
||||
|
||||
ok(controller,
|
||||
"The animation controller exists");
|
||||
ok(controller.animationsFront,
|
||||
"The animation controller has been initialized");
|
||||
ok(panel,
|
||||
"The animation panel exists");
|
||||
ok(panel.playersEl,
|
||||
"The animation panel has been initialized");
|
||||
ok(panel.animationsTimelineComponent,
|
||||
"The animation panel has been initialized");
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the update of the animation panel participate in the
|
||||
// inspector-updated event. This means that the test verifies that the
|
||||
// inspector-updated event is emitted *after* the animation panel is ready.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel, controller} = await openAnimationInspector();
|
||||
|
||||
info("Listen for the players-updated, ui-updated and " +
|
||||
"inspector-updated events");
|
||||
const receivedEvents = [];
|
||||
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
|
||||
receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
|
||||
});
|
||||
panel.once(panel.UI_UPDATED_EVENT, () => {
|
||||
receivedEvents.push(panel.UI_UPDATED_EVENT);
|
||||
});
|
||||
inspector.once("inspector-updated", () => {
|
||||
receivedEvents.push("inspector-updated");
|
||||
});
|
||||
|
||||
info("Selecting an animated node");
|
||||
const node = await getNodeFront(".animated", inspector);
|
||||
await selectNodeAndWaitForAnimations(node, inspector);
|
||||
|
||||
info("Check that all events were received");
|
||||
// Only assert that the inspector-updated event is last, the order of the
|
||||
// first 2 events is irrelevant.
|
||||
|
||||
is(receivedEvents.length, 3, "3 events were received");
|
||||
is(receivedEvents[2], "inspector-updated",
|
||||
"The third event received was the inspector-updated event");
|
||||
|
||||
ok(receivedEvents.includes(controller.PLAYERS_UPDATED_EVENT),
|
||||
"The players-updated event was received");
|
||||
ok(receivedEvents.includes(panel.UI_UPDATED_EVENT),
|
||||
"The ui-updated event was received");
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the AnimationPlayerFront objects lifecycle is managed by the
|
||||
// AnimationController.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {controller, inspector} = await openAnimationInspector();
|
||||
|
||||
info("Selecting an animated node");
|
||||
// selectNode waits for the inspector-updated event before resolving, which
|
||||
// means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
|
||||
// and players are ready.
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 1,
|
||||
"One AnimationPlayerFront has been created");
|
||||
|
||||
info("Selecting a node with mutliple animations");
|
||||
await selectNodeAndWaitForAnimations(".multi", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 2,
|
||||
"2 AnimationPlayerFronts have been created");
|
||||
|
||||
info("Selecting a node with no animations");
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 0,
|
||||
"There are no more AnimationPlayerFront objects");
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that player widgets are displayed right when the animation panel is
|
||||
// initialized, if the selected node (<body> by default) is animated.
|
||||
|
||||
const { ANIMATION_TYPES } = require("devtools/server/actors/animation");
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_animation_types.html");
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
is(panel.animationsTimelineComponent.animations.length, 3,
|
||||
"Three animations are handled by the timeline after init");
|
||||
assertAnimationsDisplayed(panel, 3,
|
||||
"Three animations are displayed after init");
|
||||
is(
|
||||
panel.animationsTimelineComponent
|
||||
.animationsEl
|
||||
.querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`)
|
||||
.length,
|
||||
1,
|
||||
"One script-generated animation is displayed");
|
||||
is(
|
||||
panel.animationsTimelineComponent
|
||||
.animationsEl
|
||||
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`)
|
||||
.length,
|
||||
1,
|
||||
"One CSS animation is displayed");
|
||||
is(
|
||||
panel.animationsTimelineComponent
|
||||
.animationsEl
|
||||
.querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`)
|
||||
.length,
|
||||
1,
|
||||
"One CSS transition is displayed");
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that player widgets display information about target nodes
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Select the simple animated node");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
const targetNodeComponent = getAnimationTargetNodes(panel)[0];
|
||||
const {previewer} = targetNodeComponent;
|
||||
|
||||
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
|
||||
// yet been retrieved by the TargetNodeComponent.
|
||||
if (!previewer.nodeFront) {
|
||||
await targetNodeComponent.once("target-retrieved");
|
||||
}
|
||||
|
||||
is(previewer.el.textContent, "div#.ball.animated",
|
||||
"The target element's content is correct");
|
||||
|
||||
const highlighterEl = previewer.el.querySelector(".node-highlighter");
|
||||
ok(highlighterEl,
|
||||
"The icon to highlight the target element in the page exists");
|
||||
});
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that animated pseudo-elements do show in the timeline.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_pseudo_elements.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("With <body> selected by default check the content of the timeline");
|
||||
is(getAnimationTimeBlocks(panel).length, 3, "There are 3 animations in the timeline");
|
||||
|
||||
const targetNodes = getAnimationTargetNodes(panel);
|
||||
const getTargetNodeText = index => {
|
||||
const el = targetNodes[index].previewer.previewEl;
|
||||
return [...el.childNodes]
|
||||
.map(n => n.style.display === "none" ? "" : n.textContent)
|
||||
.join("");
|
||||
};
|
||||
|
||||
is(getTargetNodeText(0), "body", "The first animated node is <body>");
|
||||
is(getTargetNodeText(1), "::before", "The second animated node is ::before");
|
||||
is(getTargetNodeText(2), "::after", "The third animated node is ::after");
|
||||
|
||||
info("Getting the before and after nodeFronts");
|
||||
const bodyContainer = await getContainerForSelector("body", inspector);
|
||||
const getBodyChildNodeFront = index => {
|
||||
return bodyContainer.elt.children[1].childNodes[index].container.node;
|
||||
};
|
||||
const beforeNode = getBodyChildNodeFront(0);
|
||||
const afterNode = getBodyChildNodeFront(1);
|
||||
|
||||
info("Select the ::before pseudo-element in the inspector");
|
||||
await selectNodeAndWaitForAnimations(beforeNode, inspector);
|
||||
is(getAnimationTimeBlocks(panel).length, 1, "There is 1 animation in the timeline");
|
||||
is(getAnimationTargetNodes(panel)[0].previewer.nodeFront,
|
||||
inspector.selection.nodeFront,
|
||||
"The right node front is displayed in the timeline");
|
||||
|
||||
info("Select the ::after pseudo-element in the inspector");
|
||||
await selectNodeAndWaitForAnimations(afterNode, inspector);
|
||||
is(getAnimationTimeBlocks(panel).length, 1, "There is 1 animation in the timeline");
|
||||
is(getAnimationTargetNodes(panel)[0].previewer.nodeFront,
|
||||
inspector.selection.nodeFront,
|
||||
"The right node front is displayed in the timeline");
|
||||
});
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the panel content refreshes when new animations are added.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Select a non animated node");
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 0);
|
||||
|
||||
info("Start an animation on the node");
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
await changeElementAndWait({
|
||||
selector: ".still",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball animated"
|
||||
}, panel, inspector);
|
||||
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
|
||||
info("Remove the animation class on the node");
|
||||
await changeElementAndWait({
|
||||
selector: ".ball.animated",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball still"
|
||||
}, panel, inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 0);
|
||||
});
|
||||
|
||||
async function changeElementAndWait(options, panel, inspector) {
|
||||
const onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
const onInspectorUpdated = inspector.once("inspector-updated");
|
||||
|
||||
await executeInContent("devtools:test:setAttribute", options);
|
||||
|
||||
await promise.all([onInspectorUpdated, onPanelUpdated]);
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the panel content refreshes when animations are removed.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
await testRefreshOnRemove(inspector, panel);
|
||||
});
|
||||
|
||||
async function testRefreshOnRemove(inspector, panel) {
|
||||
info("Select a animated node");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
|
||||
info("Listen to the next UI update event");
|
||||
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
info("Remove the animation on the node by removing the class");
|
||||
await executeInContent("devtools:test:setAttribute", {
|
||||
selector: ".animated",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball still test-node"
|
||||
});
|
||||
|
||||
await onPanelUpdated;
|
||||
ok(true, "The panel update event was fired");
|
||||
|
||||
assertAnimationsDisplayed(panel, 0);
|
||||
|
||||
info("Add an finite animation on the node again, and wait for it to appear");
|
||||
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
await executeInContent("devtools:test:setAttribute", {
|
||||
selector: ".test-node",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball short test-node"
|
||||
});
|
||||
await onPanelUpdated;
|
||||
await waitForAnimationSelecting(panel);
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the panel only refreshes when it is visible in the sidebar.
|
||||
|
||||
add_task(async function() {
|
||||
info("Switch to 2 pane inspector to see if the panel only refreshes when visible");
|
||||
await pushPref("devtools.inspector.three-pane-enabled", false);
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
await testRefresh(inspector, panel);
|
||||
});
|
||||
|
||||
async function testRefresh(inspector, panel) {
|
||||
info("Select a non animated node");
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
info("Switch to the rule-view panel");
|
||||
inspector.sidebar.select("ruleview");
|
||||
|
||||
info("Select the animated node now");
|
||||
await selectNode(".animated", inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 0,
|
||||
"The panel doesn't show the animation data while inactive");
|
||||
|
||||
info("Switch to the animation panel");
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
inspector.sidebar.select("animationinspector");
|
||||
await panel.once(panel.UI_UPDATED_EVENT);
|
||||
await onRendered;
|
||||
|
||||
assertAnimationsDisplayed(panel, 1,
|
||||
"The panel shows the animation data after selecting it");
|
||||
|
||||
info("Switch again to the rule-view");
|
||||
inspector.sidebar.select("ruleview");
|
||||
|
||||
info("Select the non animated node again");
|
||||
await selectNode(".still", inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 1,
|
||||
"The panel still shows the previous animation data since it is inactive");
|
||||
|
||||
info("Switch to the animation panel again");
|
||||
inspector.sidebar.select("animationinspector");
|
||||
await panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
assertAnimationsDisplayed(panel, 0,
|
||||
"The panel is now empty after refreshing");
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that refresh animation UI while the panel is hidden.
|
||||
|
||||
const EXPECTED_GRAPH_PATH_SEGMENTS = [{ x: 0, y: 0 },
|
||||
{ x: 49999, y: 0.0 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 99999, y: 0.5 },
|
||||
{ x: 100000, y: 0 }];
|
||||
|
||||
add_task(async function() {
|
||||
info("Switch to 2 pane inspector to see if the panel only refreshes when visible");
|
||||
await pushPref("devtools.inspector.three-pane-enabled", false);
|
||||
|
||||
info("Open animation inspector once so that activate animation mutations listener");
|
||||
await addTab("data:text/html;charset=utf8,<div id='target'>test</div>");
|
||||
const { controller, inspector, panel } = await openAnimationInspector();
|
||||
|
||||
info("Select other tool to hide animation inspector");
|
||||
await inspector.sidebar.select("ruleview");
|
||||
|
||||
// Count players-updated event in controller.
|
||||
let updatedEventCount = 0;
|
||||
controller.on("players-updated", () => {
|
||||
updatedEventCount += 1;
|
||||
});
|
||||
|
||||
info("Make animation by eval in content");
|
||||
await evalInDebuggee(`document.querySelector('#target').animate(
|
||||
{ transform: 'translate(100px)' },
|
||||
{ duration: 100000, easing: 'steps(2)' });`);
|
||||
info("Wait for animation mutations event");
|
||||
await controller.animationsFront.once("mutations");
|
||||
info("Check players-updated events count");
|
||||
is(updatedEventCount, 0, "players-updated event shoud not be fired");
|
||||
|
||||
info("Re-select animation inspector and check the UI");
|
||||
await inspector.sidebar.select("animationinspector");
|
||||
await waitForAnimationTimelineRendering(panel);
|
||||
|
||||
const timeBlocks = getAnimationTimeBlocks(panel);
|
||||
is(timeBlocks.length, 1, "One animation should display");
|
||||
const timeBlock = timeBlocks[0];
|
||||
const state = timeBlock.animation.state;
|
||||
const effectEl = timeBlock.containerEl.querySelector(".effect-easing");
|
||||
ok(effectEl, "<g> element for effect easing should exist");
|
||||
const pathEl = effectEl.querySelector("path");
|
||||
ok(pathEl, "<path> element for effect easing should exist");
|
||||
assertPathSegments(pathEl, state.duration, false, EXPECTED_GRAPH_PATH_SEGMENTS);
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that when animations displayed in the timeline are running on the
|
||||
// compositor, they get a special icon and information in the tooltip.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
info("Select a test node we know has an animation running on the compositor");
|
||||
await selectNodeAndWaitForAnimations(".compositor-all", inspector);
|
||||
|
||||
let animationEl = timeline.animationsEl.querySelector(".animation");
|
||||
ok(animationEl.classList.contains("fast-track"),
|
||||
"The animation element has the fast-track css class");
|
||||
ok(hasTooltip(animationEl,
|
||||
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
|
||||
"The animation element has the right tooltip content");
|
||||
|
||||
info("Select a node we know doesn't have an animation on the compositor");
|
||||
await selectNodeAndWaitForAnimations(".no-compositor", inspector);
|
||||
|
||||
animationEl = timeline.animationsEl.querySelector(".animation");
|
||||
ok(!animationEl.classList.contains("fast-track"),
|
||||
"The animation element does not have the fast-track css class");
|
||||
ok(!hasTooltip(animationEl,
|
||||
ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")),
|
||||
"The animation element does not have oncompositor tooltip content");
|
||||
ok(!hasTooltip(animationEl,
|
||||
ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
|
||||
"The animation element does not have oncompositor tooltip content");
|
||||
|
||||
info("Select a node we know has animation on the compositor and not on the" +
|
||||
" compositor");
|
||||
await selectNodeAndWaitForAnimations(".compositor-notall", inspector);
|
||||
|
||||
animationEl = timeline.animationsEl.querySelector(".animation");
|
||||
ok(animationEl.classList.contains("fast-track"),
|
||||
"The animation element has the fast-track css class");
|
||||
ok(hasTooltip(animationEl,
|
||||
ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")),
|
||||
"The animation element has the right tooltip content");
|
||||
});
|
||||
|
||||
function hasTooltip(animationEl, expected) {
|
||||
const el = animationEl.querySelector(".name");
|
||||
const tooltip = el.getAttribute("title");
|
||||
|
||||
return tooltip.includes(expected);
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that when playerFronts are updated, the same number of playerWidgets
|
||||
// are created in the panel.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel, controller} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
info("Selecting the test animated node again");
|
||||
await selectNodeAndWaitForAnimations(".multi", inspector);
|
||||
|
||||
is(controller.animationPlayers.length,
|
||||
timeline.animationsEl.querySelectorAll(".animation").length,
|
||||
"As many timeline elements were created as there are playerFronts");
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the panel shows an animation player when an animated node is
|
||||
// selected.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Select node .animated and check that the panel is not empty");
|
||||
const node = await getNodeFront(".animated", inspector);
|
||||
await selectNodeAndWaitForAnimations(node, inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the spacebar key press toggles the toggleAll button state
|
||||
// when a node with no animation is selected.
|
||||
// This test doesn't need to test if animations actually pause/resume
|
||||
// because there's an other test that does this :
|
||||
// browser_animation_toggle_button_toggles_animation.js
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel, inspector, window, controller} = await openAnimationInspector();
|
||||
const {toggleAllButtonEl} = panel;
|
||||
|
||||
// select a node without animations
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
// ensure the focus is on the animation panel
|
||||
window.focus();
|
||||
|
||||
info("Simulate spacebar stroke and check toggleAll button" +
|
||||
" is in paused state");
|
||||
|
||||
// sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
|
||||
let onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
|
||||
EventUtils.sendKey("SPACE", window);
|
||||
await onToggled;
|
||||
ok(toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle all button is in its paused state");
|
||||
|
||||
info("Simulate spacebar stroke and check toggleAll button" +
|
||||
" is in playing state");
|
||||
|
||||
// sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT
|
||||
onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT);
|
||||
EventUtils.sendKey("SPACE", window);
|
||||
await onToggled;
|
||||
ok(!toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle all button is in its playing state again");
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the spacebar key press toggles the play/resume button state.
|
||||
// This test doesn't need to test if animations actually pause/resume
|
||||
// because there's an other test that does this.
|
||||
// There are animations in the test page and since, by default, the <body> node
|
||||
// is selected, animations will be displayed in the timeline, so the timeline
|
||||
// play/resume button will be displayed
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
add_task(async function() {
|
||||
requestLongerTimeout(2);
|
||||
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel, window} = await openAnimationInspector();
|
||||
const {playTimelineButtonEl} = panel;
|
||||
|
||||
// ensure the focus is on the animation panel
|
||||
window.focus();
|
||||
|
||||
info("Simulate spacebar stroke and check playResume button" +
|
||||
" is in paused state");
|
||||
|
||||
// sending the key will lead to render animation timeline
|
||||
EventUtils.sendKey("SPACE", window);
|
||||
await waitForAnimationTimelineRendering(panel);
|
||||
ok(playTimelineButtonEl.classList.contains("paused"),
|
||||
"The play/resume button is in its paused state");
|
||||
|
||||
info("Simulate spacebar stroke and check playResume button" +
|
||||
" is in playing state");
|
||||
|
||||
// sending the key will lead to render animation timeline
|
||||
EventUtils.sendKey("SPACE", window);
|
||||
await waitForAnimationTimelineRendering(panel);
|
||||
ok(!playTimelineButtonEl.classList.contains("paused"),
|
||||
"The play/resume button is in its play state again");
|
||||
});
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test summary graph for animations that have multiple easing.
|
||||
// There are two ways that we can set the easing for animations.
|
||||
// One is effect easing, another one is keyframe easing for properties.
|
||||
// The summary graph shows effect easing as dashed line if the easing is not 'linear'.
|
||||
// If 'linear', does not show.
|
||||
// Also, shows graph which combine the keyframe easings of all properties.
|
||||
|
||||
const TEST_CASES = {
|
||||
"no-easing": {
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 25000, y: 0.25 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 75000, y: 0.75 },
|
||||
{ x: 99000, y: 0.99 },
|
||||
{ x: 100000, y: 0 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"effect-easing": {
|
||||
expectedEffectEasingGraph: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 19999, y: 0.0 },
|
||||
{ x: 20000, y: 0.25 },
|
||||
{ x: 39999, y: 0.25 },
|
||||
{ x: 40000, y: 0.5 },
|
||||
{ x: 59999, y: 0.5 },
|
||||
{ x: 60000, y: 0.75 },
|
||||
{ x: 79999, y: 0.75 },
|
||||
{ x: 80000, y: 1 },
|
||||
{ x: 99999, y: 1 },
|
||||
{ x: 100000, y: 0 },
|
||||
],
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 19999, y: 0.0 },
|
||||
{ x: 20000, y: 0.25 },
|
||||
{ x: 39999, y: 0.25 },
|
||||
{ x: 40000, y: 0.5 },
|
||||
{ x: 59999, y: 0.5 },
|
||||
{ x: 60000, y: 0.75 },
|
||||
{ x: 79999, y: 0.75 },
|
||||
{ x: 80000, y: 1 },
|
||||
{ x: 99999, y: 1 },
|
||||
{ x: 100000, y: 0 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"keyframe-easing": {
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 49999, y: 0.0 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 99999, y: 0.5 },
|
||||
{ x: 100000, y: 0 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"both-easing": {
|
||||
expectedEffectEasingGraph: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 9999, y: 0.0 },
|
||||
{ x: 10000, y: 0.1 },
|
||||
{ x: 19999, y: 0.1 },
|
||||
{ x: 20000, y: 0.2 },
|
||||
{ x: 29999, y: 0.2 },
|
||||
{ x: 30000, y: 0.3 },
|
||||
{ x: 39999, y: 0.3 },
|
||||
{ x: 40000, y: 0.4 },
|
||||
{ x: 49999, y: 0.4 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 59999, y: 0.5 },
|
||||
{ x: 60000, y: 0.6 },
|
||||
{ x: 69999, y: 0.6 },
|
||||
{ x: 70000, y: 0.7 },
|
||||
{ x: 79999, y: 0.7 },
|
||||
{ x: 80000, y: 0.8 },
|
||||
{ x: 89999, y: 0.8 },
|
||||
{ x: 90000, y: 0.9 },
|
||||
{ x: 99999, y: 0.9 },
|
||||
{ x: 100000, y: 0 },
|
||||
],
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
// KeyframeEffect::GetProperties returns sorted properties by the name.
|
||||
// Therefor, the test html, the 'marginLeft' is upper than 'opacity'.
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 19999, y: 0.0 },
|
||||
{ x: 20000, y: 0.2 },
|
||||
{ x: 39999, y: 0.2 },
|
||||
{ x: 40000, y: 0.4 },
|
||||
{ x: 59999, y: 0.4 },
|
||||
{ x: 60000, y: 0.6 },
|
||||
{ x: 79999, y: 0.6 },
|
||||
{ x: 80000, y: 0.8 },
|
||||
{ x: 99999, y: 0.8 },
|
||||
{ x: 100000, y: 0 },
|
||||
],
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 49999, y: 0.0 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 99999, y: 0.5 },
|
||||
{ x: 100000, y: 0 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"narrow-keyframes": {
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 10000, y: 0.1 },
|
||||
{ x: 11000, y: 0.1 },
|
||||
{ x: 11500, y: 0.1 },
|
||||
{ x: 12999, y: 0.1 },
|
||||
{ x: 13000, y: 0.13 },
|
||||
{ x: 13500, y: 0.135 },
|
||||
{ x: 14000, y: 0.14 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"duplicate-offsets": {
|
||||
expectedKeyframeEasingGraphs: [
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 25000, y: 0.25 },
|
||||
{ x: 50000, y: 0.5 },
|
||||
{ x: 75000, y: 0.5 },
|
||||
{ x: 99999, y: 0.5 },
|
||||
]
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_multiple_easings.html");
|
||||
const { panel } = await openAnimationInspector();
|
||||
getAnimationTimeBlocks(panel).forEach(timeBlock => {
|
||||
const state = timeBlock.animation.state;
|
||||
const testcase = TEST_CASES[state.name];
|
||||
if (!testcase) {
|
||||
return;
|
||||
}
|
||||
|
||||
info(`Test effect easing graph of ${ state.name }`);
|
||||
const effectEl = timeBlock.containerEl.querySelector(".effect-easing");
|
||||
if (testcase.expectedEffectEasingGraph) {
|
||||
ok(effectEl, "<g> element for effect easing should exist");
|
||||
const pathEl = effectEl.querySelector("path");
|
||||
ok(pathEl, "<path> element for effect easing should exist");
|
||||
assertPathSegments(pathEl, state.duration, false,
|
||||
testcase.expectedEffectEasingGraph);
|
||||
} else {
|
||||
ok(!effectEl, "<g> element for effect easing should not exist");
|
||||
}
|
||||
|
||||
info(`Test keyframes easing graph of ${ state.name }`);
|
||||
const keyframeEls = timeBlock.containerEl.querySelectorAll(".keyframes-easing");
|
||||
const expectedKeyframeEasingGraphs = testcase.expectedKeyframeEasingGraphs;
|
||||
is(keyframeEls.length, expectedKeyframeEasingGraphs.length,
|
||||
`There should be ${ expectedKeyframeEasingGraphs.length } <g> elements `
|
||||
+ `for keyframe easing`);
|
||||
expectedKeyframeEasingGraphs.forEach((expectedValues, index) => {
|
||||
const pathEl = keyframeEls[index].querySelector("path");
|
||||
ok(pathEl, "<path> element for keyframe easing should exist");
|
||||
assertPathSegments(pathEl, state.duration, false, expectedValues);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the DOM element targets displayed in animation player widgets can
|
||||
// be used to highlight elements in the DOM and select them in the inspector.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {toolbox, inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Select the simple animated node");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
let targets = getAnimationTargetNodes(panel);
|
||||
// Arbitrary select the first one
|
||||
let targetNodeComponent = targets[0];
|
||||
|
||||
info("Retrieve the part of the widget that highlights the node on hover");
|
||||
const highlightingEl = targetNodeComponent.previewer.previewEl;
|
||||
|
||||
info("Listen to node-highlight event and mouse over the widget");
|
||||
const onHighlight = toolbox.once("node-highlight");
|
||||
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
|
||||
highlightingEl.ownerDocument.defaultView);
|
||||
const nodeFront = await onHighlight;
|
||||
|
||||
// Do not forget to mouseout, otherwise we get random mouseover event
|
||||
// when selecting another node, which triggers some requests in animation
|
||||
// inspector.
|
||||
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
|
||||
highlightingEl.ownerDocument.defaultView);
|
||||
|
||||
ok(true, "The node-highlight event was fired");
|
||||
is(targetNodeComponent.previewer.nodeFront, nodeFront,
|
||||
"The highlighted node is the one stored on the animation widget");
|
||||
is(nodeFront.tagName, "DIV",
|
||||
"The highlighted node has the correct tagName");
|
||||
is(nodeFront.attributes[0].name, "class",
|
||||
"The highlighted node has the correct attributes");
|
||||
is(nodeFront.attributes[0].value, "ball animated",
|
||||
"The highlighted node has the correct class");
|
||||
|
||||
info("Select the body node in order to have the list of all animations");
|
||||
await selectNodeAndWaitForAnimations("body", inspector);
|
||||
|
||||
targets = getAnimationTargetNodes(panel);
|
||||
targetNodeComponent = targets[0];
|
||||
|
||||
info("Click on the first animated node component and wait for the " +
|
||||
"selection to change");
|
||||
const onSelection = inspector.selection.once("new-node-front");
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
const nodeEl = targetNodeComponent.previewer.previewEl;
|
||||
EventUtils.sendMouseEvent({type: "click"}, nodeEl,
|
||||
nodeEl.ownerDocument.defaultView);
|
||||
await onSelection;
|
||||
|
||||
is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront,
|
||||
"The selected node is the one stored on the animation widget");
|
||||
|
||||
await onRendered;
|
||||
});
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the DOM element targets displayed in animation player widgets can
|
||||
// be used to highlight elements in the DOM and select them in the inspector.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const targets = getAnimationTargetNodes(panel);
|
||||
|
||||
info("Click on the highlighter icon for the first animated node");
|
||||
const domNodePreview1 = targets[0].previewer;
|
||||
await lockHighlighterOn(domNodePreview1);
|
||||
ok(domNodePreview1.highlightNodeEl.classList.contains("selected"),
|
||||
"The highlighter icon is selected");
|
||||
|
||||
info("Click on the highlighter icon for the second animated node");
|
||||
const domNodePreview2 = targets[1].previewer;
|
||||
await lockHighlighterOn(domNodePreview2);
|
||||
ok(domNodePreview2.highlightNodeEl.classList.contains("selected"),
|
||||
"The highlighter icon is selected");
|
||||
ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"),
|
||||
"The highlighter icon for the first node is unselected");
|
||||
|
||||
info("Click again to unhighlight");
|
||||
await unlockHighlighterOn(domNodePreview2);
|
||||
ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"),
|
||||
"The highlighter icon for the second node is unselected");
|
||||
});
|
||||
|
||||
async function lockHighlighterOn(domNodePreview) {
|
||||
const onLocked = domNodePreview.once("target-highlighter-locked");
|
||||
clickOnHighlighterIcon(domNodePreview);
|
||||
await onLocked;
|
||||
}
|
||||
|
||||
async function unlockHighlighterOn(domNodePreview) {
|
||||
const onUnlocked = domNodePreview.once("target-highlighter-unlocked");
|
||||
clickOnHighlighterIcon(domNodePreview);
|
||||
await onUnlocked;
|
||||
}
|
||||
|
||||
function clickOnHighlighterIcon(domNodePreview) {
|
||||
const lockEl = domNodePreview.highlightNodeEl;
|
||||
EventUtils.sendMouseEvent({type: "click"}, lockEl,
|
||||
lockEl.ownerDocument.defaultView);
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test re-used animation element since we re-use existent animation element
|
||||
// for the render performance.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_add_animation.html");
|
||||
const {panel, controller} = await openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
|
||||
// Add new animation which has delay and endDelay.
|
||||
await startNewAnimation(controller, panel, "#target2");
|
||||
const previousAnimationEl =
|
||||
timelineComponent.animationsEl.querySelector(".animation:nth-child(2)");
|
||||
const previousSummaryGraphEl = previousAnimationEl.querySelector(".summary");
|
||||
const previousDelayEl = previousAnimationEl.querySelector(".delay");
|
||||
const previousEndDelayEl = previousAnimationEl.querySelector(".end-delay");
|
||||
const previousSummaryGraphWidth = previousSummaryGraphEl.viewBox.baseVal.width;
|
||||
const previousDelayBounds = previousDelayEl.getBoundingClientRect();
|
||||
const previousEndDelayBounds = previousEndDelayEl.getBoundingClientRect();
|
||||
|
||||
// Add another animation.
|
||||
await startNewAnimation(controller, panel, "#target3");
|
||||
const currentAnimationEl =
|
||||
timelineComponent.animationsEl.querySelector(".animation:nth-child(2)");
|
||||
const currentSummaryGraphEl = currentAnimationEl.querySelector(".summary");
|
||||
const currentDelayEl = currentAnimationEl.querySelector(".delay");
|
||||
const currentEndDelayEl = currentAnimationEl.querySelector(".end-delay");
|
||||
const currentSummaryGraphWidth = currentSummaryGraphEl.viewBox.baseVal.width;
|
||||
const currentDelayBounds = currentDelayEl.getBoundingClientRect();
|
||||
const currentEndDelayBounds = currentEndDelayEl.getBoundingClientRect();
|
||||
|
||||
is(previousAnimationEl, currentAnimationEl, ".animation element should be reused");
|
||||
is(previousSummaryGraphEl, currentSummaryGraphEl, ".summary element should be reused");
|
||||
is(previousDelayEl, currentDelayEl, ".delay element should be reused");
|
||||
is(previousEndDelayEl, currentEndDelayEl, ".end-delay element should be reused");
|
||||
|
||||
ok(currentSummaryGraphWidth > previousSummaryGraphWidth,
|
||||
"Reused .summary element viewBox width should be longer");
|
||||
ok(currentDelayBounds.left < previousDelayBounds.left,
|
||||
"Reused .delay element should move to the left");
|
||||
ok(currentDelayBounds.width < previousDelayBounds.width,
|
||||
"Reused .delay element should be shorter");
|
||||
ok(currentEndDelayBounds.left < previousEndDelayBounds.left,
|
||||
"Reused .end-delay element should move to the left");
|
||||
ok(currentEndDelayBounds.width < previousEndDelayBounds.width,
|
||||
"Reused .end-delay element should be shorter");
|
||||
});
|
||||
|
||||
async function startNewAnimation(controller, panel, selector) {
|
||||
info("Add a new animation to the page and check the time again");
|
||||
const onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
|
||||
await executeInContent("devtools:test:setAttribute", {
|
||||
selector: selector,
|
||||
attributeName: "class",
|
||||
attributeValue: "animation"
|
||||
});
|
||||
|
||||
await onPlayerAdded;
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline toolbar displays the current time, and that it
|
||||
// changes when animations are playing, gets back to 0 when animations are
|
||||
// rewound, and stops when animations are paused.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
const label = panel.timelineCurrentTimeEl;
|
||||
ok(label, "The current time label exists");
|
||||
|
||||
// On page load animations are playing so the time shoud change, although we
|
||||
// don't want to test the exact value of the time displayed, just that it
|
||||
// actually changes.
|
||||
info("Make sure the time displayed actually changes");
|
||||
await isCurrentTimeLabelChanging(panel, true);
|
||||
|
||||
info("Pause the animations and check that the time stops changing");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
await isCurrentTimeLabelChanging(panel, false);
|
||||
|
||||
info("Rewind the animations and check that the time stops changing");
|
||||
await clickTimelineRewindButton(panel);
|
||||
await isCurrentTimeLabelChanging(panel, false);
|
||||
is(label.textContent, "00:00.000");
|
||||
});
|
||||
|
||||
async function isCurrentTimeLabelChanging(panel, isChanging) {
|
||||
const label = panel.timelineCurrentTimeEl;
|
||||
|
||||
const time1 = label.textContent;
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const time2 = label.textContent;
|
||||
|
||||
if (isChanging) {
|
||||
ok(time1 !== time2, "The text displayed in the label changes with time");
|
||||
} else {
|
||||
is(time1, time2, "The text displayed in the label doesn't change");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline shows correct time graduations in the header.
|
||||
|
||||
const {findOptimalTimeInterval, TimeScale} = require("devtools/client/inspector/animation-old/utils");
|
||||
|
||||
// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
|
||||
// animation-timeline.js
|
||||
const TIME_GRADUATION_MIN_SPACING = 40;
|
||||
|
||||
add_task(async function() {
|
||||
await pushPref("devtools.inspector.three-pane-enabled", false);
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
// System scrollbar is enabled by default on our testing envionment and it
|
||||
// would shrink width of inspector and affect number of time-ticks causing
|
||||
// unexpected results. So, we set it wider to avoid this kind of edge case.
|
||||
await pushPref("devtools.toolsidebar-width.inspector", 350);
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const headerEl = timeline.timeHeaderEl;
|
||||
|
||||
info("Find out how many time graduations should there be");
|
||||
const width = headerEl.offsetWidth;
|
||||
|
||||
const animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
|
||||
const minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
|
||||
|
||||
// Note that findOptimalTimeInterval is tested separately in xpcshell test
|
||||
// test_findOptimalTimeInterval.js, so we assume that it works here.
|
||||
const interval = findOptimalTimeInterval(minTimeInterval);
|
||||
const nb = Math.ceil(animationDuration / interval);
|
||||
|
||||
is(headerEl.querySelectorAll(".header-item").length, nb,
|
||||
"The expected number of time ticks were found");
|
||||
|
||||
info("Make sure graduations are evenly distributed and show the right times");
|
||||
[...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
|
||||
const left = parseFloat(tick.style.left);
|
||||
const expectedPos = i * interval * 100 / animationDuration;
|
||||
is(Math.round(left), Math.round(expectedPos),
|
||||
`Graduation ${i} is positioned correctly`);
|
||||
|
||||
// Note that the distancetoRelativeTime and formatTime functions are tested
|
||||
// separately in xpcshell test test_timeScale.js, so we assume that they
|
||||
// work here.
|
||||
const formattedTime = TimeScale.formatTime(
|
||||
TimeScale.distanceToRelativeTime(expectedPos, width));
|
||||
is(tick.textContent, formattedTime,
|
||||
`Graduation ${i} has the right text content`);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that the iteration start is displayed correctly in time blocks.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_script_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const timeBlockComponents = getAnimationTimeBlocks(panel);
|
||||
const detailsComponent = timelineComponent.details;
|
||||
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
info(`Expand time block ${i} so its keyframes are visible`);
|
||||
await clickOnAnimation(panel, i);
|
||||
|
||||
info(`Check the state of time block ${i}`);
|
||||
const {containerEl, animation: {state}} = timeBlockComponents[i];
|
||||
|
||||
checkAnimationTooltip(containerEl, state);
|
||||
checkProgressAtStartingTime(containerEl, state);
|
||||
|
||||
// Get the first set of keyframes (there's only one animated property
|
||||
// anyway), and the first frame element from there, we're only interested in
|
||||
// its offset.
|
||||
const keyframeComponent = detailsComponent.keyframeComponents[0];
|
||||
const frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
|
||||
checkKeyframeOffset(containerEl, frameEl, state);
|
||||
}
|
||||
});
|
||||
|
||||
function checkAnimationTooltip(el, {iterationStart, duration}) {
|
||||
info("Check an animation's iterationStart data in its tooltip");
|
||||
const title = el.querySelector(".name").getAttribute("title");
|
||||
|
||||
const iterationStartTime = iterationStart * duration / 1000;
|
||||
const iterationStartTimeString = iterationStartTime.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2
|
||||
}).replace(".", "\\.");
|
||||
const iterationStartString = iterationStart.toString().replace(".", "\\.");
|
||||
|
||||
const regex = new RegExp("Iteration start: " + iterationStartString +
|
||||
" \\(" + iterationStartTimeString + "s\\)");
|
||||
ok(title.match(regex), "The tooltip shows the expected iteration start");
|
||||
}
|
||||
|
||||
function checkProgressAtStartingTime(el, { delay, iterationStart }) {
|
||||
info("Check the progress of starting time");
|
||||
const groupEls = el.querySelectorAll("svg g");
|
||||
groupEls.forEach(groupEl => {
|
||||
const pathEl = groupEl.querySelector(".iteration-path");
|
||||
const pathSegList = pathEl.pathSegList;
|
||||
const pathSeg = pathSegList.getItem(1);
|
||||
const progress = pathSeg.y;
|
||||
is(progress, iterationStart % 1,
|
||||
`The progress at starting point should be ${ iterationStart % 1 }`);
|
||||
|
||||
if (delay) {
|
||||
const delayPathEl = groupEl.querySelector(".delay-path");
|
||||
const delayPathSegList = delayPathEl.pathSegList;
|
||||
const delayStartingPathSeg = delayPathSegList.getItem(1);
|
||||
const delayEndingPathSeg =
|
||||
delayPathSegList.getItem(delayPathSegList.numberOfItems - 2);
|
||||
const startingX = 0;
|
||||
const endingX = delay;
|
||||
is(delayStartingPathSeg.x, startingX,
|
||||
`The x of starting point should be ${ startingX }`);
|
||||
is(delayStartingPathSeg.y, progress,
|
||||
"The y of starting point should be same to starting point of iteration-path "
|
||||
+ progress);
|
||||
is(delayEndingPathSeg.x, endingX,
|
||||
`The x of ending point should be ${ endingX }`);
|
||||
is(delayStartingPathSeg.y, progress,
|
||||
"The y of ending point should be same to starting point of iteration-path "
|
||||
+ progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
|
||||
info("Check that the first keyframe is offset correctly");
|
||||
|
||||
const start = getKeyframeOffset(frameEl);
|
||||
is(start, 0, "The frame offset for iteration start");
|
||||
}
|
||||
|
||||
function getKeyframeOffset(el) {
|
||||
return parseFloat(/(\d+)%/.exec(el.style.left)[1]);
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline toolbar contains a pause button and that this pause button can
|
||||
// be clicked. Check that when it is, the button changes state and the scrubber stops and
|
||||
// resumes.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
const btn = panel.playTimelineButtonEl;
|
||||
|
||||
ok(btn, "The play/pause button exists");
|
||||
ok(!btn.classList.contains("paused"), "The play/pause button is in its playing state");
|
||||
|
||||
info("Click on the button to pause all timeline animations");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
ok(btn.classList.contains("paused"), "The play/pause button is in its paused state");
|
||||
await assertScrubberMoving(panel, false);
|
||||
|
||||
info("Click again on the button to play all timeline animations");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
ok(!btn.classList.contains("paused"),
|
||||
"The play/pause button is in its playing state again");
|
||||
await assertScrubberMoving(panel, true);
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Checks that the play/pause button goes to the right state when the scrubber has reached
|
||||
// the end of the timeline but there are infinite animations playing.
|
||||
|
||||
add_task(async function() {
|
||||
// TODO see if this is needed?
|
||||
// let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
|
||||
// Preferences.set("privacy.reduceTimerPrecision", false);
|
||||
|
||||
// registerCleanupFunction(function () {
|
||||
// Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
|
||||
// });
|
||||
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel, inspector} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const btn = panel.playTimelineButtonEl;
|
||||
|
||||
info("Select an infinite animation and wait for the scrubber to reach the end");
|
||||
await selectNodeAndWaitForAnimations(".multi", inspector);
|
||||
await waitForOutOfBoundScrubber(timeline);
|
||||
|
||||
ok(!btn.classList.contains("paused"),
|
||||
"The button is in its playing state still, animations are infinite.");
|
||||
await assertScrubberMoving(panel, true);
|
||||
|
||||
info("Click on the button after the scrubber has moved out of bounds");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
ok(btn.classList.contains("paused"),
|
||||
"The button can be paused after the scrubber has moved out of bounds");
|
||||
await assertScrubberMoving(panel, false);
|
||||
});
|
||||
|
||||
function waitForOutOfBoundScrubber({win, scrubberEl}) {
|
||||
return new Promise(resolve => {
|
||||
function check() {
|
||||
const pos = scrubberEl.getBoxQuads()[0].getBounds().right;
|
||||
const width = win.document.documentElement.offsetWidth;
|
||||
if (pos >= width) {
|
||||
setTimeout(resolve, 50);
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
}
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Also checks that the button goes to the right state when the scrubber has
|
||||
// reached the end of the timeline: continues to be in playing mode for infinite
|
||||
// animations, goes to paused mode otherwise.
|
||||
// And test that clicking the button once the scrubber has reached the end of
|
||||
// the timeline does the right thing.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel, inspector} = await openAnimationInspector();
|
||||
const btn = panel.playTimelineButtonEl;
|
||||
|
||||
// For a finite animation, once the scrubber reaches the end of the timeline, the pause
|
||||
// button should go back to paused mode.
|
||||
info("Select a finite animation and wait for the animation to complete");
|
||||
await selectNodeAndWaitForAnimations(".negative-delay", inspector);
|
||||
|
||||
await reloadTab(inspector);
|
||||
|
||||
if (!btn.classList.contains("paused")) {
|
||||
await waitForButtonPaused(btn);
|
||||
}
|
||||
|
||||
ok(btn.classList.contains("paused"),
|
||||
"The button is in paused state once finite animations are done");
|
||||
await assertScrubberMoving(panel, false);
|
||||
|
||||
info("Click again on the button to play the animation from the start again");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
ok(!btn.classList.contains("paused"),
|
||||
"Clicking the button once finite animations are done should restart them");
|
||||
await assertScrubberMoving(panel, true);
|
||||
});
|
||||
|
||||
function waitForButtonPaused(btn) {
|
||||
return new Promise(resolve => {
|
||||
const observer = new btn.ownerDocument.defaultView.MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes" &&
|
||||
mutation.attributeName === "class" &&
|
||||
!mutation.oldValue.includes("paused") &&
|
||||
btn.classList.contains("paused")) {
|
||||
observer.disconnect();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(btn, { attributes: true, attributeOldValue: true });
|
||||
});
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline toolbar contains a playback rate selector UI and that
|
||||
// it can be used to change the playback rate of animations in the timeline.
|
||||
// Also check that it displays the rate of the current animations in case they
|
||||
// all have the same rate, or that it displays the empty value in case they
|
||||
// have mixed rates.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel, controller, inspector, toolbox} = await openAnimationInspector();
|
||||
|
||||
// In this test, we disable the highlighter on purpose because of the way
|
||||
// events are simulated to select an option in the playbackRate <select>.
|
||||
// Indeed, this may cause mousemove events to be triggered on the nodes that
|
||||
// are underneath the <select>, and these are AnimationTargetNode instances.
|
||||
// Simulating mouse events on them will cause the highlighter to emit requests
|
||||
// and this might cause the test to fail if they happen after it has ended.
|
||||
disableHighlighter(toolbox);
|
||||
|
||||
const select = panel.rateSelectorEl.firstChild;
|
||||
|
||||
ok(select, "The rate selector exists");
|
||||
|
||||
info("Change all of the current animations' rates to 0.5");
|
||||
await changeTimelinePlaybackRate(panel, .5);
|
||||
checkAllAnimationsRatesChanged(controller, select, .5);
|
||||
|
||||
info("Select just one animated node and change its rate only");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
await changeTimelinePlaybackRate(panel, 2);
|
||||
checkAllAnimationsRatesChanged(controller, select, 2);
|
||||
|
||||
info("Select the <body> again, it should now have mixed-rates animations");
|
||||
await selectNodeAndWaitForAnimations("body", inspector);
|
||||
|
||||
is(select.value, "", "The selected rate is empty");
|
||||
|
||||
info("Change the rate for these mixed-rate animations");
|
||||
await changeTimelinePlaybackRate(panel, 1);
|
||||
checkAllAnimationsRatesChanged(controller, select, 1);
|
||||
});
|
||||
|
||||
function checkAllAnimationsRatesChanged({animationPlayers}, select, rate) {
|
||||
ok(animationPlayers.every(({state}) => state.playbackRate === rate),
|
||||
"All animations' rates have been set to " + rate);
|
||||
is(select.value, rate, "The right value is displayed in the select");
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline toolbar contains a rewind button and that it can be
|
||||
// clicked. Check that when it is, the current animations displayed in the
|
||||
// timeline get their playstates changed to paused, and their currentTimes
|
||||
// reset to 0, and that the scrubber stops moving and is positioned to the
|
||||
// start.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel, controller} = await openAnimationInspector();
|
||||
const players = controller.animationPlayers;
|
||||
const btn = panel.rewindTimelineButtonEl;
|
||||
|
||||
ok(btn, "The rewind button exists");
|
||||
|
||||
info("Click on the button to rewind all timeline animations");
|
||||
await clickTimelineRewindButton(panel);
|
||||
|
||||
info("Check that the scrubber has stopped moving");
|
||||
await assertScrubberMoving(panel, false);
|
||||
|
||||
ok(players.every(({state}) => state.currentTime === 0),
|
||||
"All animations' currentTimes have been set to 0");
|
||||
ok(players.every(({state}) => state.playState === "paused"),
|
||||
"All animations have been paused");
|
||||
|
||||
info("Play the animations again");
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
info("And pause them after a short while");
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
info("Check that rewinding when animations are paused works too");
|
||||
await clickTimelineRewindButton(panel);
|
||||
|
||||
info("Check that the scrubber has stopped moving");
|
||||
await assertScrubberMoving(panel, false);
|
||||
|
||||
ok(players.every(({state}) => state.currentTime === 0),
|
||||
"All animations' currentTimes have been set to 0");
|
||||
ok(players.every(({state}) => state.playState === "paused"),
|
||||
"All animations have been paused");
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline does have a scrubber element.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const scrubberEl = timeline.scrubberEl;
|
||||
|
||||
ok(scrubberEl, "The scrubber element exists");
|
||||
ok(scrubberEl.classList.contains("scrubber"), "It has the right classname");
|
||||
});
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the scrubber in the timeline can be moved by clicking & dragging
|
||||
// in the header area.
|
||||
// Also check that doing so changes the timeline's play/pause button to paused
|
||||
// state.
|
||||
// Finally, also check that the scrubber can be moved using the scrubber handle.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const {win, timeHeaderEl, scrubberEl, scrubberHandleEl} = timeline;
|
||||
const playTimelineButtonEl = panel.playTimelineButtonEl;
|
||||
|
||||
ok(!playTimelineButtonEl.classList.contains("paused"),
|
||||
"The timeline play button is in its playing state by default");
|
||||
|
||||
info("Mousedown in the header to move the scrubber");
|
||||
await synthesizeInHeaderAndWaitForChange(timeline, 50, 1, "mousedown");
|
||||
checkScrubberIsAt(scrubberEl, timeHeaderEl, 50);
|
||||
|
||||
ok(playTimelineButtonEl.classList.contains("paused"),
|
||||
"The timeline play button is in its paused state after mousedown");
|
||||
|
||||
info("Continue moving the mouse and verify that the scrubber tracks it");
|
||||
await synthesizeInHeaderAndWaitForChange(timeline, 100, 1, "mousemove");
|
||||
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
|
||||
|
||||
ok(playTimelineButtonEl.classList.contains("paused"),
|
||||
"The timeline play button is in its paused state after mousemove");
|
||||
|
||||
info("Release the mouse and move again and verify that the scrubber stays");
|
||||
EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
|
||||
EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
|
||||
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
|
||||
|
||||
info("Try to drag the scrubber handle and check that the scrubber moves");
|
||||
const onDataChanged = timeline.once("timeline-data-changed");
|
||||
EventUtils.synthesizeMouse(scrubberHandleEl, 1, 20, {type: "mousedown"}, win);
|
||||
EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mousemove"}, win);
|
||||
EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mouseup"}, win);
|
||||
await onDataChanged;
|
||||
|
||||
checkScrubberIsAt(scrubberEl, timeHeaderEl, 0);
|
||||
|
||||
// Wait for promise of setCurrentTimes if setCurrentTimes is running.
|
||||
if (panel.setCurrentTimeAllPromise) {
|
||||
await panel.setCurrentTimeAllPromise;
|
||||
}
|
||||
});
|
||||
|
||||
async function synthesizeInHeaderAndWaitForChange(timeline, x, y, type) {
|
||||
const onDataChanged = timeline.once("timeline-data-changed");
|
||||
EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win);
|
||||
await onDataChanged;
|
||||
}
|
||||
|
||||
function getPositionPercentage(pos, headerEl) {
|
||||
return pos * 100 / headerEl.offsetWidth;
|
||||
}
|
||||
|
||||
function checkScrubberIsAt(scrubberEl, timeHeaderEl, pos) {
|
||||
const newPos = Math.round(parseFloat(scrubberEl.style.left));
|
||||
const expectedPos = Math.round(getPositionPercentage(pos, timeHeaderEl));
|
||||
is(newPos, expectedPos,
|
||||
`The scrubber is at position ${pos} (${expectedPos}%)`);
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the scrubber in the timeline moves when animations are playing.
|
||||
// The animations in the test page last for a very long time, so the test just
|
||||
// measures the position of the scrubber once, then waits for some time to pass
|
||||
// and measures its position again.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const scrubberEl = timeline.scrubberEl;
|
||||
const startPos = scrubberEl.getBoundingClientRect().left;
|
||||
|
||||
info("Wait for some time to check that the scrubber moves");
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
const endPos = scrubberEl.getBoundingClientRect().left;
|
||||
|
||||
ok(endPos > startPos, "The scrubber has moved");
|
||||
});
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Animation.currentTime ignores neagtive delay and positive/negative endDelay
|
||||
// during fill-mode, even if they are set.
|
||||
// For example, when the animation timing is
|
||||
// { duration: 1000, iterations: 1, endDelay: -500, easing: linear },
|
||||
// the animation progress is 0.5 at 700ms because the progress stops as 0.5 at
|
||||
// 500ms in original animation. However, if you set as
|
||||
// animation.currentTime = 700 manually, the progress will be 0.7.
|
||||
// So we modify setCurrentTime method since
|
||||
// AnimationInspector should re-produce same as original animation.
|
||||
// In these tests,
|
||||
// we confirm the behavior of setCurrentTime by delay and endDelay.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_timing_combination_animation.html");
|
||||
const { panel, controller } = await openAnimationInspector();
|
||||
|
||||
await clickTimelinePlayPauseButton(panel);
|
||||
|
||||
const timeBlockComponents = getAnimationTimeBlocks(panel);
|
||||
|
||||
// Test -5000ms.
|
||||
let time = -5000;
|
||||
await controller.setCurrentTimeAll(time, true);
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
await timeBlockComponents[i].animation.refreshState();
|
||||
const state = await timeBlockComponents[i].animation.state;
|
||||
info(`Check the state at ${ time }ms with `
|
||||
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
|
||||
is(state.currentTime, 0,
|
||||
`The currentTime should be 0 at setCurrentTime(${ time })`);
|
||||
}
|
||||
|
||||
// Test 10000ms.
|
||||
time = 10000;
|
||||
await controller.setCurrentTimeAll(time, true);
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
await timeBlockComponents[i].animation.refreshState();
|
||||
const state = await timeBlockComponents[i].animation.state;
|
||||
info(`Check the state at ${ time }ms with `
|
||||
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
|
||||
const expected = state.delay < 0 ? 0 : time;
|
||||
is(state.currentTime, expected,
|
||||
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
|
||||
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
|
||||
}
|
||||
|
||||
// Test 60000ms.
|
||||
time = 60000;
|
||||
await controller.setCurrentTimeAll(time, true);
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
await timeBlockComponents[i].animation.refreshState();
|
||||
const state = await timeBlockComponents[i].animation.state;
|
||||
info(`Check the state at ${ time }ms with `
|
||||
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
|
||||
const expected = state.delay < 0 ? time + state.delay : time;
|
||||
is(state.currentTime, expected,
|
||||
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
|
||||
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
|
||||
}
|
||||
|
||||
// Test 150000ms.
|
||||
time = 150000;
|
||||
await controller.setCurrentTimeAll(time, true);
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
await timeBlockComponents[i].animation.refreshState();
|
||||
const state = await timeBlockComponents[i].animation.state;
|
||||
info(`Check the state at ${ time }ms with `
|
||||
+ `delay:${ state.delay } and endDelay:${ state.endDelay }`);
|
||||
const currentTime = state.delay < 0 ? time + state.delay : time;
|
||||
const endTime =
|
||||
state.delay + state.iterationCount * state.duration + state.endDelay;
|
||||
const expected =
|
||||
state.endDelay < 0 && state.fill === "both" && currentTime > endTime
|
||||
? endTime : currentTime;
|
||||
is(state.currentTime, expected,
|
||||
`The currentTime should be ${ expected } at setCurrentTime(${ time }).`
|
||||
+ ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test short duration (e.g. 1ms) animation.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_short_duration_animation.html");
|
||||
const { panel, inspector } = await openAnimationInspector();
|
||||
|
||||
info("Check the listed time blocks");
|
||||
const timeBlocks = getAnimationTimeBlocks(panel);
|
||||
for (let i = 0; i < timeBlocks.length; i++) {
|
||||
info(`Check the time block ${i}`);
|
||||
const {containerEl, animation: {state}} = timeBlocks[i];
|
||||
checkSummaryGraph(containerEl, state);
|
||||
}
|
||||
|
||||
info("Check the time block one by one");
|
||||
info("Check #onetime");
|
||||
await selectNodeAndWaitForAnimations("#onetime", inspector);
|
||||
let timeBlock = getAnimationTimeBlocks(panel)[0];
|
||||
let containerEl = timeBlock.containerEl;
|
||||
let state = timeBlock.animation.state;
|
||||
checkSummaryGraph(containerEl, state, true);
|
||||
|
||||
info("Check #infinite");
|
||||
await selectNodeAndWaitForAnimations("#infinite", inspector);
|
||||
timeBlock = getAnimationTimeBlocks(panel)[0];
|
||||
containerEl = timeBlock.containerEl;
|
||||
state = timeBlock.animation.state;
|
||||
checkSummaryGraph(containerEl, state, true);
|
||||
});
|
||||
|
||||
function checkSummaryGraph(el, state, isDetail) {
|
||||
info("Check the coordinates of summary graph");
|
||||
const groupEls = el.querySelectorAll("svg g");
|
||||
groupEls.forEach(groupEl => {
|
||||
const pathEls = groupEl.querySelectorAll(".iteration-path");
|
||||
let expectedIterationCount = 0;
|
||||
if (isDetail) {
|
||||
expectedIterationCount = state.iterationCount ? state.iterationCount : 1;
|
||||
} else {
|
||||
expectedIterationCount = state.iterationCount ? state.iterationCount : 2;
|
||||
}
|
||||
is(pathEls.length, expectedIterationCount,
|
||||
`The count of path shoud be ${ expectedIterationCount }`);
|
||||
pathEls.forEach((pathEl, index) => {
|
||||
const startX = index * state.duration;
|
||||
const endX = startX + state.duration;
|
||||
|
||||
const pathSegList = pathEl.pathSegList;
|
||||
const firstPathSeg = pathSegList.getItem(0);
|
||||
is(firstPathSeg.x, startX,
|
||||
`The x of first segment should be ${ startX }`);
|
||||
is(firstPathSeg.y, 0, "The y of first segment should be 0");
|
||||
|
||||
// The easing of test animation is 'linear'.
|
||||
// Therefore, the y of second path segment will be 0.
|
||||
const secondPathSeg = pathSegList.getItem(1);
|
||||
is(secondPathSeg.x, startX,
|
||||
`The x of second segment should be ${ startX }`);
|
||||
is(secondPathSeg.y, 0, "The y of second segment should be 0");
|
||||
|
||||
const thirdLastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 4);
|
||||
approximate(thirdLastPathSeg.x, endX - 0.001, 0.005,
|
||||
"The x of third last segment should be approximately "
|
||||
+ (endX - 0.001));
|
||||
approximate(thirdLastPathSeg.y, 0.999, 0.005,
|
||||
" The y of third last segment should be approximately "
|
||||
+ thirdLastPathSeg.x);
|
||||
|
||||
// The test animation is not 'forwards' fill-mode.
|
||||
// Therefore, the y of second last path segment will be 0.
|
||||
const secondLastPathSeg =
|
||||
pathSegList.getItem(pathSegList.numberOfItems - 3);
|
||||
is(secondLastPathSeg.x, endX,
|
||||
`The x of second last segment should be ${ endX }`);
|
||||
// We use computed style of 'opacity' to create summary graph.
|
||||
// So, if currentTime is same to the duration, although progress is null
|
||||
// opacity is 0.
|
||||
const expectedY = 0;
|
||||
is(secondLastPathSeg.y, expectedY,
|
||||
`The y of second last segment should be ${ expectedY }`);
|
||||
|
||||
const lastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
|
||||
is(lastPathSeg.x, endX, `The x of last segment should be ${ endX }`);
|
||||
is(lastPathSeg.y, 0, "The y of last segment should be 0");
|
||||
|
||||
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
|
||||
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
|
||||
`The actual last segment should be close path`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function approximate(value, expected, permissibleRange, message) {
|
||||
const min = expected - permissibleRange;
|
||||
const max = expected + permissibleRange;
|
||||
ok(min <= value && value <= max, message);
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that animation delay is visualized in the timeline when the animation
|
||||
// is delayed.
|
||||
// Also check that negative delays do not overflow the UI, and are shown like
|
||||
// positive delays.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Selecting a delayed animated node");
|
||||
await selectNodeAndWaitForAnimations(".delayed", inspector);
|
||||
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
|
||||
checkDelayAndName(timelineEl, true);
|
||||
let animationEl = timelineEl.querySelector(".animation");
|
||||
let state = getAnimationTimeBlocks(panel)[0].animation.state;
|
||||
checkPath(animationEl, state);
|
||||
|
||||
info("Selecting a no-delay animated node");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
checkDelayAndName(timelineEl, false);
|
||||
animationEl = timelineEl.querySelector(".animation");
|
||||
state = getAnimationTimeBlocks(panel)[0].animation.state;
|
||||
checkPath(animationEl, state);
|
||||
|
||||
info("Selecting a negative-delay animated node");
|
||||
await selectNodeAndWaitForAnimations(".negative-delay", inspector);
|
||||
checkDelayAndName(timelineEl, true);
|
||||
animationEl = timelineEl.querySelector(".animation");
|
||||
state = getAnimationTimeBlocks(panel)[0].animation.state;
|
||||
checkPath(animationEl, state);
|
||||
});
|
||||
|
||||
function checkDelayAndName(timelineEl, hasDelay) {
|
||||
const delay = timelineEl.querySelector(".delay");
|
||||
|
||||
is(!!delay, hasDelay, "The timeline " +
|
||||
(hasDelay ? "contains" : "does not contain") +
|
||||
" a delay element, as expected");
|
||||
|
||||
if (hasDelay) {
|
||||
const targetNode = timelineEl.querySelector(".target");
|
||||
|
||||
// Check that the delay element does not cause the timeline to overflow.
|
||||
const delayLeft = Math.round(delay.getBoundingClientRect().x);
|
||||
const sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
|
||||
ok(delayLeft >= sidebarWidth,
|
||||
"The delay element isn't displayed over the sidebar");
|
||||
}
|
||||
}
|
||||
|
||||
function checkPath(animationEl, state) {
|
||||
const groupEls = animationEl.querySelectorAll("svg g");
|
||||
groupEls.forEach(groupEl => {
|
||||
// Check existance of delay path.
|
||||
const delayPathEl = groupEl.querySelector(".delay-path");
|
||||
if (!state.iterationCount && state.delay < 0) {
|
||||
// Infinity
|
||||
ok(!delayPathEl, "The delay path for Infinity should not exist");
|
||||
return;
|
||||
}
|
||||
if (state.delay === 0) {
|
||||
ok(!delayPathEl, "The delay path for zero delay should not exist");
|
||||
return;
|
||||
}
|
||||
ok(delayPathEl, "The delay path should exist");
|
||||
|
||||
// Check delay path coordinates.
|
||||
const pathSegList = delayPathEl.pathSegList;
|
||||
const startingPathSeg = pathSegList.getItem(0);
|
||||
const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
|
||||
if (state.delay < 0) {
|
||||
ok(delayPathEl.classList.contains("negative"),
|
||||
"The delay path should have 'negative' class");
|
||||
const expectedY = 0;
|
||||
const startingX = state.delay;
|
||||
const endingX = 0;
|
||||
is(startingPathSeg.x, startingX,
|
||||
`The x of starting point should be ${ startingX }`);
|
||||
is(startingPathSeg.y, expectedY,
|
||||
`The y of starting point should be ${ expectedY }`);
|
||||
is(endingPathSeg.x, endingX,
|
||||
`The x of ending point should be ${ endingX }`);
|
||||
is(endingPathSeg.y, expectedY,
|
||||
`The y of ending point should be ${ expectedY }`);
|
||||
} else {
|
||||
ok(!delayPathEl.classList.contains("negative"),
|
||||
"The delay path should not have 'negative' class");
|
||||
const expectedY = 0;
|
||||
const startingX = 0;
|
||||
const endingX = state.delay;
|
||||
is(startingPathSeg.x, startingX,
|
||||
`The x of starting point should be ${ startingX }`);
|
||||
is(startingPathSeg.y, expectedY,
|
||||
`The y of starting point should be ${ expectedY }`);
|
||||
is(endingPathSeg.x, endingX,
|
||||
`The x of ending point should be ${ endingX }`);
|
||||
is(endingPathSeg.y, expectedY,
|
||||
`The y of ending point should be ${ expectedY }`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that animation endDelay is visualized in the timeline when the
|
||||
// animation is delayed.
|
||||
// Also check that negative endDelays do not overflow the UI, and are shown
|
||||
// like positive endDelays.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_end_delay.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
const selectors = ["#target1", "#target2", "#target3", "#target4"];
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
const selector = selectors[i];
|
||||
await selectNode(selector, inspector);
|
||||
await waitForAnimationSelecting(panel);
|
||||
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
|
||||
const animationEl = timelineEl.querySelector(".animation");
|
||||
checkEndDelayAndName(animationEl);
|
||||
const state = getAnimationTimeBlocks(panel)[0].animation.state;
|
||||
checkPath(animationEl, state);
|
||||
}
|
||||
});
|
||||
|
||||
function checkEndDelayAndName(animationEl) {
|
||||
const endDelay = animationEl.querySelector(".end-delay");
|
||||
const name = animationEl.querySelector(".name");
|
||||
const targetNode = animationEl.querySelector(".target");
|
||||
|
||||
// Check that the endDelay element does not cause the timeline to overflow.
|
||||
const endDelayLeft = Math.round(endDelay.getBoundingClientRect().x);
|
||||
const sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
|
||||
ok(endDelayLeft >= sidebarWidth,
|
||||
"The endDelay element isn't displayed over the sidebar");
|
||||
|
||||
// Check that the endDelay is not displayed on top of the name.
|
||||
const endDelayRight = Math.round(endDelay.getBoundingClientRect().right);
|
||||
const nameLeft = Math.round(name.getBoundingClientRect().left);
|
||||
ok(endDelayRight >= nameLeft,
|
||||
"The endDelay element does not span over the name element");
|
||||
}
|
||||
|
||||
function checkPath(animationEl, state) {
|
||||
const groupEls = animationEl.querySelectorAll("svg g");
|
||||
groupEls.forEach(groupEl => {
|
||||
// Check existance of enddelay path.
|
||||
const endDelayPathEl = groupEl.querySelector(".enddelay-path");
|
||||
ok(endDelayPathEl, "The endDelay path should exist");
|
||||
|
||||
// Check enddelay path coordinates.
|
||||
const pathSegList = endDelayPathEl.pathSegList;
|
||||
const startingPathSeg = pathSegList.getItem(0);
|
||||
const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
|
||||
if (state.endDelay < 0) {
|
||||
ok(endDelayPathEl.classList.contains("negative"),
|
||||
"The endDelay path should have 'negative' class");
|
||||
const endingX = state.delay + state.iterationCount * state.duration;
|
||||
const startingX = endingX + state.endDelay;
|
||||
is(startingPathSeg.x, startingX,
|
||||
`The x of starting point should be ${ startingX }`);
|
||||
is(endingPathSeg.x, endingX,
|
||||
`The x of ending point should be ${ endingX }`);
|
||||
} else {
|
||||
ok(!endDelayPathEl.classList.contains("negative"),
|
||||
"The endDelay path should not have 'negative' class");
|
||||
const startingX =
|
||||
state.delay + state.iterationCount * state.duration;
|
||||
const endingX = startingX + state.endDelay;
|
||||
is(startingPathSeg.x, startingX,
|
||||
`The x of starting point should be ${ startingX }`);
|
||||
is(endingPathSeg.x, endingX,
|
||||
`The x of ending point should be ${ endingX }`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline is displays as many iteration elements as there are
|
||||
// iterations in an animation.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Selecting the test node");
|
||||
await selectNodeAndWaitForAnimations(".delayed", inspector);
|
||||
|
||||
info("Getting the animation element from the panel");
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const timelineEl = timelineComponent.rootWrapperEl;
|
||||
let animation = timelineEl.querySelector(".time-block");
|
||||
|
||||
// Get iteration count from summary graph path.
|
||||
let iterationCount = getIterationCount(animation);
|
||||
|
||||
is(animation.querySelectorAll("svg g").length, 1,
|
||||
"The animation timeline contains one g element");
|
||||
is(iterationCount, 10,
|
||||
"The animation timeline contains the right number of iterations");
|
||||
ok(!animation.querySelector(".infinity"),
|
||||
"The summary graph does not have any elements "
|
||||
+ " that have infinity class");
|
||||
|
||||
info("Selecting another test node with an infinite animation");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
info("Getting the animation element from the panel again");
|
||||
animation = timelineEl.querySelector(".time-block");
|
||||
iterationCount = getIterationCount(animation);
|
||||
|
||||
is(animation.querySelectorAll("svg g").length, 1,
|
||||
"The animation timeline contains one g element");
|
||||
is(iterationCount, 1,
|
||||
"The animation timeline contains one iteration");
|
||||
ok(animation.querySelector(".infinity"),
|
||||
"The summary graph has an element that has infinity class");
|
||||
});
|
||||
|
||||
function getIterationCount(timeblockEl) {
|
||||
return timeblockEl.querySelectorAll("svg g .iteration-path").length;
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check the text content and width of name label.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Selecting 'simple-animation' animation which is running on compositor");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "simple-animation");
|
||||
|
||||
info("Selecting 'no-compositor' animation which is not running on compositor");
|
||||
await selectNodeAndWaitForAnimations(".no-compositor", inspector);
|
||||
checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "no-compositor");
|
||||
});
|
||||
|
||||
function checkNameLabel(rootWrapperEl, expectedLabelContent) {
|
||||
const labelEl = rootWrapperEl.querySelector(".name svg text");
|
||||
is(labelEl.textContent, expectedLabelContent,
|
||||
`Text content of labelEl sould be ${ expectedLabelContent }`);
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline displays animations' duration, delay iteration
|
||||
// counts and iteration start in tooltips.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel, controller} = await openAnimationInspector();
|
||||
|
||||
info("Getting the animation element from the panel");
|
||||
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
|
||||
const timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name");
|
||||
|
||||
// Verify that each time-block's name element has a tooltip that looks sort of
|
||||
// ok. We don't need to test the actual content.
|
||||
[...timeBlockNameEls].forEach((el, i) => {
|
||||
ok(el.hasAttribute("title"), "The tooltip is defined for animation " + i);
|
||||
|
||||
const title = el.getAttribute("title");
|
||||
const state = controller.animationPlayers[i].state;
|
||||
|
||||
if (state.delay) {
|
||||
ok(title.match(/Delay: [\d.,-]+s/), "The tooltip shows the delay");
|
||||
}
|
||||
ok(title.match(/Duration: [\d.,]+s/), "The tooltip shows the duration");
|
||||
if (state.endDelay) {
|
||||
ok(title.match(/End delay: [\d.,-]+s/), "The tooltip shows the endDelay");
|
||||
}
|
||||
if (state.iterationCount !== 1) {
|
||||
ok(title.match(/Repeats: /), "The tooltip shows the iterations");
|
||||
} else {
|
||||
ok(!title.match(/Repeats: /), "The tooltip doesn't show the iterations");
|
||||
}
|
||||
if (state.easing && state.easing !== "linear") {
|
||||
ok(title.match(/Overall easing: /), "The tooltip shows the easing");
|
||||
} else {
|
||||
ok(!title.match(/Overall easing: /),
|
||||
"The tooltip doesn't show the easing if it is 'linear'");
|
||||
}
|
||||
if (state.animationTimingFunction && state.animationTimingFunction !== "ease") {
|
||||
is(state.type, "cssanimation",
|
||||
"The animation type should be CSS Animations if has animation-timing-function");
|
||||
ok(title.match(/Animation timing function: /),
|
||||
"The tooltip shows animation-timing-function");
|
||||
} else {
|
||||
ok(!title.match(/Animation timing function: /),
|
||||
"The tooltip doesn't show the animation-timing-function if it is 'ease'"
|
||||
+ " or not CSS Animations");
|
||||
}
|
||||
if (state.fill) {
|
||||
ok(title.match(/Fill: /), "The tooltip shows the fill");
|
||||
}
|
||||
if (state.direction) {
|
||||
if (state.direction === "normal") {
|
||||
ok(!title.match(/Direction: /),
|
||||
"The tooltip doesn't show the direction if it is 'normal'");
|
||||
} else {
|
||||
ok(title.match(/Direction: /), "The tooltip shows the direction");
|
||||
}
|
||||
}
|
||||
ok(!title.match(/Iteration start:/),
|
||||
"The tooltip doesn't show the iteration start");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that if an animation has had its playbackRate changed via the DOM, then
|
||||
// the timeline UI shows the right delay and duration.
|
||||
// Indeed, the header in the timeline UI always shows the unaltered time,
|
||||
// because there might be multiple animations displayed at the same time, some
|
||||
// of which may have a different rate than others. Those that have had their
|
||||
// rate changed have a delay = delay/rate and a duration = duration/rate.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_modify_playbackRate.html");
|
||||
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
|
||||
|
||||
const timeBlocks = timelineEl.querySelectorAll(".time-block");
|
||||
is(timeBlocks.length, 2, "2 animations are displayed");
|
||||
|
||||
info("The first animation has its rate set to 1, let's measure it");
|
||||
|
||||
const el = timeBlocks[0];
|
||||
const delay = parseInt(el.querySelector(".delay").style.width, 10);
|
||||
let duration = null;
|
||||
el.querySelectorAll("svg g").forEach(groupEl => {
|
||||
const dur = getDuration(groupEl.querySelector("path"));
|
||||
if (!duration) {
|
||||
duration = dur;
|
||||
return;
|
||||
}
|
||||
is(duration, dur, "The durations shuld be same at all paths in one group");
|
||||
});
|
||||
|
||||
info("The second animation has its rate set to 2, so should be shorter");
|
||||
|
||||
const el2 = timeBlocks[1];
|
||||
const delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
|
||||
let duration2 = null;
|
||||
el2.querySelectorAll("svg g").forEach(groupEl => {
|
||||
const dur = getDuration(groupEl.querySelector("path"));
|
||||
if (!duration2) {
|
||||
duration2 = dur;
|
||||
return;
|
||||
}
|
||||
is(duration2, dur, "The durations shuld be same at all paths in one group");
|
||||
});
|
||||
|
||||
// The width are calculated by the animation-inspector dynamically depending
|
||||
// on the size of the panel, and therefore depends on the test machine/OS.
|
||||
// Let's not try to be too precise here and compare numbers.
|
||||
const durationDelta = (2 * duration2) - duration;
|
||||
ok(durationDelta <= 1, "The duration width is correct");
|
||||
const delayDelta = (2 * delay2) - delay;
|
||||
ok(delayDelta <= 1, "The delay width is correct");
|
||||
});
|
||||
|
||||
function getDuration(pathEl) {
|
||||
const pathSegList = pathEl.pathSegList;
|
||||
// Find the index of starting iterations.
|
||||
let startingIterationIndex = 0;
|
||||
const firstPathSeg = pathSegList.getItem(1);
|
||||
for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) {
|
||||
// Changing point of the progress acceleration is the time.
|
||||
const pathSeg = pathSegList.getItem(i);
|
||||
if (firstPathSeg.y != pathSeg.y) {
|
||||
startingIterationIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Find the index of ending iterations.
|
||||
let endingIterationIndex = 0;
|
||||
let previousPathSegment = pathSegList.getItem(startingIterationIndex);
|
||||
for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2;
|
||||
i < n; i++) {
|
||||
// Find forwards fill-mode.
|
||||
const pathSeg = pathSegList.getItem(i);
|
||||
if (previousPathSegment.y == pathSeg.y) {
|
||||
endingIterationIndex = i;
|
||||
break;
|
||||
}
|
||||
previousPathSegment = pathSeg;
|
||||
}
|
||||
if (endingIterationIndex) {
|
||||
// Not forwards fill-mode
|
||||
endingIterationIndex = pathSegList.numberOfItems - 2;
|
||||
}
|
||||
// Return the distance of starting and ending
|
||||
const startingIterationPathSegment =
|
||||
pathSegList.getItem(startingIterationIndex);
|
||||
const endingIterationPathSegment =
|
||||
pathSegList.getItem(startingIterationIndex);
|
||||
return endingIterationPathSegment.x - startingIterationPathSegment.x;
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Check that the timeline contains the right elements.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const el = timeline.rootWrapperEl;
|
||||
|
||||
ok(el.querySelector(".time-header"),
|
||||
"The header element is in the DOM of the timeline");
|
||||
ok(el.querySelectorAll(".time-header .header-item").length,
|
||||
"The header has some time graduations");
|
||||
|
||||
ok(el.querySelector(".animations"),
|
||||
"The animations container is in the DOM of the timeline");
|
||||
is(el.querySelectorAll(".animations .animation").length,
|
||||
timeline.animations.length,
|
||||
"The number of animations displayed matches the number of animations");
|
||||
|
||||
const animationEls = el.querySelectorAll(".animations .animation");
|
||||
const evenColor =
|
||||
el.ownerDocument.defaultView.getComputedStyle(animationEls[0]).backgroundColor;
|
||||
const oddColor =
|
||||
el.ownerDocument.defaultView.getComputedStyle(animationEls[1]).backgroundColor;
|
||||
isnot(evenColor, oddColor,
|
||||
"Background color of an even animation should be different from odd");
|
||||
for (let i = 0; i < timeline.animations.length; i++) {
|
||||
const animation = timeline.animations[i];
|
||||
const animationEl = animationEls[i];
|
||||
|
||||
ok(animationEl.querySelector(".target"),
|
||||
"The animated node target element is in the DOM");
|
||||
ok(animationEl.querySelector(".time-block"),
|
||||
"The timeline element is in the DOM");
|
||||
is(animationEl.querySelector(".name").textContent,
|
||||
animation.state.name,
|
||||
"The name on the timeline is correct");
|
||||
is(animationEl.querySelectorAll("svg g").length, 1,
|
||||
"The g element should be one since this doc's all animation has only one shape");
|
||||
ok(animationEl.querySelector("svg g path"),
|
||||
"The timeline has svg and path element as summary graph");
|
||||
|
||||
const expectedBackgroundColor = i % 2 === 0 ? evenColor : oddColor;
|
||||
const backgroundColor =
|
||||
el.ownerDocument.defaultView.getComputedStyle(animationEl).backgroundColor;
|
||||
is(backgroundColor, expectedBackgroundColor,
|
||||
"The background-color shoud be changed to alternate");
|
||||
}
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that a page navigation resets the state of the global toggle button.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, panel} = await openAnimationInspector();
|
||||
|
||||
info("Select the non-animated test node");
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
ok(!panel.toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle button is in its running state by default");
|
||||
|
||||
info("Toggle all animations, so that they pause");
|
||||
await panel.toggleAll();
|
||||
ok(panel.toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle button now is in its paused state");
|
||||
|
||||
info("Reloading the page");
|
||||
await reloadTab(inspector);
|
||||
|
||||
ok(!panel.toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle button is back in its running state");
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the main toggle button actually toggles animations.
|
||||
// This test doesn't need to be extra careful about checking that *all*
|
||||
// animations have been paused (including inside iframes) because there's an
|
||||
// actor test in /devtools/server/tests/browser/ that does this.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel} = await openAnimationInspector();
|
||||
|
||||
info("Click the toggle button");
|
||||
await panel.toggleAll();
|
||||
await checkState("paused");
|
||||
|
||||
info("Click again the toggle button");
|
||||
await panel.toggleAll();
|
||||
await checkState("running");
|
||||
});
|
||||
|
||||
async function checkState(state) {
|
||||
for (const selector of [".animated", ".multi", ".long"]) {
|
||||
const playState = await getAnimationPlayerState(selector);
|
||||
is(playState, state, "The animation on node " + selector + " is " + state);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Test that the animation panel has a top toolbar that contains the play/pause
|
||||
// button and that is displayed at all times.
|
||||
// Also test that this toolbar gets replaced by the timeline toolbar when there
|
||||
// are animations to be displayed.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {inspector, window} = await openAnimationInspector();
|
||||
const doc = window.document;
|
||||
const toolbar = doc.querySelector("#global-toolbar");
|
||||
|
||||
ok(toolbar, "The panel contains the toolbar element with the new UI");
|
||||
ok(!isNodeVisible(toolbar),
|
||||
"The toolbar is hidden while there are animations");
|
||||
|
||||
const timelineToolbar = doc.querySelector("#timeline-toolbar");
|
||||
ok(timelineToolbar, "The panel contains a timeline toolbar element");
|
||||
ok(isNodeVisible(timelineToolbar),
|
||||
"The timeline toolbar is visible when there are animations");
|
||||
|
||||
info("Select a node that has no animations");
|
||||
await selectNodeAndWaitForAnimations(".still", inspector);
|
||||
|
||||
ok(isNodeVisible(toolbar),
|
||||
"The toolbar is shown when there are no animations");
|
||||
ok(!isNodeVisible(timelineToolbar),
|
||||
"The timeline toolbar is hidden when there are no animations");
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// Verify that if the animation's duration, iterations or delay change in
|
||||
// content, then the widget reflects the changes.
|
||||
|
||||
add_task(async function() {
|
||||
await addTab(URL_ROOT + "doc_simple_animation.html");
|
||||
const {panel, controller, inspector} = await openAnimationInspector();
|
||||
|
||||
info("Select the test node");
|
||||
await selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
const animation = controller.animationPlayers[0];
|
||||
await setStyle(animation, panel, "animationDuration", "5.5s", ".animated");
|
||||
await setStyle(animation, panel, "animationIterationCount", "300", ".animated");
|
||||
await setStyle(animation, panel, "animationDelay", "45s", ".animated");
|
||||
|
||||
const animationsEl = panel.animationsTimelineComponent.animationsEl;
|
||||
const timeBlockEl = animationsEl.querySelector(".time-block");
|
||||
|
||||
// 45s delay + (300 * 5.5)s duration
|
||||
const expectedTotalDuration = 1695 * 1000;
|
||||
|
||||
// XXX: the nb and size of each iteration cannot be tested easily (displayed
|
||||
// using a linear-gradient background and capped at 2px wide). They should
|
||||
// be tested in bug 1173761.
|
||||
const delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
|
||||
is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
|
||||
"The timeline has the right delay");
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
background-color: lime;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#target1 {
|
||||
animation: anim 1s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target1">1</div>
|
||||
<div id="target2">2</div>
|
||||
<div id="target3">3</div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const duration = 1000;
|
||||
const delay = 1000;
|
||||
const endDelay = 1000;
|
||||
|
||||
// ".animation" is appended from test.
|
||||
const observer = new MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes" &&
|
||||
mutation.attributeName === "class" &&
|
||||
mutation.target.classList.contains("animation")) {
|
||||
const effect = new KeyframeEffect(mutation.target,
|
||||
{ opacity: [1, 0] },
|
||||
{ duration, delay, endDelay });
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.play();
|
||||
// wait
|
||||
animation.currentTime = 100;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.querySelector("#target2"), { attributes: true });
|
||||
observer.observe(document.querySelector("#target3"), { attributes: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
color: black;
|
||||
animation: change-background-color 3s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes change-background-color {
|
||||
to {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Animated body element</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
#target1 {
|
||||
animation: anim 100s;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
from {
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target1"></div>
|
||||
<div id="target2"></div>
|
||||
<div id="target3"></div>
|
||||
<div id="target4"></div>
|
||||
<div id="target5"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
.target {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: blue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target1" class="target"></div>
|
||||
<div id="target2" class="target"></div>
|
||||
<div id="target3" class="target"></div>
|
||||
<div id="target4" class="target"></div>
|
||||
<script>
|
||||
/* globals KeyframeEffect, Animation */
|
||||
"use strict";
|
||||
|
||||
const animations = [{
|
||||
id: "target1",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
id: "endDelay_animation1",
|
||||
duration: 1000000,
|
||||
endDelay: 500000,
|
||||
fill: "none"
|
||||
}
|
||||
}, {
|
||||
id: "target2",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
id: "endDelay_animation2",
|
||||
duration: 1000000,
|
||||
endDelay: -500000,
|
||||
fill: "none"
|
||||
}
|
||||
}, {
|
||||
id: "target3",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
id: "endDelay_animation3",
|
||||
duration: 1000000,
|
||||
endDelay: -1500000,
|
||||
fill: "forwards"
|
||||
}
|
||||
}, {
|
||||
id: "target4",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
id: "endDelay_animation4",
|
||||
duration: 100000,
|
||||
delay: 100000,
|
||||
endDelay: -1500000,
|
||||
fill: "forwards"
|
||||
}
|
||||
}];
|
||||
|
||||
for (const {id, frames, timing} of animations) {
|
||||
const effect = new KeyframeEffect(document.getElementById(id),
|
||||
frames, timing);
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.play();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* globals addMessageListener, sendAsyncMessage */
|
||||
|
||||
"use strict";
|
||||
|
||||
// A helper frame-script for browser/devtools/animationinspector tests.
|
||||
|
||||
/**
|
||||
* Toggle (play or pause) one of the animation players of a given node.
|
||||
* @param {Object} data
|
||||
* - {String} selector The CSS selector to get the node (can be a "super"
|
||||
* selector).
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to play
|
||||
* or pause
|
||||
* - {Boolean} pause True to pause the animation, false to play.
|
||||
*/
|
||||
addMessageListener("Test:ToggleAnimationPlayer", function(msg) {
|
||||
const {selector, animationIndex, pause} = msg.data;
|
||||
const node = superQuerySelector(selector);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animation = node.getAnimations()[animationIndex];
|
||||
if (pause) {
|
||||
animation.pause();
|
||||
} else {
|
||||
animation.play();
|
||||
}
|
||||
|
||||
sendAsyncMessage("Test:ToggleAnimationPlayer");
|
||||
});
|
||||
|
||||
/**
|
||||
* Change the currentTime of one of the animation players of a given node.
|
||||
* @param {Object} data
|
||||
* - {String} selector The CSS selector to get the node (can be a "super"
|
||||
* selector).
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to change.
|
||||
* - {Number} currentTime The current time to set.
|
||||
*/
|
||||
addMessageListener("Test:SetAnimationPlayerCurrentTime", function(msg) {
|
||||
const {selector, animationIndex, currentTime} = msg.data;
|
||||
const node = superQuerySelector(selector);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animation = node.getAnimations()[animationIndex];
|
||||
animation.currentTime = currentTime;
|
||||
|
||||
sendAsyncMessage("Test:SetAnimationPlayerCurrentTime");
|
||||
});
|
||||
|
||||
/**
|
||||
* Change the playbackRate of one of the animation players of a given node.
|
||||
* @param {Object} data
|
||||
* - {String} selector The CSS selector to get the node (can be a "super"
|
||||
* selector).
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to change.
|
||||
* - {Number} playbackRate The rate to set.
|
||||
*/
|
||||
addMessageListener("Test:SetAnimationPlayerPlaybackRate", function(msg) {
|
||||
const {selector, animationIndex, playbackRate} = msg.data;
|
||||
const node = superQuerySelector(selector);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = node.getAnimations()[animationIndex];
|
||||
player.playbackRate = playbackRate;
|
||||
|
||||
sendAsyncMessage("Test:SetAnimationPlayerPlaybackRate");
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the current playState of an animation player on a given node.
|
||||
* @param {Object} data
|
||||
* - {String} selector The CSS selector to get the node (can be a "super"
|
||||
* selector).
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to check
|
||||
*/
|
||||
addMessageListener("Test:GetAnimationPlayerState", function(msg) {
|
||||
const {selector, animationIndex} = msg.data;
|
||||
const node = superQuerySelector(selector);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animation = node.getAnimations()[animationIndex];
|
||||
animation.ready.then(() => {
|
||||
sendAsyncMessage("Test:GetAnimationPlayerState", animation.playState);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Like document.querySelector but can go into iframes too.
|
||||
* ".container iframe || .sub-container div" will first try to find the node
|
||||
* matched by ".container iframe" in the root document, then try to get the
|
||||
* content document inside it, and then try to match ".sub-container div" inside
|
||||
* this document.
|
||||
* Any selector coming before the || separator *MUST* match a frame node.
|
||||
* @param {String} superSelector.
|
||||
* @return {DOMNode} The node, or null if not found.
|
||||
*/
|
||||
function superQuerySelector(superSelector, root = content.document) {
|
||||
const frameIndex = superSelector.indexOf("||");
|
||||
if (frameIndex === -1) {
|
||||
return root.querySelector(superSelector);
|
||||
}
|
||||
|
||||
const rootSelector = superSelector.substring(0, frameIndex).trim();
|
||||
const childSelector = superSelector.substring(frameIndex + 2).trim();
|
||||
root = root.querySelector(rootSelector);
|
||||
if (!root || !root.contentWindow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return superQuerySelector(childSelector, root.contentWindow.document);
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Yay! Keyframes!</title>
|
||||
<style>
|
||||
div {
|
||||
animation: wow 100s forwards;
|
||||
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
@keyframes wow {
|
||||
0% {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 0px;
|
||||
background: #f06;
|
||||
}
|
||||
10% {
|
||||
border-radius: 2px;
|
||||
}
|
||||
20% {
|
||||
transform: rotate(13deg);
|
||||
}
|
||||
30% {
|
||||
background: gold;
|
||||
}
|
||||
40% {
|
||||
filter: blur(40px);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(720deg) translateX(300px) skew(-13deg);
|
||||
}
|
||||
60% {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
70% {
|
||||
border-radius: 10px;
|
||||
}
|
||||
80% {
|
||||
background: #333;
|
||||
}
|
||||
90% {
|
||||
border-radius: 50%;
|
||||
}
|
||||
100% {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: blue;
|
||||
animation: move 20s 20s linear;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
to {
|
||||
margin-left: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<div class="rate"></div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
var el = document.querySelector(".rate");
|
||||
var ani = el.getAnimations()[0];
|
||||
ani.playbackRate = 2;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
.ball {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.script-animation {
|
||||
background: #f06;
|
||||
}
|
||||
|
||||
.css-transition {
|
||||
background: #006;
|
||||
transition: background-color 20s;
|
||||
}
|
||||
|
||||
.css-animation {
|
||||
background: #a06;
|
||||
animation: flash 10s forwards;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ball script-animation"></div>
|
||||
<div class="ball css-animation"></div>
|
||||
<div class="ball css-transition"></div>
|
||||
|
||||
<script>
|
||||
/* globals KeyframeEffect, Animation */
|
||||
"use strict";
|
||||
|
||||
setTimeout(function() {
|
||||
document.querySelector(".css-transition").style.backgroundColor = "yellow";
|
||||
}, 0);
|
||||
|
||||
const effect = new KeyframeEffect(
|
||||
document.querySelector(".script-animation"), [
|
||||
{opacity: 1, offset: 0},
|
||||
{opacity: .1, offset: 1}
|
||||
], { duration: 10000, fill: "forwards" });
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@keyframes css-animations {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#css-animations {
|
||||
animation: css-animations 100s;
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: lime;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="css-animations"></div>
|
||||
<script>
|
||||
"use strict";
|
||||
const DURATION = 100 * 1000;
|
||||
[
|
||||
{
|
||||
id: "no-easing",
|
||||
frames: { opacity: [1, 0] },
|
||||
timing: {
|
||||
duration: DURATION
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "effect-easing",
|
||||
frames: { opacity: [1, 0] },
|
||||
timing: {
|
||||
easing: "frames(5)",
|
||||
duration: DURATION
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "keyframe-easing",
|
||||
frames: [
|
||||
{
|
||||
offset: 0,
|
||||
easing: "steps(2)",
|
||||
opacity: 1
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
opacity: 0
|
||||
}
|
||||
],
|
||||
timing: {
|
||||
duration: DURATION,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "both-easing",
|
||||
frames: [
|
||||
{
|
||||
offset: 0,
|
||||
easing: "steps(2)",
|
||||
opacity: 1
|
||||
},
|
||||
{
|
||||
offset: 0,
|
||||
easing: "steps(5)",
|
||||
marginLeft: "0px",
|
||||
marginTop: "0px"
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
opacity: 0,
|
||||
marginLeft: "100px",
|
||||
marginTop: "100px"
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
easing: "steps(10)",
|
||||
duration: DURATION,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "many-keyframes",
|
||||
frames: [
|
||||
{
|
||||
offset: 0,
|
||||
easing: "steps(2)",
|
||||
opacity: 1,
|
||||
backgroundColor: "red",
|
||||
},
|
||||
{
|
||||
offset: 0.25,
|
||||
easing: "ease-in",
|
||||
opacity: 0.25,
|
||||
},
|
||||
{
|
||||
offset: 0.3,
|
||||
easing: "ease-out",
|
||||
backgroundColor: "blue",
|
||||
},
|
||||
{
|
||||
offset: 0.5,
|
||||
easing: "linear",
|
||||
opacity: 0.5,
|
||||
},
|
||||
{
|
||||
offset: 0.75,
|
||||
easing: "ease-out",
|
||||
opacity: 0.75,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
opacity: 0,
|
||||
backgroundColor: "lime",
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
duration: DURATION,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "narrow-keyframes",
|
||||
frames: [
|
||||
{
|
||||
offset: 0,
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
offset: 0.1,
|
||||
easing: "steps(1)",
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
offset: 0.13,
|
||||
opacity: 0,
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
duration: DURATION,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "duplicate-offsets",
|
||||
frames: [
|
||||
{
|
||||
offset: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
offset: 0.5,
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
offset: 0.5,
|
||||
easing: "steps(1)",
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
opacity: 1,
|
||||
},
|
||||
],
|
||||
timing: {
|
||||
duration: DURATION,
|
||||
}
|
||||
},
|
||||
].forEach(({ id, frames, timing }) => {
|
||||
const target = document.createElement("div");
|
||||
document.body.appendChild(target);
|
||||
const effect = new KeyframeEffect(target, frames, timing);
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.id = id;
|
||||
animation.play();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id=target1>1</div>
|
||||
<div id=target2>2</div>
|
||||
<div id=target3>3</div>
|
||||
<div id=target4>4</div>
|
||||
<div id=target5>5</div>
|
||||
<div id=target6>6</div>
|
||||
<div id=target7>7</div>
|
||||
<div id=target8>8</div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const timing = {
|
||||
duration: 1000,
|
||||
fill: "forwards"
|
||||
};
|
||||
|
||||
document.querySelector("#target1").animate(
|
||||
[{ backgroundColor: "red",
|
||||
backgroundRepeat: "space",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" },
|
||||
{ backgroundColor: "lime",
|
||||
backgroundRepeat: "round",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target2").animate(
|
||||
[{ backgroundColor: "lime",
|
||||
backgroundRepeat: "space",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" },
|
||||
{ backgroundColor: "red",
|
||||
backgroundRepeat: "round",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target3").animate(
|
||||
[{ backgroundColor: "red",
|
||||
backgroundRepeat: "space",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" },
|
||||
{ backgroundColor: "blue",
|
||||
backgroundRepeat: "round",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" },
|
||||
{ backgroundColor: "lime",
|
||||
backgroundRepeat: "space",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target4").animate(
|
||||
[{ backgroundColor: "red",
|
||||
backgroundRepeat: "space",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)",
|
||||
easing: "steps(2)" },
|
||||
{ backgroundColor: "lime",
|
||||
backgroundRepeat: "round",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" }], timing).pause();
|
||||
|
||||
timing.easing = "steps(2)";
|
||||
document.querySelector("#target5").animate(
|
||||
[{ opacity: 0 }, { opacity: 1 }], timing).pause();
|
||||
|
||||
timing.easing = "linear";
|
||||
document.querySelector("#target6").animate(
|
||||
[{ opacity: 0, easing: "frames(5)" }, { opacity: 1 }], timing).pause();
|
||||
|
||||
document.querySelector("#target7").animate(
|
||||
[
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
easing: "steps(2)",
|
||||
offset: 0.1,
|
||||
},
|
||||
{
|
||||
opacity: 0,
|
||||
offset: 0.13,
|
||||
},
|
||||
], timing).pause();
|
||||
|
||||
document.querySelector("#target8").animate(
|
||||
[
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
offset: 0.5,
|
||||
},
|
||||
{
|
||||
opacity: 0,
|
||||
offset: 0.5,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
offset: 1,
|
||||
},
|
||||
], timing).pause();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -500px;
|
||||
height: 20px;
|
||||
width: 500px;
|
||||
color: red;
|
||||
background: linear-gradient(to left, currentColor, currentColor 2px, transparent);
|
||||
}
|
||||
|
||||
.zero {
|
||||
color: blue;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: green;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.negative.move { animation: 5s -1s move linear forwards; }
|
||||
.zero.move { animation: 5s 0s move linear forwards; }
|
||||
.positive.move { animation: 5s 1s move linear forwards; }
|
||||
|
||||
@keyframes move {
|
||||
to {
|
||||
transform: translateX(500px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="negative"></div>
|
||||
<div class="zero"></div>
|
||||
<div class="positive"></div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
var negative = document.querySelector(".negative");
|
||||
var zero = document.querySelector(".zero");
|
||||
var positive = document.querySelector(".positive");
|
||||
|
||||
// The non-delayed animation starts now.
|
||||
zero.classList.add("move");
|
||||
// The negative-delayed animation starts in 1 second.
|
||||
setTimeout(function() {
|
||||
negative.classList.add("move");
|
||||
}, 1000);
|
||||
// The positive-delayed animation starts in 200 ms.
|
||||
setTimeout(function() {
|
||||
positive.classList.add("move");
|
||||
}, 200);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Animated pseudo elements</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
body {
|
||||
animation: color 2s linear infinite;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@keyframes color {
|
||||
to {
|
||||
filter: hue-rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
body::before,
|
||||
body::after {
|
||||
content: "";
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
animation: grow 1s linear infinite alternate;
|
||||
}
|
||||
|
||||
body::before {
|
||||
background: hsl(120, 80%, 80%);
|
||||
}
|
||||
body::after {
|
||||
background: hsl(240, 80%, 80%);
|
||||
animation-delay: -.5s;
|
||||
}
|
||||
|
||||
@keyframes grow {
|
||||
0% {height: 100%; animation-timing-function: ease-in-out;}
|
||||
10% {height: 80%; animation-timing-function: ease-in-out;}
|
||||
20% {height: 60%; animation-timing-function: ease-in-out;}
|
||||
30% {height: 70%; animation-timing-function: ease-in-out;}
|
||||
40% {height: 50%; animation-timing-function: ease-in-out;}
|
||||
50% {height: 30%; animation-timing-function: ease-in-out;}
|
||||
60% {height: 80%; animation-timing-function: ease-in-out;}
|
||||
70% {height: 90%; animation-timing-function: ease-in-out;}
|
||||
80% {height: 70%; animation-timing-function: ease-in-out;}
|
||||
90% {height: 60%; animation-timing-function: ease-in-out;}
|
||||
100% {height: 100%; animation-timing-function: ease-in-out;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
#target1 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: red;
|
||||
}
|
||||
|
||||
#target2 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: green;
|
||||
}
|
||||
|
||||
#target3 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: blue;
|
||||
}
|
||||
|
||||
#target4 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: pink;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target1"></div>
|
||||
<div id="target2"></div>
|
||||
<div id="target3"></div>
|
||||
<div id="target4"></div>
|
||||
|
||||
<script>
|
||||
/* globals KeyframeEffect, Animation */
|
||||
"use strict";
|
||||
|
||||
const animations = [{
|
||||
id: "target1",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
duration: 100,
|
||||
iterations: 2,
|
||||
iterationStart: 0.25,
|
||||
fill: "both"
|
||||
}
|
||||
}, {
|
||||
id: "target2",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
duration: 100,
|
||||
iterations: 1,
|
||||
iterationStart: 0.25,
|
||||
fill: "both"
|
||||
}
|
||||
}, {
|
||||
id: "target3",
|
||||
frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }],
|
||||
timing: {
|
||||
duration: 100,
|
||||
iterations: 1.5,
|
||||
iterationStart: 2.5,
|
||||
fill: "both"
|
||||
}
|
||||
}, {
|
||||
id: "target4",
|
||||
frames: [{ transform: "translate(0px)", offset: 0 },
|
||||
{ transform: "translate(100px)", offset: 1 }],
|
||||
timing: {
|
||||
duration: 100,
|
||||
iterations: 2,
|
||||
iterationStart: 0.5,
|
||||
delay: 10,
|
||||
fill: "both"
|
||||
}
|
||||
}];
|
||||
|
||||
for (const {id, frames, timing} of animations) {
|
||||
const effect = new KeyframeEffect(document.getElementById(id),
|
||||
frames, timing);
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.play();
|
||||
animation.pause();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: lime;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="onetime"></div>
|
||||
<div id="twotimes"></div>
|
||||
<div id="infinite"></div>
|
||||
<script>
|
||||
"use strict";
|
||||
let target = document.querySelector("#onetime");
|
||||
target.animate({ opacity: [0, 1] },
|
||||
{ duration: 1, iterations: 1 }).pause();
|
||||
target = document.querySelector("#twotimes");
|
||||
target.animate({ opacity: [0, 1] },
|
||||
{ duration: 1, iterations: 2 }).pause();
|
||||
target = document.querySelector("#infinite");
|
||||
target.animate({ opacity: [0, 1] },
|
||||
{ duration: 1, iterations: Infinity }).pause();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
.ball {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
/* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
background: #f06;
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.still {
|
||||
top: 0;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.animated {
|
||||
top: 100px;
|
||||
left: 10px;
|
||||
|
||||
animation: simple-animation 2s infinite alternate;
|
||||
}
|
||||
|
||||
.multi {
|
||||
top: 200px;
|
||||
left: 10px;
|
||||
|
||||
animation: simple-animation 2s infinite alternate,
|
||||
other-animation 5s infinite alternate;
|
||||
}
|
||||
|
||||
.delayed {
|
||||
top: 300px;
|
||||
left: 10px;
|
||||
background: rebeccapurple;
|
||||
|
||||
animation: simple-animation 3s 60s 10;
|
||||
}
|
||||
|
||||
.multi-finite {
|
||||
top: 400px;
|
||||
left: 10px;
|
||||
background: yellow;
|
||||
|
||||
animation: simple-animation 3s,
|
||||
other-animation 4s;
|
||||
}
|
||||
|
||||
.short {
|
||||
top: 500px;
|
||||
left: 10px;
|
||||
background: red;
|
||||
|
||||
animation: simple-animation 2s normal;
|
||||
}
|
||||
|
||||
.long {
|
||||
top: 600px;
|
||||
left: 10px;
|
||||
background: blue;
|
||||
|
||||
animation: simple-animation 120s;
|
||||
}
|
||||
|
||||
.negative-delay {
|
||||
top: 700px;
|
||||
left: 10px;
|
||||
background: gray;
|
||||
|
||||
animation: simple-animation 15s -10s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.no-compositor {
|
||||
top: 0;
|
||||
right: 10px;
|
||||
background: gold;
|
||||
|
||||
animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
|
||||
}
|
||||
|
||||
.compositor-all {
|
||||
animation: compositor-all 2s infinite;
|
||||
}
|
||||
|
||||
.compositor-notall {
|
||||
animation: compositor-notall 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes simple-animation {
|
||||
100% {
|
||||
transform: translateX(300px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes other-animation {
|
||||
100% {
|
||||
background: blue;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes no-compositor {
|
||||
100% {
|
||||
margin-right: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes compositor-all {
|
||||
to { opacity: 0.5 }
|
||||
}
|
||||
|
||||
@keyframes compositor-notall {
|
||||
from {
|
||||
opacity: 0;
|
||||
width: 0px;
|
||||
transform: translate(0px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
width: 100px;
|
||||
transform: translate(100px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Comment node -->
|
||||
<div class="ball still"></div>
|
||||
<div class="ball animated"></div>
|
||||
<div class="ball multi"></div>
|
||||
<div class="ball delayed"></div>
|
||||
<div class="ball multi-finite"></div>
|
||||
<div class="ball short"></div>
|
||||
<div class="ball long"></div>
|
||||
<div class="ball negative-delay"></div>
|
||||
<div class="ball no-compositor"></div>
|
||||
<div class="ball" id="endDelayed"></div>
|
||||
<div class="ball compositor-all"></div>
|
||||
<div class="ball compositor-notall"></div>
|
||||
<script>
|
||||
/* globals KeyframeEffect, Animation */
|
||||
"use strict";
|
||||
|
||||
var el = document.getElementById("endDelayed");
|
||||
const effect = new KeyframeEffect(el, [
|
||||
{ opacity: 0, offset: 0 },
|
||||
{ opacity: 1, offset: 1 }
|
||||
], { duration: 1000000, endDelay: 500000, fill: "none" });
|
||||
const animation = new Animation(effect, document.timeline);
|
||||
animation.play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: lime;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const delayList = [0, 50000, -50000];
|
||||
const endDelayList = [0, 50000, -50000];
|
||||
|
||||
delayList.forEach(delay => {
|
||||
endDelayList.forEach(endDelay => {
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
el.animate({ opacity: [0, 1] },
|
||||
{ duration: 200000,
|
||||
iterations: 1,
|
||||
fill: "both",
|
||||
delay: delay,
|
||||
endDelay: endDelay });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,632 +0,0 @@
|
|||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* import-globals-from ../../test/head.js */
|
||||
// Import the inspector's head.js first (which itself imports shared-head.js).
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
|
||||
this);
|
||||
|
||||
const FRAME_SCRIPT_URL = CHROME_URL_ROOT + "doc_frame_script.js";
|
||||
const TAB_NAME = "animationinspector";
|
||||
const ANIMATION_L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// Auto clean-up when a test ends
|
||||
registerCleanupFunction(async function() {
|
||||
await closeAnimationInspector();
|
||||
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Disable new animation inspector.
|
||||
Services.prefs.setBoolPref("devtools.new-animationinspector.enabled", false);
|
||||
|
||||
// Clean-up all prefs that might have been changed during a test run
|
||||
// (safer here because if the test fails, then the pref is never reverted)
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("devtools.new-animationinspector.enabled");
|
||||
Services.prefs.clearUserPref("devtools.debugger.log");
|
||||
});
|
||||
|
||||
// Some animation features are not enabled by default in release/beta channels
|
||||
// yet including:
|
||||
// * parts of the Web Animations API (Bug 1264101), and
|
||||
// * the frames() timing function (Bug 1379582).
|
||||
function enableAnimationFeatures() {
|
||||
return new Promise(resolve => {
|
||||
SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.animations-api.core.enabled", true],
|
||||
["dom.animations-api.getAnimations.enabled", true],
|
||||
["dom.animations-api.implicit-keyframes.enabled", true],
|
||||
["dom.animations-api.timelines.enabled", true],
|
||||
["layout.css.frames-timing.enabled", true],
|
||||
]}, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new test tab in the browser and load the given url.
|
||||
* @param {String} url The url to be loaded in the new tab
|
||||
* @return a promise that resolves to the tab object when the url is loaded
|
||||
*/
|
||||
var _addTab = addTab;
|
||||
addTab = function(url) {
|
||||
return enableAnimationFeatures().then(() => _addTab(url)).then(tab => {
|
||||
const browser = tab.linkedBrowser;
|
||||
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
|
||||
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
|
||||
loadFrameScriptUtils(browser);
|
||||
return tab;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload the current tab location.
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
||||
* loaded in the toolbox
|
||||
*/
|
||||
async function reloadTab(inspector) {
|
||||
const onNewRoot = inspector.once("new-root");
|
||||
await executeInContent("devtools:test:reload", {}, {}, false);
|
||||
await onNewRoot;
|
||||
await inspector.once("inspector-updated");
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the inspector's current selection to a node or to the first match of the
|
||||
* given css selector and wait for the animations to be displayed
|
||||
* @param {String|NodeFront}
|
||||
* data The node to select
|
||||
* @param {InspectorPanel} inspector
|
||||
* The instance of InspectorPanel currently
|
||||
* loaded in the toolbox
|
||||
* @param {String} reason
|
||||
* Defaults to "test" which instructs the inspector not
|
||||
* to highlight the node upon selection
|
||||
* @return {Promise} Resolves when the inspector is updated with the new node
|
||||
and animations of its subtree are properly displayed.
|
||||
*/
|
||||
var selectNodeAndWaitForAnimations = async function(data, inspector, reason = "test") {
|
||||
// We want to make sure the rest of the test waits for the animations to
|
||||
// be properly displayed (wait for all target DOM nodes to be previewed).
|
||||
const {AnimationsController, AnimationsPanel} =
|
||||
inspector.sidebar.getWindowForTab(TAB_NAME);
|
||||
const onUiUpdated = AnimationsPanel.once(AnimationsPanel.UI_UPDATED_EVENT);
|
||||
|
||||
await selectNode(data, inspector, reason);
|
||||
|
||||
await onUiUpdated;
|
||||
if (AnimationsController.animationPlayers.length !== 0) {
|
||||
await waitForAnimationTimelineRendering(AnimationsPanel);
|
||||
await waitForAllAnimationTargets(AnimationsPanel);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there are the expected number of animations being displayed in the
|
||||
* panel right now.
|
||||
* @param {AnimationsPanel} panel
|
||||
* @param {Number} nbAnimations The expected number of animations.
|
||||
* @param {String} msg An optional string to be used as the assertion message.
|
||||
*/
|
||||
function assertAnimationsDisplayed(panel, nbAnimations, msg = "") {
|
||||
msg = msg || `There are ${nbAnimations} animations in the panel`;
|
||||
is(panel.animationsTimelineComponent
|
||||
.animationsEl
|
||||
.querySelectorAll(".animation").length, nbAnimations, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an Inspector panel that was just created, and waits
|
||||
* for a "inspector-updated" event as well as the animation inspector
|
||||
* sidebar to be ready. Returns a promise once these are completed.
|
||||
*
|
||||
* @param {InspectorPanel} inspector
|
||||
* @return {Promise}
|
||||
*/
|
||||
var waitForAnimationInspectorReady = async function(inspector) {
|
||||
const win = inspector.sidebar.getWindowForTab(TAB_NAME);
|
||||
const updated = inspector.once("inspector-updated");
|
||||
|
||||
// In e10s, if we wait for underlying toolbox actors to
|
||||
// load (by setting DevToolsUtils.testing to true), we miss the
|
||||
// "animationinspector-ready" event on the sidebar, so check to see if the
|
||||
// iframe is already loaded.
|
||||
const tabReady = win.document.readyState === "complete" ?
|
||||
promise.resolve() :
|
||||
inspector.sidebar.once("animationinspector-ready");
|
||||
|
||||
return promise.all([updated, tabReady]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the toolbox, with the inspector tool visible and the animationinspector
|
||||
* sidebar selected.
|
||||
* @return a promise that resolves when the inspector is ready.
|
||||
*/
|
||||
var openAnimationInspector = async function() {
|
||||
const {inspector, toolbox} = await openInspectorSidebarTab(TAB_NAME);
|
||||
|
||||
info("Waiting for the inspector and sidebar to be ready");
|
||||
await waitForAnimationInspectorReady(inspector);
|
||||
|
||||
const win = inspector.sidebar.getWindowForTab(TAB_NAME);
|
||||
const {AnimationsController, AnimationsPanel} = win;
|
||||
|
||||
info("Waiting for the animation controller and panel to be ready");
|
||||
if (AnimationsPanel.initialized) {
|
||||
await AnimationsPanel.initialized;
|
||||
} else {
|
||||
await AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
|
||||
}
|
||||
|
||||
if (AnimationsController.animationPlayers.length !== 0) {
|
||||
await waitForAnimationTimelineRendering(AnimationsPanel);
|
||||
await waitForAllAnimationTargets(AnimationsPanel);
|
||||
}
|
||||
|
||||
return {
|
||||
toolbox: toolbox,
|
||||
inspector: inspector,
|
||||
controller: AnimationsController,
|
||||
panel: AnimationsPanel,
|
||||
window: win
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the toolbox.
|
||||
* @return a promise that resolves when the toolbox has closed.
|
||||
*/
|
||||
var closeAnimationInspector = async function() {
|
||||
const target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
await gDevTools.closeToolbox(target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for a content -> chrome message on the message manager (the window
|
||||
* messagemanager is used).
|
||||
* @param {String} name The message name
|
||||
* @return {Promise} A promise that resolves to the response data when the
|
||||
* message has been received
|
||||
*/
|
||||
function waitForContentMessage(name) {
|
||||
info("Expecting message " + name + " from content");
|
||||
|
||||
const mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
return new Promise(resolve => {
|
||||
mm.addMessageListener(name, function onMessage(msg) {
|
||||
mm.removeMessageListener(name, onMessage);
|
||||
resolve(msg.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an async message to the frame script (chrome -> content) and wait for a
|
||||
* response message with the same name (content -> chrome).
|
||||
* @param {String} name The message name. Should be one of the messages defined
|
||||
* in doc_frame_script.js
|
||||
* @param {Object} data Optional data to send along
|
||||
* @param {Object} objects Optional CPOW objects to send along
|
||||
* @param {Boolean} expectResponse If set to false, don't wait for a response
|
||||
* with the same name from the content script. Defaults to true.
|
||||
* @return {Promise} Resolves to the response data if a response is expected,
|
||||
* immediately resolves otherwise
|
||||
*/
|
||||
function executeInContent(name, data = {}, objects = {},
|
||||
expectResponse = true) {
|
||||
info("Sending message " + name + " to content");
|
||||
const mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
mm.sendAsyncMessage(name, data, objects);
|
||||
if (expectResponse) {
|
||||
return waitForContentMessage(name);
|
||||
}
|
||||
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current playState of an animation player on a given node.
|
||||
*/
|
||||
var getAnimationPlayerState = async function(selector,
|
||||
animationIndex = 0) {
|
||||
const playState = await executeInContent("Test:GetAnimationPlayerState",
|
||||
{selector, animationIndex});
|
||||
return playState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the given node visible in the page (rendered in the frame tree).
|
||||
* @param {DOMNode}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isNodeVisible(node) {
|
||||
return !!node.getClientRects().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all AnimationTargetNode instances to be fully loaded
|
||||
* (fetched their related actor and rendered), and return them.
|
||||
* This method should be called after "animation-timeline-rendering-completed" is emitted,
|
||||
* since we get all the AnimationTargetNode instances using getAnimationTargetNodes().
|
||||
* @param {AnimationsPanel} panel
|
||||
* @return {Array} all AnimationTargetNode instances
|
||||
*/
|
||||
var waitForAllAnimationTargets = async function(panel) {
|
||||
const targets = getAnimationTargetNodes(panel);
|
||||
await promise.all(targets.map(t => {
|
||||
if (!t.previewer.nodeFront) {
|
||||
return t.once("target-retrieved");
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
return targets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the scrubber element in the timeline is moving.
|
||||
* @param {AnimationPanel} panel
|
||||
* @param {Boolean} isMoving
|
||||
*/
|
||||
async function assertScrubberMoving(panel, isMoving) {
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
if (isMoving) {
|
||||
// If we expect the scrubber to move, just wait for a couple of
|
||||
// timeline-data-changed events and compare times.
|
||||
const {time: time1} = await timeline.once("timeline-data-changed");
|
||||
const {time: time2} = await timeline.once("timeline-data-changed");
|
||||
ok(time2 > time1, "The scrubber is moving");
|
||||
} else {
|
||||
// If instead we expect the scrubber to remain at its position, just wait
|
||||
// for some time and make sure timeline-data-changed isn't emitted.
|
||||
let hasMoved = false;
|
||||
timeline.once("timeline-data-changed", () => {
|
||||
hasMoved = true;
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
ok(!hasMoved, "The scrubber is not moving");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the play/pause button in the timeline toolbar and wait for animations
|
||||
* to update.
|
||||
* @param {AnimationsPanel} panel
|
||||
*/
|
||||
async function clickTimelinePlayPauseButton(panel) {
|
||||
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
|
||||
const btn = panel.playTimelineButtonEl;
|
||||
const win = btn.ownerDocument.defaultView;
|
||||
EventUtils.sendMouseEvent({type: "click"}, btn, win);
|
||||
|
||||
await onUiUpdated;
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the rewind button in the timeline toolbar and wait for animations to
|
||||
* update.
|
||||
* @param {AnimationsPanel} panel
|
||||
*/
|
||||
async function clickTimelineRewindButton(panel) {
|
||||
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
|
||||
const btn = panel.rewindTimelineButtonEl;
|
||||
const win = btn.ownerDocument.defaultView;
|
||||
EventUtils.sendMouseEvent({type: "click"}, btn, win);
|
||||
|
||||
await onUiUpdated;
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a rate inside the playback rate selector in the timeline toolbar and
|
||||
* wait for animations to update.
|
||||
* @param {AnimationsPanel} panel
|
||||
* @param {Number} rate The new rate value to be selected
|
||||
*/
|
||||
async function changeTimelinePlaybackRate(panel, rate) {
|
||||
const onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
const select = panel.rateSelectorEl.firstChild;
|
||||
const win = select.ownerDocument.defaultView;
|
||||
|
||||
// Get the right option.
|
||||
const option = [...select.options].filter(o => o.value === rate + "")[0];
|
||||
if (!option) {
|
||||
ok(false,
|
||||
"Could not find an option for rate " + rate + " in the rate selector. " +
|
||||
"Values are: " + [...select.options].map(o => o.value));
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate the right events to select the option in the drop-down.
|
||||
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
|
||||
EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
|
||||
|
||||
await onUiUpdated;
|
||||
await waitForAnimationTimelineRendering(panel);
|
||||
await waitForAllAnimationTargets(panel);
|
||||
|
||||
// Simulate a mousemove outside of the rate selector area to avoid subsequent
|
||||
// tests from failing because of unwanted mouseover events.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for animation selecting.
|
||||
* @param {AnimationsPanel} panel
|
||||
*/
|
||||
async function waitForAnimationSelecting(panel) {
|
||||
await panel.animationsTimelineComponent.once("animation-selected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for rendering animation timeline.
|
||||
* @param {AnimationsPanel} panel
|
||||
*/
|
||||
function waitForAnimationTimelineRendering(panel) {
|
||||
return panel.animationsTimelineComponent.once("animation-timeline-rendering-completed");
|
||||
}
|
||||
|
||||
/**
|
||||
+ * Click the timeline header to update the animation current time.
|
||||
+ * @param {AnimationsPanel} panel
|
||||
+ * @param {Number} x position rate on timeline header.
|
||||
+ * This method calculates
|
||||
+ * `position * offsetWidth + offsetLeft of timeline header`
|
||||
+ * as the clientX of MouseEvent.
|
||||
+ * This parameter should be from 0.0 to 1.0.
|
||||
+ */
|
||||
async function clickOnTimelineHeader(panel, position) {
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const onTimelineDataChanged = timeline.once("timeline-data-changed");
|
||||
|
||||
const header = timeline.timeHeaderEl;
|
||||
const clientX = header.offsetLeft + header.offsetWidth * position;
|
||||
EventUtils.sendMouseEvent({ type: "mousedown", clientX: clientX },
|
||||
header, header.ownerDocument.defaultView);
|
||||
info(`Click at (${ clientX }, 0) on timeline header`);
|
||||
EventUtils.sendMouseEvent({ type: "mouseup", clientX: clientX }, header,
|
||||
header.ownerDocument.defaultView);
|
||||
return onTimelineDataChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the toolbox common highlighter from making backend requests.
|
||||
* @param {Toolbox} toolbox
|
||||
*/
|
||||
function disableHighlighter(toolbox) {
|
||||
toolbox._highlighter = {
|
||||
showBoxModel: () => new Promise(r => r()),
|
||||
hideBoxModel: () => new Promise(r => r()),
|
||||
pick: () => new Promise(r => r()),
|
||||
cancelPick: () => new Promise(r => r()),
|
||||
destroy: () => {},
|
||||
traits: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an animation in the timeline to select/unselect it.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {Number} index The index of the animation to click on.
|
||||
* @param {Boolean} shouldAlreadySelected Set to true
|
||||
* if the clicked animation is already selected.
|
||||
* @return {Promise} resolves to the animation whose state has changed.
|
||||
*/
|
||||
async function clickOnAnimation(panel, index, shouldAlreadySelected) {
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
|
||||
// Expect a selection event.
|
||||
const onSelectionChanged = timeline.once(shouldAlreadySelected
|
||||
? "animation-already-selected"
|
||||
: "animation-selected");
|
||||
|
||||
info("Click on animation " + index + " in the timeline");
|
||||
const timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
|
||||
// Scroll to show the timeBlock since the element may be out of displayed area.
|
||||
timeBlock.scrollIntoView(false);
|
||||
const timeBlockBounds = timeBlock.getBoundingClientRect();
|
||||
let x = timeBlockBounds.width / 2;
|
||||
const y = timeBlockBounds.height / 2;
|
||||
if (timeBlock != timeBlock.ownerDocument.elementFromPoint(x, y)) {
|
||||
// Move the mouse pointer a little since scrubber element may be at the point.
|
||||
x += timeBlockBounds.width / 4;
|
||||
}
|
||||
EventUtils.synthesizeMouse(timeBlock, x, y, {}, timeBlock.ownerDocument.defaultView);
|
||||
|
||||
return onSelectionChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the Keyframes component from the timeline.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {String} propertyName The name of the animated property.
|
||||
* @return {Keyframes} The Keyframes component instance.
|
||||
*/
|
||||
function getKeyframeComponent(panel, propertyName) {
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const detailsComponent = timeline.details;
|
||||
return detailsComponent.keyframeComponents
|
||||
.find(c => c.propertyName === propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a keyframe element from the timeline.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {String} propertyName The name of the animated property.
|
||||
* @param {Index} keyframeIndex The index of the keyframe.
|
||||
* @return {DOMNode} The keyframe element.
|
||||
*/
|
||||
function getKeyframeEl(panel, propertyName, keyframeIndex) {
|
||||
const keyframeComponent = getKeyframeComponent(panel, propertyName);
|
||||
return keyframeComponent.keyframesEl
|
||||
.querySelectorAll(".frame")[keyframeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set style to test document.
|
||||
* @param {Animation} animation - animation object.
|
||||
* @param {AnimationsPanel} panel - The panel instance.
|
||||
* @param {String} name - property name.
|
||||
* @param {String} value - property value.
|
||||
* @param {String} selector - selector for test document.
|
||||
*/
|
||||
async function setStyle(animation, panel, name, value, selector) {
|
||||
info("Change the animation style via the content DOM. Setting " +
|
||||
name + " to " + value + " of " + selector);
|
||||
|
||||
const onAnimationChanged = animation ? once(animation, "changed") : Promise.resolve();
|
||||
const onRendered = waitForAnimationTimelineRendering(panel);
|
||||
|
||||
await executeInContent("devtools:test:setStyle", {
|
||||
selector: selector,
|
||||
propertyName: name,
|
||||
propertyValue: value
|
||||
});
|
||||
|
||||
await onAnimationChanged;
|
||||
await onRendered;
|
||||
await waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph shapes of summary and detail are constructed by <path> element.
|
||||
* This function checks the vertex of path segments.
|
||||
* Also, if needed, checks the color for <stop> element.
|
||||
* @param pathEl - <path> element.
|
||||
* @param duration - float as duration which pathEl represetns.
|
||||
* @param hasClosePath - set true if the path shoud be closing.
|
||||
* @param expectedValues - JSON object format. We can test the vertex and color.
|
||||
* e.g.
|
||||
* [
|
||||
* // Test the vertex (x=0, y=0) should be passing through.
|
||||
* { x: 0, y: 0 },
|
||||
* { x: 0, y: 1 },
|
||||
* // If we have to test the color as well,
|
||||
* // we can write as following.
|
||||
* { x: 500, y: 1, color: "rgb(0, 0, 255)" },
|
||||
* { x: 1000, y: 1 }
|
||||
* ]
|
||||
*/
|
||||
function assertPathSegments(pathEl, duration, hasClosePath, expectedValues) {
|
||||
const pathSegList = pathEl.pathSegList;
|
||||
ok(pathSegList, "The tested element should have pathSegList");
|
||||
|
||||
expectedValues.forEach(expectedValue => {
|
||||
ok(isPassingThrough(pathSegList, expectedValue.x, expectedValue.y),
|
||||
`The path segment of x ${ expectedValue.x }, y ${ expectedValue.y } `
|
||||
+ `should be passing through`);
|
||||
|
||||
if (expectedValue.color) {
|
||||
assertColor(pathEl.closest("svg"), expectedValue.x / duration, expectedValue.color);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasClosePath) {
|
||||
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
|
||||
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
|
||||
"The last segment should be close path");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the color for <stop> element.
|
||||
* @param svgEl - <svg> element which has <stop> element.
|
||||
* @param offset - float which represents the "offset" attribute of <stop>.
|
||||
* @param expectedColor - e.g. rgb(0, 0, 255)
|
||||
*/
|
||||
function assertColor(svgEl, offset, expectedColor) {
|
||||
const stopEl = findStopElement(svgEl, offset);
|
||||
ok(stopEl, `stop element at offset ${ offset } should exist`);
|
||||
is(stopEl.getAttribute("stop-color"), expectedColor,
|
||||
`stop-color of stop element at offset ${ offset } should be ${ expectedColor }`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given vertex is passing throug on the path.
|
||||
* @param pathSegList - pathSegList of <path> element.
|
||||
* @param x - float x of vertex.
|
||||
* @param y - float y of vertex.
|
||||
* @return true: passing through, false: no on the path.
|
||||
*/
|
||||
function isPassingThrough(pathSegList, x, y) {
|
||||
let previousPathSeg = pathSegList.getItem(0);
|
||||
for (let i = 0; i < pathSegList.numberOfItems; i++) {
|
||||
const pathSeg = pathSegList.getItem(i);
|
||||
if (pathSeg.x === undefined) {
|
||||
continue;
|
||||
}
|
||||
const currentX = parseFloat(pathSeg.x.toFixed(3));
|
||||
const currentY = parseFloat(pathSeg.y.toFixed(6));
|
||||
if (currentX === x && currentY === y) {
|
||||
return true;
|
||||
}
|
||||
const previousX = parseFloat(previousPathSeg.x.toFixed(3));
|
||||
const previousY = parseFloat(previousPathSeg.y.toFixed(6));
|
||||
if (previousX <= x && x <= currentX &&
|
||||
Math.min(previousY, currentY) <= y && y <= Math.max(previousY, currentY)) {
|
||||
return true;
|
||||
}
|
||||
previousPathSeg = pathSeg;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find <stop> element which has given offset from given <svg> element.
|
||||
* @param svgEl - <svg> element which has <stop> element.
|
||||
* @param offset - float which represents the "offset" attribute of <stop>.
|
||||
* @return <stop> element.
|
||||
*/
|
||||
function findStopElement(svgEl, offset) {
|
||||
for (const stopEl of svgEl.querySelectorAll("stop")) {
|
||||
if (offset <= parseFloat(stopEl.getAttribute("offset"))) {
|
||||
return stopEl;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all AnimationTargetNode instances.
|
||||
* This method should be called after emit "animation-timeline-rendering-completed".
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @return {Array} all AnimationTargetNode instances.
|
||||
*/
|
||||
function getAnimationTargetNodes(panel) {
|
||||
return panel.animationsTimelineComponent.animations.map(animation => {
|
||||
return panel.animationsTimelineComponent.componentsMap[animation.actorID].targetNode;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all AnimationTargetBlock instances.
|
||||
* This method should be called after emit "animation-timeline-rendering-completed".
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @return {Array} all AnimationTargetBlock instances.
|
||||
*/
|
||||
function getAnimationTimeBlocks(panel) {
|
||||
return panel.animationsTimelineComponent.animations.map(animation => {
|
||||
return panel.animationsTimelineComponent.componentsMap[animation.actorID].timeBlock;
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the common devtools xpcshell eslintrc config.
|
||||
"extends": "../../../../../.eslintrc.xpcshell.js"
|
||||
};
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint no-eval:0 */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {findOptimalTimeInterval} = require("devtools/client/inspector/animation-old/utils");
|
||||
|
||||
// This test array contains objects that are used to test the
|
||||
// findOptimalTimeInterval function. Each object should have the following
|
||||
// properties:
|
||||
// - desc: an optional string that will be printed out
|
||||
// - minTimeInterval: a number that represents the minimum time in ms
|
||||
// that should be displayed in one interval
|
||||
// - expectedInterval: a number that you expect the findOptimalTimeInterval
|
||||
// function to return as a result.
|
||||
// Optionally you can pass a string where `interval` is the calculated
|
||||
// interval, this string will be eval'd and tested to be truthy.
|
||||
const TEST_DATA = [{
|
||||
desc: "With no minTimeInterval, expect the interval to be 0",
|
||||
minTimeInterval: null,
|
||||
expectedInterval: 0
|
||||
}, {
|
||||
desc: "With a minTimeInterval of 0 ms, expect the interval to be 0",
|
||||
minTimeInterval: 0,
|
||||
expectedInterval: 0
|
||||
}, {
|
||||
desc: "With a minInterval of 1ms, expect the interval to be the 1ms too",
|
||||
minTimeInterval: 1,
|
||||
expectedInterval: 1
|
||||
}, {
|
||||
desc: "With a very small minTimeInterval, expect the interval to be 1ms",
|
||||
minTimeInterval: 1e-31,
|
||||
expectedInterval: 1
|
||||
}, {
|
||||
desc: "With a minInterval of 2.5ms, expect the interval to be 2.5ms too",
|
||||
minTimeInterval: 2.5,
|
||||
expectedInterval: 2.5
|
||||
}, {
|
||||
desc: "With a minInterval of 5ms, expect the interval to be 5ms too",
|
||||
minTimeInterval: 5,
|
||||
expectedInterval: 5
|
||||
}, {
|
||||
desc: "With a minInterval of 7ms, expect the interval to be the next " +
|
||||
"multiple of 5",
|
||||
minTimeInterval: 7,
|
||||
expectedInterval: 10
|
||||
}, {
|
||||
minTimeInterval: 20,
|
||||
expectedInterval: 25
|
||||
}, {
|
||||
minTimeInterval: 33,
|
||||
expectedInterval: 50
|
||||
}, {
|
||||
minTimeInterval: 987,
|
||||
expectedInterval: 1000
|
||||
}, {
|
||||
minTimeInterval: 1234,
|
||||
expectedInterval: 2500
|
||||
}, {
|
||||
minTimeInterval: 9800,
|
||||
expectedInterval: 10000
|
||||
}];
|
||||
|
||||
function run_test() {
|
||||
for (const {minTimeInterval, desc, expectedInterval} of TEST_DATA) {
|
||||
info(`Testing minTimeInterval: ${minTimeInterval}.
|
||||
Expecting ${expectedInterval}.`);
|
||||
|
||||
const interval = findOptimalTimeInterval(minTimeInterval);
|
||||
if (typeof expectedInterval == "string") {
|
||||
ok(eval(expectedInterval), desc);
|
||||
} else {
|
||||
equal(interval, expectedInterval, desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {formatStopwatchTime} = require("devtools/client/inspector/animation-old/utils");
|
||||
|
||||
const TEST_DATA = [{
|
||||
desc: "Formatting 0",
|
||||
time: 0,
|
||||
expected: "00:00.000"
|
||||
}, {
|
||||
desc: "Formatting null",
|
||||
time: null,
|
||||
expected: "00:00.000"
|
||||
}, {
|
||||
desc: "Formatting undefined",
|
||||
time: undefined,
|
||||
expected: "00:00.000"
|
||||
}, {
|
||||
desc: "Formatting a small number of ms",
|
||||
time: 13,
|
||||
expected: "00:00.013"
|
||||
}, {
|
||||
desc: "Formatting a slightly larger number of ms",
|
||||
time: 500,
|
||||
expected: "00:00.500"
|
||||
}, {
|
||||
desc: "Formatting 1 second",
|
||||
time: 1000,
|
||||
expected: "00:01.000"
|
||||
}, {
|
||||
desc: "Formatting a number of seconds",
|
||||
time: 1532,
|
||||
expected: "00:01.532"
|
||||
}, {
|
||||
desc: "Formatting a big number of seconds",
|
||||
time: 58450,
|
||||
expected: "00:58.450"
|
||||
}, {
|
||||
desc: "Formatting 1 minute",
|
||||
time: 60000,
|
||||
expected: "01:00.000"
|
||||
}, {
|
||||
desc: "Formatting a number of minutes",
|
||||
time: 263567,
|
||||
expected: "04:23.567"
|
||||
}, {
|
||||
desc: "Formatting a large number of minutes",
|
||||
time: 1000 * 60 * 60 * 3,
|
||||
expected: "180:00.000"
|
||||
}];
|
||||
|
||||
function run_test() {
|
||||
for (const {desc, time, expected} of TEST_DATA) {
|
||||
equal(formatStopwatchTime(time), expected, desc);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {getCssPropertyName} = require("devtools/client/inspector/animation-old/utils");
|
||||
|
||||
const TEST_DATA = [{
|
||||
jsName: "alllowercase",
|
||||
cssName: "alllowercase"
|
||||
}, {
|
||||
jsName: "borderWidth",
|
||||
cssName: "border-width"
|
||||
}, {
|
||||
jsName: "borderTopRightRadius",
|
||||
cssName: "border-top-right-radius"
|
||||
}];
|
||||
|
||||
function run_test() {
|
||||
for (const {jsName, cssName} of TEST_DATA) {
|
||||
equal(getCssPropertyName(jsName), cssName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {TimeScale} = require("devtools/client/inspector/animation-old/utils");
|
||||
const TEST_ANIMATIONS = [{
|
||||
desc: "Testing a few standard animations",
|
||||
animations: [{
|
||||
previousStartTime: 500,
|
||||
delay: 0,
|
||||
duration: 1000,
|
||||
iterationCount: 1,
|
||||
playbackRate: 1
|
||||
}, {
|
||||
previousStartTime: 400,
|
||||
delay: 100,
|
||||
duration: 10,
|
||||
iterationCount: 100,
|
||||
playbackRate: 1
|
||||
}, {
|
||||
previousStartTime: 50,
|
||||
delay: 1000,
|
||||
duration: 100,
|
||||
iterationCount: 20,
|
||||
playbackRate: 1
|
||||
}],
|
||||
expectedMinStart: 50,
|
||||
expectedMaxEnd: 3050
|
||||
}, {
|
||||
desc: "Testing a single negative-delay animation",
|
||||
animations: [{
|
||||
previousStartTime: 100,
|
||||
delay: -100,
|
||||
duration: 100,
|
||||
iterationCount: 1,
|
||||
playbackRate: 1
|
||||
}],
|
||||
expectedMinStart: 0,
|
||||
expectedMaxEnd: 100
|
||||
}, {
|
||||
desc: "Testing a single negative-delay animation with a different rate",
|
||||
animations: [{
|
||||
previousStartTime: 3500,
|
||||
delay: -1000,
|
||||
duration: 10000,
|
||||
iterationCount: 2,
|
||||
playbackRate: 2
|
||||
}],
|
||||
expectedMinStart: 3000,
|
||||
expectedMaxEnd: 13000
|
||||
}];
|
||||
|
||||
const TEST_STARTTIME_TO_DISTANCE = [{
|
||||
time: 50,
|
||||
expectedDistance: 0
|
||||
}, {
|
||||
time: 50,
|
||||
expectedDistance: 0
|
||||
}, {
|
||||
time: 3050,
|
||||
expectedDistance: 100
|
||||
}, {
|
||||
time: 1550,
|
||||
expectedDistance: 50
|
||||
}];
|
||||
|
||||
const TEST_DURATION_TO_DISTANCE = [{
|
||||
time: 3000,
|
||||
expectedDistance: 100
|
||||
}, {
|
||||
time: 0,
|
||||
expectedDistance: 0
|
||||
}];
|
||||
|
||||
const TEST_DISTANCE_TO_TIME = [{
|
||||
distance: 100,
|
||||
expectedTime: 3050
|
||||
}, {
|
||||
distance: 0,
|
||||
expectedTime: 50
|
||||
}, {
|
||||
distance: 25,
|
||||
expectedTime: 800
|
||||
}];
|
||||
|
||||
const TEST_DISTANCE_TO_RELATIVE_TIME = [{
|
||||
distance: 100,
|
||||
expectedTime: 3000
|
||||
}, {
|
||||
distance: 0,
|
||||
expectedTime: 0
|
||||
}, {
|
||||
distance: 25,
|
||||
expectedTime: 750
|
||||
}];
|
||||
|
||||
const TEST_FORMAT_TIME_MS = [{
|
||||
time: 0,
|
||||
expectedFormattedTime: "0ms"
|
||||
}, {
|
||||
time: 3540.341,
|
||||
expectedFormattedTime: "3540ms"
|
||||
}, {
|
||||
time: 1.99,
|
||||
expectedFormattedTime: "2ms"
|
||||
}, {
|
||||
time: 4000,
|
||||
expectedFormattedTime: "4000ms"
|
||||
}];
|
||||
|
||||
const TEST_FORMAT_TIME_S = [{
|
||||
time: 0,
|
||||
expectedFormattedTime: "0.0s"
|
||||
}, {
|
||||
time: 3540.341,
|
||||
expectedFormattedTime: "3.5s"
|
||||
}, {
|
||||
time: 1.99,
|
||||
expectedFormattedTime: "0.0s"
|
||||
}, {
|
||||
time: 4000,
|
||||
expectedFormattedTime: "4.0s"
|
||||
}, {
|
||||
time: 102540,
|
||||
expectedFormattedTime: "102.5s"
|
||||
}, {
|
||||
time: 102940,
|
||||
expectedFormattedTime: "102.9s"
|
||||
}];
|
||||
|
||||
function run_test() {
|
||||
info("Check the default min/max range values");
|
||||
equal(TimeScale.minStartTime, Infinity);
|
||||
equal(TimeScale.maxEndTime, 0);
|
||||
|
||||
for (const {desc, animations, expectedMinStart, expectedMaxEnd} of
|
||||
TEST_ANIMATIONS) {
|
||||
info("Test adding a few animations: " + desc);
|
||||
for (const state of animations) {
|
||||
TimeScale.addAnimation(state);
|
||||
}
|
||||
|
||||
info("Checking the time scale range");
|
||||
equal(TimeScale.minStartTime, expectedMinStart);
|
||||
equal(TimeScale.maxEndTime, expectedMaxEnd);
|
||||
|
||||
info("Test reseting the animations");
|
||||
TimeScale.reset();
|
||||
equal(TimeScale.minStartTime, Infinity);
|
||||
equal(TimeScale.maxEndTime, 0);
|
||||
}
|
||||
|
||||
info("Add a set of animations again");
|
||||
for (const state of TEST_ANIMATIONS[0].animations) {
|
||||
TimeScale.addAnimation(state);
|
||||
}
|
||||
|
||||
info("Test converting start times to distances");
|
||||
for (const {time, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
|
||||
const distance = TimeScale.startTimeToDistance(time);
|
||||
equal(distance, expectedDistance);
|
||||
}
|
||||
|
||||
info("Test converting durations to distances");
|
||||
for (const {time, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
|
||||
const distance = TimeScale.durationToDistance(time);
|
||||
equal(distance, expectedDistance);
|
||||
}
|
||||
|
||||
info("Test converting distances to times");
|
||||
for (const {distance, expectedTime} of TEST_DISTANCE_TO_TIME) {
|
||||
const time = TimeScale.distanceToTime(distance);
|
||||
equal(time, expectedTime);
|
||||
}
|
||||
|
||||
info("Test converting distances to relative times");
|
||||
for (const {distance, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
|
||||
const time = TimeScale.distanceToRelativeTime(distance);
|
||||
equal(time, expectedTime);
|
||||
}
|
||||
|
||||
info("Test formatting times (millis)");
|
||||
for (const {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) {
|
||||
const formattedTime = TimeScale.formatTime(time);
|
||||
equal(formattedTime, expectedFormattedTime);
|
||||
}
|
||||
|
||||
// Add 1 more animation to increase the range and test more time formatting
|
||||
// cases.
|
||||
TimeScale.addAnimation({
|
||||
startTime: 3000,
|
||||
duration: 5000,
|
||||
delay: 0,
|
||||
iterationCount: 1
|
||||
});
|
||||
|
||||
info("Test formatting times (seconds)");
|
||||
for (const {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) {
|
||||
const formattedTime = TimeScale.formatTime(time);
|
||||
equal(formattedTime, expectedFormattedTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {require} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {TimeScale} = require("devtools/client/inspector/animation-old/utils");
|
||||
|
||||
const TEST_ENDDELAY_X = [{
|
||||
desc: "Testing positive-endDelay animations",
|
||||
animations: [{
|
||||
previousStartTime: 0,
|
||||
duration: 500,
|
||||
playbackRate: 1,
|
||||
iterationCount: 3,
|
||||
delay: 500,
|
||||
endDelay: 500
|
||||
}],
|
||||
expectedEndDelayX: 80
|
||||
}, {
|
||||
desc: "Testing negative-endDelay animations",
|
||||
animations: [{
|
||||
previousStartTime: 0,
|
||||
duration: 500,
|
||||
playbackRate: 1,
|
||||
iterationCount: 9,
|
||||
delay: 500,
|
||||
endDelay: -500
|
||||
}],
|
||||
expectedEndDelayX: 90
|
||||
}];
|
||||
|
||||
function run_test() {
|
||||
info("Test calculating endDelayX");
|
||||
|
||||
// Be independent of possible prior tests
|
||||
TimeScale.reset();
|
||||
|
||||
for (const {desc, animations, expectedEndDelayX} of TEST_ENDDELAY_X) {
|
||||
info(`Adding animations: ${desc}`);
|
||||
|
||||
for (const state of animations) {
|
||||
TimeScale.addAnimation(state);
|
||||
|
||||
const {endDelayX} = TimeScale.getAnimationDimensions({state});
|
||||
equal(endDelayX, expectedEndDelayX);
|
||||
|
||||
TimeScale.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[DEFAULT]
|
||||
tags = devtools
|
||||
head =
|
||||
firefox-appdir = browser
|
||||
skip-if = toolkit == 'android'
|
||||
|
||||
[test_findOptimalTimeInterval.js]
|
||||
[test_formatStopwatchTime.js]
|
||||
[test_getCssPropertyName.js]
|
||||
[test_timeScale.js]
|
||||
[test_timeScale_dimensions.js]
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
/* -*- 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 { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// How many times, maximum, can we loop before we find the optimal time
|
||||
// interval in the timeline graph.
|
||||
const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
|
||||
// Time graduations should be multiple of one of these number.
|
||||
const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
|
||||
|
||||
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
|
||||
|
||||
// SVG namespace
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
* DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* - nodeType {String} Optional, defaults to "div",
|
||||
* - attributes {Object} Optional attributes object like
|
||||
* {attrName1:value1, attrName2: value2, ...}
|
||||
* - parent {DOMNode} Mandatory node to append the newly created node to.
|
||||
* - textContent {String} Optional text for the node.
|
||||
* - namespace {String} Optional namespace
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createNode(options) {
|
||||
if (!options.parent) {
|
||||
throw new Error("Missing parent DOMNode to create new node");
|
||||
}
|
||||
|
||||
const type = options.nodeType || "div";
|
||||
const node =
|
||||
options.namespace
|
||||
? options.parent.ownerDocument.createElementNS(options.namespace, type)
|
||||
: options.parent.ownerDocument.createElement(type);
|
||||
|
||||
for (const name in options.attributes || {}) {
|
||||
const value = options.attributes[name];
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
|
||||
if (options.textContent) {
|
||||
node.textContent = options.textContent;
|
||||
}
|
||||
|
||||
options.parent.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
exports.createNode = createNode;
|
||||
|
||||
/**
|
||||
* SVG DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* - nodeType {String} Optional, defaults to "div",
|
||||
* - attributes {Object} Optional attributes object like
|
||||
* {attrName1:value1, attrName2: value2, ...}
|
||||
* - parent {DOMNode} Mandatory node to append the newly created node to.
|
||||
* - textContent {String} Optional text for the node.
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createSVGNode(options) {
|
||||
options.namespace = SVG_NS;
|
||||
return createNode(options);
|
||||
}
|
||||
exports.createSVGNode = createSVGNode;
|
||||
|
||||
/**
|
||||
* Find the optimal interval between time graduations in the animation timeline
|
||||
* graph based on a minimum time interval
|
||||
* @param {Number} minTimeInterval Minimum time in ms in one interval
|
||||
* @return {Number} The optimal interval time in ms
|
||||
*/
|
||||
function findOptimalTimeInterval(minTimeInterval) {
|
||||
let numIters = 0;
|
||||
let multiplier = 1;
|
||||
|
||||
if (!minTimeInterval) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let interval;
|
||||
while (true) {
|
||||
for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
|
||||
interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
|
||||
if (minTimeInterval <= interval) {
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
|
||||
return interval;
|
||||
}
|
||||
multiplier *= 10;
|
||||
}
|
||||
}
|
||||
|
||||
exports.findOptimalTimeInterval = findOptimalTimeInterval;
|
||||
|
||||
/**
|
||||
* Format a timestamp (in ms) as a mm:ss.mmm string.
|
||||
* @param {Number} time
|
||||
* @return {String}
|
||||
*/
|
||||
function formatStopwatchTime(time) {
|
||||
// Format falsy values as 0
|
||||
if (!time) {
|
||||
return "00:00.000";
|
||||
}
|
||||
|
||||
let milliseconds = parseInt(time % 1000, 10);
|
||||
let seconds = parseInt((time / 1000) % 60, 10);
|
||||
let minutes = parseInt((time / (1000 * 60)), 10);
|
||||
|
||||
const pad = (nb, max) => {
|
||||
if (nb < max) {
|
||||
return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
|
||||
}
|
||||
return nb;
|
||||
};
|
||||
|
||||
minutes = pad(minutes, 10);
|
||||
seconds = pad(seconds, 10);
|
||||
milliseconds = pad(milliseconds, 100);
|
||||
|
||||
return `${minutes}:${seconds}.${milliseconds}`;
|
||||
}
|
||||
|
||||
exports.formatStopwatchTime = formatStopwatchTime;
|
||||
|
||||
/**
|
||||
* The TimeScale helper object is used to know which size should something be
|
||||
* displayed with in the animation panel, depending on the animations that are
|
||||
* currently displayed.
|
||||
* If there are 5 animations displayed, and the first one starts at 10000ms and
|
||||
* the last one ends at 20000ms, then this helper can be used to convert any
|
||||
* time in this range to a distance in pixels.
|
||||
*
|
||||
* For the helper to know how to convert, it needs to know all the animations.
|
||||
* Whenever a new animation is added to the panel, addAnimation(state) should be
|
||||
* called. reset() can be called to start over.
|
||||
*/
|
||||
var TimeScale = {
|
||||
minStartTime: Infinity,
|
||||
maxEndTime: 0,
|
||||
|
||||
/**
|
||||
* Add a new animation to time scale.
|
||||
* @param {Object} state A PlayerFront.state object.
|
||||
*/
|
||||
addAnimation: function(state) {
|
||||
let {previousStartTime, delay, duration, endDelay,
|
||||
iterationCount, playbackRate} = state;
|
||||
|
||||
endDelay = typeof endDelay === "undefined" ? 0 : endDelay;
|
||||
const toRate = v => v / playbackRate;
|
||||
const minZero = v => Math.max(v, 0);
|
||||
const rateRelativeDuration =
|
||||
toRate(duration * (!iterationCount ? 1 : iterationCount));
|
||||
// Negative-delayed animations have their startTimes set such that we would
|
||||
// be displaying the delay outside the time window if we didn't take it into
|
||||
// account here.
|
||||
const relevantDelay = delay < 0 ? toRate(delay) : 0;
|
||||
previousStartTime = previousStartTime || 0;
|
||||
|
||||
const startTime = toRate(minZero(delay)) +
|
||||
rateRelativeDuration +
|
||||
endDelay;
|
||||
this.minStartTime = Math.min(
|
||||
this.minStartTime,
|
||||
previousStartTime +
|
||||
relevantDelay +
|
||||
Math.min(startTime, 0)
|
||||
);
|
||||
const length = toRate(delay) +
|
||||
rateRelativeDuration +
|
||||
toRate(minZero(endDelay));
|
||||
const endTime = previousStartTime + length;
|
||||
this.maxEndTime = Math.max(this.maxEndTime, endTime);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the current time scale.
|
||||
*/
|
||||
reset: function() {
|
||||
this.minStartTime = Infinity;
|
||||
this.maxEndTime = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a startTime to a distance in %, in the current time scale.
|
||||
* @param {Number} time
|
||||
* @return {Number}
|
||||
*/
|
||||
startTimeToDistance: function(time) {
|
||||
time -= this.minStartTime;
|
||||
return this.durationToDistance(time);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a duration to a distance in %, in the current time scale.
|
||||
* @param {Number} time
|
||||
* @return {Number}
|
||||
*/
|
||||
durationToDistance: function(duration) {
|
||||
return duration * 100 / this.getDuration();
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a distance in % to a time, in the current time scale.
|
||||
* @param {Number} distance
|
||||
* @return {Number}
|
||||
*/
|
||||
distanceToTime: function(distance) {
|
||||
return this.minStartTime + (this.getDuration() * distance / 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a distance in % to a time, in the current time scale.
|
||||
* The time will be relative to the current minimum start time.
|
||||
* @param {Number} distance
|
||||
* @return {Number}
|
||||
*/
|
||||
distanceToRelativeTime: function(distance) {
|
||||
const time = this.distanceToTime(distance);
|
||||
return time - this.minStartTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Depending on the time scale, format the given time as milliseconds or
|
||||
* seconds.
|
||||
* @param {Number} time
|
||||
* @return {String} The formatted time string.
|
||||
*/
|
||||
formatTime: function(time) {
|
||||
// Format in milliseconds if the total duration is short enough.
|
||||
if (this.getDuration() <= MILLIS_TIME_FORMAT_MAX_DURATION) {
|
||||
return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
|
||||
}
|
||||
|
||||
// Otherwise format in seconds.
|
||||
return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
|
||||
},
|
||||
|
||||
getDuration: function() {
|
||||
return this.maxEndTime - this.minStartTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an animation, get the various dimensions (in %) useful to draw the
|
||||
* animation in the timeline.
|
||||
*/
|
||||
getAnimationDimensions: function({state}) {
|
||||
const start = state.previousStartTime || 0;
|
||||
const duration = state.duration;
|
||||
const rate = state.playbackRate;
|
||||
const count = state.iterationCount;
|
||||
const delay = state.delay || 0;
|
||||
const endDelay = state.endDelay || 0;
|
||||
|
||||
// The start position.
|
||||
const x = this.startTimeToDistance(start + (delay / rate));
|
||||
// The width for a single iteration.
|
||||
const w = this.durationToDistance(duration / rate);
|
||||
// The width for all iterations.
|
||||
const iterationW = w * (count || 1);
|
||||
// The start position of the delay.
|
||||
const delayX = delay < 0 ? x : this.startTimeToDistance(start);
|
||||
// The width of the delay.
|
||||
const delayW = this.durationToDistance(Math.abs(delay) / rate);
|
||||
// The width of the delay if it is negative, 0 otherwise.
|
||||
const negativeDelayW = delay < 0 ? delayW : 0;
|
||||
// The width of the endDelay.
|
||||
const endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
|
||||
// The start position of the endDelay.
|
||||
const endDelayX = endDelay < 0 ? x + iterationW - endDelayW
|
||||
: x + iterationW;
|
||||
|
||||
return {x, w, iterationW, delayX, delayW, negativeDelayW,
|
||||
endDelayX, endDelayW};
|
||||
}
|
||||
};
|
||||
|
||||
exports.TimeScale = TimeScale;
|
||||
|
||||
/**
|
||||
* Convert given CSS property name to JavaScript CSS name.
|
||||
* @param {String} CSS property name (e.g. background-color).
|
||||
* @return {String} JavaScript CSS property name (e.g. backgroundColor).
|
||||
*/
|
||||
function getJsPropertyName(cssPropertyName) {
|
||||
if (cssPropertyName == "float") {
|
||||
return "cssFloat";
|
||||
}
|
||||
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
|
||||
return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
|
||||
return group.toUpperCase();
|
||||
});
|
||||
}
|
||||
exports.getJsPropertyName = getJsPropertyName;
|
||||
|
||||
/**
|
||||
* Turn propertyName into property-name.
|
||||
* @param {String} jsPropertyName A camelcased CSS property name. Typically
|
||||
* something that comes out of computed styles. E.g. borderBottomColor
|
||||
* @return {String} The corresponding CSS property name: border-bottom-color
|
||||
*/
|
||||
function getCssPropertyName(jsPropertyName) {
|
||||
return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
|
||||
}
|
||||
exports.getCssPropertyName = getCssPropertyName;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
exports.getFormattedAnimationTitle = getFormattedAnimationTitle;
|
||||
|
|
@ -110,8 +110,8 @@ class AnimationInspector {
|
|||
|
||||
const provider = createElement(Provider,
|
||||
{
|
||||
id: "newanimationinspector",
|
||||
key: "newanimationinspector",
|
||||
id: "animationinspector",
|
||||
key: "animationinspector",
|
||||
store: this.inspector.store
|
||||
},
|
||||
App(
|
||||
|
|
@ -275,7 +275,7 @@ class AnimationInspector {
|
|||
isPanelVisible() {
|
||||
return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
|
||||
this.inspector.toolbox.currentToolId === "inspector" &&
|
||||
this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
|
||||
this.inspector.sidebar.getCurrentTabID() === "animationinspector";
|
||||
}
|
||||
|
||||
onAnimationStateChanged() {
|
||||
|
|
@ -625,7 +625,7 @@ class AnimationInspector {
|
|||
}
|
||||
|
||||
async update() {
|
||||
const done = this.inspector.updating("newanimationinspector");
|
||||
const done = this.inspector.updating("animationinspector");
|
||||
|
||||
const selection = this.inspector.selection;
|
||||
const animations =
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue