Last active
January 24, 2023 21:07
-
-
Save schickling/5d721ff81ed954e5e43e9be37c4ef867 to your computer and use it in GitHub Desktop.
FPS Meter Canvas
This file contains hidden or 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 | |
resolutionInMs?: number | |
className?: string | |
} | |
export const FPSMeter: React.FC<FPSMeterProps> = ({ width = 100, height = 30, resolutionInMs = 10, className }) => { | |
const adjustedWidth = Math.round(width * PR) | |
const adjustedHeight = Math.round(height * PR) | |
const numberOfBars = Math.floor(adjustedWidth / FRAME_BAR_WIDTH) | |
const numberOfBucketsPerSecond = 1000 / resolutionInMs | |
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 | |
let maxFps = 0 | |
// each element is a frame count for a `resolutionInMs` time bucket | |
// eslint-disable-next-line unicorn/no-new-array | |
const frameCountBuckets: number[] = new Array(numberOfBars + 1).fill(0) | |
const ctx = canvas.getContext('2d')! | |
// TODO adjust this for 120+ Hz monitors | |
const draw = () => { | |
ctx.clearRect(0, 0, adjustedWidth, adjustedHeight) | |
const frameRateThreshold = maxFps * 0.75 | |
for (const [i, frameCount] of frameCountBuckets.slice(0, -1).entries()) { | |
const fpsBasedOnFrameCount = frameCount * resolutionInMs | |
const barHeight = (fpsBasedOnFrameCount / maxFps) * adjustedHeight | |
const x = i * FRAME_BAR_WIDTH | |
ctx.fillStyle = fpsBasedOnFrameCount > frameRateThreshold ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 0, 0, 1)' | |
ctx.fillRect(x, adjustedHeight, FRAME_BAR_WIDTH, -barHeight) | |
} | |
// write last frame rate as text | |
ctx.fillStyle = 'white' | |
const fontSize = PR * 10 | |
ctx.font = `${fontSize}px monospace` | |
// NOTE larger values can result in more items taken from array than it has | |
const numberOfSecondsForAverage = 0.5 | |
const averageFps = Math.round( | |
frameCountBuckets.slice(-numberOfBucketsPerSecond * numberOfSecondsForAverage).reduce((a, b) => a + b, 0) / | |
(resolutionInMs * numberOfSecondsForAverage), | |
) | |
ctx.fillText(`${averageFps} FPS`, 2 * PR, adjustedHeight - 3 * PR) | |
} | |
let previousTimeBucket = 0 | |
const loop = () => { | |
animationFrameRef.current = window.requestAnimationFrame((now) => { | |
const timeBucket = Math.floor(now / numberOfBucketsPerSecond) | |
if (timeBucket === previousTimeBucket) { | |
frameCountBuckets[numberOfBars]++ | |
} else { | |
previousTimeBucket = timeBucket | |
const lastFps = frameCountBuckets[numberOfBars]! * resolutionInMs | |
if (lastFps > maxFps) { | |
maxFps = lastFps | |
} | |
frameCountBuckets.shift() | |
frameCountBuckets.push(1) | |
draw() | |
} | |
loop() | |
}) | |
} | |
loop() | |
}, | |
[adjustedHeight, adjustedWidth, numberOfBars, numberOfBucketsPerSecond, resolutionInMs], | |
) | |
return ( | |
<canvas | |
width={adjustedWidth} | |
height={adjustedHeight} | |
className={className} | |
ref={canvasRef} | |
style={{ width, height }} | |
/> | |
) | |
} |
Author
schickling
commented
Jan 23, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment