cloud-game/web/js/stats.js
Sergey Stepanov 7ee98c1b03 Add keyboard and mouse support
Keyboard and mouse controls will now work if you use the kbMouseSupport parameter in the config for Libretro cores. Be aware that capturing mouse and keyboard controls properly is only possible in fullscreen mode.

Note: In the case of DOSBox, a virtual filesystem handler is not yet implemented, thus each game state will be shared between all rooms (DOS game instances) of CloudRetro.
2024-08-02 11:04:44 +03:00

242 lines
5.8 KiB
JavaScript

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:
* `<div><div>LABEL</div><span>VALUE</span>[<span><canvas/><span>]</div>`
*
* @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,
}