495 lines
17 KiB
JavaScript
495 lines
17 KiB
JavaScript
/*
|
|
* 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.Canvas = class Canvas extends WI.Object
|
|
{
|
|
constructor(identifier, contextType, {domNode, cssCanvasName, contextAttributes, memoryCost, stackTrace} = {})
|
|
{
|
|
super();
|
|
|
|
console.assert(identifier);
|
|
console.assert(contextType);
|
|
console.assert(!stackTrace || stackTrace instanceof WI.StackTrace, stackTrace);
|
|
|
|
this._identifier = identifier;
|
|
this._contextType = contextType;
|
|
this._domNode = domNode || null;
|
|
this._cssCanvasName = cssCanvasName || "";
|
|
this._contextAttributes = contextAttributes || {};
|
|
this._extensions = new Set;
|
|
this._memoryCost = memoryCost || NaN;
|
|
this._stackTrace = stackTrace || null;
|
|
|
|
this._clientNodes = null;
|
|
this._shaderProgramCollection = new WI.ShaderProgramCollection;
|
|
this._recordingCollection = new WI.RecordingCollection;
|
|
|
|
this._nextShaderProgramDisplayNumber = null;
|
|
|
|
this._requestNodePromise = null;
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.Inactive;
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
let contextType = null;
|
|
switch (payload.contextType) {
|
|
case InspectorBackend.Enum.Canvas.ContextType.Canvas2D:
|
|
contextType = WI.Canvas.ContextType.Canvas2D;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.BitmapRenderer:
|
|
contextType = WI.Canvas.ContextType.BitmapRenderer;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGL:
|
|
contextType = WI.Canvas.ContextType.WebGL;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGL2:
|
|
contextType = WI.Canvas.ContextType.WebGL2;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGPU:
|
|
contextType = WI.Canvas.ContextType.WebGPU;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebMetal:
|
|
contextType = WI.Canvas.ContextType.WebMetal;
|
|
break;
|
|
default:
|
|
console.error("Invalid canvas context type", payload.contextType);
|
|
}
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `backtrace` was renamed to `stackTrace`.
|
|
if (payload.backtrace)
|
|
payload.stackTrace = {callFrames: payload.backtrace};
|
|
|
|
return new WI.Canvas(payload.canvasId, contextType, {
|
|
domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
|
|
cssCanvasName: payload.cssCanvasName,
|
|
contextAttributes: payload.contextAttributes,
|
|
memoryCost: payload.memoryCost,
|
|
stackTrace: WI.StackTrace.fromPayload(WI.assumingMainTarget(), payload.stackTrace),
|
|
});
|
|
}
|
|
|
|
static displayNameForContextType(contextType)
|
|
{
|
|
switch (contextType) {
|
|
case WI.Canvas.ContextType.Canvas2D:
|
|
return WI.UIString("2D");
|
|
case WI.Canvas.ContextType.BitmapRenderer:
|
|
return WI.UIString("Bitmap Renderer", "Canvas Context Type Bitmap Renderer", "Bitmap Renderer is a type of rendering context associated with a <canvas> element");
|
|
case WI.Canvas.ContextType.WebGL:
|
|
return WI.unlocalizedString("WebGL");
|
|
case WI.Canvas.ContextType.WebGL2:
|
|
return WI.unlocalizedString("WebGL2");
|
|
case WI.Canvas.ContextType.WebGPU:
|
|
return WI.unlocalizedString("Web GPU");
|
|
case WI.Canvas.ContextType.WebMetal:
|
|
return WI.unlocalizedString("WebMetal");
|
|
}
|
|
|
|
console.assert(false, "Unknown canvas context type", contextType);
|
|
return null;
|
|
}
|
|
|
|
static displayNameForColorSpace(colorSpace)
|
|
{
|
|
switch(colorSpace) {
|
|
case WI.Canvas.ColorSpace.SRGB:
|
|
return WI.unlocalizedString("sRGB");
|
|
case WI.Canvas.ColorSpace.DisplayP3:
|
|
return WI.unlocalizedString("Display P3");
|
|
}
|
|
|
|
console.assert(false, "Unknown canvas color space", colorSpace);
|
|
return null;
|
|
}
|
|
|
|
static resetUniqueDisplayNameNumbers()
|
|
{
|
|
Canvas._nextContextUniqueDisplayNameNumber = 1;
|
|
Canvas._nextDeviceUniqueDisplayNameNumber = 1;
|
|
}
|
|
|
|
static supportsRequestContentForContextType(contextType)
|
|
{
|
|
switch (contextType) {
|
|
case Canvas.ContextType.WebGPU:
|
|
case Canvas.ContextType.WebMetal:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Public
|
|
|
|
get identifier() { return this._identifier; }
|
|
get contextType() { return this._contextType; }
|
|
get cssCanvasName() { return this._cssCanvasName; }
|
|
get contextAttributes() { return this._contextAttributes; }
|
|
get extensions() { return this._extensions; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
get shaderProgramCollection() { return this._shaderProgramCollection; }
|
|
get recordingCollection() { return this._recordingCollection; }
|
|
get recordingFrameCount() { return this._recordingFrames.length; }
|
|
get recordingBufferUsed() { return this._recordingBufferUsed; }
|
|
|
|
get recordingActive()
|
|
{
|
|
return this._recordingState !== WI.Canvas.RecordingState.Inactive;
|
|
}
|
|
|
|
get memoryCost()
|
|
{
|
|
return this._memoryCost;
|
|
}
|
|
|
|
set memoryCost(memoryCost)
|
|
{
|
|
if (memoryCost === this._memoryCost)
|
|
return;
|
|
|
|
this._memoryCost = memoryCost;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (this._cssCanvasName)
|
|
return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);
|
|
|
|
if (this._domNode) {
|
|
let idSelector = this._domNode.escapedIdSelector;
|
|
if (idSelector)
|
|
return WI.UIString("Canvas %s").format(idSelector);
|
|
}
|
|
|
|
if (this._contextType === Canvas.ContextType.WebGPU) {
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = Canvas._nextDeviceUniqueDisplayNameNumber++;
|
|
return WI.UIString("Device %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = Canvas._nextContextUniqueDisplayNameNumber++;
|
|
return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
requestNode()
|
|
{
|
|
if (!this._requestNodePromise) {
|
|
this._requestNodePromise = new Promise((resolve, reject) => {
|
|
WI.domManager.ensureDocument();
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CanvasAgent.requestNode(this._identifier, (error, nodeId) => {
|
|
if (error) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
this._domNode = WI.domManager.nodeForId(nodeId);
|
|
if (!this._domNode) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
resolve(this._domNode);
|
|
});
|
|
});
|
|
}
|
|
return this._requestNodePromise;
|
|
}
|
|
|
|
requestContent()
|
|
{
|
|
if (!Canvas.supportsRequestContentForContextType(this._contextType))
|
|
return Promise.resolve(null);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
|
|
}
|
|
|
|
requestClientNodes(callback)
|
|
{
|
|
if (this._clientNodes) {
|
|
callback(this._clientNodes);
|
|
return;
|
|
}
|
|
|
|
WI.domManager.ensureDocument();
|
|
|
|
let wrappedCallback = (error, clientNodeIds) => {
|
|
if (error) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
|
|
this._clientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
|
|
callback(this._clientNodes);
|
|
};
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (iOS 13): Canvas.requestCSSCanvasClientNodes was renamed to Canvas.requestClientNodes.
|
|
if (!target.hasCommand("Canvas.requestClientNodes")) {
|
|
target.CanvasAgent.requestCSSCanvasClientNodes(this._identifier, wrappedCallback);
|
|
return;
|
|
}
|
|
|
|
target.CanvasAgent.requestClientNodes(this._identifier, wrappedCallback);
|
|
}
|
|
|
|
requestSize()
|
|
{
|
|
function calculateSize(domNode) {
|
|
function getAttributeValue(name) {
|
|
let value = Number(domNode.getAttribute(name));
|
|
if (!Number.isInteger(value) || value < 0)
|
|
return NaN;
|
|
return value;
|
|
}
|
|
|
|
return {
|
|
width: getAttributeValue("width"),
|
|
height: getAttributeValue("height")
|
|
};
|
|
}
|
|
|
|
function getPropertyValue(remoteObject, name) {
|
|
return new Promise((resolve, reject) => {
|
|
remoteObject.getProperty(name, (error, result) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve(result);
|
|
});
|
|
});
|
|
}
|
|
|
|
return this.requestNode().then((domNode) => {
|
|
if (!domNode)
|
|
return null;
|
|
|
|
let size = calculateSize(domNode);
|
|
if (!isNaN(size.width) && !isNaN(size.height))
|
|
return size;
|
|
|
|
// Since the "width" and "height" properties of canvas elements are more than just
|
|
// attributes, we need to invoke the getter for each to get the actual value.
|
|
// - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
|
|
// - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
|
|
let remoteObject = null;
|
|
return WI.RemoteObject.resolveNode(domNode).then((object) => {
|
|
remoteObject = object;
|
|
return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
|
|
}).then((values) => {
|
|
let width = values[0].value;
|
|
let height = values[1].value;
|
|
values[0].release();
|
|
values[1].release();
|
|
remoteObject.release();
|
|
return {width, height};
|
|
});
|
|
});
|
|
}
|
|
|
|
startRecording(singleFrame)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
|
|
let handleStartRecording = (error) => {
|
|
if (error) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
|
|
|
|
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
|
|
if (target.hasEvent("Canvas.recordingStarted"))
|
|
return;
|
|
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
|
|
};
|
|
|
|
// COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
|
|
if (target.hasCommand("Canvas.startRecording", "singleFrame")) {
|
|
target.CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
|
|
return;
|
|
}
|
|
|
|
if (singleFrame) {
|
|
const frameCount = 1;
|
|
target.CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
|
|
} else
|
|
target.CanvasAgent.startRecording(this._identifier, handleStartRecording);
|
|
}
|
|
|
|
stopRecording()
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
target.CanvasAgent.stopRecording(this._identifier);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
if (this._cssCanvasName)
|
|
cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
|
|
else if (this._domNode)
|
|
cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;
|
|
|
|
}
|
|
|
|
enableExtension(extension)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._extensions.add(extension);
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
|
|
}
|
|
|
|
clientNodesChanged()
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._clientNodes = null;
|
|
|
|
this.dispatchEventToListeners(Canvas.Event.ClientNodesChanged);
|
|
}
|
|
|
|
recordingStarted(initiator)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
if (initiator === InspectorBackend.Enum.Recording.Initiator.Console)
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
|
|
else if (initiator === InspectorBackend.Enum.Recording.Initiator.AutoCapture)
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
|
|
else {
|
|
console.assert(initiator === InspectorBackend.Enum.Recording.Initiator.Frontend);
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
|
|
}
|
|
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
|
|
}
|
|
|
|
recordingProgress(framesPayload, bufferUsed)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._recordingFrames.pushAll(framesPayload.map(WI.RecordingFrame.fromPayload));
|
|
|
|
this._recordingBufferUsed = bufferUsed;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
|
|
}
|
|
|
|
recordingFinished(recordingPayload)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;
|
|
|
|
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
|
|
if (!initiatedByUser && !InspectorBackend.hasEvent("Canvas.recordingStarted"))
|
|
initiatedByUser = !!this.recordingActive;
|
|
|
|
let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
|
|
if (recording) {
|
|
recording.source = this;
|
|
recording.createDisplayName(recordingPayload.name);
|
|
|
|
this._recordingCollection.add(recording);
|
|
}
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.Inactive;
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
|
|
}
|
|
|
|
nextShaderProgramDisplayNumberForProgramType(programType)
|
|
{
|
|
// Called from WI.ShaderProgram.
|
|
|
|
if (!this._nextShaderProgramDisplayNumber)
|
|
this._nextShaderProgramDisplayNumber = {};
|
|
|
|
this._nextShaderProgramDisplayNumber[programType] = (this._nextShaderProgramDisplayNumber[programType] || 0) + 1;
|
|
return this._nextShaderProgramDisplayNumber[programType];
|
|
}
|
|
};
|
|
|
|
WI.Canvas._nextContextUniqueDisplayNameNumber = 1;
|
|
WI.Canvas._nextDeviceUniqueDisplayNameNumber = 1;
|
|
|
|
WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
|
|
WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";
|
|
|
|
WI.Canvas.ContextType = {
|
|
Canvas2D: "canvas-2d",
|
|
BitmapRenderer: "bitmaprenderer",
|
|
WebGL: "webgl",
|
|
WebGL2: "webgl2",
|
|
WebGPU: "webgpu",
|
|
WebMetal: "webmetal",
|
|
};
|
|
|
|
WI.Canvas.ColorSpace = {
|
|
SRGB: "srgb",
|
|
DisplayP3: "display-p3",
|
|
};
|
|
|
|
WI.Canvas.RecordingState = {
|
|
Inactive: "canvas-recording-state-inactive",
|
|
ActiveFrontend: "canvas-recording-state-active-frontend",
|
|
ActiveConsole: "canvas-recording-state-active-console",
|
|
ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
|
|
};
|
|
|
|
WI.Canvas.Event = {
|
|
MemoryChanged: "canvas-memory-changed",
|
|
ExtensionEnabled: "canvas-extension-enabled",
|
|
ClientNodesChanged: "canvas-client-nodes-changed",
|
|
RecordingStarted: "canvas-recording-started",
|
|
RecordingProgress: "canvas-recording-progress",
|
|
RecordingStopped: "canvas-recording-stopped",
|
|
};
|