UNPKG

chrome-devtools-frontend

Version:
1,260 lines (1,116 loc) • 45.2 kB
/* * Copyright (C) 2014 Google 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: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT * OWNER OR 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. */ import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import layers3DViewStyles from './layers3DView.css.js'; import type * as Protocol from '../../generated/protocol.js'; import type * as SDK from '../../core/sdk/sdk.js'; import * as UI from '../../ui/legacy/legacy.js'; import { LayerSelection, Selection, SnapshotSelection, Type, ScrollRectSelection, type LayerView, type LayerViewHost, } from './LayerViewHost.js'; import {Events as TransformControllerEvents, TransformController} from './TransformController.js'; const UIStrings = { /** *@description Text of a DOM element in DView of the Layers panel */ layerInformationIsNotYet: 'Layer information is not yet available.', /** *@description Accessibility label for canvas view in Layers tool */ dLayersView: '3D Layers View', /** *@description Text in DView of the Layers panel */ cantDisplayLayers: 'Can\'t display layers,', /** *@description Text in DView of the Layers panel */ webglSupportIsDisabledInYour: 'WebGL support is disabled in your browser.', /** *@description Text in DView of the Layers panel *@example {about:gpu} PH1 */ checkSForPossibleReasons: 'Check {PH1} for possible reasons.', /** *@description Text for a checkbox in the toolbar of the Layers panel to show the area of slow scroll rect */ slowScrollRects: 'Slow scroll rects', /** * @description Text for a checkbox in the toolbar of the Layers panel. This is a noun, for a * setting meaning 'display paints in the layers viewer'. 'Paints' here means 'paint events' i.e. * when the browser draws pixels to the screen. */ paints: 'Paints', /** *@description A context menu item in the DView of the Layers panel */ resetView: 'Reset View', /** *@description A context menu item in the DView of the Layers panel */ showPaintProfiler: 'Show Paint Profiler', }; const str_ = i18n.i18n.registerUIStrings('panels/layer_viewer/Layers3DView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const vertexPositionAttributes = new Map<WebGLProgram, number>(); const vertexColorAttributes = new Map<WebGLProgram, number>(); const textureCoordAttributes = new Map<WebGLProgram, number>(); const uniformMatrixLocations = new Map<WebGLProgram, WebGLUniformLocation|null>(); const uniformSamplerLocations = new Map<WebGLProgram, WebGLUniformLocation|null>(); const imageForTexture = new Map<WebGLTexture, HTMLImageElement>(); export class Layers3DView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) implements LayerView { private readonly failBanner: UI.Widget.VBox; private readonly layerViewHost: LayerViewHost; private transformController: TransformController; private canvasElement: HTMLCanvasElement; private lastSelection: {[x: string]: Selection|null}; private layerTree: SDK.LayerTreeBase.LayerTreeBase|null; private readonly textureManager: LayerTextureManager; private chromeTextures: (WebGLTexture|undefined)[]; private rects: Rectangle[]; private snapshotLayers: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>; private shaderProgram!: WebGLProgram|null; private oldTextureScale!: number|undefined; private depthByLayerId!: Map<string, number>; private visibleLayers!: Set<SDK.LayerTreeBase.Layer>; private maxDepth!: number; private scale!: number; private layerTexture?: {layer: SDK.LayerTreeBase.Layer, texture: WebGLTexture}|null; private projectionMatrix?: DOMMatrix; private whiteTexture?: WebGLTexture|null; private gl?: WebGLRenderingContext|null; private dimensionsForAutoscale?: {width: number, height: number}; private needsUpdate?: boolean; private panelToolbar?: UI.Toolbar.Toolbar; private showSlowScrollRectsSetting?: Common.Settings.Setting<boolean>; private showPaintsSetting?: Common.Settings.Setting<boolean>; private mouseDownX?: number; private mouseDownY?: number; constructor(layerViewHost: LayerViewHost) { super(true); this.contentElement.classList.add('layers-3d-view'); this.failBanner = new UI.Widget.VBox(); this.failBanner.element.classList.add('full-widget-dimmed-banner'); UI.UIUtils.createTextChild(this.failBanner.element, i18nString(UIStrings.layerInformationIsNotYet)); this.layerViewHost = layerViewHost; this.layerViewHost.registerView(this); this.transformController = new TransformController(this.contentElement as HTMLElement); this.transformController.addEventListener(TransformControllerEvents.TransformChanged, this.update, this); this.initToolbar(); this.canvasElement = this.contentElement.createChild('canvas') as HTMLCanvasElement; this.canvasElement.tabIndex = 0; this.canvasElement.addEventListener('dblclick', this.onDoubleClick.bind(this), false); this.canvasElement.addEventListener('mousedown', this.onMouseDown.bind(this), false); this.canvasElement.addEventListener('mouseup', this.onMouseUp.bind(this), false); this.canvasElement.addEventListener('mouseleave', this.onMouseMove.bind(this), false); this.canvasElement.addEventListener('mousemove', this.onMouseMove.bind(this), false); this.canvasElement.addEventListener('contextmenu', this.onContextMenu.bind(this), false); UI.ARIAUtils.setAccessibleName(this.canvasElement, i18nString(UIStrings.dLayersView)); this.lastSelection = {}; this.layerTree = null; this.textureManager = new LayerTextureManager(this.update.bind(this)); this.chromeTextures = []; this.rects = []; this.snapshotLayers = new Map(); this.layerViewHost.setLayerSnapshotMap(this.snapshotLayers); this.layerViewHost.showInternalLayersSetting().addChangeListener(this.update, this); } setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void { this.layerTree = layerTree; this.layerTexture = null; delete this.oldTextureScale; if (this.showPaints()) { this.textureManager.setLayerTree(layerTree); } this.update(); } showImageForLayer(layer: SDK.LayerTreeBase.Layer, imageURL?: string): void { if (!imageURL) { this.layerTexture = null; this.update(); return; } void UI.UIUtils.loadImage(imageURL).then(image => { const texture = image && LayerTextureManager.createTextureForImage(this.gl || null, image); this.layerTexture = texture ? {layer: layer, texture: texture} : null; this.update(); }); } override onResize(): void { this.resizeCanvas(); this.update(); } override willHide(): void { this.textureManager.suspend(); } override wasShown(): void { this.textureManager.resume(); this.registerCSSFiles([layers3DViewStyles]); if (!this.needsUpdate) { return; } this.resizeCanvas(); this.update(); } updateLayerSnapshot(layer: SDK.LayerTreeBase.Layer): void { this.textureManager.layerNeedsUpdate(layer); } private setOutline(type: OutlineType, selection: Selection|null): void { this.lastSelection[type] = selection; this.update(); } hoverObject(selection: Selection|null): void { this.setOutline(OutlineType.Hovered, selection); } selectObject(selection: Selection|null): void { this.setOutline(OutlineType.Hovered, null); this.setOutline(OutlineType.Selected, selection); } snapshotForSelection(selection: Selection): Promise<SDK.PaintProfiler.SnapshotWithRect|null> { if (selection.type() === Type.Snapshot) { const snapshotWithRect = (selection as SnapshotSelection).snapshot(); snapshotWithRect.snapshot.addReference(); return Promise.resolve(snapshotWithRect); } if (selection.layer()) { const promise = selection.layer().snapshots()[0]; if (promise !== undefined) { return promise; } } return Promise.resolve(null); } private initGL(canvas: HTMLCanvasElement): WebGLRenderingContext|null { const gl = canvas.getContext('webgl'); if (!gl) { return null; } gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); gl.clearColor(0.0, 0.0, 0.0, 0.0); gl.enable(gl.DEPTH_TEST); return gl; } private createShader(type: number, script: string): void { if (!this.gl) { return; } const shader = this.gl.createShader(type); if (shader && this.shaderProgram) { this.gl.shaderSource(shader, script); this.gl.compileShader(shader); this.gl.attachShader(this.shaderProgram, shader); } } private initShaders(): void { if (!this.gl) { return; } this.shaderProgram = this.gl.createProgram(); if (!this.shaderProgram) { return; } this.createShader(this.gl.FRAGMENT_SHADER, FragmentShader); this.createShader(this.gl.VERTEX_SHADER, VertexShader); this.gl.linkProgram(this.shaderProgram); this.gl.useProgram(this.shaderProgram); const aVertexPositionAttribute = this.gl.getAttribLocation(this.shaderProgram, 'aVertexPosition'); this.gl.enableVertexAttribArray(aVertexPositionAttribute); vertexPositionAttributes.set(this.shaderProgram, aVertexPositionAttribute); const aVertexColorAttribute = this.gl.getAttribLocation(this.shaderProgram, 'aVertexColor'); this.gl.enableVertexAttribArray(aVertexColorAttribute); vertexColorAttributes.set(this.shaderProgram, aVertexColorAttribute); const aTextureCoordAttribute = this.gl.getAttribLocation(this.shaderProgram, 'aTextureCoord'); this.gl.enableVertexAttribArray(aTextureCoordAttribute); textureCoordAttributes.set(this.shaderProgram, aTextureCoordAttribute); const uMatrixLocation = this.gl.getUniformLocation(this.shaderProgram, 'uPMatrix'); uniformMatrixLocations.set(this.shaderProgram, uMatrixLocation); const uSamplerLocation = this.gl.getUniformLocation(this.shaderProgram, 'uSampler'); uniformSamplerLocations.set(this.shaderProgram, uSamplerLocation); } private resizeCanvas(): void { this.canvasElement.width = this.canvasElement.offsetWidth * window.devicePixelRatio; this.canvasElement.height = this.canvasElement.offsetHeight * window.devicePixelRatio; } private updateTransformAndConstraints(): void { const paddingFraction = 0.1; const dimensionsForAutoscale = this.dimensionsForAutoscale || {width: 0, height: 0}; const viewport = this.layerTree ? this.layerTree.viewportSize() : null; const baseWidth = viewport ? viewport.width : dimensionsForAutoscale.width; const baseHeight = viewport ? viewport.height : dimensionsForAutoscale.height; const canvasWidth = this.canvasElement.width; const canvasHeight = this.canvasElement.height; const paddingX = canvasWidth * paddingFraction; const paddingY = canvasHeight * paddingFraction; const scaleX = (canvasWidth - 2 * paddingX) / baseWidth; const scaleY = (canvasHeight - 2 * paddingY) / baseHeight; const viewScale = Math.min(scaleX, scaleY); const minScaleConstraint = Math.min(baseWidth / dimensionsForAutoscale.width, baseHeight / dimensionsForAutoscale.width) / 2; this.transformController.setScaleConstraints( minScaleConstraint, 10 / viewScale); // 1/viewScale is 1:1 in terms of pixels, so allow zooming to 10x of native size const scale = this.transformController.scale(); const rotateX = this.transformController.rotateX(); const rotateY = this.transformController.rotateY(); this.scale = scale * viewScale; const textureScale = Platform.NumberUtilities.clamp(this.scale, 0.1, 1); if (textureScale !== this.oldTextureScale) { this.oldTextureScale = textureScale; this.textureManager.setScale(textureScale); this.dispatchEventToListeners(Events.ScaleChanged, textureScale); } const scaleAndRotationMatrix = new WebKitCSSMatrix() .scale(scale, scale, scale) .translate(canvasWidth / 2, canvasHeight / 2, 0) .rotate(rotateX, rotateY, 0) .scale(viewScale, viewScale, viewScale) .translate(-baseWidth / 2, -baseHeight / 2, 0); let bounds; for (let i = 0; i < this.rects.length; ++i) { bounds = UI.Geometry.boundsForTransformedPoints(scaleAndRotationMatrix, this.rects[i].vertices, bounds); } if (bounds) { this.transformController.clampOffsets( (paddingX - bounds.maxX) / window.devicePixelRatio, (canvasWidth - paddingX - bounds.minX) / window.devicePixelRatio, (paddingY - bounds.maxY) / window.devicePixelRatio, (canvasHeight - paddingY - bounds.minY) / window.devicePixelRatio); } const offsetX = this.transformController.offsetX() * window.devicePixelRatio; const offsetY = this.transformController.offsetY() * window.devicePixelRatio; // Multiply to translation matrix on the right rather than translate (which would implicitly multiply on the left). this.projectionMatrix = new WebKitCSSMatrix().translate(offsetX, offsetY, 0).multiply(scaleAndRotationMatrix); const glProjectionMatrix = new WebKitCSSMatrix() .scale(1, -1, -1) .translate(-1, -1, 0) .scale(2 / this.canvasElement.width, 2 / this.canvasElement.height, 1 / 1000000) .multiply(this.projectionMatrix); if (this.shaderProgram) { const pMatrixUniform = uniformMatrixLocations.get(this.shaderProgram); if (this.gl && pMatrixUniform) { this.gl.uniformMatrix4fv(pMatrixUniform, false, this.arrayFromMatrix(glProjectionMatrix)); } } } private arrayFromMatrix(m: DOMMatrix): Float32Array { return new Float32Array([ m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34, m.m41, m.m42, m.m43, m.m44, ]); } private initWhiteTexture(): void { if (!this.gl) { return; } this.whiteTexture = this.gl.createTexture(); this.gl.bindTexture(this.gl.TEXTURE_2D, this.whiteTexture); const whitePixel = new Uint8Array([255, 255, 255, 255]); this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, whitePixel); } private initChromeTextures(): void { function loadChromeTexture(this: Layers3DView, index: ChromeTexture, url: string): void { void UI.UIUtils.loadImage(url).then(image => { this.chromeTextures[index] = image && LayerTextureManager.createTextureForImage(this.gl || null, image) || undefined; }); } loadChromeTexture.call(this, ChromeTexture.Left, 'Images/chromeLeft.avif'); loadChromeTexture.call(this, ChromeTexture.Middle, 'Images/chromeMiddle.avif'); loadChromeTexture.call(this, ChromeTexture.Right, 'Images/chromeRight.avif'); } private initGLIfNecessary(): WebGLRenderingContext|null { if (this.gl) { return this.gl; } this.gl = this.initGL(this.canvasElement); if (!this.gl) { return null; } this.initShaders(); this.initWhiteTexture(); this.initChromeTextures(); this.textureManager.setContext(this.gl); return this.gl; } private calculateDepthsAndVisibility(): void { this.depthByLayerId = new Map(); let depth = 0; const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get(); if (!this.layerTree) { return; } const root = showInternalLayers ? this.layerTree.root() : (this.layerTree.contentRoot() || this.layerTree.root()); if (!root) { return; } const queue = [root]; this.depthByLayerId.set(root.id(), 0); this.visibleLayers = new Set(); while (queue.length > 0) { const layer = queue.shift(); if (!layer) { break; } if (showInternalLayers || layer.drawsContent()) { this.visibleLayers.add(layer); } const children = layer.children(); for (let i = 0; i < children.length; ++i) { this.depthByLayerId.set(children[i].id(), ++depth); queue.push(children[i]); } } this.maxDepth = depth; } private depthForLayer(layer: SDK.LayerTreeBase.Layer): number { return (this.depthByLayerId.get(layer.id()) || 0) * LayerSpacing; } private calculateScrollRectDepth(layer: SDK.LayerTreeBase.Layer, index: number): number { return this.depthForLayer(layer) + index * ScrollRectSpacing + 1; } private updateDimensionsForAutoscale(layer: SDK.LayerTreeBase.Layer): void { // We don't want to be precise, but rather pick something least affected by // animationtransforms, so that we don't change scale too often. So let's // disregard transforms, scrolling and relative layer positioning and choose // the largest dimensions of all layers. if (!this.dimensionsForAutoscale) { this.dimensionsForAutoscale = {width: 0, height: 0}; } this.dimensionsForAutoscale.width = Math.max(layer.width(), this.dimensionsForAutoscale.width); this.dimensionsForAutoscale.height = Math.max(layer.height(), this.dimensionsForAutoscale.height); } private calculateLayerRect(layer: SDK.LayerTreeBase.Layer): void { if (!this.visibleLayers.has(layer)) { return; } const selection = new LayerSelection(layer); const rect = new Rectangle(selection); rect.setVertices(layer.quad(), this.depthForLayer(layer)); this.appendRect(rect); this.updateDimensionsForAutoscale(layer); } private appendRect(rect: Rectangle): void { const selection = rect.relatedObject; const isSelected = Selection.isEqual(this.lastSelection[OutlineType.Selected], selection); const isHovered = Selection.isEqual(this.lastSelection[OutlineType.Hovered], selection); if (isSelected) { rect.borderColor = SelectedBorderColor; } else if (isHovered) { rect.borderColor = HoveredBorderColor; const fillColor = rect.fillColor || [255, 255, 255, 1]; const maskColor = HoveredImageMaskColor; rect.fillColor = [ fillColor[0] * maskColor[0] / 255, fillColor[1] * maskColor[1] / 255, fillColor[2] * maskColor[2] / 255, fillColor[3] * maskColor[3], ]; } else { rect.borderColor = BorderColor; } rect.lineWidth = isSelected ? SelectedBorderWidth : BorderWidth; this.rects.push(rect); } private calculateLayerScrollRects(layer: SDK.LayerTreeBase.Layer): void { const scrollRects = layer.scrollRects(); for (let i = 0; i < scrollRects.length; ++i) { const selection = new ScrollRectSelection(layer, i); const rect = new Rectangle(selection); rect.calculateVerticesFromRect(layer, scrollRects[i].rect, this.calculateScrollRectDepth(layer, i)); rect.fillColor = ScrollRectBackgroundColor; this.appendRect(rect); } } private calculateLayerTileRects(layer: SDK.LayerTreeBase.Layer): void { const tiles = this.textureManager.tilesForLayer(layer); for (let i = 0; i < tiles.length; ++i) { const tile = tiles[i]; if (!tile.texture) { continue; } const selection = new SnapshotSelection(layer, {rect: tile.rect, snapshot: tile.snapshot}); const rect = new Rectangle(selection); if (!this.snapshotLayers.has(layer)) { this.snapshotLayers.set(layer, selection); } rect.calculateVerticesFromRect(layer, tile.rect, this.depthForLayer(layer) + 1); rect.texture = tile.texture; this.appendRect(rect); } } private calculateRects(): void { this.rects = []; this.snapshotLayers.clear(); this.dimensionsForAutoscale = {width: 0, height: 0}; if (this.layerTree) { this.layerTree.forEachLayer(this.calculateLayerRect.bind(this)); } if (this.showSlowScrollRectsSetting && this.showSlowScrollRectsSetting.get() && this.layerTree) { this.layerTree.forEachLayer(this.calculateLayerScrollRects.bind(this)); } if (this.layerTexture && this.visibleLayers.has(this.layerTexture.layer)) { const layer = this.layerTexture.layer; const selection = new LayerSelection(layer); const rect = new Rectangle(selection); rect.setVertices(layer.quad(), this.depthForLayer(layer)); rect.texture = this.layerTexture.texture; this.appendRect(rect); } else if (this.showPaints() && this.layerTree) { this.layerTree.forEachLayer(this.calculateLayerTileRects.bind(this)); } } private makeColorsArray(color: number[]): number[] { let colors: number[] = []; const normalizedColor = [color[0] / 255, color[1] / 255, color[2] / 255, color[3]]; for (let i = 0; i < 4; i++) { colors = colors.concat(normalizedColor); } return colors; } private setVertexAttribute(attribute: number, array: number[], length: number): void { const gl = this.gl; if (!gl) { return; } const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), gl.STATIC_DRAW); gl.vertexAttribPointer(attribute, length, gl.FLOAT, false, 0, 0); } private drawRectangle(vertices: number[], mode: number, color?: number[], texture?: Object): void { const gl = this.gl; const white = [255, 255, 255, 1]; color = color || white; if (!this.shaderProgram) { return; } const vertexPositionAttribute = vertexPositionAttributes.get(this.shaderProgram); const textureCoordAttribute = textureCoordAttributes.get(this.shaderProgram); const vertexColorAttribute = vertexColorAttributes.get(this.shaderProgram); if (typeof vertexPositionAttribute !== 'undefined') { this.setVertexAttribute(vertexPositionAttribute, vertices, 3); } if (typeof textureCoordAttribute !== 'undefined') { this.setVertexAttribute(textureCoordAttribute, [0, 1, 1, 1, 1, 0, 0, 0], 2); } if (typeof vertexColorAttribute !== 'undefined') { this.setVertexAttribute(vertexColorAttribute, this.makeColorsArray(color), color.length); } if (!gl) { return; } const samplerUniform = uniformSamplerLocations.get(this.shaderProgram); if (texture) { if (samplerUniform) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(samplerUniform, 0); } } else if (this.whiteTexture) { gl.bindTexture(gl.TEXTURE_2D, this.whiteTexture); } const numberOfVertices = vertices.length / 3; gl.drawArrays(mode, 0, numberOfVertices); } private drawTexture(vertices: number[], texture: WebGLTexture, color?: number[]): void { if (!this.gl) { return; } this.drawRectangle(vertices, this.gl.TRIANGLE_FAN, color, texture); } private drawViewportAndChrome(): void { if (!this.layerTree) { return; } const viewport = this.layerTree.viewportSize(); if (!viewport) { return; } const drawChrome = !Common.Settings.Settings.instance().moduleSetting('frameViewerHideChromeWindow').get() && this.chromeTextures.length >= 3 && this.chromeTextures.indexOf(undefined) < 0; const z = (this.maxDepth + 1) * LayerSpacing; const borderWidth = Math.ceil(ViewportBorderWidth * this.scale); let vertices: number[] = [viewport.width, 0, z, viewport.width, viewport.height, z, 0, viewport.height, z, 0, 0, z]; if (!this.gl) { return; } this.gl.lineWidth(borderWidth); this.drawRectangle(vertices, drawChrome ? this.gl.LINE_STRIP : this.gl.LINE_LOOP, ViewportBorderColor); if (!drawChrome) { return; } const viewportSize = this.layerTree.viewportSize(); if (!viewportSize) { return; } const borderAdjustment = ViewportBorderWidth / 2; const viewportWidth = viewportSize.width + 2 * borderAdjustment; if (this.chromeTextures[0] && this.chromeTextures[2]) { const chromeTextureImage = imageForTexture.get(this.chromeTextures[0] as WebGLTexture) || {naturalHeight: 0, naturalWidth: 0}; const chromeHeight = chromeTextureImage.naturalHeight; const middleTextureImage = imageForTexture.get(this.chromeTextures[2] as WebGLTexture) || {naturalHeight: 0, naturalWidth: 0}; const middleFragmentWidth = viewportWidth - chromeTextureImage.naturalWidth - middleTextureImage.naturalWidth; let x = -borderAdjustment; const y = -chromeHeight; for (let i = 0; i < this.chromeTextures.length; ++i) { const texture = this.chromeTextures[i]; if (!texture) { continue; } const image = imageForTexture.get(texture); if (!image) { continue; } const width = i === ChromeTexture.Middle ? middleFragmentWidth : image.naturalWidth; if (width < 0 || x + width > viewportWidth) { break; } vertices = [x, y, z, x + width, y, z, x + width, y + chromeHeight, z, x, y + chromeHeight, z]; this.drawTexture(vertices, this.chromeTextures[i] as WebGLTexture); x += width; } } } private drawViewRect(rect: Rectangle): void { if (!this.gl) { return; } const vertices = rect.vertices; if (rect.texture) { this.drawTexture(vertices, rect.texture, rect.fillColor || undefined); } else if (rect.fillColor) { this.drawRectangle(vertices, this.gl.TRIANGLE_FAN, rect.fillColor); } this.gl.lineWidth(rect.lineWidth); if (rect.borderColor) { this.drawRectangle(vertices, this.gl.LINE_LOOP, rect.borderColor); } } private update(): void { if (!this.isShowing()) { this.needsUpdate = true; return; } if (!this.layerTree || !this.layerTree.root()) { this.failBanner.show(this.contentElement); return; } const gl = this.initGLIfNecessary(); if (!gl) { this.failBanner.element.removeChildren(); this.failBanner.element.appendChild(this.webglDisabledBanner()); this.failBanner.show(this.contentElement); return; } this.failBanner.detach(); const viewportWidth = this.canvasElement.width; const viewportHeight = this.canvasElement.height; this.calculateDepthsAndVisibility(); this.calculateRects(); this.updateTransformAndConstraints(); gl.viewport(0, 0, viewportWidth, viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); this.rects.forEach(this.drawViewRect.bind(this)); this.drawViewportAndChrome(); } private webglDisabledBanner(): Node { const fragment = this.contentElement.ownerDocument.createDocumentFragment(); fragment.createChild('div').textContent = i18nString(UIStrings.cantDisplayLayers); fragment.createChild('div').textContent = i18nString(UIStrings.webglSupportIsDisabledInYour); fragment.appendChild(i18n.i18n.getFormatLocalizedString( str_, UIStrings.checkSForPossibleReasons, {PH1: UI.XLink.XLink.create('about:gpu')})); return fragment; } private selectionFromEventPoint(event: Event): Selection|null { const mouseEvent = event as MouseEvent; if (!this.layerTree) { return null; } let closestIntersectionPoint: number = Infinity; let closestObject: Selection|null = null; const projectionMatrix = new WebKitCSSMatrix().scale(1, -1, -1).translate(-1, -1, 0).multiply(this.projectionMatrix); const x0 = (mouseEvent.clientX - this.canvasElement.getBoundingClientRect().left) * window.devicePixelRatio; const y0 = -(mouseEvent.clientY - this.canvasElement.getBoundingClientRect().top) * window.devicePixelRatio; function checkIntersection(rect: Rectangle): void { if (!rect.relatedObject) { return; } const t = rect.intersectWithLine(projectionMatrix, x0, y0); if (t && t < closestIntersectionPoint) { closestIntersectionPoint = t; closestObject = rect.relatedObject; } } this.rects.forEach(checkIntersection); return closestObject; } private createVisibilitySetting(caption: string, name: string, value: boolean, toolbar: UI.Toolbar.Toolbar): Common.Settings.Setting<boolean> { const setting = Common.Settings.Settings.instance().createSetting(name, value); setting.setTitle(caption); setting.addChangeListener(this.update, this); toolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox(setting)); return setting; } private initToolbar(): void { this.panelToolbar = this.transformController.toolbar(); this.contentElement.appendChild(this.panelToolbar.element); this.showSlowScrollRectsSetting = this.createVisibilitySetting( i18nString(UIStrings.slowScrollRects), 'frameViewerShowSlowScrollRects', true, this.panelToolbar); this.showPaintsSetting = this.createVisibilitySetting(i18nString(UIStrings.paints), 'frameViewerShowPaints', true, this.panelToolbar); this.showPaintsSetting.addChangeListener(this.updatePaints, this); Common.Settings.Settings.instance() .moduleSetting('frameViewerHideChromeWindow') .addChangeListener(this.update, this); } private onContextMenu(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.defaultSection().appendItem( i18nString(UIStrings.resetView), () => this.transformController.resetAndNotify(), false); const selection = this.selectionFromEventPoint(event); if (selection && selection.type() === Type.Snapshot) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.showPaintProfiler), () => this.dispatchEventToListeners(Events.PaintProfilerRequested, selection), false); } this.layerViewHost.showContextMenu(contextMenu, selection); } private onMouseMove(event: Event): void { const mouseEvent = event as MouseEvent; if (mouseEvent.which) { return; } this.layerViewHost.hoverObject(this.selectionFromEventPoint(event)); } private onMouseDown(event: Event): void { const mouseEvent = event as MouseEvent; this.mouseDownX = mouseEvent.clientX; this.mouseDownY = mouseEvent.clientY; } private onMouseUp(event: Event): void { const mouseEvent = event as MouseEvent; const maxDistanceInPixels = 6; if (this.mouseDownX && Math.abs(mouseEvent.clientX - this.mouseDownX) < maxDistanceInPixels && Math.abs(mouseEvent.clientY - (this.mouseDownY || 0)) < maxDistanceInPixels) { this.canvasElement.focus(); this.layerViewHost.selectObject(this.selectionFromEventPoint(event)); } delete this.mouseDownX; delete this.mouseDownY; } private onDoubleClick(event: Event): void { const selection = this.selectionFromEventPoint(event); if (selection && (selection.type() === Type.Snapshot || selection.layer())) { this.dispatchEventToListeners(Events.PaintProfilerRequested, selection); } event.stopPropagation(); } private updatePaints(): void { if (this.showPaints()) { this.textureManager.setLayerTree(this.layerTree); this.textureManager.forceUpdate(); } else { this.textureManager.reset(); } this.update(); } private showPaints(): boolean { return this.showPaintsSetting ? this.showPaintsSetting.get() : false; } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum OutlineType { Hovered = 'hovered', Selected = 'selected', } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { PaintProfilerRequested = 'PaintProfilerRequested', ScaleChanged = 'ScaleChanged', } export type EventTypes = { [Events.PaintProfilerRequested]: Selection, [Events.ScaleChanged]: number, }; export const enum ChromeTexture { Left = 0, Middle = 1, Right = 2, } export const FragmentShader = '' + 'precision mediump float;\n' + 'varying vec4 vColor;\n' + 'varying vec2 vTextureCoord;\n' + 'uniform sampler2D uSampler;\n' + 'void main(void)\n' + '{\n' + ' gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)) * vColor;\n' + '}'; export const VertexShader = '' + 'attribute vec3 aVertexPosition;\n' + 'attribute vec2 aTextureCoord;\n' + 'attribute vec4 aVertexColor;\n' + 'uniform mat4 uPMatrix;\n' + 'varying vec2 vTextureCoord;\n' + 'varying vec4 vColor;\n' + 'void main(void)\n' + '{\n' + 'gl_Position = uPMatrix * vec4(aVertexPosition, 1.0);\n' + 'vColor = aVertexColor;\n' + 'vTextureCoord = aTextureCoord;\n' + '}'; export const HoveredBorderColor = [0, 0, 255, 1]; export const SelectedBorderColor = [0, 255, 0, 1]; export const BorderColor = [0, 0, 0, 1]; export const ViewportBorderColor = [160, 160, 160, 1]; export const ScrollRectBackgroundColor = [178, 100, 100, 0.6]; export const HoveredImageMaskColor = [200, 200, 255, 1]; export const BorderWidth = 1; export const SelectedBorderWidth = 2; export const ViewportBorderWidth = 3; export const LayerSpacing = 20; export const ScrollRectSpacing = 4; export class LayerTextureManager { private readonly textureUpdatedCallback: () => void; private readonly throttler: Common.Throttler.Throttler; private scale: number; private active: boolean; private queue!: SDK.LayerTreeBase.Layer[]; private tilesByLayer!: Map<SDK.LayerTreeBase.Layer, Tile[]>; private gl?: WebGLRenderingContext; constructor(textureUpdatedCallback: () => void) { this.textureUpdatedCallback = textureUpdatedCallback; this.throttler = new Common.Throttler.Throttler(0); this.scale = 0; this.active = false; this.reset(); } static createTextureForImage(gl: WebGLRenderingContext|null, image: HTMLImageElement): WebGLTexture { if (!gl) { throw new Error('WebGLRenderingContext not provided'); } const texture = gl.createTexture(); if (!texture) { throw new Error('Unable to create texture'); } imageForTexture.set(texture, image); gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return texture; } reset(): void { if (this.tilesByLayer) { this.setLayerTree(null); } this.tilesByLayer = new Map(); this.queue = []; } setContext(glContext: WebGLRenderingContext): void { this.gl = glContext; if (this.scale) { this.updateTextures(); } } suspend(): void { this.active = false; } resume(): void { this.active = true; if (this.queue.length) { void this.update(); } } setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void { const newLayers = new Set<SDK.LayerTreeBase.Layer>(); const oldLayers = Array.from(this.tilesByLayer.keys()); if (layerTree) { layerTree.forEachLayer(layer => { if (!layer.drawsContent()) { return; } newLayers.add(layer); if (!this.tilesByLayer.has(layer)) { this.tilesByLayer.set(layer, []); this.layerNeedsUpdate(layer); } }); } if (!oldLayers.length) { this.forceUpdate(); } for (const layer of oldLayers) { if (newLayers.has(layer)) { continue; } const tiles = this.tilesByLayer.get(layer); if (tiles) { tiles.forEach(tile => tile.dispose()); } this.tilesByLayer.delete(layer); } } private setSnapshotsForLayer(layer: SDK.LayerTreeBase.Layer, snapshots: SDK.PaintProfiler.SnapshotWithRect[]): Promise<void> { const oldSnapshotsToTiles = new Map((this.tilesByLayer.get(layer) || []).map(tile => [tile.snapshot, tile])); const newTiles = []; const reusedTiles = []; for (const snapshot of snapshots) { const oldTile = oldSnapshotsToTiles.get(snapshot.snapshot); if (oldTile) { reusedTiles.push(oldTile); oldSnapshotsToTiles.delete(snapshot.snapshot); } else { newTiles.push(new Tile(snapshot)); } } this.tilesByLayer.set(layer, reusedTiles.concat(newTiles)); for (const tile of oldSnapshotsToTiles.values()) { tile.dispose(); } const gl = this.gl; if (!gl || !this.scale) { return Promise.resolve(); } return Promise.all(newTiles.map(tile => tile.update(gl, this.scale))).then(this.textureUpdatedCallback); } setScale(scale: number): void { if (this.scale && this.scale >= scale) { return; } this.scale = scale; this.updateTextures(); } tilesForLayer(layer: SDK.LayerTreeBase.Layer): Tile[] { return this.tilesByLayer.get(layer) || []; } layerNeedsUpdate(layer: SDK.LayerTreeBase.Layer): void { if (this.queue.indexOf(layer) < 0) { this.queue.push(layer); } if (this.active) { void this.throttler.schedule(this.update.bind(this)); } } forceUpdate(): void { this.queue.forEach(layer => this.updateLayer(layer)); this.queue = []; void this.update(); } private update(): Promise<void> { const layer = this.queue.shift(); if (!layer) { return Promise.resolve(); } if (this.queue.length) { void this.throttler.schedule(this.update.bind(this)); } return this.updateLayer(layer); } private updateLayer(layer: SDK.LayerTreeBase.Layer): Promise<void> { return Promise.all(layer.snapshots()) .then( snapshots => this.setSnapshotsForLayer( layer, snapshots.filter(snapshot => snapshot !== null) as SDK.PaintProfiler.SnapshotWithRect[])); } private updateTextures(): void { if (!this.gl) { return; } if (!this.scale) { return; } for (const tiles of this.tilesByLayer.values()) { for (const tile of tiles) { const promise = tile.updateScale(this.gl, this.scale); if (promise) { void promise.then(this.textureUpdatedCallback); } } } } } export class Rectangle { relatedObject: Selection|null; lineWidth: number; borderColor: number[]|null; fillColor: number[]|null; texture: WebGLTexture|null; vertices!: number[]; constructor(relatedObject: Selection|null) { this.relatedObject = relatedObject; this.lineWidth = 1; this.borderColor = null; this.fillColor = null; this.texture = null; } setVertices(quad: number[], z: number): void { this.vertices = [quad[0], quad[1], z, quad[2], quad[3], z, quad[4], quad[5], z, quad[6], quad[7], z]; } /** * Finds coordinates of point on layer quad, having offsets (ratioX * width) and (ratioY * height) * from the left corner of the initial layer rect, where width and heigth are layer bounds. */ private calculatePointOnQuad(quad: number[], ratioX: number, ratioY: number): number[] { const x0 = quad[0]; const y0 = quad[1]; const x1 = quad[2]; const y1 = quad[3]; const x2 = quad[4]; const y2 = quad[5]; const x3 = quad[6]; const y3 = quad[7]; // Point on the first quad side clockwise const firstSidePointX = x0 + ratioX * (x1 - x0); const firstSidePointY = y0 + ratioX * (y1 - y0); // Point on the third quad side clockwise const thirdSidePointX = x3 + ratioX * (x2 - x3); const thirdSidePointY = y3 + ratioX * (y2 - y3); const x = firstSidePointX + ratioY * (thirdSidePointX - firstSidePointX); const y = firstSidePointY + ratioY * (thirdSidePointY - firstSidePointY); return [x, y]; } calculateVerticesFromRect(layer: SDK.LayerTreeBase.Layer, rect: Protocol.DOM.Rect, z: number): void { const quad = layer.quad(); const rx1 = rect.x / layer.width(); const rx2 = (rect.x + rect.width) / layer.width(); const ry1 = rect.y / layer.height(); const ry2 = (rect.y + rect.height) / layer.height(); const rectQuad = this.calculatePointOnQuad(quad, rx1, ry1) .concat(this.calculatePointOnQuad(quad, rx2, ry1)) .concat(this.calculatePointOnQuad(quad, rx2, ry2)) .concat(this.calculatePointOnQuad(quad, rx1, ry2)); this.setVertices(rectQuad, z); } /** * Intersects quad with given transform matrix and line l(t) = (x0, y0, t) */ intersectWithLine(matrix: DOMMatrix, x0: number, y0: number): number|undefined { let i; // Vertices of the quad with transform matrix applied const points = []; for (i = 0; i < 4; ++i) { points[i] = UI.Geometry.multiplyVectorByMatrixAndNormalize( new UI.Geometry.Vector(this.vertices[i * 3], this.vertices[i * 3 + 1], this.vertices[i * 3 + 2]), matrix); } // Calculating quad plane normal const normal = UI.Geometry.crossProduct( UI.Geometry.subtract(points[1], points[0]), UI.Geometry.subtract(points[2], points[1])); // General form of the equation of the quad plane: A * x + B * y + C * z + D = 0 const A = normal.x; const B = normal.y; const C = normal.z; const D = -(A * points[0].x + B * points[0].y + C * points[0].z); // Finding t from the equation const t = -(D + A * x0 + B * y0) / C; // Point of the intersection const pt = new UI.Geometry.Vector(x0, y0, t); // Vectors from the intersection point to vertices of the quad const tVects = points.map(UI.Geometry.subtract.bind(null, pt)); // Intersection point lies inside of the polygon if scalar products of normal of the plane and // cross products of successive tVects are all nonstrictly above or all nonstrictly below zero for (i = 0; i < tVects.length; ++i) { const product = UI.Geometry.scalarProduct(normal, UI.Geometry.crossProduct(tVects[i], tVects[(i + 1) % tVects.length])); if (product < 0) { return undefined; } } return t; } } export class Tile { snapshot: SDK.PaintProfiler.PaintProfilerSnapshot; rect: Protocol.DOM.Rect; scale: number; texture: WebGLTexture|null; private gl!: WebGLRenderingContext; constructor(snapshotWithRect: SDK.PaintProfiler.SnapshotWithRect) { this.snapshot = snapshotWithRect.snapshot; this.rect = snapshotWithRect.rect; this.scale = 0; this.texture = null; } dispose(): void { this.snapshot.release(); if (this.texture) { this.gl.deleteTexture(this.texture); this.texture = null; } } updateScale(glContext: WebGLRenderingContext, scale: number): Promise<void>|null { if (this.texture && this.scale >= scale) { return null; } return this.update(glContext, scale); } async update(glContext: WebGLRenderingContext, scale: number): Promise<void> { this.gl = glContext; this.scale = scale; const imageURL = await this.snapshot.replay(scale); const image = imageURL ? await UI.UIUtils.loadImage(imageURL) : null; this.texture = image ? LayerTextureManager.createTextureForImage(glContext, image) : null; } }