| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.Recording = class Recording extends WI.Object |
| { |
| constructor(version, type, initialState, frames, data) |
| { |
| super(); |
| |
| this._version = version; |
| this._type = type; |
| this._initialState = initialState; |
| this._frames = frames; |
| this._data = data; |
| this._displayName = WI.UIString("Recording"); |
| |
| this._swizzle = null; |
| this._actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions)); |
| this._visualActionIndexes = []; |
| this._source = null; |
| |
| this._processContext = null; |
| this._processStates = []; |
| this._processing = false; |
| } |
| |
| static fromPayload(payload, frames) |
| { |
| if (typeof payload !== "object" || payload === null) |
| return null; |
| |
| if (typeof payload.version !== "number") { |
| WI.Recording.synthesizeError(WI.UIString("non-number %s").format(WI.unlocalizedString("version"))); |
| return null; |
| } |
| |
| if (payload.version < 1 || payload.version > WI.Recording.Version) { |
| WI.Recording.synthesizeError(WI.UIString("unsupported %s").format(WI.unlocalizedString("version"))); |
| return null; |
| } |
| |
| if (parseInt(payload.version) !== payload.version) { |
| WI.Recording.synthesizeWarning(WI.UIString("non-integer %s").format(WI.unlocalizedString("version"))); |
| payload.version = parseInt(payload.version); |
| } |
| |
| let type = null; |
| switch (payload.type) { |
| case InspectorBackend.Enum.Recording.Type.Canvas2D: |
| type = WI.Recording.Type.Canvas2D; |
| break; |
| case InspectorBackend.Enum.Recording.Type.CanvasBitmapRenderer: |
| type = WI.Recording.Type.CanvasBitmapRenderer; |
| break; |
| case InspectorBackend.Enum.Recording.Type.CanvasWebGL: |
| type = WI.Recording.Type.CanvasWebGL; |
| break; |
| case InspectorBackend.Enum.Recording.Type.CanvasWebGL2: |
| type = WI.Recording.Type.CanvasWebGL2; |
| break; |
| default: |
| WI.Recording.synthesizeWarning(WI.UIString("unknown %s \u0022%s\u0022").format(WI.unlocalizedString("type"), payload.type)); |
| type = String(payload.type); |
| break; |
| } |
| |
| if (typeof payload.initialState !== "object" || payload.initialState === null) { |
| if ("initialState" in payload) |
| WI.Recording.synthesizeWarning(WI.UIString("non-object %s").format(WI.unlocalizedString("initialState"))); |
| |
| payload.initialState = {}; |
| } |
| |
| if (typeof payload.initialState.attributes !== "object" || payload.initialState.attributes === null) { |
| if ("attributes" in payload.initialState) |
| WI.Recording.synthesizeWarning(WI.UIString("non-object %s").format(WI.unlocalizedString("initialState.attributes"))); |
| |
| payload.initialState.attributes = {}; |
| } |
| |
| if (!Array.isArray(payload.initialState.states) || payload.initialState.states.some((item) => typeof item !== "object" || item === null)) { |
| if ("states" in payload.initialState) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("initialState.states"))); |
| |
| payload.initialState.states = []; |
| |
| // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet |
| if (!isEmptyObject(payload.initialState.attributes)) { |
| let {width, height, ...state} = payload.initialState.attributes; |
| if (!isEmptyObject(state)) |
| payload.initialState.states.push(state); |
| } |
| } |
| |
| if (!Array.isArray(payload.initialState.parameters)) { |
| if ("parameters" in payload.initialState) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("initialState.attributes"))); |
| |
| payload.initialState.parameters = []; |
| } |
| |
| if (typeof payload.initialState.content !== "string") { |
| if ("content" in payload.initialState) |
| WI.Recording.synthesizeWarning(WI.UIString("non-string %s").format(WI.unlocalizedString("initialState.content"))); |
| |
| payload.initialState.content = ""; |
| } |
| |
| if (!Array.isArray(payload.frames)) { |
| if ("frames" in payload) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("frames"))); |
| |
| payload.frames = []; |
| } |
| |
| if (!Array.isArray(payload.data)) { |
| if ("data" in payload) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("data"))); |
| |
| payload.data = []; |
| } |
| |
| if (!frames) |
| frames = payload.frames.map(WI.RecordingFrame.fromPayload) |
| |
| return new WI.Recording(payload.version, type, payload.initialState, frames, payload.data); |
| } |
| |
| static displayNameForSwizzleType(swizzleType) |
| { |
| switch (swizzleType) { |
| case WI.Recording.Swizzle.None: |
| return WI.unlocalizedString("None"); |
| case WI.Recording.Swizzle.Number: |
| return WI.unlocalizedString("Number"); |
| case WI.Recording.Swizzle.Boolean: |
| return WI.unlocalizedString("Boolean"); |
| case WI.Recording.Swizzle.String: |
| return WI.unlocalizedString("String"); |
| case WI.Recording.Swizzle.Array: |
| return WI.unlocalizedString("Array"); |
| case WI.Recording.Swizzle.TypedArray: |
| return WI.unlocalizedString("TypedArray"); |
| case WI.Recording.Swizzle.Image: |
| return WI.unlocalizedString("Image"); |
| case WI.Recording.Swizzle.ImageData: |
| return WI.unlocalizedString("ImageData"); |
| case WI.Recording.Swizzle.DOMMatrix: |
| return WI.unlocalizedString("DOMMatrix"); |
| case WI.Recording.Swizzle.Path2D: |
| return WI.unlocalizedString("Path2D"); |
| case WI.Recording.Swizzle.CanvasGradient: |
| return WI.unlocalizedString("CanvasGradient"); |
| case WI.Recording.Swizzle.CanvasPattern: |
| return WI.unlocalizedString("CanvasPattern"); |
| case WI.Recording.Swizzle.WebGLBuffer: |
| return WI.unlocalizedString("WebGLBuffer"); |
| case WI.Recording.Swizzle.WebGLFramebuffer: |
| return WI.unlocalizedString("WebGLFramebuffer"); |
| case WI.Recording.Swizzle.WebGLRenderbuffer: |
| return WI.unlocalizedString("WebGLRenderbuffer"); |
| case WI.Recording.Swizzle.WebGLTexture: |
| return WI.unlocalizedString("WebGLTexture"); |
| case WI.Recording.Swizzle.WebGLShader: |
| return WI.unlocalizedString("WebGLShader"); |
| case WI.Recording.Swizzle.WebGLProgram: |
| return WI.unlocalizedString("WebGLProgram"); |
| case WI.Recording.Swizzle.WebGLUniformLocation: |
| return WI.unlocalizedString("WebGLUniformLocation"); |
| case WI.Recording.Swizzle.ImageBitmap: |
| return WI.unlocalizedString("ImageBitmap"); |
| case WI.Recording.Swizzle.WebGLQuery: |
| return WI.unlocalizedString("WebGLQuery"); |
| case WI.Recording.Swizzle.WebGLSampler: |
| return WI.unlocalizedString("WebGLSampler"); |
| case WI.Recording.Swizzle.WebGLSync: |
| return WI.unlocalizedString("WebGLSync"); |
| case WI.Recording.Swizzle.WebGLTransformFeedback: |
| return WI.unlocalizedString("WebGLTransformFeedback"); |
| case WI.Recording.Swizzle.WebGLVertexArrayObject: |
| return WI.unlocalizedString("WebGLVertexArrayObject"); |
| default: |
| console.error("Unknown swizzle type", swizzleType); |
| return null; |
| } |
| } |
| |
| static synthesizeWarning(message) |
| { |
| message = WI.UIString("Recording Warning: %s").format(message); |
| |
| if (window.InspectorTest) { |
| console.warn(message); |
| return; |
| } |
| |
| let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message); |
| consoleMessage.shouldRevealConsole = true; |
| |
| WI.consoleLogViewController.appendConsoleMessage(consoleMessage); |
| } |
| |
| static synthesizeError(message) |
| { |
| message = WI.UIString("Recording Error: %s").format(message); |
| |
| if (window.InspectorTest) { |
| console.error(message); |
| return; |
| } |
| |
| let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message); |
| consoleMessage.shouldRevealConsole = true; |
| |
| WI.consoleLogViewController.appendConsoleMessage(consoleMessage); |
| } |
| |
| // Public |
| |
| get displayName() { return this._displayName; } |
| get type() { return this._type; } |
| get initialState() { return this._initialState; } |
| get frames() { return this._frames; } |
| get data() { return this._data; } |
| get actions() { return this._actions; } |
| get visualActionIndexes() { return this._visualActionIndexes; } |
| |
| get source() { return this._source; } |
| set source(source) { this._source = source; } |
| |
| get processing() { return this._processing; } |
| |
| get ready() |
| { |
| return this._actions.lastValue.ready; |
| } |
| |
| startProcessing() |
| { |
| console.assert(!this._processing, "Cannot start an already started process()."); |
| console.assert(!this.ready, "Cannot start a completed process()."); |
| if (this._processing || this.ready) |
| return; |
| |
| this._processing = true; |
| |
| this._process(); |
| } |
| |
| stopProcessing() |
| { |
| console.assert(this._processing, "Cannot stop an already stopped process()."); |
| console.assert(!this.ready, "Cannot stop a completed process()."); |
| if (!this._processing || this.ready) |
| return; |
| |
| this._processing = false; |
| } |
| |
| createDisplayName(suggestedName) |
| { |
| let recordingNameSet; |
| if (this._source) { |
| recordingNameSet = this._source[WI.Recording.CanvasRecordingNamesSymbol]; |
| if (!recordingNameSet) |
| this._source[WI.Recording.CanvasRecordingNamesSymbol] = recordingNameSet = new Set; |
| } else |
| recordingNameSet = WI.Recording._importedRecordingNameSet; |
| |
| let name; |
| if (suggestedName) { |
| name = suggestedName; |
| let duplicateNumber = 2; |
| while (recordingNameSet.has(name)) |
| name = `${suggestedName} (${duplicateNumber++})`; |
| } else { |
| let recordingNumber = 1; |
| do { |
| name = WI.UIString("Recording %d").format(recordingNumber++); |
| } while (recordingNameSet.has(name)); |
| } |
| |
| recordingNameSet.add(name); |
| this._displayName = name; |
| } |
| |
| async swizzle(index, type) |
| { |
| if (!this._swizzle) |
| this._swizzle = {}; |
| |
| if (typeof this._swizzle[index] !== "object") |
| this._swizzle[index] = {}; |
| |
| if (type === WI.Recording.Swizzle.Number) |
| return parseFloat(index); |
| |
| if (type === WI.Recording.Swizzle.Boolean) |
| return !!index; |
| |
| if (type === WI.Recording.Swizzle.Array) |
| return Array.isArray(index) ? index : []; |
| |
| if (type === WI.Recording.Swizzle.DOMMatrix) |
| return new DOMMatrix(index); |
| |
| // FIXME: <https://webkit.org/b/176009> Web Inspector: send data for WebGL objects during a recording instead of a placeholder string |
| if (type === WI.Recording.Swizzle.TypedArray |
| || type === WI.Recording.Swizzle.WebGLBuffer |
| || type === WI.Recording.Swizzle.WebGLFramebuffer |
| || type === WI.Recording.Swizzle.WebGLRenderbuffer |
| || type === WI.Recording.Swizzle.WebGLTexture |
| || type === WI.Recording.Swizzle.WebGLShader |
| || type === WI.Recording.Swizzle.WebGLProgram |
| || type === WI.Recording.Swizzle.WebGLUniformLocation |
| || type === WI.Recording.Swizzle.WebGLQuery |
| || type === WI.Recording.Swizzle.WebGLSampler |
| || type === WI.Recording.Swizzle.WebGLSync |
| || type === WI.Recording.Swizzle.WebGLTransformFeedback |
| || type === WI.Recording.Swizzle.WebGLVertexArrayObject) { |
| return index; |
| } |
| |
| if (!(type in this._swizzle[index])) { |
| try { |
| let data = this._data[index]; |
| switch (type) { |
| case WI.Recording.Swizzle.None: |
| this._swizzle[index][type] = data; |
| break; |
| |
| case WI.Recording.Swizzle.String: |
| if (Array.isArray(data)) |
| this._swizzle[index][type] = await Promise.all(data.map((item) => this.swizzle(item, WI.Recording.Swizzle.String))); |
| else |
| this._swizzle[index][type] = String(data); |
| break; |
| |
| case WI.Recording.Swizzle.Image: |
| this._swizzle[index][type] = await WI.ImageUtilities.promisifyLoad(data); |
| this._swizzle[index][type].__data = data; |
| break; |
| |
| case WI.Recording.Swizzle.ImageData: { |
| let [object, width, height] = await Promise.all([ |
| this.swizzle(data[0], WI.Recording.Swizzle.Array), |
| this.swizzle(data[1], WI.Recording.Swizzle.Number), |
| this.swizzle(data[2], WI.Recording.Swizzle.Number), |
| ]); |
| |
| object = await Promise.all(object.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number))); |
| |
| this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(object), width, height); |
| this._swizzle[index][type].__data = {data: object, width, height}; |
| break; |
| } |
| |
| case WI.Recording.Swizzle.Path2D: |
| this._swizzle[index][type] = new Path2D(data); |
| this._swizzle[index][type].__data = data; |
| break; |
| |
| case WI.Recording.Swizzle.CanvasGradient: { |
| let [gradientType, points] = await Promise.all([ |
| this.swizzle(data[0], WI.Recording.Swizzle.String), |
| this.swizzle(data[1], WI.Recording.Swizzle.Array), |
| ]); |
| |
| points = await Promise.all(points.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number))); |
| |
| WI.ImageUtilities.scratchCanvasContext2D((context) => { |
| this._swizzle[index][type] = gradientType === "radial-gradient" ? context.createRadialGradient(...points) : context.createLinearGradient(...points); |
| }); |
| |
| let stops = []; |
| for (let stop of data[2]) { |
| let [offset, color] = await Promise.all([ |
| this.swizzle(stop[0], WI.Recording.Swizzle.Number), |
| this.swizzle(stop[1], WI.Recording.Swizzle.String), |
| ]); |
| this._swizzle[index][type].addColorStop(offset, color); |
| |
| stops.push({offset, color}); |
| } |
| |
| this._swizzle[index][type].__data = {type: gradientType, points, stops}; |
| break; |
| } |
| |
| case WI.Recording.Swizzle.CanvasPattern: { |
| let [image, repeat] = await Promise.all([ |
| this.swizzle(data[0], WI.Recording.Swizzle.Image), |
| this.swizzle(data[1], WI.Recording.Swizzle.String), |
| ]); |
| |
| WI.ImageUtilities.scratchCanvasContext2D((context) => { |
| this._swizzle[index][type] = context.createPattern(image, repeat); |
| this._swizzle[index][type].__image = image; |
| }); |
| |
| this._swizzle[index][type].__data = {image: image.__data, repeat}; |
| break; |
| } |
| |
| case WI.Recording.Swizzle.ImageBitmap: { |
| let image = await this.swizzle(index, WI.Recording.Swizzle.Image); |
| this._swizzle[index][type] = await createImageBitmap(image); |
| this._swizzle[index][type].__data = data; |
| break; |
| } |
| |
| case WI.Recording.Swizzle.CallStack: { |
| let array = await this.swizzle(data, WI.Recording.Swizzle.Array); |
| this._swizzle[index][type] = await Promise.all(array.map((item) => this.swizzle(item, WI.Recording.Swizzle.CallFrame))); |
| break; |
| } |
| |
| case WI.Recording.Swizzle.CallFrame: { |
| let array = await this.swizzle(data, WI.Recording.Swizzle.Array); |
| let [functionName, url] = await Promise.all([ |
| this.swizzle(array[0], WI.Recording.Swizzle.String), |
| this.swizzle(array[1], WI.Recording.Swizzle.String), |
| ]); |
| this._swizzle[index][type] = WI.CallFrame.fromPayload(WI.assumingMainTarget(), { |
| functionName, |
| url, |
| lineNumber: array[2], |
| columnNumber: array[3], |
| }); |
| break; |
| } |
| } |
| } catch { } |
| } |
| |
| return this._swizzle[index][type]; |
| } |
| |
| createContext() |
| { |
| let createCanvasContext = (type) => { |
| let canvas = document.createElement("canvas"); |
| if ("width" in this._initialState.attributes) |
| canvas.width = this._initialState.attributes.width; |
| if ("height" in this._initialState.attributes) |
| canvas.height = this._initialState.attributes.height; |
| return canvas.getContext(type, ...this._initialState.parameters); |
| }; |
| |
| if (this._type === WI.Recording.Type.Canvas2D) |
| return createCanvasContext("2d"); |
| |
| if (this._type === WI.Recording.Type.CanvasBitmapRenderer) |
| return createCanvasContext("bitmaprenderer"); |
| |
| if (this._type === WI.Recording.Type.CanvasWebGL) |
| return createCanvasContext("webgl"); |
| |
| if (this._type === WI.Recording.Type.CanvasWebGL2) |
| return createCanvasContext("webgl2"); |
| |
| console.error("Unknown recording type", this._type); |
| return null; |
| } |
| |
| toJSON() |
| { |
| let initialState = {}; |
| if (!isEmptyObject(this._initialState.attributes)) |
| initialState.attributes = this._initialState.attributes; |
| if (this._initialState.states.length) |
| initialState.states = this._initialState.states; |
| if (this._initialState.parameters.length) |
| initialState.parameters = this._initialState.parameters; |
| if (this._initialState.content && this._initialState.content.length) |
| initialState.content = this._initialState.content; |
| |
| return { |
| version: this._version, |
| type: this._type, |
| initialState, |
| frames: this._frames.map((frame) => frame.toJSON()), |
| data: this._data, |
| }; |
| } |
| |
| toHTML() |
| { |
| console.assert(this._type === WI.Recording.Type.Canvas2D); |
| console.assert(this.ready); |
| |
| let lines = []; |
| let objects = []; |
| |
| function processObject(object) { |
| objects.push({object, index: objects.length}); |
| return `objects[${objects.length - 1}]`; |
| } |
| |
| function processValue(value) { |
| if (typeof value === "object" && !Array.isArray(value)) |
| return processObject(value); |
| return JSON.stringify(value); |
| } |
| |
| function escapeHTML(s) { |
| return s.replace(/[^0-9A-Za-z ]/g, (c) => { |
| return `&#${c.charCodeAt(0)};`; |
| }); |
| } |
| |
| lines.push(`<!DOCTYPE html>`); |
| lines.push(`<head>`); |
| lines.push(`<title>${escapeHTML(this._displayName)}</title>`); |
| lines.push(`<style>`); |
| lines.push(` body {`); |
| lines.push(` margin: 0;`); |
| lines.push(` }`); |
| lines.push(` canvas {`); |
| lines.push(` max-width: calc(100% - 40px);`); |
| lines.push(` max-height: calc(100% - 40px);`); |
| lines.push(` padding: 20px;`); |
| lines.push(` }`); |
| lines.push(`</style>`); |
| lines.push(`</head>`); |
| lines.push(`<body>`); |
| lines.push(`<script>`); |
| lines.push(`"use strict";`); |
| |
| lines.push(``); |
| |
| lines.push(`let promises = [];`); |
| lines.push(`let objects = {};`); |
| |
| lines.push(``); |
| |
| lines.push(`let canvas = document.body.appendChild(document.createElement("canvas"));`); |
| for (let [attribute, value] of Object.entries(this._initialState.attributes)) |
| lines.push(`canvas.${attribute} = ${JSON.stringify(value)};`); |
| |
| lines.push(``); |
| |
| let parametersString = this._initialState.parameters.map(processValue).join(`, `); |
| lines.push(`let context = canvas.getContext("2d"${parametersString ? ", " + parametersString : ""});`); |
| |
| lines.push(``); |
| |
| lines.push(`let frames = [`); |
| |
| lines.push(` function initialState() {`); |
| if (this._initialState.content) { |
| let image = new Image; |
| image.__data = this._initialState.content; |
| lines.push(` context.drawImage(${processObject(image)}, 0, 0);`); |
| lines.push(``); |
| } |
| for (let state of this._actions[0].states) { |
| for (let [name, value] of state) { |
| if (name === "getPath" || name === "currentX" || name === "currentY") |
| continue; |
| |
| let contextString = `context`; |
| if (name === "setPath") { |
| lines.push(` if (${JSON.stringify(name)} in context)`); |
| contextString = ` ` + contextString; |
| } |
| |
| let callString = ``; |
| if (WI.RecordingAction.isFunctionForType(this._type, name)) |
| callString = `(` + value.map(processValue).join(`, `) + `)`; |
| else |
| callString = ` = ${processValue(value)}`; |
| |
| lines.push(` ${contextString}.${name}${callString};`); |
| } |
| |
| if (state !== this._actions[0].states.lastValue) { |
| lines.push(` context.save();`); |
| lines.push(``); |
| } |
| } |
| lines.push(` },`); |
| |
| lines.push(` function startRecording() {`); |
| lines.push(` if (typeof console.record === "function")`); |
| lines.push(` console.record(context, {name: ${JSON.stringify(this._displayName)}});`); |
| lines.push(` },`); |
| |
| for (let i = 0; i < this._frames.length; ++i) { |
| lines.push(` function frame${i + 1}() {`); |
| |
| for (let action of this._frames[i].actions) { |
| let contextString = `context`; |
| if (action.contextReplacer) |
| contextString += `.${action.contextReplacer}`; |
| |
| if (!action.valid) |
| contextString = `// ` + contextString; |
| |
| let callString = ``; |
| if (action.isFunction) |
| callString += `(` + action.parameters.map(processValue).join(`, `) + `)`; |
| else if (!action.isGetter) |
| callString += ` = ` + processValue(action.parameters[0]); |
| |
| lines.push(` ${contextString}.${action.name}${callString};`); |
| } |
| |
| lines.push(` },`); |
| } |
| |
| lines.push(` function stopRecording() {`); |
| lines.push(` if (typeof console.recordEnd === "function")`); |
| lines.push(` console.recordEnd(context);`); |
| lines.push(` },`); |
| |
| lines.push(`];`); |
| |
| lines.push(``); |
| |
| if (objects.length) { |
| if (objects.some(({object}) => object instanceof CanvasGradient)) { |
| lines.push(`function rebuildCanvasGradient(key, data) {`); |
| lines.push(` let gradient = null;`); |
| lines.push(` if (data.type === "radial-gradient")`); |
| lines.push(` gradient = context.createRadialGradient(data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]);`); |
| lines.push(` else`); |
| lines.push(` gradient = context.createLinearGradient(data.points[0], data.points[1], data.points[2], data.points[3]);`); |
| lines.push(` for (let stop of data.stops)`); |
| lines.push(` gradient.addColorStop(stop.offset, stop.color);`); |
| lines.push(` objects[key] = gradient;`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof CanvasPattern)) { |
| lines.push(`function rebuildCanvasPattern(key, data) {`); |
| lines.push(` promises.push(new Promise(function(resolve, reject) {`); |
| lines.push(` let image = new Image;`); |
| lines.push(` function resolveWithImage(event) {`); |
| lines.push(` objects[key] = context.createPattern(image, data.repeat);`); |
| lines.push(` resolve();`); |
| lines.push(` }`); |
| lines.push(` image.addEventListener("load", resolveWithImage);`); |
| lines.push(` image.addEventListener("error", resolveWithImage);`); |
| lines.push(` image.src = data.image;`); |
| lines.push(` }));`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof DOMMatrix)) { |
| lines.push(`function rebuildDOMMatrix(key, data) {`); |
| lines.push(` objects[key] = new DOMMatrix(data);`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof Image)) { |
| lines.push(`function rebuildImage(key, data) {`); |
| lines.push(` promises.push(new Promise(function(resolve, reject) {`); |
| lines.push(` let image = new Image;`); |
| lines.push(` function resolveWithImage(event) {`); |
| lines.push(` objects[key] = image;`); |
| lines.push(` resolve();`); |
| lines.push(` }`); |
| lines.push(` image.addEventListener("load", resolveWithImage);`); |
| lines.push(` image.addEventListener("error", resolveWithImage);`); |
| lines.push(` image.src = data;`); |
| lines.push(` }));`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof ImageBitmap)) { |
| lines.push(`function rebuildImageBitmap(key, data) {`); |
| lines.push(` promises.push(new Promise(function(resolve, reject) {`); |
| lines.push(` let image = new Image;`); |
| lines.push(` function resolveWithImage(event) {`); |
| lines.push(` createImageBitmap(image).then(function(imageBitmap) {`); |
| lines.push(` objects[key] = imageBitmap;`); |
| lines.push(` resolve();`); |
| lines.push(` });`); |
| lines.push(` }`); |
| lines.push(` image.addEventListener("load", resolveWithImage);`); |
| lines.push(` image.addEventListener("error", resolveWithImage);`); |
| lines.push(` image.src = data;`); |
| lines.push(` }));`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof ImageData)) { |
| lines.push(`function rebuildImageData(key, data) {`); |
| lines.push(` objects[key] = new ImageData(new Uint8ClampedArray(data.data), parseInt(data.width), parseInt(data.height));`); |
| lines.push(`}`); |
| } |
| |
| if (objects.some(({object}) => object instanceof Path2D)) { |
| lines.push(`function rebuildPath2D(key, data) {`); |
| lines.push(` objects[key] = new Path2D(data);`); |
| lines.push(`}`); |
| } |
| |
| lines.push(``); |
| |
| for (let {object, index} of objects) { |
| if (object instanceof CanvasGradient) { |
| lines.push(`rebuildCanvasGradient(${index}, ${JSON.stringify(object.__data)});`); |
| continue; |
| } |
| |
| if (object instanceof CanvasPattern) { |
| lines.push(`rebuildCanvasPattern(${index}, ${JSON.stringify(object.__data)});`); |
| continue; |
| } |
| |
| if (object instanceof DOMMatrix) { |
| lines.push(`rebuildDOMMatrix(${index}, ${JSON.stringify(object.toString())});`) |
| continue; |
| } |
| |
| if (object instanceof Image) { |
| lines.push(`rebuildImage(${index}, ${JSON.stringify(object.__data)});`) |
| continue; |
| } |
| |
| if (object instanceof ImageBitmap) { |
| lines.push(`rebuildImageBitmap(${index}, ${JSON.stringify(object.__data)});`) |
| continue; |
| } |
| |
| if (object instanceof ImageData) { |
| lines.push(`rebuildImageData(${index}, ${JSON.stringify(object.__data)});`); |
| continue; |
| } |
| |
| if (object instanceof Path2D) { |
| lines.push(`rebuildPath2D(${index}, ${JSON.stringify(object.__data || "")});`) |
| continue; |
| } |
| } |
| |
| lines.push(``); |
| } |
| |
| lines.push(`Promise.all(promises).then(function() {`); |
| lines.push(` window.requestAnimationFrame(function executeFrame() {`); |
| lines.push(` frames.shift()();`); |
| lines.push(` if (frames.length)`); |
| lines.push(` window.requestAnimationFrame(executeFrame);`); |
| lines.push(` });`); |
| lines.push(`});`); |
| |
| lines.push(`</script>`); |
| lines.push(`</body>`); |
| return lines.join(`\n`); |
| } |
| |
| // Private |
| |
| async _process() |
| { |
| if (!this._processContext) { |
| this._processContext = this.createContext(); |
| |
| if (this._type === WI.Recording.Type.Canvas2D) { |
| let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content); |
| this._processContext.drawImage(initialContent, 0, 0); |
| |
| for (let initialState of this._initialState.states) { |
| let state = await WI.RecordingState.swizzleInitialState(this, initialState); |
| state.apply(this._type, this._processContext); |
| |
| // The last state represents the current state, which should not be saved. |
| if (initialState !== this._initialState.states.lastValue) { |
| this._processContext.save(); |
| this._processStates.push(WI.RecordingState.fromContext(this._type, this._processContext)); |
| } |
| } |
| } |
| } |
| |
| // The first action is always a WI.RecordingInitialStateAction, which doesn't need to swizzle(). |
| // Since it is not associated with a WI.RecordingFrame, it has to manually process(). |
| if (!this._actions[0].ready) { |
| this._actions[0].process(this, this._processContext, this._processStates); |
| this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0}); |
| } |
| |
| const workInterval = 10; |
| let startTime = Date.now(); |
| |
| let cumulativeActionIndex = 0; |
| let lastAction = this._actions[cumulativeActionIndex]; |
| for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) { |
| let frame = this._frames[frameIndex]; |
| |
| if (frame.actions.lastValue.ready) { |
| cumulativeActionIndex += frame.actions.length; |
| lastAction = frame.actions.lastValue; |
| continue; |
| } |
| |
| for (let actionIndex = 0; actionIndex < frame.actions.length; ++actionIndex) { |
| ++cumulativeActionIndex; |
| |
| let action = frame.actions[actionIndex]; |
| if (action.ready) { |
| lastAction = action; |
| continue; |
| } |
| |
| await action.swizzle(this); |
| |
| action.process(this, this._processContext, this._processStates, {lastAction}); |
| |
| if (action.isVisual) |
| this._visualActionIndexes.push(cumulativeActionIndex); |
| |
| if (!actionIndex) |
| this.dispatchEventToListeners(WI.Recording.Event.StartProcessingFrame, {frame, index: frameIndex}); |
| |
| this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action, index: cumulativeActionIndex}); |
| |
| if (Date.now() - startTime > workInterval) { |
| await Promise.delay(); // yield |
| |
| startTime = Date.now(); |
| } |
| |
| lastAction = action; |
| |
| if (!this._processing) |
| return; |
| } |
| |
| if (!this._processing) |
| return; |
| } |
| |
| this._swizzle = null; |
| this._processContext = null; |
| this._processing = false; |
| } |
| }; |
| |
| // Keep this in sync with Inspector::Protocol::Recording::VERSION. |
| WI.Recording.Version = 1; |
| |
| WI.Recording.Event = { |
| ProcessedAction: "recording-processed-action", |
| StartProcessingFrame: "recording-start-processing-frame", |
| }; |
| |
| WI.Recording._importedRecordingNameSet = new Set; |
| |
| WI.Recording.CanvasRecordingNamesSymbol = Symbol("canvas-recording-names"); |
| |
| WI.Recording.Type = { |
| Canvas2D: "canvas-2d", |
| CanvasBitmapRenderer: "canvas-bitmaprenderer", |
| CanvasWebGL: "canvas-webgl", |
| CanvasWebGL2: "canvas-webgl2", |
| }; |
| |
| // Keep this in sync with WebCore::RecordingSwizzleTypes. |
| WI.Recording.Swizzle = { |
| None: 0, |
| Number: 1, |
| Boolean: 2, |
| String: 3, |
| Array: 4, |
| TypedArray: 5, |
| Image: 6, |
| ImageData: 7, |
| DOMMatrix: 8, |
| Path2D: 9, |
| CanvasGradient: 10, |
| CanvasPattern: 11, |
| WebGLBuffer: 12, |
| WebGLFramebuffer: 13, |
| WebGLRenderbuffer: 14, |
| WebGLTexture: 15, |
| WebGLShader: 16, |
| WebGLProgram: 17, |
| WebGLUniformLocation: 18, |
| ImageBitmap: 19, |
| WebGLQuery: 20, |
| WebGLSampler: 21, |
| WebGLSync: 22, |
| WebGLTransformFeedback: 23, |
| WebGLVertexArrayObject: 24, |
| |
| // Special frontend-only swizzle types. |
| CallStack: Symbol("CallStack"), |
| CallFrame: Symbol("CallFrame"), |
| }; |