Created
November 4, 2024 22:39
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, { useEffect, useRef, useState } from 'react'; | |
class VolumeGenerator { | |
public name: string; | |
private volume: number; | |
private delta: number; | |
constructor(name: string, initialVolume = 50) { | |
this.name = name; | |
this.volume = initialVolume; | |
this.delta = 0; | |
} | |
read() { | |
this.volume = Math.min(100, Math.max(0, this.volume + (Math.random() - 0.5) * 20)); | |
this.delta = Math.min(1, Math.max(-1, this.delta + (Math.random() - 0.5) * 0.2)); | |
return { volume: this.volume, delta: this.delta }; | |
} | |
} | |
function getColor(delta: number): string { | |
const hue = (delta + 1) * 60; // Maps -1->0 and 1->120 (red through yellow to green) | |
return `hsl(${hue}, 80%, 50%)`; | |
} | |
// Get contrasting text color based on background hue | |
// For our scale: red(0°) -> yellow(60°) -> green(120°) | |
function getContrastingColor(delta: number): string { | |
const hue = (delta + 1) * 60; | |
// Yellow-ish hues (around 60°) need dark text | |
// We can extend this a bit on either side for yellow-green and orange | |
const needsDarkText = hue > 30 && hue < 90; | |
return needsDarkText ? '#000000' : '#ffffff'; | |
} | |
export default function Demo() { | |
const canvasRef = useRef<HTMLCanvasElement>(null); | |
const [cellSize, setCellSize] = useState({ width: 0, height: 0 }); | |
useEffect(() => { | |
const canvas = canvasRef.current; | |
const cell = canvas?.parentElement; | |
if (!canvas || !cell) return; | |
const resizeObserver = new ResizeObserver(() => { | |
const cellRect = cell.getBoundingClientRect(); | |
setCellSize(prev => | |
prev.height !== cellRect.height || prev.width !== cellRect.width | |
? { width: cellRect.width, height: cellRect.height } | |
: prev | |
); | |
}); | |
resizeObserver.observe(cell); | |
return () => resizeObserver.disconnect(); | |
}, []); | |
useEffect(() => { | |
const canvas = canvasRef.current; | |
if (!canvas) return; | |
const ctx = canvas.getContext('2d'); | |
if (!ctx) return; | |
const PADDING = 4; | |
const SAMPLE_INTERVAL = 50; | |
const fontSize = Math.round(cellSize.height * 0.67); | |
const TEXT_FONT = `${fontSize}px monospace`; | |
const dataGenerator = new VolumeGenerator('volume'); | |
let rafId: number; | |
let lastSampleTime = performance.now(); | |
let previousValue = { volume: 0, delta: 0 }; | |
function draw() { | |
const now = performance.now(); | |
const deltaTime = now - lastSampleTime; | |
if (deltaTime >= SAMPLE_INTERVAL) { | |
const newValue = dataGenerator.read(); | |
// Clear canvas | |
ctx.clearRect(0, 0, cellSize.width, cellSize.height); | |
// Draw background bar | |
ctx.fillStyle = '#f3f4f6'; | |
ctx.fillRect(PADDING, PADDING, cellSize.width - PADDING * 2, cellSize.height - PADDING * 2); | |
// Draw volume bar | |
const barWidth = ((cellSize.width - PADDING * 2) * (newValue.volume / 100)); | |
ctx.fillStyle = getColor(newValue.delta); | |
ctx.fillRect( | |
PADDING, | |
PADDING, | |
barWidth, | |
cellSize.height - PADDING * 2 | |
); | |
// Set up text properties | |
ctx.font = TEXT_FONT; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
const text = newValue.volume.toFixed(2); | |
const textX = cellSize.width / 2; | |
// Check if text is over the bar | |
const isOverBar = textX < (PADDING + barWidth); | |
// Choose text color based on position and background color | |
ctx.fillStyle = isOverBar ? | |
getContrastingColor(newValue.delta) : | |
'#000000'; | |
// Only add shadow for light text on dark backgrounds | |
if (isOverBar && ctx.fillStyle === '#ffffff') { | |
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; | |
ctx.shadowBlur = 3; | |
} else { | |
ctx.shadowColor = 'transparent'; | |
ctx.shadowBlur = 0; | |
} | |
ctx.fillText( | |
text, | |
textX, | |
cellSize.height / 2 | |
); | |
previousValue = newValue; | |
lastSampleTime = now; | |
} | |
rafId = requestAnimationFrame(draw); | |
} | |
draw(); | |
return () => { | |
cancelAnimationFrame(rafId); | |
ctx.clearRect(0, 0, cellSize.width, cellSize.height); | |
}; | |
}, [cellSize]); | |
return ( | |
<div className="h-24"> | |
<canvas | |
ref={canvasRef} | |
style={{ display: 'block', verticalAlign: 'top' }} | |
width={cellSize.width} | |
height={cellSize.height} | |
/> | |
</div> | |
); | |
} |
Author
sebinsua
commented
Nov 4, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment