@quick-game/cli
Version:
Command line interface for rapid qg development
523 lines • 18 kB
JavaScript
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as SDK from '../../core/sdk/sdk.js';
export class AnimationModel extends SDK.SDKModel.SDKModel {
runtimeModel;
agent;
#animationsById;
animationGroups;
#pendingAnimations;
playbackRate;
#screenshotCapture;
#enabled;
constructor(target) {
super(target);
this.runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
this.agent = target.animationAgent();
target.registerAnimationDispatcher(new AnimationDispatcher(this));
this.#animationsById = new Map();
this.animationGroups = new Map();
this.#pendingAnimations = new Set();
this.playbackRate = 1;
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.reset, this);
const screenCaptureModel = target.model(SDK.ScreenCaptureModel.ScreenCaptureModel);
if (screenCaptureModel) {
this.#screenshotCapture = new ScreenshotCapture(this, screenCaptureModel);
}
}
reset() {
this.#animationsById.clear();
this.animationGroups.clear();
this.#pendingAnimations.clear();
this.dispatchEventToListeners(Events.ModelReset);
}
animationCreated(id) {
this.#pendingAnimations.add(id);
}
animationCanceled(id) {
this.#pendingAnimations.delete(id);
this.flushPendingAnimationsIfNeeded();
}
animationStarted(payload) {
// We are not interested in animations without effect or target.
if (!payload.source || !payload.source.backendNodeId) {
return;
}
const animation = AnimationImpl.parsePayload(this, payload);
if (!animation) {
return;
}
// Ignore Web Animations custom effects & groups.
const keyframesRule = animation.source().keyframesRule();
if (animation.type() === 'WebAnimation' && keyframesRule && keyframesRule.keyframes().length === 0) {
this.#pendingAnimations.delete(animation.id());
}
else {
this.#animationsById.set(animation.id(), animation);
this.#pendingAnimations.add(animation.id());
}
this.flushPendingAnimationsIfNeeded();
}
flushPendingAnimationsIfNeeded() {
for (const id of this.#pendingAnimations) {
if (!this.#animationsById.get(id)) {
return;
}
}
while (this.#pendingAnimations.size) {
this.matchExistingGroups(this.createGroupFromPendingAnimations());
}
}
matchExistingGroups(incomingGroup) {
let matchedGroup = null;
for (const group of this.animationGroups.values()) {
if (group.matches(incomingGroup)) {
matchedGroup = group;
group.update(incomingGroup);
break;
}
}
if (!matchedGroup) {
this.animationGroups.set(incomingGroup.id(), incomingGroup);
if (this.#screenshotCapture) {
this.#screenshotCapture.captureScreenshots(incomingGroup.finiteDuration(), incomingGroup.screenshotsInternal);
}
}
this.dispatchEventToListeners(Events.AnimationGroupStarted, matchedGroup || incomingGroup);
return Boolean(matchedGroup);
}
createGroupFromPendingAnimations() {
console.assert(this.#pendingAnimations.size > 0);
const firstAnimationId = this.#pendingAnimations.values().next().value;
this.#pendingAnimations.delete(firstAnimationId);
const firstAnimation = this.#animationsById.get(firstAnimationId);
if (!firstAnimation) {
throw new Error('Unable to locate first animation');
}
const groupedAnimations = [firstAnimation];
const groupStartTime = firstAnimation.startTime();
const remainingAnimations = new Set();
for (const id of this.#pendingAnimations) {
const anim = this.#animationsById.get(id);
if (anim.startTime() === groupStartTime) {
groupedAnimations.push(anim);
}
else {
remainingAnimations.add(id);
}
}
this.#pendingAnimations = remainingAnimations;
return new AnimationGroup(this, firstAnimationId, groupedAnimations);
}
setPlaybackRate(playbackRate) {
this.playbackRate = playbackRate;
void this.agent.invoke_setPlaybackRate({ playbackRate });
}
releaseAnimations(animations) {
void this.agent.invoke_releaseAnimations({ animations });
}
async suspendModel() {
this.reset();
await this.agent.invoke_disable();
}
async resumeModel() {
if (!this.#enabled) {
return;
}
await this.agent.invoke_enable();
}
async ensureEnabled() {
if (this.#enabled) {
return;
}
await this.agent.invoke_enable();
this.#enabled = true;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export var Events;
(function (Events) {
Events["AnimationGroupStarted"] = "AnimationGroupStarted";
Events["ModelReset"] = "ModelReset";
})(Events || (Events = {}));
export class AnimationImpl {
#animationModel;
#payloadInternal;
#sourceInternal;
#playStateInternal;
constructor(animationModel, payload) {
this.#animationModel = animationModel;
this.#payloadInternal = payload;
this.#sourceInternal =
new AnimationEffect(animationModel, this.#payloadInternal.source);
}
static parsePayload(animationModel, payload) {
return new AnimationImpl(animationModel, payload);
}
payload() {
return this.#payloadInternal;
}
id() {
return this.#payloadInternal.id;
}
name() {
return this.#payloadInternal.name;
}
paused() {
return this.#payloadInternal.pausedState;
}
playState() {
return this.#playStateInternal || this.#payloadInternal.playState;
}
setPlayState(playState) {
this.#playStateInternal = playState;
}
playbackRate() {
return this.#payloadInternal.playbackRate;
}
startTime() {
return this.#payloadInternal.startTime;
}
endTime() {
if (!this.source().iterations) {
return Infinity;
}
return this.startTime() + this.source().delay() + this.source().duration() * this.source().iterations() +
this.source().endDelay();
}
finiteDuration() {
const iterations = Math.min(this.source().iterations(), 3);
return this.source().delay() + this.source().duration() * iterations;
}
currentTime() {
return this.#payloadInternal.currentTime;
}
source() {
return this.#sourceInternal;
}
type() {
return this.#payloadInternal.type;
}
overlaps(animation) {
// Infinite animations
if (!this.source().iterations() || !animation.source().iterations()) {
return true;
}
const firstAnimation = this.startTime() < animation.startTime() ? this : animation;
const secondAnimation = firstAnimation === this ? animation : this;
return firstAnimation.endTime() >= secondAnimation.startTime();
}
setTiming(duration, delay) {
void this.#sourceInternal.node().then(node => {
if (!node) {
throw new Error('Unable to find node');
}
this.updateNodeStyle(duration, delay, node);
});
this.#sourceInternal.durationInternal = duration;
this.#sourceInternal.delayInternal = delay;
void this.#animationModel.agent.invoke_setTiming({ animationId: this.id(), duration, delay });
}
updateNodeStyle(duration, delay, node) {
let animationPrefix;
if (this.type() === "CSSTransition" /* Protocol.Animation.AnimationType.CSSTransition */) {
animationPrefix = 'transition-';
}
else if (this.type() === "CSSAnimation" /* Protocol.Animation.AnimationType.CSSAnimation */) {
animationPrefix = 'animation-';
}
else {
return;
}
if (!node.id) {
throw new Error('Node has no id');
}
const cssModel = node.domModel().cssModel();
cssModel.setEffectivePropertyValueForNode(node.id, animationPrefix + 'duration', duration + 'ms');
cssModel.setEffectivePropertyValueForNode(node.id, animationPrefix + 'delay', delay + 'ms');
}
async remoteObjectPromise() {
const payload = await this.#animationModel.agent.invoke_resolveAnimation({ animationId: this.id() });
if (!payload) {
return null;
}
return this.#animationModel.runtimeModel.createRemoteObject(payload.remoteObject);
}
cssId() {
return this.#payloadInternal.cssId || '';
}
}
export class AnimationEffect {
#animationModel;
#payload;
#keyframesRuleInternal;
delayInternal;
durationInternal;
#deferredNodeInternal;
constructor(animationModel, payload) {
this.#animationModel = animationModel;
this.#payload = payload;
if (payload.keyframesRule) {
this.#keyframesRuleInternal = new KeyframesRule(payload.keyframesRule);
}
this.delayInternal = this.#payload.delay;
this.durationInternal = this.#payload.duration;
}
delay() {
return this.delayInternal;
}
endDelay() {
return this.#payload.endDelay;
}
iterationStart() {
return this.#payload.iterationStart;
}
iterations() {
// Animations with zero duration, zero delays and infinite iterations can't be shown.
if (!this.delay() && !this.endDelay() && !this.duration()) {
return 0;
}
return this.#payload.iterations || Infinity;
}
duration() {
return this.durationInternal;
}
direction() {
return this.#payload.direction;
}
fill() {
return this.#payload.fill;
}
node() {
if (!this.#deferredNodeInternal) {
this.#deferredNodeInternal =
new SDK.DOMModel.DeferredDOMNode(this.#animationModel.target(), this.backendNodeId());
}
return this.#deferredNodeInternal.resolvePromise();
}
deferredNode() {
return new SDK.DOMModel.DeferredDOMNode(this.#animationModel.target(), this.backendNodeId());
}
backendNodeId() {
return this.#payload.backendNodeId;
}
keyframesRule() {
return this.#keyframesRuleInternal || null;
}
easing() {
return this.#payload.easing;
}
}
export class KeyframesRule {
#payload;
#keyframesInternal;
constructor(payload) {
this.#payload = payload;
this.#keyframesInternal = this.#payload.keyframes.map(function (keyframeStyle) {
return new KeyframeStyle(keyframeStyle);
});
}
setKeyframesPayload(payload) {
this.#keyframesInternal = payload.map(function (keyframeStyle) {
return new KeyframeStyle(keyframeStyle);
});
}
name() {
return this.#payload.name;
}
keyframes() {
return this.#keyframesInternal;
}
}
export class KeyframeStyle {
#payload;
#offsetInternal;
constructor(payload) {
this.#payload = payload;
this.#offsetInternal = this.#payload.offset;
}
offset() {
return this.#offsetInternal;
}
setOffset(offset) {
this.#offsetInternal = offset * 100 + '%';
}
offsetAsNumber() {
return parseFloat(this.#offsetInternal) / 100;
}
easing() {
return this.#payload.easing;
}
}
export class AnimationGroup {
#animationModel;
#idInternal;
#animationsInternal;
#pausedInternal;
screenshotsInternal;
#screenshotImages;
constructor(animationModel, id, animations) {
this.#animationModel = animationModel;
this.#idInternal = id;
this.#animationsInternal = animations;
this.#pausedInternal = false;
this.screenshotsInternal = [];
this.#screenshotImages = [];
}
id() {
return this.#idInternal;
}
animations() {
return this.#animationsInternal;
}
release() {
this.#animationModel.animationGroups.delete(this.id());
this.#animationModel.releaseAnimations(this.animationIds());
}
animationIds() {
function extractId(animation) {
return animation.id();
}
return this.#animationsInternal.map(extractId);
}
startTime() {
return this.#animationsInternal[0].startTime();
}
finiteDuration() {
let maxDuration = 0;
for (let i = 0; i < this.#animationsInternal.length; ++i) {
maxDuration = Math.max(maxDuration, this.#animationsInternal[i].finiteDuration());
}
return maxDuration;
}
seekTo(currentTime) {
void this.#animationModel.agent.invoke_seekAnimations({ animations: this.animationIds(), currentTime });
}
paused() {
return this.#pausedInternal;
}
togglePause(paused) {
if (paused === this.#pausedInternal) {
return;
}
this.#pausedInternal = paused;
void this.#animationModel.agent.invoke_setPaused({ animations: this.animationIds(), paused });
}
currentTimePromise() {
let longestAnim = null;
for (const anim of this.#animationsInternal) {
if (!longestAnim || anim.endTime() > longestAnim.endTime()) {
longestAnim = anim;
}
}
if (!longestAnim) {
throw new Error('No longest animation found');
}
return this.#animationModel.agent.invoke_getCurrentTime({ id: longestAnim.id() })
.then(({ currentTime }) => currentTime || 0);
}
matches(group) {
function extractId(anim) {
if (anim.type() === "WebAnimation" /* Protocol.Animation.AnimationType.WebAnimation */) {
return anim.type() + anim.id();
}
return anim.cssId();
}
if (this.#animationsInternal.length !== group.#animationsInternal.length) {
return false;
}
const left = this.#animationsInternal.map(extractId).sort();
const right = group.#animationsInternal.map(extractId).sort();
for (let i = 0; i < left.length; i++) {
if (left[i] !== right[i]) {
return false;
}
}
return true;
}
update(group) {
this.#animationModel.releaseAnimations(this.animationIds());
this.#animationsInternal = group.#animationsInternal;
}
screenshots() {
for (let i = 0; i < this.screenshotsInternal.length; ++i) {
const image = new Image();
image.src = 'data:image/jpeg;base64,' + this.screenshotsInternal[i];
this.#screenshotImages.push(image);
}
this.screenshotsInternal = [];
return this.#screenshotImages;
}
}
export class AnimationDispatcher {
#animationModel;
constructor(animationModel) {
this.#animationModel = animationModel;
}
animationCreated({ id }) {
this.#animationModel.animationCreated(id);
}
animationCanceled({ id }) {
this.#animationModel.animationCanceled(id);
}
animationStarted({ animation }) {
this.#animationModel.animationStarted(animation);
}
}
export class ScreenshotCapture {
#requests;
#screenCaptureModel;
#animationModel;
#stopTimer;
#endTime;
#capturing;
constructor(animationModel, screenCaptureModel) {
this.#requests = [];
this.#screenCaptureModel = screenCaptureModel;
this.#animationModel = animationModel;
this.#animationModel.addEventListener(Events.ModelReset, this.stopScreencast, this);
}
captureScreenshots(duration, screenshots) {
const screencastDuration = Math.min(duration / this.#animationModel.playbackRate, 3000);
const endTime = screencastDuration + window.performance.now();
this.#requests.push({ endTime: endTime, screenshots: screenshots });
if (!this.#endTime || endTime > this.#endTime) {
clearTimeout(this.#stopTimer);
this.#stopTimer = window.setTimeout(this.stopScreencast.bind(this), screencastDuration);
this.#endTime = endTime;
}
if (this.#capturing) {
return;
}
this.#capturing = true;
this.#screenCaptureModel.startScreencast("jpeg" /* Protocol.Page.StartScreencastRequestFormat.Jpeg */, 80, undefined, 300, 2, this.screencastFrame.bind(this), _visible => { });
}
screencastFrame(base64Data, _metadata) {
function isAnimating(request) {
return request.endTime >= now;
}
if (!this.#capturing) {
return;
}
const now = window.performance.now();
this.#requests = this.#requests.filter(isAnimating);
for (const request of this.#requests) {
request.screenshots.push(base64Data);
}
}
stopScreencast() {
if (!this.#capturing) {
return;
}
this.#stopTimer = undefined;
this.#endTime = undefined;
this.#requests = [];
this.#capturing = false;
this.#screenCaptureModel.stopScreencast();
}
}
SDK.SDKModel.SDKModel.register(AnimationModel, { capabilities: SDK.Target.Capability.DOM, autostart: false });
//# sourceMappingURL=AnimationModel.js.map