lazy-widgets
Version:
Typescript retained mode GUI for the HTML canvas API
183 lines • 7.36 kB
JavaScript
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