import { sub, HELP_OVERLAY_TOGGLED } from 'event'; const _modules = []; let tempHide = false; // internal rendering stuff const fps = 30; let time = 0; let active = false; // !to add connection drop notice const statsOverlayEl = document.getElementById('stats-overlay'); /** * The graph element. */ const graph = (parent, opts = { historySize: 60, width: 60 * 2 + 2, height: 20, pad: 4, scale: 1, style: { barColor: '#9bd914', barFallColor: '#c12604' } }) => { const _canvas = document.createElement('canvas'); const _context = _canvas.getContext('2d'); let data = []; _canvas.setAttribute('class', 'graph'); _canvas.width = opts.width * opts.scale; _canvas.height = opts.height * opts.scale; _context.scale(opts.scale, opts.scale); _context.imageSmoothingEnabled = false; _context.fillStyle = opts.fillStyle; if (parent) parent.append(_canvas); // bar size const barWidth = Math.round(_canvas.width / opts.scale / opts.historySize); const barHeight = Math.round(_canvas.height / opts.scale); let maxN = 0, minN = 0; const max = () => maxN const get = () => _canvas const add = (value) => { if (data.length > opts.historySize) data.shift(); data.push(value); render(); } /** * Draws a bar graph on the canvas. * * @example * +-------+ +-------+ +---------+ * | | |+---+ | |+---+ | * | | |||||| | ||||||+---+ * | | |||||| | ||||||||||| * +-------+ +----+--+ +---------+ * [] [3] [3, 2] */ const render = () => { _context.clearRect(0, 0, _canvas.width, _canvas.height); maxN = data[0] || 1; minN = 0; for (let k = 1; k < data.length; k++) { if (data[k] > maxN) maxN = data[k]; if (data[k] < minN) minN = data[k]; } for (let j = 0; j < data.length; j++) { let x = j * barWidth, y = (barHeight - opts.pad * 2) * (data[j] - minN) / (maxN - minN) + opts.pad; const color = j > 0 && data[j] > data[j - 1] ? opts.style.barFallColor : opts.style.barColor; drawRect(x, barHeight - Math.round(y), barWidth, barHeight, color); } } const drawRect = (x, y, w, h, color = opts.style.barColor) => { _context.fillStyle = color; _context.fillRect(x, y, w, h); } const clear = () => { data = []; render(); } return {add, get, max, render, clear} } /** * Get cached module UI. * * HTML: * `
LABEL
VALUE[]
` * * @param label The name of the stat to show. * @param nan A value to show when zero. * @param withGraph True if to draw a graph. * @param postfix Supposed to be the name of the stat passed as a function. * @param cl Class of the UI div element. * @returns {{el: HTMLDivElement, update: function}} */ const moduleUi = (label = '', nan = '', withGraph = false, postfix = () => 'ms', cl = '') => { const ui = document.createElement('div'), _label = document.createElement('div'), _value = document.createElement('span'); ui.append(_label, _value); cl && ui.classList.add(cl) let postfix_ = postfix; let _graph; if (withGraph) { const _container = document.createElement('span'); ui.append(_container); _graph = graph(_container); } _label.innerHTML = label; const withPostfix = (value) => (postfix_ = value); const update = (value) => { if (_graph) _graph.add(value); // 203 (333) ms _value.textContent = `${value < 1 ? nan : value}${_graph ? `(${_graph.max()}) ` : ''}${postfix_(value)}`; } const clear = () => { _graph && _graph.clear(); } return {el: ui, update, withPostfix, clear} } const modules = (fn, force = true) => _modules.forEach(m => (force || m.get) && fn(m)) const module = (mod) => { mod = { val: 0, enable: () => ({}), ...mod, _disable: function () { // mod.val = 0; mod.disable && mod.disable(); mod.mui && mod.mui.clear(); }, ...(mod.mui && { get: () => mod.mui.el, render: () => mod.mui.update(mod.val) }) } mod.init?.(); _modules.push(mod); modules(m => m.get && statsOverlayEl.append(m.get()), false); } const enable = () => { active = true; modules(m => m.enable()) render(); draw(); _show(); }; function draw(timestamp) { if (!active) return; const time_ = time + 1000 / fps; if (timestamp > time_) { time = timestamp; render(); } requestAnimationFrame(draw); } const disable = () => { active = false; modules(m => m._disable()); _hide(); } const _show = () => (statsOverlayEl.style.visibility = 'visible'); const _hide = () => (statsOverlayEl.style.visibility = 'hidden'); /** * Handles help overlay toggle event. * Workaround for a not normal app layout layering. * * !to remove when app layering is fixed * * @param {Object} overlay Overlay data. * @param {boolean} overlay.shown A flag if the overlay is being currently showed. */ const onHelpOverlayToggle = (overlay) => { if (statsOverlayEl.style.visibility === 'visible' && overlay.shown && !tempHide) { _hide(); tempHide = true; } else { if (tempHide) { _show(); tempHide = false; } } } const render = () => modules(m => m.render(), false); sub(HELP_OVERLAY_TOGGLED, onHelpOverlayToggle) /** * App statistics module. */ export const stats = { toggle: () => active ? disable() : enable(), set modules(m) { m && m.forEach(mod => module(mod)) }, mui: moduleUi, }