Falling Shapes
This is my take-home for a front-end role at Reevo. It's a small PixiJS app written with TypeScript classes. Shapes fall, you tap to add or remove them, and two buttons change how fast it all goes. Have a play with it below.
This is Figure 1 from the brief, except it works. The blue letters point to the same things as in the brief.
- A
- Shapes start just above the board, somewhere random.
- B
- They fall out the bottom and get cleaned up.
- C
- Anything outside the board is hidden.
- D
- Tap an empty spot to drop a random lumpy shape.
- E
- Tap a shape to get rid of it.
- F
- Change how many shapes appear and how fast they fall.
- G
- How many shapes are on the board, and how much space they take up.
About the project
Decisions I had to make The parts of the brief that could be read more than one way
- Which shapes count as "on screen"
- Only the ones you can actually see, even partly. A shape that just spawned is still above the board, so it doesn't count yet.
- What "surface area" means
- I add up the real area of each visible shape: πr² for circles, π × a × b for ellipses, and the shoelace formula for everything with corners. I don't subtract overlaps. Doing that would mean checking every pixel every frame, which is too slow on a phone.
- Where a new shape starts
- I measure the shape first, then put it so its bottom edge sits exactly on the top of the board. That way it slides in instead of popping in, whatever its size.
- How the lumpy shapes stay tidy
- They get 5 to 10 corners. I cut the circle into equal slices and put one corner somewhere random inside each slice. The corners always go round in order, so the edges never cross.
- Gravity speeds things up
- Gravity changes the speed, and the speed moves the shape. So shapes start slow and get faster, like dropping something for real.
- Limits on the buttons
- Gravity goes from 0.1 to 2, and shapes from 1 to 10 per second. With gravity at 0, new shapes would sit above the board forever and never get cleaned up.
Performance and memory So it can run for a long time on an average phone
- Nothing piles up. A shape is deleted as soon as it falls out of the board or gets tapped. I checked by logging the count: it stays at 1 or 2 instead of climbing forever.
- Graphics get reused. When a shape is removed, its Pixi graphic goes back into a pool (up to 50) and gets reused for the next one.
-
Same speed on every device. Movement uses
Pixi's
deltaTime, so a slow phone at 30fps moves shapes as fast as a laptop at 60fps. - The page only updates when it needs to. The numbers in the HTML only change when the value actually changes, not 60 times a second.
- Sharp, but not wasteful. The canvas draws as many pixels as it takes up on the screen, and no more. A phone draws far fewer than a laptop, and I cap the pixel ratio at 2 so a 3× screen doesn't draw 9 times the pixels.
- Made for fingers. Pointer events handle mouse and touch with the same code, and the buttons are 48px so they're easy to hit.
Why I added a game An optional 30-second round on top of the brief
Once everything worked, it felt a bit like a screensaver. You could tap things, but there was no reason to. So I added a short game that gives each feature from the brief a job, without changing how any of them work. Free play is still the default.
- You get a shape to find. It changes every 10 seconds.
-
Tap the right shape to score
10 × combo × bonus. - Tap the wrong shape or an empty spot, and your combo resets. An empty tap also drops a lumpy shape in your way.
- More shapes and stronger gravity give you a bigger bonus. So the buttons become a "how brave are you" setting.
| Feature from the brief | Its job in the game |
|---|---|
| Random shapes and colours | What you look for |
| Tap a shape to remove it | How you score |
| Tap the board to add a shape | The penalty for missing |
| Shape count and area | How messy the board is getting |
| Shapes per second and gravity | How hard it is, and how big your bonus gets |
Fun problems along the way The bits that made me stop and think
This was my first project with PixiJS, and my experience carried over well. Years of building components made splitting the app into small classes feel natural, and TypeScript caught mistakes before they ever reached the screen. Pixi itself was quick to pick up. The interesting part was the geometry and the game loop, so here are a few things I enjoyed figuring out.
- The area of any polygon
- Triangles and squares have simple formulas, but my random lumpy shapes don't. The shoelace formula works for all of them. Before trusting it in code, I checked it by hand on a 2×2 square and got 4.
- Closing the loop with one % sign
-
The last corner of a shape has to connect back to the first.
(i + 1) % lengthdoes that in one go. It works like a clock: after 12 comes 1, not 13. - My square was a diamond
- Technically still a square, just turned 45°. Shapes with an even number of sides now start half a step round, so they all sit flat.
- A star with no points
- Give a star the same inner and outer radius and it turns into a round blob with 10 sides. A nice reminder that the zig-zag between far and near corners is the only thing that makes it a star.
- 0.1 + 0.2 = 0.30000000000000004
- The classic JavaScript decimal problem. Adding 0.1 to gravity a few times would give numbers like that, so I round it to one decimal and the display always stays clean.
- A sliver that shouldn't be there
- I noticed new shapes peeking into the board for a single frame. The cause was the order inside the game loop, and the fix was one swapped line: move the old shapes first, then add the new ones.
splice(-1, 1)doesn't do nothing- It deletes the last item. So removing a shape that wasn't in the list would have quietly deleted a different one. A small check stops that before it can ever happen.