control/ShowFps.tstypescript
import { Button } from "../ui/Button";
import { Control } from "./Control";
import type { IControlParams } from "./Control";

const UPDATE_INTERVAL = 500;

const MAX_MEASURE_TIME = UPDATE_INTERVAL * 4;

/**
 * Frames per second(FPS) display control. It looks like a map button in the top right
 * corner and shows the current frame rate instead of an icon.
 *
 * Note that the rate is measured by the control itself and not taken from
 * {@link Handler#deltaTime}, which is clamped and does not represent the real frame time.
 * @class
 * @extends {Control}
 */
export class ShowFps extends Control {
    protected _btn: Button | null;
    protected _text: HTMLElement | null;

    protected _frames: number;
    protected _measureStart: number;

    constructor(options: IControlParams = {}) {
        super({ name: "ShowFps", ...options });

        this._btn = null;
        this._text = null;
        this._frames = 0;
        this._measureStart = 0;
    }

    public override oninit() {
        this._btn = new Button({
            classList: ["og-map-button", "og-fps-button"],
            title: "Frames per second",
            text: "--"
        });

        this._btn.appendTo(this.renderer!.topRightContainer());
        this._text = this._btn.$text;

        this._frames = 0;
        this._measureStart = window.performance.now();

        this.renderer!.events.on("predraw", this._draw);
    }

    public override onremove() {
        this.renderer && this.renderer.events.off("predraw", this._draw);

        this._btn && this._btn.remove();
        this._btn = null;
        this._text = null;
    }

    protected _draw = () => {
        if (!this._text) return;

        this._frames++;

        let now = window.performance.now();
        let elapsed = now - this._measureStart;

        if (elapsed >= UPDATE_INTERVAL) {
            if (elapsed <= MAX_MEASURE_TIME) {
                this._text.innerText = ((this._frames * 1000) / elapsed).toFixed(0);
            }
            this._frames = 0;
            this._measureStart = now;
        }
    };
}