diff --git a/party-stage/src/scene/fps-counter.js b/party-stage/src/scene/fps-counter.js new file mode 100644 index 0000000..bb94e2f --- /dev/null +++ b/party-stage/src/scene/fps-counter.js @@ -0,0 +1,205 @@ +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class FPSCounter extends SceneFeature { + constructor() { + super(); + this.frames = 0; + this.timeAccumulator = 0; + this.fpsElement = null; + this.textElement = null; + this.canvas = null; + this.ctx = null; + this.history = []; + this.memTextElement = null; + this.memCanvas = null; + this.memCtx = null; + this.memHistory = []; + sceneFeatureManager.register(this); + } + + init() { + // Create the FPS display container + this.fpsElement = document.createElement('div'); + Object.assign(this.fpsElement.style, { + position: 'absolute', + top: '0', + right: '0', + zIndex: '10000', + padding: '5px', + background: 'rgba(0, 0, 0, 0.5)', + pointerEvents: 'none', + userSelect: 'none', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end' + }); + + // Text Display + this.textElement = document.createElement('div'); + Object.assign(this.textElement.style, { + color: '#00ff00', + fontFamily: 'monospace', + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '2px' + }); + this.textElement.innerText = 'FPS: --'; + this.fpsElement.appendChild(this.textElement); + + // Graph Canvas + this.canvas = document.createElement('canvas'); + this.canvas.width = 100; + this.canvas.height = 40; + Object.assign(this.canvas.style, { + width: '100px', + height: '40px', + background: '#222', + border: '1px solid #444' + }); + this.ctx = this.canvas.getContext('2d'); + this.fpsElement.appendChild(this.canvas); + + // Memory Text Display + this.memTextElement = document.createElement('div'); + Object.assign(this.memTextElement.style, { + color: '#00ffff', + fontFamily: 'monospace', + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '2px', + marginTop: '5px' + }); + this.memTextElement.innerText = 'MEM: --'; + this.fpsElement.appendChild(this.memTextElement); + + // Memory Graph Canvas + this.memCanvas = document.createElement('canvas'); + this.memCanvas.width = 100; + this.memCanvas.height = 40; + Object.assign(this.memCanvas.style, { + width: '100px', + height: '40px', + background: '#222', + border: '1px solid #444' + }); + this.memCtx = this.memCanvas.getContext('2d'); + this.fpsElement.appendChild(this.memCanvas); + + document.body.appendChild(this.fpsElement); + + this.history = new Array(this.canvas.width).fill(0); + this.memHistory = new Array(this.memCanvas.width).fill(0); + } + + update(deltaTime) { + this.frames++; + this.timeAccumulator += deltaTime; + + const segment = 0.25; + if (this.timeAccumulator >= segment) { + const fps = Math.round(this.frames / this.timeAccumulator); + if (this.textElement) { + this.textElement.innerText = `FPS: ${fps}`; + if (fps >= 58) { + this.textElement.style.color = '#00ff00'; + } else if (fps >= 55) { + this.textElement.style.color = '#ffff00'; + } else { + this.textElement.style.color = '#ff0000'; + } + } + + // Update Graph + this.history.push(fps); + if (this.history.length > this.canvas.width) { + this.history.shift(); + } + this.drawGraph(); + + // Update Memory + if (performance && performance.memory) { + const mem = performance.memory.usedJSHeapSize / 1048576; // MB + if (this.memTextElement) this.memTextElement.innerText = `MEM: ${Math.round(mem)} MB`; + + this.memHistory.push(mem); + if (this.memHistory.length > this.memCanvas.width) { + this.memHistory.shift(); + } + this.drawMemGraph(); + } else { + if (this.memTextElement) this.memTextElement.style.display = 'none'; + if (this.memCanvas) this.memCanvas.style.display = 'none'; + } + + this.frames = 0; + this.timeAccumulator = 0; + } + } + + drawGraph() { + if (!this.ctx) return; + const ctx = this.ctx; + const w = this.canvas.width; + const h = this.canvas.height; + const maxFps = 70; + + ctx.clearRect(0, 0, w, h); + + // Draw 60 FPS reference line + const y60 = h - (60 / maxFps) * h; + ctx.beginPath(); + ctx.strokeStyle = '#555'; + ctx.moveTo(0, y60); + ctx.lineTo(w, y60); + ctx.stroke(); + + // Draw Graph + ctx.lineWidth = 1.5; + + for (let i = 0; i < this.history.length - 1; i++) { + const val = this.history[i + 1]; + const x1 = i; + const y1 = h - (this.history[i] / maxFps) * h; + const x2 = i + 1; + const y2 = h - (val / maxFps) * h; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + + if (val >= 58) ctx.strokeStyle = '#00ff00'; + else if (val >= 55) ctx.strokeStyle = '#ffff00'; + else ctx.strokeStyle = '#ff0000'; + + ctx.stroke(); + } + } + + drawMemGraph() { + if (!this.memCtx) return; + const ctx = this.memCtx; + const w = this.memCanvas.width; + const h = this.memCanvas.height; + // Auto-scale max memory + const maxMem = Math.max(...this.memHistory, 100) * 1.1; + + ctx.clearRect(0, 0, w, h); + + // Draw Graph + ctx.beginPath(); + ctx.strokeStyle = '#00ffff'; + ctx.lineWidth = 1.5; + + for (let i = 0; i < this.memHistory.length; i++) { + const val = this.memHistory[i]; + const x = i; + const y = h - (val / maxMem) * h; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + } +} + +new FPSCounter(); \ No newline at end of file diff --git a/party-stage/src/scene/root.js b/party-stage/src/scene/root.js index 50e7622..a7cd076 100644 --- a/party-stage/src/scene/root.js +++ b/party-stage/src/scene/root.js @@ -20,6 +20,7 @@ import { ProjectionScreen } from './projection-screen.js'; import { StageLasers } from './stage-lasers.js'; import { ConfigUI } from './config-ui.js'; import { StageLightBars } from './stage-light-bars.js'; +import { FPSCounter } from './fps-counter.js'; // Scene Features ^^^ // --- Scene Modeling Function ---