Created
July 26, 2023 08:36
-
-
Save schickling/1e10eede72f5a183a6d4ebe6dd94ead3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
const PR = Math.round(window.devicePixelRatio || 1) | |
const FRAME_BAR_WIDTH = 2 | |
export type FPSMeterProps = { | |
width?: number | |
height?: number | |
systemFps?: number | |
className?: string | |
onClick?: () => void | |
} | |
const FRAME_HIT = 1 | |
const FRAME_MISS = 0 | |
const FRAME_UNINITIALIZED = -1 | |
// TODO handle frames differently if browser went to background | |
export const FPSMeter: React.FC<FPSMeterProps> = ({ width = 120, height = 30, systemFps = 60, className, onClick }) => { | |
const adjustedWidth = Math.round(width * PR) | |
const adjustedHeight = Math.round(height * PR) | |
const numberOfVisibleFrames = Math.floor(adjustedWidth / FRAME_BAR_WIDTH) | |
const resolutionInMs = 1000 / systemFps! | |
// NOTE larger values can result in more items taken from array than it has and makes stuff go boom | |
const numberOfSecondsForAverageFps = 2 | |
// Depending on bar size and screen refresh rate, it can happen that the count of visible frames | |
// is smaller than the count of frames used for calculating the average FPS. | |
// To avoid this case, we force the number of frames used to calculate average FPS to always be less | |
// than the number of visible frames. | |
const numberOfFramesForAverageFps = Math.min(numberOfSecondsForAverageFps * systemFps, numberOfVisibleFrames) | |
const animationFrameRef = React.useRef<number | undefined>(undefined) | |
const canvasRef = React.useCallback( | |
(canvas: HTMLCanvasElement | null) => { | |
if (animationFrameRef.current !== undefined) { | |
window.cancelAnimationFrame(animationFrameRef.current) | |
} | |
if (canvas === null) return | |
if (numberOfFramesForAverageFps > numberOfVisibleFrames) { | |
throw new Error( | |
`numberOfFramesForAverageFps (${numberOfFramesForAverageFps}) must be smaller than numberOfVisibleFrames (${numberOfVisibleFrames}). Either increase the width or increase the resolutionInMs.`, | |
) | |
} | |
// eslint-disable-next-line unicorn/no-new-array | |
const frames: number[] = new Array(numberOfVisibleFrames).fill(FRAME_UNINITIALIZED) | |
const ctx = canvas.getContext('2d')! | |
const draw = () => { | |
ctx.clearRect(0, 0, adjustedWidth, adjustedHeight) | |
for (let i = 0; i < numberOfVisibleFrames; i++) { | |
const frameHit = frames[i]! | |
if (frameHit === FRAME_UNINITIALIZED) continue | |
const x = i * FRAME_BAR_WIDTH | |
ctx.fillStyle = frameHit > 0 ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 0, 0, 1)' | |
ctx.fillRect(x, adjustedHeight, FRAME_BAR_WIDTH, -adjustedHeight) | |
} | |
let frameCount = 0 | |
let numberOfInitializedFrames = 0 | |
for (let i = 0; i < numberOfFramesForAverageFps; i++) { | |
const frameHit = frames.at(-i - 1)! | |
if (frameHit !== FRAME_UNINITIALIZED) { | |
frameCount += frameHit | |
numberOfInitializedFrames++ | |
} | |
} | |
if (numberOfInitializedFrames >= numberOfFramesForAverageFps) { | |
ctx.fillStyle = 'white' | |
const fontSize = PR * 10 | |
ctx.font = `${fontSize}px monospace` | |
const averageFps = Math.round((systemFps * frameCount) / numberOfInitializedFrames) | |
ctx.fillText(`${averageFps} FPS`, 2 * PR, 12 * PR) | |
} | |
} | |
let previousFrameCounter = 0 | |
const loop = () => { | |
animationFrameRef.current = window.requestAnimationFrame((now) => { | |
loop() | |
const frameCounter = Math.floor(now / resolutionInMs) | |
const numberOfSkippedFrames = frameCounter - previousFrameCounter - 1 | |
// Checking for skipped frames | |
for (let i = 0; i < numberOfSkippedFrames; i++) { | |
frames.shift()! | |
frames.push(FRAME_MISS) | |
} | |
frames.shift()! | |
frames.push(FRAME_HIT) | |
previousFrameCounter = frameCounter | |
draw() | |
}) | |
} | |
loop() | |
}, | |
[adjustedHeight, adjustedWidth, numberOfVisibleFrames, numberOfFramesForAverageFps, resolutionInMs, systemFps], | |
) | |
return ( | |
<canvas | |
width={adjustedWidth} | |
height={adjustedHeight} | |
className={className} | |
onClick={onClick} | |
ref={canvasRef} | |
style={{ width, height }} | |
/> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment