/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as timeline from './timeline'; function waitTime(time) { return new Promise(resolve => { setTimeout(() => { resolve(); }, time); }); }; export class ActionPlayback { constructor() { this._timer = 0; this._current = 0; this._ops = []; this._currentOpIndex = 0; this._isLastOpMousewheel = false; } getContext() { return { elapsedTime: this._elapsedTime, currentOpIndex: this._currentOpIndex, isLastOpMouseWheel: this._isLastOpMousewheel } } _reset() { this._currentOpIndex = 0; this._current = Date.now(); this._elapsedTime = 0; this._isLastOpMousewheel = false; } _restoreContext(ctx) { this._elapsedTime = ctx.elapsedTime; this._currentOpIndex = ctx.currentOpIndex; this._isLastOpMousewheel = ctx.isLastOpMouseWheel; } async runAction(action, ctxToRestore) { this.stop(); if (!action.ops.length) { return; } this._ops = action.ops.slice().sort((a, b) => { return a.time - b.time; }); let firstOp = this._ops[0]; this._ops.forEach(op => { op.time -= firstOp.time; }); this._reset(); if (ctxToRestore) { this._restoreContext(ctxToRestore); // Usually restore context happens when page is reloaded after mouseup. // In this case the _currentOpIndex is not increased yet. this._currentOpIndex++; } let self = this; async function takeScreenshot() { // Pause timeline when doing screenshot to avoid screenshot needs taking a while. timeline.pause(); await __VRT_ACTION_SCREENSHOT__(action); timeline.resume(); } return new Promise((resolve, reject) => { async function tick() { let current = Date.now(); let dTime = current - self._current; self._elapsedTime += dTime; self._current = current; try { // Execute all if there are multiple ops in one frame. do { const executed = await self._update(takeScreenshot); if (!executed) { break; } } while (true); } catch (e) { // Stop running and throw error. reject(e); return; } if (self._currentOpIndex >= self._ops.length) { // Finished resolve(); } else { self._timer = setTimeout(tick, 0); } } tick(); }); } stop() { if (this._timer) { clearTimeout(this._timer); this._timer = 0; } } async _update(takeScreenshot) { let op = this._ops[this._currentOpIndex]; if (!op || (op.time > this._elapsedTime)) { // Not yet. return; } let screenshotTaken = false; switch (op.type) { case 'mousedown': // Pause timeline to avoid frame not sync. timeline.pause(); await __VRT_MOUSE_MOVE__(op.x, op.y); await __VRT_MOUSE_DOWN__(); timeline.resume(); break; case 'mouseup': timeline.pause(); await __VRT_MOUSE_MOVE__(op.x, op.y); await __VRT_MOUSE_UP__(); if (window.__VRT_RELOAD_TRIGGERED__) { return; } timeline.resume(); break; case 'mousemove': timeline.pause(); await __VRT_MOUSE_MOVE__(op.x, op.y); timeline.resume(); break; case 'mousewheel': let element = document.elementFromPoint(op.x, op.y); // Here dispatch mousewheel event because echarts used it. // TODO Consider upgrade? let event = new WheelEvent('mousewheel', { // PENDING // Needs inverse delta? deltaY: op.deltaY, clientX: op.x, clientY: op.y, // Needs bubble to parent container bubbles: true }); element.dispatchEvent(event); this._isLastOpMousewheel = true; break; case 'screenshot': await takeScreenshot(); screenshotTaken = true; break; case 'valuechange': const selector = document.querySelector(op.selector); selector.value = op.value; // changing value via js won't trigger `change` event, so trigger it manually selector.dispatchEvent(new Event('change')); break; } this._currentOpIndex++; // If next op is an auto screenshot let nextOp = this._ops[this._currentOpIndex]; if (nextOp && nextOp.type === 'screenshot-auto') { let delay = nextOp.delay == null ? 400 : nextOp.delay; // TODO Configuration time await waitTime(delay); await takeScreenshot(); screenshotTaken = true; this._currentOpIndex++; } if (this._isLastOpMousewheel && op.type !== 'mousewheel') { // Only take screenshot after mousewheel finished if (!screenshotTaken) { await takeScreenshot(); } this._isLastOpMousewheel = false; } return true; } };