UNPKG

lazy-widgets

Version:

Typescript retained mode GUI for the HTML canvas API

183 lines 7.36 kB
import { CanvasViewport } from './CanvasViewport.js'; import { Msg } from './Strings.js'; const DEBUG_EVENT_TIMEOUT = 1000; export class DebuggableCanvasViewport extends CanvasViewport { constructor(child, resolution = 1, preventBleeding = false, preventAtlasBleeding = false, startingWidth = 64, startingHeight = 64) { super(child, resolution, preventBleeding, preventAtlasBleeding, startingWidth, startingHeight); /** The list of recently pushed dirty rects, with their timestamps */ this.events = new Set(); /** Is the overlay enabled? Disabled by default */ this._overlayEnabled = false; /** Does the overlay need to be updated? */ this._overlayDirty = false; /** When was the last time the PPS was measured? */ this._lastPPSMeasurement = 0; /** Current PPS text value */ this._ppsText = 'PPS: measuring...'; /** Paints since last measurement */ this._paintCounter = 0; // make overlay canvas this.overlayCanvas = document.createElement('canvas'); this.overlayCanvas.width = startingWidth; this.overlayCanvas.height = startingHeight; // get context out of overlay canvas const overlayContext = this.overlayCanvas.getContext('2d', { alpha: true }); if (overlayContext === null) { throw new Error(Msg.CANVAS_CONTEXT); } this.overlayContext = overlayContext; // make output canvas this.outputCanvas = document.createElement('canvas'); this.outputCanvas.width = startingWidth; this.outputCanvas.height = startingHeight; // get context out of output canvas const outputContext = this.outputCanvas.getContext('2d', { alpha: true }); if (outputContext === null) { throw new Error(Msg.CANVAS_CONTEXT); } outputContext.fontKerning = 'normal'; this.outputContext = outputContext; } get overlayEnabled() { return this._overlayEnabled; } set overlayEnabled(overlayEnabled) { if (overlayEnabled === this._overlayEnabled) { return; } this._overlayEnabled = overlayEnabled; this.updateOutputCanvas(); } updateOutputCanvas() { const width = this.canvas.width; const height = this.canvas.height; // generate overlay if (this._overlayEnabled) { // draw damage this.overlayContext.clearRect(0, 0, width, height); this.overlayContext.globalCompositeOperation = 'lighter'; const expired = []; const now = Date.now(); for (const event of this.events) { const animDelta = (now - event[1]) / DEBUG_EVENT_TIMEOUT; if (animDelta >= 1) { expired.push(event); continue; } this.overlayContext.fillStyle = `rgba(${event[2] ? 0 : 255}, 0, ${event[2] ? 255 : 0}, ${1 - animDelta})`; if (this.preventAtlasBleeding && !event[2]) { const rect = event[0]; this.overlayContext.fillRect(rect[0] + 1, rect[1] + 1, rect[2], rect[3]); } else { this.overlayContext.fillRect(...event[0]); } } for (const event of expired) { this.events.delete(event); } // update overlay dirty flag this._overlayDirty = this.events.size > 0; } // merge internal canvas and overlay this.outputContext.globalAlpha = 1; this.outputContext.globalCompositeOperation = 'copy'; this.outputContext.drawImage(this.canvas, 0, 0); if (this._overlayEnabled) { // apply overlay this.outputContext.globalAlpha = 0.5; this.outputContext.globalCompositeOperation = 'source-over'; this.outputContext.drawImage(this.overlayCanvas, 0, 0); // paint PPS text this.outputContext.save(); this.outputContext.globalAlpha = 1; this.outputContext.globalCompositeOperation = 'source-over'; this.outputContext.lineWidth = 4; this.outputContext.textBaseline = 'top'; this.outputContext.font = '16px sans-serif'; this.outputContext.strokeStyle = 'black'; this.outputContext.fillStyle = 'white'; const xy = this.preventAtlasBleeding ? 9 : 8; this.outputContext.strokeText(this._ppsText, xy, xy); this.outputContext.fillText(this._ppsText, xy, xy); this.outputContext.restore(); } } addDebugEvent(rect, isMerged) { // intercept dirty rectangles const event = [[...rect], Date.now(), isMerged]; if (this.events) { this.events.add(event); this._overlayDirty = true; } else { // HACK CanvasViewport calls pushDirtyRect in the constructor, so we // might not be ready when this method is called. queue it up with a // setTimeout setTimeout(() => { this.events.add(event); this._overlayDirty = true; }, 0); } } pushDirtyRects(rects) { for (const rect of rects) { this.addDebugEvent(rect, false); } super.pushDirtyRects(rects); } pushDirtyRect(rect) { this.addDebugEvent(rect, false); super.pushDirtyRect(rect); } resolveLayout() { const oldW = this.canvas.width; const oldH = this.canvas.height; const wasResized = super.resolveLayout(); const newW = this.canvas.width; const newH = this.canvas.height; if (oldW !== newW || oldH !== newH) { this.overlayCanvas.width = newW; this.overlayCanvas.height = newH; this.outputCanvas.width = newW; this.outputCanvas.height = newH; } return wasResized; } paintToInternal() { const paintDirtyRects = super.paintToInternal(); if (paintDirtyRects) { this._paintCounter++; for (const rect of paintDirtyRects) { this.addDebugEvent(rect, true); } } if (this._overlayEnabled) { // update PPS text const now = Date.now(); const elapsed = now - this._lastPPSMeasurement; if (elapsed > 1000) { const pps = this._paintCounter * 1000 / elapsed; this._lastPPSMeasurement = now; this._ppsText = `PPS: ${pps.toFixed(4)} (${this._paintCounter} since last)`; this._overlayDirty = true; this._paintCounter = 0; } // paint overlay if (paintDirtyRects || this._overlayDirty) { this.updateOutputCanvas(); return [[0, 0, this.canvas.width, this.canvas.height]]; } else { return null; } } else { if (paintDirtyRects) { this.updateOutputCanvas(); } return paintDirtyRects; } } } //# sourceMappingURL=DebuggableCanvasViewport.js.map