1(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[2937],{9756:function(e,n,s){(window.__NEXT_P=window.__NEXT_P||[]).push(["/blog/breaktime",function(){return s(7958)}])},7958:function(e,n,s){"use strict";s.r(n),s.d(n,{default:function(){return h},frontmatter:function(){return i},tableOfContents:function(){return c}});var a=s(5893),t=s(9843),o=s(1582);let i={title:"BreakTime: Brickbreaker inside Google Calendar",short_title:"BreakTime",subhead:"Giving google calendar critical functionality",date:"2024-03-12",filter_categories:["game","game-in-thing","best-of"],assets_dir:"breaktime",image:"gameplay-firstframe.png",description:"A version of brickbreaker inside google calendar - your meetings are bricks, and the game declines them when you smash them!",rss_description:"I made a game. It's called BreakTime. It's Breakout (aka BrickBreaker) but it runs inside your google calendar. Your meetings are bricks, and shattering a meeting (optionally) declines it."},c=[{level:2,text:"Inspiration",id:"toc:inspiration",href:"#toc:inspiration"},{level:2,text:"Prototype",id:"toc:prototype",href:"#toc:prototype"},{level:3,text:"Collision handling",id:"toc:collision-handling",href:"#toc:collision-handling"},{level:3,text:"From jank to juice",id:"toc:from-jank-to-juice",href:"#toc:from-jank-to-juice"},{level:3,text:"Particle effects",id:"toc:particle-effects",href:"#toc:particle-effects"},{level:3,text:"Screen Shake",id:"toc:screen-shake",href:"#toc:screen-shake"},{level:3,text:"Other stuff",id:"toc:other-stuff",href:"#toc:other-stuff"},{level:2,text:"Smoother animations",id:"toc:smoother-animations",href:"#toc:smoother-animations"},{level:2,text:"Declining events",id:"toc:declining-events",href:"#toc:declining-events"},{level:2,text:"Moving to a chrome extension",id:"toc:moving-to-a-chrome-extension",href:"#toc:moving-to-a-chrome-extension"},{level:2,text:"Wrapping up",id:"toc:wrapping-up",href:"#toc:wrapping-up"}],l=function(e){let{children:n}=e;return(0,a.jsx)(o.Z,{post:i,tableOfContents:c,children:n})};function r(e){let n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",span:"span",ul:"ul",...(0,t.a)(),...e.components},{FT:s,FTD:o,SingleImage:i,SingleVideo:c}=n;return s||d("FT",!0),o||d("FTD",!0),i||d("SingleImage",!0),c||d("SingleVideo",!0),(0,a.jsxs)(a.Fragment,{children:["\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(c,{src:"gameplay.mp4",width:2392,height:1436,description:"shattering some meetings",alt:"Breaktime gameplay. A breakout game starts running inside google calendar. There is a paddle at the bottom of the screen and a ball bouncing around. When the ball hits a meeting the meeting explodes. At the end the user is asked if they want to decline their meetings; they say yes and the meetings are declined."}),"\n",(0,a.jsxs)(n.p,{children:["Itâs a chrome extension. You can install it ",(0,a.jsx)(n.a,{href:"https://chromewebstore.google.com/detail/breaktime/efajcgehdfpliglonbkjmifmibpphafk?hl=en&authuser=0",children:"here"}),". It has no external dependencies; itâs 1,500 lines of javascript including a little game engine I made for the project."]}),"\n",(0,a.jsx)(n.p,{children:"Making it was a ton of fun. Let me tell you about it."}),"\n","\n",(0,a.jsx)(n.h2,{id:"toc:inspiration",children:"Inspiration"}),"\n",(0,a.jsx)(n.p,{children:"BreakTime started as an iOS shortcut."}),"\n",(0,a.jsxs)(n.p,{children:["Iâm ",(0,a.jsx)(n.em,{children:"fascinated"})," with iOS shortcuts. Iâm determined to build something using them. I like ",(0,a.jsx)(n.a,{href:"/nonsense/game-11-flappy-bird-finder/",children:"putting games in weird places"})," and using shortcuts to build a game deep inside the iOS walled garden is a goal of mine ",(0,a.jsx)(s,{i:1}),"."]}),"\n",(0,a.jsx)(o,{i:1,children:(0,a.jsx)(n.p,{children:"Iâm particularly determined to build something on the iOS home screen that\nuses focus states to move app icons around, but I havenât gotten something\ngood working yet."})}),"\n",(0,a.jsxs)(n.p,{children:["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 ",(0,a.jsx)(n.a,{href:"https://twitter.com/itseieio/status/1757415329783795786",children:"tweeted about it"}),"."]}),"\n",(0,a.jsx)(c,{src:"pong.mp4",width:1030,height:720,description:"looks... fun?",alt:"A pong game running inside the iOS calendar app."}),"\n",(0,a.jsxs)(n.p,{children:["I floated Breakout as another potential game and my friend ",(0,a.jsx)(n.a,{href:"https://twitter.com/ianthehenry/status/1757464426616435093",children:"Ian Henry"})," suggested actually declining calendar events."]}),"\n",(0,a.jsx)(i,{src:"iantweet.png",width:1182,height:750,description:"ian i am so grateful for this tweet",alt:"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."}),"\n",(0,a.jsx)(n.p,{children:"And so BreakTime was born."}),"\n",(0,a.jsx)(n.h2,{id:"toc:prototype",children:"Prototype"}),"\n",(0,a.jsxs)(n.p,{children:["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 ",(0,a.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect",children:(0,a.jsx)(n.code,{children:"getBoundingClientRect"})}),". This might not work well for elements that arenât rectangles but in BreakTime almost everything is a rectangle!"]}),"\n",(0,a.jsx)(n.p,{children:"So the prototype was basically:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Figure out a dom selector that gives me all the calendar events"}),"\n",(0,a.jsx)(n.li,{children:"Figure out a dom selector that gives me a play area"}),"\n",(0,a.jsx)(n.li,{children:"Add a div to represent the ball"}),"\n",(0,a.jsx)(n.li,{children:"Write some javascript to make the ball bounce around, checking for collisions with the rectangles."}),"\n",(0,a.jsx)(n.li,{children:"Inject in some CSS to make the ball div round, fade out events that the ball hit, etc."}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"It looked something like this:"}),"\n",(0,a.jsx)(c,{src:"breaktime-prototype-1.mp4",width:720,height:720,description:"why did i make the ball so big",alt:"A prototype of BreakTime running inside the iOS calendar app. The ball bounces around the screen and destroys events when it hits them. The ball is huge."}),"\n",(0,a.jsx)(n.p,{children:"I cut some pretty big corners to get this all working."}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsxs)(n.p,{children:["To animate the ballâs position I set up a ",(0,a.jsx)(n.code,{children:"setInterval"})," loop that moved the ball every 50 milliseconds, moved itâs position via the css ",(0,a.jsx)(n.code,{children:"transform"})," property, and set the ballâs ",(0,a.jsx)(n.code,{children:"transition"})," property to ",(0,a.jsx)(n.code,{children:"linear 50ms"})," - so long as the ",(0,a.jsx)(n.code,{children:"setInterval"})," loop runs ",(0,a.jsx)(n.em,{children:"exactly"})," every 50ms this produces relatively smooth movement (which is to say the movement is not very smooth)."]}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(n.p,{children:"The whole thing came out to something like 300 lines including the CSS that I was injecting directly from javascript."}),"\n",(0,a.jsx)(n.p,{children:"Those hacks made the game pretty gross. I waited far too long to unwind them which created some headaches later."}),"\n",(0,a.jsxs)(n.p,{children:["That said, this janky prototype was enough to ",(0,a.jsx)(n.a,{href:"https://www.tiktok.com/@itseiei
1o/video/7335551871991418155",children:"do pretty well on tiktok"})," and catch the Google social teamâs attention, which was pretty exciting."]}),"\n",(0,a.jsx)(i,{src:"tiktok-screencap.png",width:590,height:330,description:"never thought this would be a development highlight",alt:"A screenshot of tiktok. The official google account has responded to my video saying 'brilliant'"}),"\n",(0,a.jsx)(n.h3,{id:"toc:collision-handling",children:"Collision handling"}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(n.p,{children:"It turns out that determining whether a circle collides with an (unrotated) rectangle is delightfully elegant! The process is:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Find the point P on the rectangle that is closest to the circle"}),"\n",(0,a.jsx)(n.li,{children:"Measure the distance from P to the circleâs center"}),"\n",(0,a.jsx)(n.li,{children:"If that distance is smaller than the circleâs radius you have a collision"}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"And finding P is even more elegant. To find the X coordinate of P:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"If the circleâs center is to the left of the rectangle, itâs the left edge of the rectangle"}),"\n",(0,a.jsx)(n.li,{children:"If the circleâs center is to the right of the rectangle, itâs the right edge of the rectangle"}),"\n",(0,a.jsx)(n.li,{children:"Otherwise, itâs the X coordinate of the circleâs center"}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"You can repeat the same process for the Y coordinate. Easy!"}),"\n",(0,a.jsx)(i,{src:"chatgpt.png",width:1258,height:464,description:"chatgpt did a good job here",alt:"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."}),"\n",(0,a.jsxs)(n.p,{children:["However, thereâs also the problem of figuring out which ",(0,a.jsx)(n.em,{children:"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."]}),"\n",(0,a.jsx)(n.p,{children:"The approach I took is:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Take the circleâs current position"}),"\n",(0,a.jsx)(n.li,{children:"Rewind time to the moment of collision"}),"\n",(0,a.jsx)(n.li,{children:"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"}),"\n"]}),"\n",(0,a.jsxs)(n.p,{children:["There are a ton of edge cases here. And when you get them wrong you get some ",(0,a.jsx)(n.em,{children:"weird"})," results ",(0,a.jsx)(s,{i:2}),"."]}),"\n",(0,a.jsx)(o,{i:2,children:(0,a.jsx)(n.p,{children:"I donât have good videos of these bugs so Iâve re-created one that I remember\nhere."})}),"\n",(0,a.jsx)(c,{src:"funnybounce.mp4",width:1470,height:1470,description:"that bounce didn't look right...",alt:"A video of BreakTime. The ball bounces off the top of a rectangle but it's not clear why."}),"\n",(0,a.jsx)(n.p,{children:"The big problems I ran into are:"}),"\n",(0,a.jsxs)(n.ol,{children:["\n",(0,a.jsx)(n.li,{children:"The ball should only bounce off the left side of a rectangle when moving to the right (and vice-versa)"}),"\n",(0,a.jsxs)(n.li,{children:["The ball should only collide off a side if, after rewinding time, itâs center is ",(0,a.jsx)(n.em,{children:"outside"})," of the rectangle"]}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsxs)(n.p,{children:["I feel kinda silly typing this out because it feels obvious now but man, this stuff is ",(0,a.jsx)(n.em,{children:"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)."]}),"\n",(0,a.jsxs)(n.p,{children:["Iâve since learned that the typical approach here is about looking at the ",(0,a.jsx)(n.em,{children:"angle"})," of collision instead. But it was a lot of fun to flail through my approach - and I learned a whole lot doing it."]}),"\n",(0,a.jsx)(n.h3,{id:"toc:from-jank-to-juice",children:"From jank to juice"}),"\n",(0,a.jsxs)(n.p,{children:["Proper collision detection made the game feel a lot better but it didnât ",(0,a.jsx)(n.em,{children:"look"})," great. I saw two big problems."]}),"\n",(0,a.jsxs)(n.ol,{children:["\n",(0,a.jsx)(n.li,{children:"The assets (the ball, paddle, and background) looked simplistic and bad"}),"\n",(0,a.jsx)(n.li,{children:"There was no âjuiceâ"}),"\n"]}),"\n",(0,a.jsxs)(n.p,{children:["Game devs use âjuiceâ to refer to all the stuff that makes a game feel ",(0,a.jsx)(n.em,{children:"alive"})," - objects scaling up and down when they collide, particle effects, screenshake, color, good ",(0,a.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Inbetweening",children:"tweening"})," - stuff that doesnât change gameplay but instead enhances how the existing gameplay feels."]}),"\n",(0,a.jsxs)(n.p,{children:["One of the best talks on juice is called ",(0,a.jsx)(n.a,{href:"https://www.youtube.com/watch?v=Fy0aCDmgnxg",children:"â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."]}),"\n",(0,a.jsxs)(n.p,{children:["To motivate myself to get some effects added quickly I signed up to give a 5 minute presentation at ",(0,a.jsx)(n.a,{href:"https://www.recurse.com/",children:"Recurse Centerâs"})," weekly presentations. Iâm a Recurse alum and always find presenting there motivating."]}),"\n",(0,a.jsx)(n.p,{children:"I brought something like this:"}),"\n",(0,a.jsx)(c,{src:"somewhat-juiced.mp4",width:720,height:720,description:"the color of the ball here really irks me",alt:"A video of BreakTime. The ball bounces off the paddle and the paddle shrinks. The ball's color changes over time."}),"\n",(0,a.jsx)(n.p,{children:"That looks a lot better! But thereâs still a lot to do. The full set of juice I added includes:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Scaling the ball up and down when it bounces"}),"\n",(0,a.jsx)(n.li,{children:"Shrinking the paddle when the ball hits it"}),"\n",(0,a.jsx)(n.li,{children:"Changing the ballâs color over time (more quickly when it bounces)"}),"\n",(0,a.jsx)(n.li,{children:"Shaking the screen when an event is destroyed"}),"\n",(0,a.jsx)(n.li,{children:"Particle effects that match an eventâs color when it shatters"}),"\n",(0,a.jsx)(n.li,{children:"No background (the background here is ugly)"}),"\n",(0,a.jsx)(n.li,{children:"Sliding in the gameplay elements at the start"}),"\n",(0,a.jsxs)(n.li,{children:["Blurring the bottom of the screen since thereâs no collision allowed there ",(0,a.jsx)(s,{i:3}),"."]}),"\n",(0,a.jsx)(n.li,{children:"Adding a trail to the ball that traces its path"}),"\n"]}),"\n",(0,a.jsx)(o,{i:3,children:(0,a.jsx)(n.p,{children:"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."})}),"\n",(0,a.jsx)(n.p,{children:"Itâd take wayyy too much time to talk about how I approached each of these problems. Letâs talk about two."}),"\n",(0,a.jsx)(n.h3,{id:"toc:particle-effects",children:"Particle effects"}),"\n",(0,a.jsxs)(n.p,{children:["Creating particles for event shattering was delightfully straightforward. I borrowed heavily from ",(0,a.jsx)(n.a,{href:"https://css-tricks.
1com/adding-particle-effects-to-dom-elements-with-canvas/",children:"this CSS tricks post"}),"."]}),"\n",(0,a.jsx)(n.p,{children:"The basic idea is:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Get the bounds of the event to shatter"}),"\n",(0,a.jsx)(n.li,{children:"Get the background color of the computed style of the event"}),"\n",(0,a.jsx)(n.li,{children:"Divide the bounds of the event into 30 equal-ish rectangles"}),"\n",(0,a.jsxs)(n.li,{children:["Create colored divs for each of those rectangles using the eventâs ",(0,a.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle",children:"computed style"})]}),"\n",(0,a.jsx)(n.li,{children:"Animate their position, rotation, color, and opacity with some jitter"}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"Hereâs approximately what that looks like."}),"\n",(0,a.jsx)(n.pre,{className:"language-javascript",children:(0,a.jsxs)(n.code,{className:"language-javascript code-highlight",children:[(0,a.jsxs)(n.span,{className:"code-line",children:[(0,a.jsx)(n.span,{className:"token keyword",children:"function"})," ",(0,a.jsx)(n.span,{className:"token function",children:"addParticlesForEvent"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsxs)(n.span,{className:"token parameter",children:["bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," color"]}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," width ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"width"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"/"})," numberOfRows",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," height ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"height"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"/"})," numberOfColumns",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"for"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token keyword",children:"let"})," y_ ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token number",children:"0"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"})," y ",(0,a.jsx)(n.span,{className:"token operator",children:"<"})," numberOfRows",(0,a.jsx)(n.span,{className:"token punctuation",children:";"})," y",(0,a.jsx)(n.span,{className:"token operator",children:"++"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"for"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token keyword",children:"let"})," x_ ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token number",children:"0"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"})," x ",(0,a.jsx)(n.span,{className:"token operator",children:"<"})," numberOfColumns",(0,a.jsx)(n.span,{className:"token punctuation",children:";"})," x",(0,a.jsx)(n.span,{className:"token operator",children:"++"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," x ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"left"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," width ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," x_",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," y ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"top"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," height ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," y_",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token comment",children:"// Explode out from the center"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," center ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"})," ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"x"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," bounds",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"left"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," width ",(0,a.jsx)(n.span,{className:"token operator",children:"/"})," ",(0,a.jsx)(n.span,{className:"token number",children:"2"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","})," ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"y"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," top ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," height ",(0,a.jsx)(n.span,{className:"token operator",children:"/"})," ",(0,a.jsx)(n.span,{className:"token number",children:"2"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," vector ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token function",children:"normalizeVector"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token function",children:"subtractVectors"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:"{"})," x",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," y ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","})," center",(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," distance ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"floor"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"random"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token number",children:"75"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," ",(0,a.jsx)(n.span,{className:"token number",children:"25"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," toX ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," vector",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"x"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," distance ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token function",children:"makeJitter"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," toY ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," vector",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"y"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," distance ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token function",children:"makeJitter"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," rotation ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"random"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"-"})," ",(0,a.jsx)(n.span,{className:"token number",children:"0.5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token number",children:"720"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"+"})," ",(0,a.jsx)(n.span,{className:"token string",children:'"deg"'}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," particle ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token function",children:"makeParticle"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),"width",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," height",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," color",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," x",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," y",(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," startingAnimation ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"})," ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"opacity"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsx)(n.span,{className:"token number",children:"1"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," endingAnimation ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"opacity"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsx)(n.span,{className:"token number",children:"0"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"transform"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsxs)(n.span,{className:"token template-string",children:[(0,a.jsx)(n.span,{className:"token template-punctuation string",children:"`"}),(0,a.jsx)(n.span,{className:"token string",children:"translate("}),(0,a.jsxs)(n.span,{className:"token interpolation",children:[(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"${"}),"toX",(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"}"})]}),(0,a.jsx)(n.span,{className:"token string",children:"px, "}),(0,a.jsxs)(n.span,{className:"token interpolation",children:[(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"${"}),"toY",(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"}"})]}),(0,a.jsx)(n.span,{className:"token string",children:"px) rotate("}),(0,a.jsxs)(n.span,{className:"token interpolation",children:[(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"${"}),"rotation",(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"}"})]}),(0,a.jsx)(n.span,{className:"token string",children:")"}),(0,a.jsx)(n.span,{className:"token template-punctuation string",children:"`"})]}),(0,a.jsx)(n.span,{className:"token punctuation",children:","}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," animation ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," particle",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"animate"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:"["}),"startingAnimation",(0,a.jsx)(n.span,{className:"token punctuation",children:","})," endingAnimation",(0,a.jsx)(n.span,{className:"token punctuation",children:"]"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"duration"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsx)(n.span,{className:"token number",children:"250"})," ",(0,a.
1jsx)(n.span,{className:"token operator",children:"+"})," ",(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"random"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token number",children:"500"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"delay"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"random"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," ",(0,a.jsx)(n.span,{className:"token number",children:"100"}),(0,a.jsx)(n.span,{className:"token punctuation",children:","}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token literal-property property",children:"easing"}),(0,a.jsx)(n.span,{className:"token operator",children:":"})," ",(0,a.jsx)(n.span,{className:"token string",children:'"ease"'}),(0,a.jsx)(n.span,{className:"token punctuation",children:","}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" animation",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method-variable function-variable method function property-access",children:"onfinish"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token arrow operator",children:"=>"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" particle",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"remove"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]})]})}),"\n",(0,a.jsx)(n.p,{children:"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)."}),"\n",(0,a.jsx)(n.h3,{id:"toc:screen-shake",children:"Screen Shake"}),"\n",(0,a.jsxs)(n.p,{children:["Screen shake is controversial ",(0,a.jsx)(s,{i: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!"]}),"\n",(0,a.jsx)(o,{i:4,alignItemsForSidenote:"center",children:(0,a.jsx)(n.p,{children:"I suspect at least one commenter will not like it :)"})}),"\n",(0,a.jsx)(n.p,{children:"My entire implementation is:"}),"\n",(0,a.jsx)(n.pre,{className:"language-javascript",children:(0,a.jsxs)(n.code,{className:"language-javascript code-highlight",children:[(0,a.jsxs)(n.span,{className:"code-line",children:[(0,a.jsx)(n.span,{className:"token keyword",children:"function"})," ",(0,a.jsx)(n.span,{className:"token function",children:"makeScreenShake"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," duration ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token number",children:"250"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"let"})," magnitude ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token number",children:"7.5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"let"})," startTime ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token keyword null nil",children:"null"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"let"})," isShaking ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token boolean",children:"false"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"function"})," ",(0,a.jsx)(n.span,{className:"token function",children:"shake"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token parameter",children:"currentTime"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," elapsedTime ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," currentTime ",(0,a.jsx)(n.span,{className:"token operator",children:"-"})," startTime",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," remainingTime ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," duration ",(0,a.jsx)(n.span,{className:"token operator",children:"-"})," elapsedTime",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"if"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),"remainingTime ",(0,a.jsx)(n.span,{className:"token operator",children:">"})," ",(0,a.jsx)(n.span,{className:"token number",children:"0"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," randomX ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"random"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"-"})," ",(0,a.jsx)(n.span,{className:"token number",children:"0.5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," magnitude",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," randomY ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token known-class-name class-name",children:"Math"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"random"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"-"})," ",(0,a.jsx)(n.span,{className:"token number",children:"0.5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"*"})," magnitude",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" mainElt",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"style"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"transform"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsxs)(n.span,{className:"token template-string",children:[(0,a.jsx)(n.span,{className:"token template-punctuation string",children:"`"}),(0,a.jsx)(n.span,{className:"token string",children:"translate("}),(0,a.jsxs)(n.span,{className:"token interpolation",children:[(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"${"}),"randomX",(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"}"})]}),(0,a.jsx)(n.span,{className:"token string",children:"px, "}),(0,a.jsxs)(n.span,{className:"token interpolation",children:[(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"${"}),"randomY",(0,a.jsx)(n.span,{className:"token interpolation-punctuation punctuation",children:"}"})]}),(0,a.jsx)(n.span,{className:"token string",children:"px)"}),(0,a.jsx)(n.span,{className:"token template-punctuation string",children:"`"})]}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token function",children:"requestAnimationFrame"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),"shake",(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"})," ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"else"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" mainElt",(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"style"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token property-access",children:"transform"})," ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token string",children:'"translate(0px, 0px)"'}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" magnitude ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token number",children:"5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" isShaking ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token boolean",children:"false"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsx)(n.span,{className:"code-line",children:"\n"}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword",children:"function"})," ",(0,a.jsx)(n.span,{className:"token function",children:"startOrContinueShaking"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" startTime ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token dom variable",children:"performance"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"."}),(0,a.jsx)(n.span,{className:"token method function property-access",children:"now"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"if"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),"isShaking",(0,a.jsx)(n.span,{className:"token punctuation",children:")"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" magnitude ",(0,a.jsx)(n.span,{className:"token operator",children:"+="})," ",(0,a.jsx)(n.span,{className:"token number",children:"5"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"})," ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"else"})," ",(0,a.jsx)(n.span,{className:"token punctuation",children:"{"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token function",children:"requestAnimationFrame"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),"shake",(0,a.jsx)(n.span,{className:"token punctuation",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[" ",(0,a.jsx)(n.span,{className:"token keyword control-flow",children:"return"})," startOrContinueShaking",(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[(0,a.jsx)(n.span,{className:"token punctuation",children:"}"}),"\n"]}),(0,a.jsxs)(n.span,{className:"code-line",children:[(0,a.jsx)(n.span,{className:"token keyword",children:"const"})," screenShake ",(0,a.jsx)(n.span,{className:"token operator",children:"="})," ",(0,a.jsx)(n.span,{className:"token function",children:"makeScreenShake"}),(0,a.jsx)(n.span,{className:"token punctuation",children:"("}),(0,a.jsx)(n.span,{className:"token punctua
1tion",children:")"}),(0,a.jsx)(n.span,{className:"token punctuation",children:";"}),"\n"]})]})}),"\n",(0,a.jsx)(n.p,{children:"And I think it makes a huge difference. Here Iâve changed the game to only start the screenshake after a few collisions:"}),"\n",(0,a.jsx)(c,{src:"noscreenshake.mp4",width:720,height:720,description:"screenshake starts on the 6th collision",alt:"Breaktime gameplay. The screen begins to shake after the 6th time that the ball hits the paddle. The entire document shakes, emphasizing the impact of the collision."}),"\n",(0,a.jsxs)(n.p,{children:["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). ",(0,a.jsx)(n.a,{href:"https://kidscancode.org/godot_recipes/3.x/2d/screen_shake/index.html",children:"This article"})," describes an approach Iâve taken in previous games."]}),"\n",(0,a.jsx)(n.h3,{id:"toc:other-stuff",children:"Other stuff"}),"\n",(0,a.jsxs)(n.p,{children:["Thereâs so much more stuff that I implemented here! I added ",(0,a.jsx)(n.a,{href:"https://github.com/nolenroyalty/breaktime/blob/856cdb2ad6f161837023b66d233e59958a674422/code/main.js#L1184",children:"custom tweening logic"})," for ball/paddle scaling because using CSS tweening would interfere with moving the objects around smoothly. The ",(0,a.jsx)(n.a,{href:"https://github.com/nolenroyalty/breaktime/blob/856cdb2ad6f161837023b66d233e59958a674422/code/main.js#L1010",children:"ball trail"})," adds elements that track the position of the ball over time while slowly fading out. The ",(0,a.jsx)(n.a,{href:"https://github.com/nolenroyalty/breaktime/blob/856cdb2ad6f161837023b66d233e59958a674422/code/main.js#L973",children:"color changing"})," relies on the lovely CSS ",(0,a.jsx)(n.code,{children:"hue-rotation"})," property."]}),"\n",(0,a.jsx)(n.p,{children:"But this post is super long and thereâs still more to cover. Letâs keep going!"}),"\n",(0,a.jsx)(n.h2,{id:"toc:smoother-animations",children:"Smoother animations"}),"\n",(0,a.jsxs)(n.p,{children:["Remember, my original approach to moving the ball was âevery 50ms, move it a fixed amount and rely on the CSS ",(0,a.jsx)(n.code,{children:"transition"})," property to ensure that that movement is linear.â"]}),"\n",(0,a.jsxs)(n.p,{children:["This works ok, but produces unnatural movement if that loop doesnât run ",(0,a.jsx)(n.em,{children:"exactly"})," every 50ms."]}),"\n",(0,a.jsx)(c,{src:"notsmooth.mp4",width:736,height:736,description:"ignore that bounce off the floor there...",alt:"Breaktime gameplay. The ball moves in jerky, inconsistent bursts. The movement is not smooth."}),"\n",(0,a.jsxs)(n.p,{children:["Most of the time this would result in movement that felt just a little bit off, but occasionally youâd get ",(0,a.jsx)(n.em,{children:"huge"})," leaps."]}),"\n",(0,a.jsxs)(n.p,{children:["To fix this I moved my game loop to rely on ",(0,a.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame",children:(0,a.jsx)(n.code,{children:"requestAnimationFrame"})}),"."]}),"\n",(0,a.jsxs)(n.p,{children:["The idea is that you give ",(0,a.jsx)(n.code,{children:"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 ",(0,a.jsx)(s,{i:5}),"."]}),"\n",(0,a.jsx)(o,{i:5,children:(0,a.jsxs)(n.p,{children:["I believe this makes it hard to capture a great video of how bad the\n",(0,a.jsx)(n.code,{children:"setInterval"})," approach looks on my monitor, since my monitorâs refresh rate is\nsignficantly higher than the frame count in the video."]})}),"\n",(0,a.jsxs)(n.p,{children:["Moving things based on ",(0,a.jsx)(n.code,{children:"deltaT
1ime"})," is standard practice in games. ",(0,a.jsx)(n.a,{href:"https://www.youtube.com/watch?v=yGhfUcPjXuE",children:"This video"})," provides a nice overview."]}),"\n",(0,a.jsx)(n.h2,{id:"toc:declining-events",children:"Declining events"}),"\n",(0,a.jsx)(n.p,{children:"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.â"}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsxs)(n.p,{children:["The approach I took relied on ",(0,a.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver",children:(0,a.jsx)(n.code,{children:"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."]}),"\n",(0,a.jsx)(n.p,{children:"This was tricky and a little fragile. A few things to highlight:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["Itâs important to decline events one at a time ",(0,a.jsx)(s,{i:6}),". To handle this I ",(0,a.jsx)(n.a,{href:"https://github.com/nolenroyalty/breaktime/blob/856cdb2ad6f161837023b66d233e59958a674422/code/main.js#L152",children:"use a second observer to wait for all dialogs tied to the current event to clear"})]}),"\n",(0,a.jsx)(n.li,{children:"Some events donât have a decline option (e.g. if they have no guests); the code must handle that gracefully"}),"\n",(0,a.jsx)(n.li,{children:"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)"}),"\n"]}),"\n",(0,a.jsx)(o,{i:6,children:(0,a.jsx)(n.p,{children:"Otherwise you end up with a broken state with multiple event detail dialogs\nshowing at the same time!"})}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(n.p,{children:"The final result is a flurry of events popping up and disappearing, which I think is pretty fun."}),"\n",(0,a.jsx)(c,{src:"declines.mp4",width:736,height:736,description:"that's a lot of clicking",alt:"Breaktime gameplay. A flurry of events pop up and disappear, each with a decline button. The game automatically and quickly clicks each decline button in turn."}),"\n",(0,a.jsx)(n.h2,{id:"toc:moving-to-a-chrome-extension",children:"Moving to a chrome extension"}),"\n",(0,a.jsxs)(n.p,{children:["For some reason I thought there was no chance that I could get this into the Chrome Webstore ",(0,a.jsx)(s,{i:7}),". But my friend ",(0,a.jsx)(n.a,{href:"https://www.carolynzhang.com/",children:"Kelin"})," convinced me that I totally could, and I was encouraged by Googleâs social accounts ",(0,a.jsx)(n.a,{href:"https://twitter.com/googlecalendar/status/1764769037156843555",children:"tweeting about my stuff"}),"."]}),"\n",(0,a.jsx)(o,{i:7,children:"Maybe because of how janky this project seemed? I donât know."}),"\n",(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsxs)(n.p,{children:["It turns out that making an extension is pretty easy! The ",(0,a.jsx)(n.a,{href:"https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world",children:"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."]}),"\n",(0,a.jsx)(n.p,{children:"Getting the extension approved took under a day - it probably helps that the extension only runs when invoked so its permissions are pretty simple."}),"\n",(0,a.jsx)(n.h2,{id:"toc:wrapping-up",children:"Wrapping up"}),"\n",(0,a.jsxs)(n.p,{children:["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 ",(0,a.jsx)(n.a,{href:"/nonsense/game-11-flappy-bird-finder/",children:"Flappy Dird"})," and ",(0,a.jsx)(n.a,{href:"/nonsense/implementing-wordle-in-the-firefox-address-bar/",children:"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."]}),"\n",(0,a.jsxs)(n.p,{children:["Building a little engine for this project was ",(0,a.jsx)(n.em,{children:"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."]}),"\n",(0,a.jsxs)(n.p,{children:["I also have a ton of people to thank for this project. Thank y
1ou to ",(0,a.jsx)(n.a,{href:"https://twitter.com/ianthehenry/status/1757464426616435093",children:"Ian Henry"})," for the initial inspiration, ",(0,a.jsx)(n.a,{href:"https://twitter.com/ChanaMessinger/status/1757941634975859132",children:"Chana Messinger"})," for naming the game, ",(0,a.jsx)(n.a,{href:"https://kelin.online",children:"Kelin"})," for convincing me to make a chrome extension, ",(0,a.jsx)(n.a,{href:"https://recurse.com",children:"Recurse Center"})," for the encouragement, and ",(0,a.jsx)(n.a,{href:"https://www.joshwcomeau.com/",children:"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!)."]}),"\n",(0,a.jsxs)(n.p,{children:["If you enjoyed this post Iâd encourage you to apply to ",(0,a.jsx)(n.a,{href:"https://www.recurse.com/",children:"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 ",(0,a.jsx)(n.a,{href:"https://eieio.substack.com/",children:"mailing list"})," or follow me ",(0,a.jsx)(n.a,{href:"https://twitter.com/itseieio",children:"on twitter"}),"."]}),"\n",(0,a.jsxs)(n.p,{children:["Iâm going to be at ",(0,a.jsx)(n.a,{href:"https://gdconf.com/",children:"GDC"})," next week to present on ",(0,a.jsx)(n.a,{href:"https://eieio.games/nonsense/game-12-stranger-video/",children:"stranger video"})," at the Experimental Games Workshop and to meet people - if youâre going to be there Iâd love to meet up :)"]}),"\n",(0,a.jsx)(n.p,{children:"Iâll be back in April with something new and horrifying."})]})}function h(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(0,a.jsx)(l,{...e,children:(0,a.jsx)(r,{...e})})}function d(e,n){throw Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}},1582:function(e,n,s){"use strict";s.d(n,{Z:function(){return I}});var a=s(5893),t=s(7294),o=s(1754),i=s(1686),c=s(9711),l=s(2172);let r=i.ZP.a.withConfig({componentId:"sc-d1d98fc4-0"})(["font-family:var(--regular-font);color:var(--color-white-dim);text-decoration:none;line-height:1.2;margin:0;width:fit-content;&:visited{color:var(--color-white-dim);}@media (hover:hover){&:hover{color:var(--color-white);}}"]),h=(0,i.ZP)(r).withConfig({componentId:"sc-d1d98fc4-1"})(["font-size:var(--font-size-lsmall);&:not(:first-child){margin-top:0.375rem;}"]),d=(0,i.ZP)(r).withConfig({componentId:"sc-d1d98fc4-2"})(["font-size:var(--font-size-lsmall);margin-top:0.375rem;"]),p=(0,i.ZP)(r).withConfig({componentId:"sc-d1d98fc4-3"})(["font-size:var(--font-size-small);margin-left:0.25rem;margin-top:0.125rem;"]),m=(0,i.ZP)(r).withConfig({componentId:"sc-d1d98fc4-4"})(["font-size:var(--font-size-xsmall);margin-left:0.25rem;margin-top:0.125rem;"]),u=(0,i.ZP)(r).withConfig({componentId:"sc-d1d98fc4-5"})(["font-size:var(--font-size-xsmall);margin-left:0.25rem;margin-top:0.125rem;"]),x=i.ZP.nav.withConfig({componentId:"sc-d1d98fc4-6"})(["display:flex;flex-direction:column;padding-right:64px;"]),j="toc:title";var k=t.memo(function(e){let{items:n,title:s}=e;return s&&(n=[{level:1,text:s,id:j,href:"#".concat(j)},...n]),(0,a.jsx)(x,{children:n.map(e=>{let{id:n,href:s}=e,t=null;return 1===e.level?t=h:2===e.level?t=d:3===e.level?t=p:4===e.level?t=m:5===e.level&&(t=u),(0,a.jsx)(t,{href:s,children:e.text},n)})})}),g=s(3715),f=s(9332),N=s(1916),y=s(337),w=s(1413);let b=i.ZP.nav.withConfig({componentId:"sc-67c5266d-0"})(["display:none;@media ","{display:revert;position:sticky;top:2rem;}"],o.vb.TWO_COLUMN_LAYOUT),v=i.ZP.h4.withConfig({componentId:"sc-67c5266d-1"})(["margin-top:0;font-size:var(--font-size-large);"]);var I=function(e){let{children:n,post:s,tableOfContents:o,noFooter:i=!1}=e,r=t.useRef(null),h=null!=o&&o.length>0,d=(0,f.usePathname)(),p=h?(0,a.jsxs)(b,{children:[(0,a.jsx)(v,{children:"Table of Contents"}),(0,a.jsx)(k,{items:o,title:s.short_title})]}):null,m=(0,a.jsx)(c.Dr,{contentRef:r}),u=s.image?"https://eieio.games/images/".concat(s.assets_dir,"/").concat(s.image):null;return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(w.Z,{title:s.title,de
1scription:s.description,image:u,type:"article",date:s.date}),(0,a.jsx)(c.pD,{children:(0,a.jsx)(c.Od,{id:1,children:(0,a.jsx)(l.CL,{path:s.assets_dir,children:(0,a.jsx)(N.Z,{additionalLeftContent:p,additionalRightContent:m,noFooter:i,children:(0,a.jsxs)(y.Ls,{ref:r,children:[(0,a.jsx)(g.Z,{titleId:j,title:s.title,subhead:s.subhead,date:s.date,href:d}),n]})})})})})]})}}},function(e){e.O(0,[959,4952,3299,2888,9774,179],function(){return e(e.s=9756)}),_N_E=e.O()}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.