eieio.games

by nolen royalty

BreakTime: Brickbreaker inside Google Calendar

Giving google calendar critical functionality

Mar 12, 2024

I made a game. It’s called BreakTime. It’s Breakout (aka Brick Breaker) running inside Google Calendar. Your meetings are bricks. It (optionally) declines the meetings you destroy.

shattering some meetings

It’s a chrome extension. You can install it here. It has no external dependencies; it’s 1,500 lines of javascript including a little game engine I made for the project.

Making it was a ton of fun. Let me tell you about it.

Inspiration

BreakTime started as an iOS shortcut.

I’m fascinated with iOS shortcuts. I’m determined to build something using them. I like putting games in weird places and using shortcuts to build a game deep inside the iOS walled garden is a goal of mine 1.

1

I’m particularly determined to build something on the iOS home screen that uses focus states to move app icons around, but I haven’t gotten something good working yet.

I realized that the calendar shortcut API was pretty powerful - I could build animations by moving events around. I whipped up the bones of what I thought could be a pong demo and tweeted about it.

looks... fun?

I floated Breakout as another potential game and my friend Ian Henry suggested actually declining calendar events.

Ian Henry tweets suggesting that running brick breaker and actually declining events would be a good idea. I respond saying that this idea is so good that I'm going to make a chrome extension out of it.

ian i am so grateful for this tweet

And so BreakTime was born.

Prototype

Whipping up a janky prototype of BreakTime was pretty straightforward. The browser needs to know the coordinates of the elements on screen - it has to draw them all! - and you can ask it for those coordinates with getBoundingClientRect. This might not work well for elements that aren’t rectangles but in BreakTime almost everything is a rectangle!

So the prototype was basically:

  • Figure out a dom selector that gives me all the calendar events
  • Figure out a dom selector that gives me a play area
  • Add a div to represent the ball
  • Write some javascript to make the ball bounce around, checking for collisions with the rectangles.
  • Inject in some CSS to make the ball div round, fade out events that the ball hit, etc.

It looked something like this:

why did i make the ball so big

I cut some pretty big corners to get this all working.

To make collision handling trivial I treated the ball as a square instead of as a circle; you can see that this causes some weird looking bounces.

To animate the ball’s position I set up a setInterval loop that moved the ball every 50 milliseconds, moved it’s position via the css transform property, and set the ball’s transition property to linear 50ms - so long as the setInterval loop runs exactly every 50ms this produces relatively smooth movement (which is to say the movement is not very smooth).

And at the time I figured this wouldn’t get approved as a chrome extension so I planned to make a bookmarklet. This is where the goal of “no external dependencies” came from. To run the prototype I’d just copy-paste the javascript directly into the browser console.

The whole thing came out to something like 300 lines including the CSS that I was injecting directly from javascript.

Those hacks made the game pretty gross. I waited far too long to unwind them which created some headaches later.

That said, this janky prototype was enough to do pretty well on tiktok and catch the Google social team’s attention, which was pretty exciting.

A screenshot of tiktok. The official google account has responded to my video saying 'brilliant'

never thought this would be a development highlight

Collision handling

The response from tiktok convinced me that this was worth pursuing, and I figured the first thing I needed to do was move to a proper system for collision handling.

It turns out that determining whether a circle collides with an (unrotated) rectangle is delightfully elegant! The process is:

  • Find the point P on the rectangle that is closest to the circle
  • Measure the distance from P to the circle’s center
  • If that distance is smaller than the circle’s radius you have a collision

And finding P is even more elegant. To find the X coordinate of P:

  • If the circle’s center is to the left of the rectangle, it’s the left edge of the rectangle
  • If the circle’s center is to the right of the rectangle, it’s the right edge of the rectangle
  • Otherwise, it’s the X coordinate of the circle’s center

You can repeat the same process for the Y coordinate. Easy!

A screenshot of chatgpt. I asked it to explain how to determine if a circle has collided with a rectangle and it gave me the correct answer.

chatgpt did a good job here

However, there’s also the problem of figuring out which side of the rectangle the circle bounced off of. And in my journey to solve this problem from first principles (why!) I went a little off the rails.

The approach I took is:

  • Take the circle’s current position
  • Rewind time to the moment of collision
  • Compare the center of the circle to the sides of the rectangle - if the circle is (for example) now above the rectangle, bounce off the top

There are a ton of edge cases here. And when you get them wrong you get some weird results 2.

2

I don’t have good videos of these bugs so I’ve re-created one that I remember here.

that bounce didn't look right...

The big problems I ran into are:

  1. The ball should only bounce off the left side of a rectangle when moving to the right (and vice-versa)
  2. The ball should only collide off a side if, after rewinding time, it’s center is outside of the rectangle

These problems were particularly pernicious when handling corner bounces (where we invert both the X and Y direction of the ball). I eventually realized that you should only bounce off a corner when the ball intersects two sides of a rectangle in the same tick.

I feel kinda silly typing this out because it feels obvious now but man, this stuff is finicky. It probably didn’t help that I wrote most of this code over one late night (I was excited about the game and thought I could finish it in another day or two. I could not).

