Your "Simulation" Might Not Need State
The bouncing DVD logo is a fun and easy coding project. Watching Daniel Shiffman code it in p5.js is one of my earliest programming memories. However, the naive solution is far from optimal.
If you haven’t already made this DVD bouncing logo, give a try! Really, take a break from reading and come back. We are going to go over the naive implementation, and I don’t want to spoil it.
Okay, welcome back! You probably wrote code that works very similar to Daniel’s. You need to track the logo’s x
, y
, dx
, and dy
. If the logo hits a side wall you flip its dx
, and if it hits the ceiling or floor you flip the dy
. This totally works! Doing this in TypeScript it might look like:
const dvdLogo = document.getElementById("dvd-logo") as HTMLDivElement
let x = 0
let y = 0
let dx = 1
let dy = 1
function update() {
x += dx
y += dy
dvdLogo.style.left = x + "px"
dvdLogo.style.top = y + "px"
if (x < 0 || x + dvdLogo.clientWidth > window.innerWidth) dx *= -1
if (y < 0 || y + dvdLogo.clientHeight > window.innerHeight) dy *= -1
}
;(function loop() {
update()
requestAnimationFrame(loop)
})()
But there’s actually a totally different way to do this. First, let’s define some things.
-
Animation: takes time as a parameter and returns an image.
-
Simulation: takes some
state
then returns updatedstate
and image.
These types can be defined in TypeScript.
type Animation = (time: number) => Image
type Simulation = <T>(state: T) => { state: T; image: Image }
(If you don’t understand these types it’s OK)
Our naive DVD logo implementation would be a Simulation
. Ok… maybe it doesn’t fit the exact type definition. We aren’t doing pure functional programming. But the essence is the same. We read state (x
, y
, dx
, dy
), compute new state, and render (update the position).
Would it be possible to describe the bouncing DVD logo as an Animation
? Can we actually eliminate the need for state? Think about this for a while.
If you’re like me, at first, this sounds impossible. We would need to come up with some advanced formula to account for collisions and calculate the position of our logo for any given time.
But, it’s really not too complex. Let’s simplify things by thinking about one dimension.
function update(time: number) {
const xRange = window.innerWidth - dvdLogo.clientWidth
const x = time % (xRange * 2)
dvdLogo.style.left = `${x <= xRange ? x : xRange * 2 - x}px`
}
A little math goes a long way! Isn’t that cool? You can probably guess how to calculate the y
position now. Here’s the full solution.
const dvdLogo = document.getElementById("dvd-logo") as HTMLDivElement
function update(time: number) {
const xRange = window.innerWidth - dvdLogo.clientWidth
const yRange = window.innerHeight - dvdLogo.clientHeight
const x = time % (xRange * 2)
const y = time % (yRange * 2)
dvdLogo.style.left = `${x <= xRange ? x : xRange * 2 - x}px`
dvdLogo.style.top = `${y <= yRange ? y : yRange * 2 - y}px`
}
;(function loop() {
update(Date.now() / 10)
requestAnimationFrame(loop)
})()
We converted our previous Simulation
into an Animation
. Instead of maintaing state, we calculate positions using just time
. Not only did we make the code simpler, but we also made the code function better!
How? Well, there were actually a couple of problems with our previous code:
-
The
Simulation
solution is framerate dependent. The DVD would move faster with higher framerates. TheAnimation
solution is framerate independent. -
The
Simulation
actually has a bug. If you resize the window to be smaller you can trap the DVD logo on an edge. Resizing the window in theAnimation
solution means we recalculate to the correct position.
In addition to all this, Animations
have a lot of benefits over Simulations
. They are easier to test, easier to rewind or fast forward, and it’s possible to instantly get the result for any point in time.
But wait, we can take this DVD example further! The DVD logo changes color every time it hits a wall. Surely we need state for that, right?
Actually, it’s possible to calculate the amount of bounces that happened since time
equalled 0.
const bounces = Math.floor(time / xRange) + Math.floor(time / yRange)
And we can use that to choose a color.
const colors = ["red", "green", "blue", "yellow"]
dvdLogo.style.backgroundColor = colors[bounces % colors.length]!
Isn’t that cool?! Or maybe you think we’ve been getting really lucky with these examples. Maybe you remembered something tricky. The actual DVD logo picks a color randomly. You probably think this is where we are finally stumped.
Well, with a seeded random function this is possible too!
const random = (seed: number) => Math.sin(seed * 1000) * 0.5 + 0.5
const randIndex = Math.floor(random(bounces) * colors.length)
dvdLogo.style.backgroundColor = colors[randIndex]!
Ok, well, maybe this isn’t the best random function. I’m getting a bit lazy now. But I’m sure you get the idea.
Try out the live demo! Try refreshing the page. The DVD logo magically remembers its state!
Next time you find yourself rushing to use state, try spending some more time at the whiteboard. Not only will it make your code simpler, but it also might make it better.
View discussion for this post at Hacker News.