UNPKG

jdebugcanvasjs

Version:

Nice features-rich debugging utility for canvas rendering

446 lines (393 loc) 13.8 kB
/** * 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;