mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 23:14:39 +00:00
c5484696f7
## Description: Display an FPS monitor to track performance on each new feature. It only appears in the development environment, positioned at the top center to remain visible—especially on mobile. The display can be closed via a close button. I already use it to evaluate my other PR on low end device. <img width="1126" height="845" alt="image" src="https://github.com/user-attachments/assets/a7197572-6aea-47df-9dd2-e84947c7aee0" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I have read and accepted the CLA aggreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: devalnor
269 lines
6.4 KiB
TypeScript
269 lines
6.4 KiB
TypeScript
import { LitElement, css, html } from "lit";
|
||
import { customElement, property, state } from "lit/decorators.js";
|
||
import { EventBus } from "../../../core/EventBus";
|
||
import { UserSettings } from "../../../core/game/UserSettings";
|
||
import { TogglePerformanceOverlayEvent } from "../../InputHandler";
|
||
import { Layer } from "./Layer";
|
||
|
||
@customElement("fps-display")
|
||
export class FPSDisplay extends LitElement implements Layer {
|
||
@property({ type: Object })
|
||
public eventBus!: EventBus;
|
||
|
||
@property({ type: Object })
|
||
public userSettings!: UserSettings;
|
||
|
||
@state()
|
||
private currentFPS: number = 0;
|
||
|
||
@state()
|
||
private averageFPS: number = 0;
|
||
|
||
@state()
|
||
private frameTime: number = 0;
|
||
|
||
@state()
|
||
private isVisible: boolean = false;
|
||
|
||
@state()
|
||
private isDragging: boolean = false;
|
||
|
||
@state()
|
||
private position: { x: number; y: number } = { x: 50, y: 20 }; // Percentage values
|
||
|
||
private frameCount: number = 0;
|
||
private lastTime: number = 0;
|
||
private frameTimes: number[] = [];
|
||
private fpsHistory: number[] = [];
|
||
private lastSecondTime: number = 0;
|
||
private framesThisSecond: number = 0;
|
||
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
|
||
|
||
static styles = css`
|
||
.fps-display {
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
z-index: 9999;
|
||
user-select: none;
|
||
cursor: move;
|
||
transition: none;
|
||
}
|
||
|
||
.fps-display.dragging {
|
||
cursor: grabbing;
|
||
transition: none;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.fps-line {
|
||
margin: 2px 0;
|
||
}
|
||
|
||
.fps-good {
|
||
color: #4ade80; /* green-400 */
|
||
}
|
||
|
||
.fps-warning {
|
||
color: #fbbf24; /* amber-400 */
|
||
}
|
||
|
||
.fps-bad {
|
||
color: #f87171; /* red-400 */
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
border-radius: 4px;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 1;
|
||
user-select: none;
|
||
pointer-events: auto;
|
||
}
|
||
`;
|
||
|
||
constructor() {
|
||
super();
|
||
}
|
||
|
||
init() {
|
||
this.eventBus.on(TogglePerformanceOverlayEvent, () => {
|
||
this.userSettings.togglePerformanceOverlay();
|
||
});
|
||
}
|
||
|
||
setVisible(visible: boolean) {
|
||
this.isVisible = visible;
|
||
}
|
||
|
||
private handleClose() {
|
||
this.userSettings.togglePerformanceOverlay();
|
||
}
|
||
|
||
private handleMouseDown = (e: MouseEvent) => {
|
||
// Don't start dragging if clicking on close button
|
||
if ((e.target as HTMLElement).classList.contains("close-button")) {
|
||
return;
|
||
}
|
||
|
||
this.isDragging = true;
|
||
this.dragStart = {
|
||
x: e.clientX - this.position.x,
|
||
y: e.clientY - this.position.y,
|
||
};
|
||
|
||
document.addEventListener("mousemove", this.handleMouseMove);
|
||
document.addEventListener("mouseup", this.handleMouseUp);
|
||
e.preventDefault();
|
||
};
|
||
|
||
private handleMouseMove = (e: MouseEvent) => {
|
||
if (!this.isDragging) return;
|
||
|
||
const newX = e.clientX - this.dragStart.x;
|
||
const newY = e.clientY - this.dragStart.y;
|
||
|
||
// Convert to percentage of viewport
|
||
const viewportWidth = window.innerWidth;
|
||
const viewportHeight = window.innerHeight;
|
||
|
||
this.position = {
|
||
x: Math.max(0, Math.min(viewportWidth - 100, newX)), // Keep within viewport bounds
|
||
y: Math.max(0, Math.min(viewportHeight - 100, newY)),
|
||
};
|
||
|
||
this.requestUpdate();
|
||
};
|
||
|
||
private handleMouseUp = () => {
|
||
this.isDragging = false;
|
||
document.removeEventListener("mousemove", this.handleMouseMove);
|
||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||
};
|
||
|
||
updateFPS(frameDuration: number) {
|
||
this.isVisible = this.userSettings.performanceOverlay();
|
||
|
||
if (!this.isVisible) return;
|
||
|
||
const now = performance.now();
|
||
|
||
// Initialize timing on first call
|
||
if (this.lastTime === 0) {
|
||
this.lastTime = now;
|
||
this.lastSecondTime = now;
|
||
return;
|
||
}
|
||
|
||
const deltaTime = now - this.lastTime;
|
||
|
||
// Track frame times for current FPS calculation (last 60 frames)
|
||
this.frameTimes.push(deltaTime);
|
||
if (this.frameTimes.length > 60) {
|
||
this.frameTimes.shift();
|
||
}
|
||
|
||
// Calculate current FPS based on average frame time
|
||
if (this.frameTimes.length > 0) {
|
||
const avgFrameTime =
|
||
this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
|
||
this.currentFPS = Math.round(1000 / avgFrameTime);
|
||
this.frameTime = Math.round(avgFrameTime);
|
||
}
|
||
|
||
// Track FPS for 60-second average
|
||
this.framesThisSecond++;
|
||
|
||
// Update every second
|
||
if (now - this.lastSecondTime >= 1000) {
|
||
this.fpsHistory.push(this.framesThisSecond);
|
||
if (this.fpsHistory.length > 60) {
|
||
this.fpsHistory.shift();
|
||
}
|
||
|
||
// Calculate 60-second average
|
||
if (this.fpsHistory.length > 0) {
|
||
this.averageFPS = Math.round(
|
||
this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length,
|
||
);
|
||
}
|
||
|
||
this.framesThisSecond = 0;
|
||
this.lastSecondTime = now;
|
||
}
|
||
|
||
this.lastTime = now;
|
||
this.frameCount++;
|
||
|
||
this.requestUpdate();
|
||
}
|
||
|
||
shouldTransform(): boolean {
|
||
return false;
|
||
}
|
||
|
||
private getFPSColor(fps: number): string {
|
||
if (fps >= 55) return "fps-good";
|
||
if (fps >= 30) return "fps-warning";
|
||
return "fps-bad";
|
||
}
|
||
|
||
render() {
|
||
if (!this.isVisible) {
|
||
return html``;
|
||
}
|
||
|
||
const style = `
|
||
left: ${this.position.x}px;
|
||
top: ${this.position.y}px;
|
||
transform: none;
|
||
`;
|
||
|
||
return html`
|
||
<div
|
||
class="fps-display ${this.isDragging ? "dragging" : ""}"
|
||
style="${style}"
|
||
@mousedown="${this.handleMouseDown}"
|
||
>
|
||
<button class="close-button" @click="${this.handleClose}">×</button>
|
||
<div class="fps-line">
|
||
FPS:
|
||
<span class="${this.getFPSColor(this.currentFPS)}"
|
||
>${this.currentFPS}</span
|
||
>
|
||
</div>
|
||
<div class="fps-line">
|
||
Avg (60s):
|
||
<span class="${this.getFPSColor(this.averageFPS)}"
|
||
>${this.averageFPS}</span
|
||
>
|
||
</div>
|
||
<div class="fps-line">
|
||
Frame:
|
||
<span class="${this.getFPSColor(1000 / this.frameTime)}"
|
||
>${this.frameTime}ms</span
|
||
>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|