jdebugcanvasjs
Version:
Nice features-rich debugging utility for canvas rendering
446 lines (393 loc) • 13.8 kB
JavaScript
/**
* Interface that allows to debug canvas content easily as you develop your app, offering a couple
* of interresting features:
*
* - `preview`: In case you're using `OffscreenCanvas` or your canvas simply invisible, this utiliy
* helps you preview their content.
* - `settings`: You can adjust the debugger settings on the go, providing more comfortable usage.
* - `logging`: Also offers the ability to log almost anything on the fly.
*/
class JDebugCanvas {
/**
* Debugging canvas
* @type {HTMLCanvasElement}
*/
#canvas = null;
/**
* Rendering context
* @type {CanvasRenderingContext2D}
*/
ctx = null;
/**
* Flag of whether debugger is opened or not, accessibly via `opened` attribute
* @type {boolean}
*/
#on = false;
#logger = {
/**
* Lines rendered to the console
* @type {Array<Array<string>>}
*/
lines: [],
visibleLineCount: Math.floor(200 / 15),
lineHeight: 15,
fontSize: 12,
stackSize: 50
}
/**
* Holds the current debugger state
* - `auto`: automatic rendering on the moment
* - `canvas`: when rendering sources
* - `console`: when using console
* @type {"canvas" | "console" | "auto"}
*/
#current = "auto";
/**
* Debugger settings
*/
settings = {
/**
* Autoshow is whether to auto open the debugger when used or not
* @type {boolean}
*/
autoshow: true,
width: 200,
height: 200,
cpos: null,
bg: null,
position: ['top-left', 'bottom-left']
}
/**
* Used to handle timeouts
* @type {number}
*/
#to = null;
/**
* Last rendered source, useful for performance!
*/
#lastRendered = {
src: null,
dimensions: {}
}
constructor() {
// creating the canvas
this.#canvas = document.createElement('canvas');
this.#canvas.classList.add('debugging-canvas');
this.#canvas.width = 200;
this.#canvas.height = 200;
// we will set some default style
this.#canvas.style.position = "fixed";
this.#canvas.style.width = "200px";
this.#canvas.style.height = "200px";
this.#canvas.style.border = "1px solid white";
this.#canvas.style.outline = "3px solid black";
document.body.append(this.#canvas);
this.ctx = this.#canvas.getContext('2d');
this.ctx.imageSmoothingEnabled = false;
this.#bindEvents();
this.setSetting('position', 'top-right');
return this;
}
/**
* Returns whether debugger is opened or not
*/
get opened() {
return this.#on;
}
/**
* Currently active debugging mode
* @type {"canvas" | "console" | "auto"}
*/
get mode() {
return this.#current;
}
/**
* Attach some event listeners
*/
#bindEvents() {
let _this = this;
this.#canvas.addEventListener('click', function () {
_this.close();
})
this.#canvas.addEventListener('mouseenter', function () {
_this.repos();
})
}
/**
* Render the source `src` to the debugger
* @param {CanvasRenderingContext2D} src
* @returns {JDebugCanvas}
*/
render(src) {
if (this.#current == "console") return; // stop silently
let ctx = this.ctx,
_this = this;
let dim = { x: 0, y: 0, width: ctx.canvas.width, height: ctx.canvas.height };
if (this.settings.autoshow) this.open();
// calculate bounds
if (src) {
if (this.#lastRendered.src === src)
dim = this.#lastRendered.dimensions;
else {
this.#lastRendered = {
src: src,
dimensions: this.#calculateBounds(src)
}
dim = this.#lastRendered.dimensions;
}
}
// draw content
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (typeof this.settings.bg == "string") {
// rendering bg color
ctx.fillStyle = this.settings.bg;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
if (src)
ctx.drawImage(src.canvas, dim.x, dim.y, dim.width, dim.height);
if (this.#to) clearTimeout(this.#to);
if (this.settings.autoshow === true)
this.#to = setTimeout(function () {
_this.close();
}, 5000)
return this;
}
/**
* Renders an interactive console for quick logging and other features
*/
#playConsole() {
let ctx = this.ctx;
let { lines, visibleLineCount, lineHeight, fontSize } = this.#logger;
// black console
ctx.fillStyle = "black";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// setting font
ctx.font = `${fontSize}px consolas`;
ctx.fillStyle = "rgb(240,240,240)";
let cwid = Math.floor(fontSize * .6); // the rule to get the width of consolas font character
// displaying the last visible lines
let y = this.#canvas.height; // line height is 30px by default
for (let i = lines.length - 1; i > (lines.length - 1 - visibleLineCount); i--) {
const line = lines[i];
if (!line) continue;
// getting line width
let lwid = line.length * cwid;
// dividing the line width by canvas available width to get how many line to display
let lcount = Math.ceil(lwid / (ctx.canvas.width - 5));
let my_y = (y -= (lineHeight * lcount)); // make space for lines
// displaying lines
let x = 0; // line start at x=0
line.forEach((c, j) => {
if (x + 5 > ctx.canvas.width) {
x = 0;
my_y += lineHeight;
}
ctx.fillText(c, x, my_y);
x += cwid; // next character
})
}
if (this.#to) clearTimeout(this.#to);
if (this.settings.autoshow === true)
this.#to = setTimeout(function () {
_this.close();
}, 5000)
}
/**
* Log some text out to the debugger, booleans, strings, Objects..
* @param {...any} vars
* @returns {JDebugCanvas}
*/
log(...vars) {
this.#logger.lines.push((">> " + this.#stringify(...vars)).split(""));
if (this.#logger.lines.length > this.#logger.stackSize)
this.#logger.lines.splice(1, 1)
if (this.#current == "canvas") return;
// it's added to the log but not visible
this.#playConsole();
return this;
}
/**
* Switch the debugger mode
* @param {"canvas" | "console" | "auto"} mode
* @returns {JDebugCanvas}
*/
switch(mode) {
if (mode == "canvas")
this.#current = "canvas";
else if (mode == "console")
this.#current = "console";
else if (mode == "auto")
this.#current = "auto";
return this;
}
/**
* Return an object representation for variables ready to be output in the console
* @param {...any} vars
* @returns {string}
*/
#stringify(...vars) {
let final = [];
for (let i = 0; i < vars.length; i++) {
const v = vars[i];
// if(typeof v === "")
if (["boolean", "string", "number", "bigint"].includes(typeof v)) {
final.push(v);
} else if (Array.isArray(v)) {
final.push("[");
final.push(this.#stringify(...v).split(" ").join(","));
final.push("]");
} else if (typeof v == "function") {
final.push("fn " + v.name + "()");
} else if (v instanceof Date) {
final.push(v.toString());
} else if (Object.prototype.toString.call(v) == "[object Object]") {
final.push("{");
let obj = [];
for (const k in v) {
const c = v[k];
obj.push(` ${k}: ${c.toString()}`);
}
final.push(obj.join(','))
final.push("}");
} else {
final.push(v.constructor.name); // for classes getting the constructor name
}
}
return final.join(" ");
}
/**
* Reposition the canvas, mainly used internally to avoid interrupting user
* exploring your app.
* @param {number} index
* @returns {JDebugCanvas}
*/
repos(index) {
if (typeof index != "number") index = this.settings.cpos === 0 ? 1 : 0; // switch between 0 & 1
let position = this.settings.position[index];
switch (position) {
case "top-left":
this.#canvas.style.left = "20px";
this.#canvas.style.top = "20px";
this.#canvas.style.right = "";
this.#canvas.style.bottom = "";
break;
case "top-right":
this.#canvas.style.left = "";
this.#canvas.style.top = "20px";
this.#canvas.style.right = "20px";
this.#canvas.style.bottom = "";
break;
case "bottom-left":
this.#canvas.style.left = "20px";
this.#canvas.style.top = "";
this.#canvas.style.right = "";
this.#canvas.style.bottom = "20px";
break;
case "bottom-right":
this.#canvas.style.left = "";
this.#canvas.style.top = "";
this.#canvas.style.right = "20px";
this.#canvas.style.bottom = "20px";
break;
}
this.settings.cpos = index
return this;
}
/**
* Modify debugger settings
* @param {"position" | "size" | "autoshow" | "bg"} param
* @param {*} value
* @returns {JDebugCanvas}
*/
setSetting(param, value) {
if (param === "position") {
switch (value) {
case "top-left":
this.settings.position = ['top-left', 'bottom-left'];
this.settings.cpos = 1;
break;
case "top-right":
this.settings.position = ['top-right', 'bottom-right'];
this.settings.cpos = 1;
break;
case "bottom-left":
this.settings.position = ['bottom-left', 'top-left'];
this.settings.cpos = 1;
break;
case "bottom-right":
this.settings.position = ['bottom-right', 'top-right'];
this.settings.cpos = 1;
break;
}
this.repos(); // apply position
} else if (param == "size") {
this.#canvas.style.width = (this.#canvas.width = this.settings.width = value[0]) + "px";
this.#canvas.style.height = (this.#canvas.height = this.settings.height = value[1]) + "px";
this.ctx.imageSmoothingEnabled = false;
this.#lastRendered.src = null; // to reset dimensions
this.#logger.visibleLineCount = Math.floor(value[1] / this.#logger.lineHeight);
} else if (param == "autoshow")
this.settings.autoshow = typeof value == "boolean" ? value : this.settings.autoshow;
else if (param == "bg") {
this.settings.bg = value;
this.render();
}
return this;
}
/**
* Open the debugger
* @returns {JDebugCanvas}
*/
open() {
this.#on = true;
this.#canvas.style.display = "inline-block";
return this;
}
/**
* Close the debugger
* @returns {JDebugCanvas}
*/
close() {
this.#on = false;
this.#canvas.style.display = "none";
return this;
}
/**
* Toggle the debugger
* @returns {JDebugCanvas}
*/
toggle() {
if (this.#on) this.close();
else this.open();
return this;
}
/**
* Get the src to be renderer dimensions to draw on
* @param {CanvasRenderingContext2D} src
* @returns {{ x: number ,y: number ,width: number ,height: number }}
*/
#calculateBounds(src) {
let canvas = src.canvas;
let width = 0,
height = 0,
x = 0,
y = 0;
const { width: pwidth, height: pheight } = this.ctx.canvas;
const { width: owidth, height: oheight } = canvas;
if (owidth >= oheight) {
let z = pwidth / owidth;
width = pwidth;
height = oheight * z;
x = 0;
y = (pheight / 2) - (height / 2);
} else {
let z = pheight / oheight;
height = pheight;
width = owidth * z;
y = 0;
x = (pwidth / 2) - (width / 2);
}
return { x, y, width, height }
}
}
export default JDebugCanvas;