BreakTime: Brick Breaker inside Google Calendar
I made a recreation. It’s referred to as BreakTime. It’s Breakout (aka Brick Breaker) working inside Google Calendar. Your conferences are bricks. It (optionally) declines the conferences you destroy.
It’s a chrome extension. You’ll be able to set up it here. It has no exterior dependencies; it’s 1,500 traces of javascript together with a little bit recreation engine I made for the challenge.
Making it was a ton of enjoyable. Let me inform you about it.
BreakTime began as an iOS shortcut.
I’m fascinated with iOS shortcuts. I’m decided to construct one thing utilizing them. I like putting games in weird places and utilizing shortcuts to construct a recreation deep contained in the iOS walled backyard is a aim of mine.
I spotted that the calendar shortcut API was fairly highly effective – I might construct animations by shifting occasions round. I whipped up the bones of what I assumed may very well be a pong demo and tweeted about it.
I floated Breakout as one other potential recreation and my buddy Ian Henry steered really declining calendar occasions.
ian i’m so grateful for this tweet
And so BreakTime was born.
Whipping up a janky prototype of BreakTime was fairly simple. The browser must know the coordinates of the weather on display – it has to attract all of them! – and you’ll ask it for these coordinates with getBoundingClientRect
. This won’t work effectively for parts that aren’t rectangles however in BreakTime nearly every little thing is a rectangle!
So the prototype was principally:
- Work out a dom selector that offers me all of the calendar occasions
- Work out a dom selector that offers me a play space
- Add a div to characterize the ball
- Write some javascript to make the ball bounce round, checking for collisions with the rectangles.
- Inject in some CSS to make the ball div spherical, fade out occasions that the ball hit, and so on.
It appeared one thing like this:
why did i make the ball so massive
I reduce some fairly massive corners to get this all working.
To make collision dealing with trivial I handled the ball as a sq. as an alternative of as a circle; you’ll be able to see that this causes some bizarre wanting bounces.
To animate the ball’s place I arrange a setInterval
loop that moved the ball each 50 milliseconds, moved it’s place by way of the css rework
property, and set the ball’s transition
property to linear 50ms
– as long as the setInterval
loop runs precisely each 50ms this produces comparatively clean motion (which is to say the motion just isn’t very clean).
And on the time I figured this wouldn’t get accepted as a chrome extension so I deliberate to make a bookmarklet. That is the place the aim of “no exterior dependencies” got here from. To run the prototype I’d simply copy-paste the javascript instantly into the browser console.
The entire thing got here out to one thing like 300 traces together with the CSS that I used to be injecting instantly from javascript.
These hacks made the sport fairly gross. I waited far too lengthy to unwind them which created some complications later.
That stated, this janky prototype was sufficient to do pretty well on tiktok and catch the Google social workforce’s consideration, which was fairly thrilling.
by no means thought this is able to be a improvement spotlight
The response from tiktok satisfied me that this was value pursuing, and I figured the very first thing I wanted to do was transfer to a correct system for collision dealing with.
It seems that figuring out whether or not a circle collides with an (unrotated) rectangle is delightfully elegant! The method is:
- Discover the purpose P on the rectangle that’s closest to the circle
- Measure the gap from P to the circle’s heart
- If that distance is smaller than the circle’s radius you’ve gotten a collision
And discovering P is much more elegant. To search out the X coordinate of P:
- If the circle’s heart is to the left of the rectangle, it’s the left fringe of the rectangle
- If the circle’s heart is to the appropriate of the rectangle, it’s the appropriate fringe of the rectangle
- In any other case, it’s the X coordinate of the circle’s heart
You’ll be able to repeat the identical course of for the Y coordinate. Simple!
chatgpt did a great job right here
Nonetheless, there’s additionally the issue of determining which aspect of the rectangle the circle bounced off of. And in my journey to unravel this drawback from first ideas (why!) I went a little bit off the rails.
The method I took is:
- Take the circle’s present place
- Rewind time to the second of collision
- Evaluate the middle of the circle to the edges of the rectangle – if the circle is (for instance) now above the rectangle, bounce off the highest
There are a ton of edge instances right here. And whenever you get them incorrect you get some bizarre outcomes.
that bounce did not look proper…
The large issues I bumped into are:
- The ball ought to solely bounce off the left aspect of a rectangle when shifting to the appropriate (and vice-versa)
- The ball ought to solely collide off a aspect if, after rewinding time, it’s heart is outdoors of the rectangle
These issues have been significantly pernicious when dealing with nook bounces (the place we invert each the X and Y route of the ball). I ultimately realized that it’s best to solely bounce off a nook when the ball intersects two sides of a rectangle in the identical tick.
I really feel kinda foolish typing this out as a result of it feels apparent now however man, these items is finicky. It in all probability didn’t assist that I wrote most of this code over one late evening (I used to be excited concerning the recreation and thought I might end it in one other day or two. I couldn’t).
I’ve since realized that the everyday method right here is about wanting on the angle of collision as an alternative. Nevertheless it was quite a lot of enjoyable to flail by means of my method – and I realized an entire lot doing it.
Correct collision detection made the sport really feel loads higher but it surely didn’t look nice. I noticed two massive issues.
- The belongings (the ball, paddle, and background) appeared simplistic and dangerous
- There was no “juice”
Sport devs use “juice” to consult with all of the stuff that makes a recreation really feel alive – objects scaling up and down after they collide, particle results, screenshake, coloration, good tweening – stuff that doesn’t change gameplay however as an alternative enhances how the present gameplay feels.
Among the finest talks on juice is known as “Juice it or lose it”. The discuss takes a barebones recreation and progressively provides extra juice (however no gameplay modifications) to indicate how a lot it modifications the texture of the sport. Conveniently the instance recreation is a Breakout clone! I cribbed quite a lot of concepts instantly from it.
To encourage myself to get some results added rapidly I signed as much as give a 5 minute presentation at Recurse Center’s weekly displays. I’m a Recurse alum and at all times discover presenting there motivating.
I introduced one thing like this:
the colour of the ball right here actually irks me
That appears loads higher! However there’s nonetheless loads to do. The complete set of juice I added consists of:
- Scaling the ball up and down when it bounces
- Shrinking the paddle when the ball hits it
- Altering the ball’s coloration over time (extra rapidly when it bounces)
- Shaking the display when an occasion is destroyed
- Particle results that match an occasion’s coloration when it shatters
- No background (the background right here is ugly)
- Sliding within the gameplay parts at first
- Blurring the underside of the display since there’s no collision allowed there
- Including a path to the ball that traces its path
It’d take wayyy an excessive amount of time to speak about how I approached every of those issues. Let’s discuss two.
Particle results
Creating particles for occasion shattering was delightfully simple. I borrowed closely from this CSS tricks post.
The essential concept is:
- Get the bounds of the occasion to shatter
- Get the background coloration of the computed fashion of the occasion
- Divide the bounds of the occasion into 30 equal-ish rectangles
- Create coloured divs for every of these rectangles utilizing the occasion’s computed style
- Animate their place, rotation, coloration, and opacity with some jitter
Right here’s roughly what that appears like.
operate addParticlesForEvent(bounds, coloration) {
const width = bounds.width / numberOfRows;
const peak = bounds.peak / numberOfColumns;
for (let y_ = 0; y < numberOfRows; y++) {
for (let x_ = 0; x < numberOfColumns; x++) {
const x = bounds.left + width * x_;
const y = bounds.high + peak * y_;
// Explode out from the middle
const heart = { x: bounds.left + width / 2, y: high + peak / 2 };
const vector = normalizeVector(subtractVectors({ x, y }, heart));
const distance = Math.flooring(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, peak, coloration, x, y);
const startingAnimation = { opacity: 1 };
const endingAnimation = {
opacity: 0,
rework: `translate(${toX}px, ${toY}px) rotate(${rotation})`,
};
const animation = particle.animate(
[startingAnimation, endingAnimation],
{
length: 250 + Math.random * 500,
delay: Math.random() * 100,
easing: "ease",
}
);
animation.onfinish = () => {
particle.take away();
};
}
}
}
I did one thing much like create particles when the ball bounces in opposition to the paddle. And I ended up making a pool of 300 ready-to-use particles that I reused to keep away from creating and eradicating tons of divs (this was in all probability pointless).
Display Shake
Display shake is controversial – an excessive amount of of it may be nauseating and disorienting. However I’ve discovered that sprinkling a bit in can actually make you are feeling the influence of a collision. And it seems that it’s tremendous simple so as to add!
My whole implementation is:
operate makeScreenShake() {
const length = 250;
let magnitude = 7.5;
let startTime = null;
let isShaking = false;
operate shake(currentTime) {
const elapsedTime = currentTime - startTime;
const remainingTime = length - elapsedTime;
if (remainingTime > 0) {
const randomX = (Math.random() - 0.5) * magnitude;
const randomY = (Math.random() - 0.5) * magnitude;
mainElt.fashion.rework = `translate(${randomX}px, ${randomY}px)`;
requestAnimationFrame(shake);
} else {
mainElt.fashion.rework = "translate(0px, 0px)";
magnitude = 5;
isShaking = false;
}
}
operate startOrContinueShaking() {
startTime = efficiency.now();
if (isShaking) {
magnitude += 5;
} else {
requestAnimationFrame(shake);
}
}
return startOrContinueShaking;
}
const screenShake = makeScreenShake();
And I believe it makes an enormous distinction. Right here I’ve modified the sport to solely begin the screenshake after a number of collisions:
screenshake begins on the sixth collision
One enchancment I might have made right here is to have every body of screenshake rely on the prior body utilizing a noise technology algorithm (as an alternative of simply producing random values). This article describes an method I’ve taken in earlier video games.
Different stuff
There’s a lot extra stuff that I carried out right here! I added custom tweening logic for ball/paddle scaling as a result of utilizing CSS tweening would intervene with shifting the objects round easily. The ball trail provides parts that monitor the place of the ball over time whereas slowly fading out. The color changing depends on the stunning CSS hue-rotation
property.
However this submit is tremendous lengthy and there’s nonetheless extra to cowl. Let’s maintain going!
Keep in mind, my authentic method to shifting the ball was “each 50ms, transfer it a hard and fast quantity and depend on the CSS transition
property to make sure that that motion is linear.”
This works okay, however produces unnatural motion if that loop doesn’t run precisely each 50ms.
ignore that bounce off the ground there…
More often than not this is able to lead to motion that felt just a bit bit off, however often you’d get big leaps.
To repair this I moved my recreation loop to depend on requestAnimationFrame
.
The concept is that you just give requestAnimationFrame
a operate that you just wish to run instantly earlier than the browser repaints the display. That callback will get a timestamp representing the period of time that’s handed because the earlier body. So if it’s solely been 25ms because the final body, you solely transfer the ball half so long as if it’s been 50ms. You find yourself with a variable body fee however a lot smoother animations – particularly because the browser will attempt to sync issues up together with your monitor’s refresh fee.
Transferring issues primarily based on deltaTime
is customary follow in video games. This video gives a pleasant overview.
I wished BreakTime to truly be capable of decline your occasions. I believe quite a lot of the enjoyment of a challenge like that is absolutely committing to the bit, and the bit right here undoubtedly consists of “not going to the conferences I destroy.”
This was a problem as a result of I didn’t need any dependencies so I couldn’t use the calendar API. As an alternative I wanted to script the method of declining the occasions.
The method I took relied on MutationObservers
– an object that watches for modifications within the DOM. My observer watches for something that appears like a calendar dialog, searches that dialog for a “decline” button, and clicks it.
This was tough and a little bit fragile. Just a few issues to focus on:
- It’s essential to say no occasions one after the other. To deal with this I use a second observer to wait for all dialogs tied to the current event to clear
- Some occasions don’t have a decline possibility (e.g. in the event that they haven’t any friends); the code should deal with that gracefully
- Some occasions are recurring, which implies they spawn a second modal on decline (the modal asks whether or not you wish to decline one or all the occasions)
However probably the most fragile little bit of logic is that I seek for buttons primarily based on their textual content. Which means that occasion declining solely works if Google Calendar is in English. The one different method I might consider was to hardcode the situation within the DOM of the buttons which appeared even worse. However I’m positive there’s one thing smarter I might do right here.
The ultimate result’s a flurry of occasions popping up and disappearing, which I believe is fairly enjoyable.
For some purpose I assumed there was no likelihood that I might get this into the Chrome Webstore. However my buddy Kelin satisfied me that I completely might, and I used to be inspired by Google’s social accounts tweeting about my stuff.
I used to be additionally fairly sick of copy-pasting what had develop into nearly 2,000 traces of javascript and CSS into the browser console each time I wished to run the sport.
It seems that making an extension is fairly simple! The intro docs are fairly good and I didn’t have to do something fancy. Transferring to an extension additionally made me train me code to run a number of occasions with out refreshing a tab, which I doubt I might have achieved with a bookmarklet.
Getting the extension accepted took beneath a day – it in all probability helps that the extension solely runs when invoked so its permissions are fairly easy.
This was certainly one of my favourite tasks. I’m significantly glad that, whereas it entails embedding a recreation someplace it doesn’t belong, the sport is sensible for the medium. I’m very happy with Flappy Dird and Wordle in the Firefox address bar however each of these video games are completely unrelated to their constraints – they’re simply one of the best concepts I had for the way to construct a recreation given the constraints I had chosen. This feels totally different.
Constructing a little bit engine for this challenge was nice. It undoubtedly took extra time however I realized a ton – there’s merely no manner I’d perceive collisions in the identical manner if I hadn’t written the logic myself. And I’m a lot handier with the DOM after including my very own particles and scripting occasion declines.
I even have a ton of individuals to thank for this challenge. Thanks to Ian Henry for the preliminary inspiration, Chana Messinger for naming the sport, Kelin for convincing me to make a chrome extension, Recurse Center for the encouragement, and Josh W Comeau for instructing me all the CSS and Javascript that I do know (I knew principally nothing at first of this 12 months!).
In case you loved this submit I’d encourage you to use to Recurse, which is one of the best place on the planet to construct stuff like this in a supportive atmosphere. And if you wish to hear about my future nonsense it’s best to join my mailing list or comply with me on twitter.
I’m going to be at GDC subsequent week to current on stranger video on the Experimental Video games Workshop and to satisfy individuals – for those who’re going to be there I’d love to satisfy up 🙂
I’ll be again in April with one thing new and horrifying.