Disco generator
It's Friday disco time!
TL;DR
- A grid of cells each get a random
rgb(...)color, and theuseIntervalcustom hook reassigns every color on each beat. - The frequency in hertz maps to the interval delay as
1000 / frequency; dropping below 1 Hz passesnulland pauses the disco. - The CSS grid layout and each cell's color are set with inline styles, which suit values that change on every render better than CSS classes.
Try changing some of the values below and see what happens with the disco lights:
Building a disco generator
This disco generator is built with React and TypeScript. The lights are driven by the useInterval custom hook, which gets its own walkthrough.
Random colors
Each cell needs its own color:
function randomColor() {
const red = Math.floor(Math.random() * 256)
const green = Math.floor(Math.random() * 256)
const blue = Math.floor(Math.random() * 256)
return `rgb(${red}, ${green}, ${blue})`
}
function buildInitialColors(count: number) {
return Array.from({ length: count }, randomColor)
}
randomColor picks three channel values between 0 and 255 and assembles an rgb(...) string. buildInitialColors calls it once per cell, so a grid of rows * columns cells gets that many random colors.
Flashing on every beat
The lights change on a timer driven by the useInterval hook:
const isRunning = frequency >= 1
const transitionDuration = 1 / frequency / 2
useInterval(
() => setColors(buildInitialColors(rows * columns)),
isRunning ? 1000 / frequency : null,
)
The frequency is in beats per second (Hz), so the interval delay is 1000 / frequency milliseconds. Every beat rebuilds all the colors at once. Passing null when the frequency drops below 1 pauses the disco — useInterval tears the timer down on its own. transitionDuration is half a beat, so each color fades in fully before the next one replaces it.
Laying out the grid
The cells live in a CSS grid, with both the layout and the colors set inline:
<section
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gridTemplateRows: `repeat(${rows}, ${height}px)`,
}}
>
{Array.from({ length: rows * columns }).map((_, index) => (
<div
key={index}
style={{
backgroundColor: colors[index],
transition: `background-color ${transitionDuration}s linear`,
}}
/>
))}
</section>
The grid's columns, rows and row height are computed from state, and each cell's background color and transition are updated on every tick. Inline styles are well suited for values that change frequently (like color) because they don't require recomputing CSS classes on every change.
Putting it together
Here's the full code:
import { useInterval } from "hooks/useInterval"
import { useState } from "react"
function randomColor() {
const red = Math.floor(Math.random() * 256)
const green = Math.floor(Math.random() * 256)
const blue = Math.floor(Math.random() * 256)
return `rgb(${red}, ${green}, ${blue})`
}
function buildInitialColors(count: number) {
return Array.from({ length: count }, randomColor)
}
export const DiscoGenerator = () => {
const [columns, setColumns] = useState(3)
const [rows, setRows] = useState(3)
const [height, setHeight] = useState(100)
const [frequency, setFrequency] = useState(1)
const [colors, setColors] = useState(buildInitialColors(rows * columns))
const isRunning = frequency >= 1
const transitionDuration = isRunning ? 1 / frequency / 2 : 0
useInterval(
() => setColors(buildInitialColors(rows * columns)),
isRunning ? 1000 / frequency : null,
)
return (
<>
{/* inputs for rows, columns, frequency and height */}
<section
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gridTemplateRows: `repeat(${rows}, ${height}px)`,
}}
>
{Array.from({ length: rows * columns }).map((_, index) => (
<div
key={index}
style={{
backgroundColor: colors[index],
transition: `background-color ${transitionDuration}s linear`,
}}
/>
))}
</section>
</>
)
}
There are a lot of possibilities here. I'll explore more in upcoming posts!