I’ve since learned that the typical approach here is about looking at the angle of collision instead. But it was a lot of fun to flail through my approach - and I learned a whole lot doing it.

From jank to juice

Proper collision detection made the game feel a lot better but it didn’t look great. I saw two big problems.

  1. The assets (the ball, paddle, and background) looked simplistic and bad
  2. There was no “juice”

Game devs use “juice” to refer to all the stuff that makes a game feel alive - objects scaling up and down when they collide, particle effects, screenshake, color, good tweening - stuff that doesn’t change gameplay but instead enhances how the existing gameplay feels.

One of the best talks on juice is called “Juice it or lose it”. The talk takes a barebones game and progressively adds more juice (but no gameplay changes) to show how much it changes the feel of the game. Conveniently the example game is a Breakout clone! I cribbed a lot of ideas directly from it.

To motivate myself to get some effects added quickly I signed up to give a 5 minute presentation at Recurse Center’s weekly presentations. I’m a Recurse alum and always find presenting there motivating.

I brought something like this:

the color of the ball here really irks me

That looks a lot better! But there’s still a lot to do. The full set of juice I added includes:

  • Scaling the ball up and down when it bounces
  • Shrinking the paddle when the ball hits it
  • Changing the ball’s color over time (more quickly when it bounces)
  • Shaking the screen when an event is destroyed
  • Particle effects that match an event’s color when it shatters
  • No background (the background here is ugly)
  • Sliding in the gameplay elements at the start
  • Blurring the bottom of the screen since there’s no collision allowed there 3.
  • Adding a trail to the ball that traces its path
3

Breakout is not very playable if you can collide with stuff right at the bottom of the screen. I spent like 3 days agonizing about how to fix this. The downside of my current approach is that you can’t shatter events that start really late in the day since you can’t scroll them out of the blurred zone. But the other approaches I found (such as moving up the entire calendar) had their own downsides.

It’d take wayyy too much time to talk about how I approached each of these problems. Let’s talk about two.

Particle effects

Creating particles for event shattering was delightfully straightforward. I borrowed heavily from this CSS tricks post.

The basic idea is:

  • Get the bounds of the event to shatter
  • Get the background color of the computed style of the event
  • Divide the bounds of the event into 30 equal-ish rectangles
  • Create colored divs for each of those rectangles using the event’s computed style
  • Animate their position, rotation, color, and opacity with some jitter

Here’s approximately what that looks like.

function addParticlesForEvent(bounds, color) {
  const width = bounds.width / numberOfRows;
  const height = bounds.height / numberOfColumns;

  for (let y_ = 0; y < numberOfRows; y++) {
    for (let x_ = 0; x < numberOfColumns; x++) {
      const x = bounds.left + width * x_;
      const y = bounds.top + height * y_;
      // Explode out from the center
      const center = { x: bounds.left + width / 2, y: top + height / 2 };
      const vector = normalizeVector(subtractVectors({ x, y }, center));

      const distance = Math.floor(Math.random() * 75 + 25);
      const toX = vector.x * distance * makeJitter();
      const toY = vector.y * distance * makeJitter();
      const rotation = (Math.random() - 0.5) * 720 + "deg";
      const particle = makeParticle(width, height, color, x, y);

      const startingAnimation = { opacity: 1 };
      const endingAnimation = {
        opacity: 0,
        transform: `translate(${toX}px, ${toY}px) rotate(${rotation})`,
      };
      const animation = particle.animate([startingAnimation, endingAnimation], {
        duration: 250 + Math.random * 500,
        delay: Math.random() * 100,
        easing: "ease",
      });

      animation.onfinish = () => {
        particle.remove();
      };
    }
  }
}

I did something similar to create particles when the ball bounces against the paddle. And I ended up making a pool of 300 ready-to-use particles that I reused to avoid creating and removing tons of divs (this was probably unnecessary).

Screen Shake

Screen shake is controversial 4 - too much of it can be nauseating and disorienting. But I’ve found that sprinkling a bit in can really make you feel the impact of a collision. And it turns out that it’s super easy to add!

4

I suspect at least one commenter will not like it :)

My entire implementation is:

function makeScreenShake() {
  const duration = 250;
  let magnitude = 7.5;
  let startTime = null;
  let isShaking = false;

  function shake(currentTime) {
    const elapsedTime = currentTime - startTime;
    const remainingTime = duration - elapsedTime;
    if (remainingTime > 0) {
      const randomX = (Math.random() - 0.5) * magnitude;
      const randomY = (Math.random() - 0.5) * magnitude;
      mainElt.style.transform = `translate(${randomX}px, ${randomY}px)`;
      requestAnimationFrame(shake);
    } else {
      mainElt.style.transform = "translate(0px, 0px)";
      magnitude = 5;
      isShaking = false;
    }
  }

  function startOrContinueShaking() {
    startTime = performance.now();
    if (isShaking) {
      magnitude += 5;
    } else {
      requestAnimationFrame(shake);
    }
  }
  return startOrContinueShaking;
}
const screenShake = makeScreenShake();

