Skip to main content

Disco generator

· 3 min read
Filip Tammergård
Software Engineer at Frilans Finans

It's Friday disco time!

TL;DR

  • A grid of cells each get a random rgb(...) color, and the useInterval custom hook reassigns every color on each beat.
  • The frequency in hertz maps to the interval delay as 1000 / frequency; dropping below 1 Hz passes null and 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!

References