And I think it makes a huge difference. Here I’ve changed the game to only start the screenshake after a few collisions:

screenshake starts on the 6th collision

One improvement I could have made here is to have each frame of screenshake depend on the prior frame using a noise generation algorithm (instead of just generating random values). This article describes an approach I’ve taken in previous games.

Other stuff

There’s so much more stuff that I implemented here! I added custom tweening logic for ball/paddle scaling because using CSS tweening would interfere with moving the objects around smoothly. The ball trail adds elements that track the position of the ball over time while slowly fading out. The color changing relies on the lovely CSS hue-rotation property.

But this post is super long and there’s still more to cover. Let’s keep going!

Smoother animations

Remember, my original approach to moving the ball was “every 50ms, move it a fixed amount and rely on the CSS transition property to ensure that that movement is linear.”

This works ok, but produces unnatural movement if that loop doesn’t run exactly every 50ms.

ignore that bounce off the floor there...

Most of the time this would result in movement that felt just a little bit off, but occasionally you’d get huge leaps.

To fix this I moved my game loop to rely on requestAnimationFrame.

The idea is that you give requestAnimationFrame a function that you want to run immediately before the browser repaints the screen. That callback gets a timestamp representing the amount of time that’s passed since the previous frame. So if it’s only been 25ms since the last frame, you only move the ball half as long as if it’s been 50ms. You end up with a variable frame rate but much smoother animations - especially since the browser will try to sync things up with your monitor’s refresh rate 5.

5

I believe this makes it hard to capture a great video of how bad the setInterval approach looks on my monitor, since my monitor’s refresh rate is signficantly higher than the frame count in the video.

Moving things based on deltaTime is standard practice in games. This video provides a nice overview.

Declining events

I wanted BreakTime to actually be able to decline your events. I think a lot of the joy of a project like this is fully committing to the bit, and the bit here definitely includes “not going to the meetings I destroy.”

This was a challenge because I didn’t want any dependencies so I couldn’t use the calendar API. Instead I needed to script the process of declining the events.

The approach I took relied on MutationObservers - an object that watches for changes in the DOM. My observer watches for anything that looks like a calendar dialog, searches that dialog for a “decline” button, and clicks it.

This was tricky and a little fragile. A few things to highlight:

  • It’s important to decline events one at a time 6. To handle this I use a second observer to wait for all dialogs tied to the current event to clear
  • Some events don’t have a decline option (e.g. if they have no guests); the code must handle that gracefully
  • Some events are recurring, which means they spawn a second modal on decline (the modal asks whether you want to decline one or all of the events)
6

Otherwise you end up with a broken state with multiple event detail dialogs showing at the same time!

But the most fragile bit of logic is that I search for buttons based on their text. This means that event declining only works if Google Calendar is in English. The only other approach I could think of was to hardcode the location in the DOM of the buttons which seemed even worse. But I’m sure there’s something smarter I could do here.

The final result is a flurry of events popping up and disappearing, which I think is pretty fun.

that's a lot of clicking

Moving to a chrome extension

For some reason I thought there was no chance that I could get this into the Chrome Webstore 7. But my friend Kelin convinced me that I totally could, and I was encouraged by Google’s social accounts tweeting about my stuff.

7

Maybe because of how janky this project seemed? I don’t know.

I was also pretty sick of copy-pasting what had become almost 2,000 lines of javascript and CSS into the browser console every time I wanted to run the game.

It turns out that making an extension is pretty easy! The intro docs are pretty good and I didn’t need to do anything fancy. Moving to an extension also made me teach me code to run multiple times without refreshing a tab, which I doubt I would have done with a bookmarklet.

Getting the extension approved took under a day - it probably helps that the extension only runs when invoked so its permissions are pretty simple.

Wrapping up

This was one of my favorite projects. I’m particularly happy that, while it involves embedding a game somewhere it doesn’t belong, the game makes sense for the medium. I’m very proud of Flappy Dird and Wordle in the Firefox address bar but both of those games are totally unrelated to their constraints - they’re just the best ideas I had for how to build a game given the constraints I had chosen. This feels different.

Building a little engine for this project was great. It definitely took more time but I learned a ton - there’s simply no way I’d understand collisions in the same way if I hadn’t written the logic myself. And I’m much handier with the DOM after adding my own particles and scripting event declines.

I also have a ton of people to thank for this project. Thank you to Ian Henry for the initial inspiration, Chana Messinger for naming the game, Kelin for convincing me to make a chrome extension, Recurse Center for the encouragement, and Josh W Comeau for teaching me all of the CSS and Javascript that I know (I knew basically nothing at the start of this year!).

If you enjoyed this post I’d encourage you to apply to Recurse, which is the best place in the world to build stuff like this in a supportive environment. And if you want to hear about my future nonsense you should sign up for my mailing list or follow me on twitter.

I’m going to be at GDC next week to present on stranger video at the Experimental Games Workshop and to meet people - if you’re going to be there I’d love to meet up :)

I’ll be back in April with something new and horrifying.

Thanks for reading!

Keep up with me on my socials 👆

Or sub to my newsletter here! 👇