# data[].attributes.blurb.html
Let’s take CSS where it has not gone before. Join Niels on a journey through some truly out-there Web experiments, from creating animations and games with CSS and SVG on an 1980s oscilloscope to fully recreating the classic game DOOM in modern CSS. And where will we end up? Perhaps a game of Chrome’s offline dinosaur, beamed onto a laser projector.

# data[].attributes.slidedeck.data[].slides[].desc.html
Hi! I’m Niels and it is so good to be back on the stage here at CSS Day. Today I want to talk about some projects I created in the last year or so. Now, I’m going say up front that these projects are a bit…. Let’s just say… Out there.

# data[].attributes.slidedeck.data[].slides[].desc.html
You’ve probably seen the CSS DOOM game upstairs and maybe even played it, and that is something that I created. I originally I never intended to recreate DOOM in CSS. Because that is… Insane. But here we are.

# data[].attributes.slidedeck.data[].slides[].desc.html
Oh, PPK…? Did we update the code of conduct, because shooting other attendees in the back with a rocket launcher feels like it would be frowned upon.

But cssDOOM is just small part of the whole story. In fact, it is kind of an accident that happened - an aside to a completely different project – one that is just as unhinged. And I want to start with that story.

# data[].attributes.slidedeck.data[].slides[].desc.html
I get obsessed sometimes. Well… Not sometimes. Often. And not the unhealthy kind of obsessions, but periods of genuine interest and focus. And that often leads to ideas and rabbit holes and sometimes unexpected, and sometimes weird, wonderful results.

# data[].attributes.slidedeck.data[].slides[].desc.html
I get obsessed by web standards, but also northern lights, lego, clocks, user agent strings, astro-photography, oscilloscopes, receipt printers, reverse engineering bluetooth devices…

# data[].attributes.slidedeck.data[].slides[].desc.html
Video’s about watch repair, evolution of germanic languages, DMX controlled stage lights and pyrotechnics, browser compatibility before 2000, the space shuttle challenger accident report, how the browser parses HTML, history of writing

# data[].attributes.slidedeck.data[].slides[].desc.html
Typography and getting annoyed that proper use of the em dash now makes you look like an AI bot, making remote controlled lego cars, how barcodes and QR codes are encoded… And so much more

# data[].attributes.slidedeck.data[].slides[].desc.html
But mostly about the web and how I can use the web beyond the edges of the browser window. I fell in love with the web back in 1994 and that has pretty much been a constant in everything I create. But lately my obsession has been…

# data[].attributes.slidedeck.data[].slides[].desc.html
Making a laser clock. Even since I seen a talk by Seb Lee Delisle talk about lasers 10 years ago I’ve been fascinated by laser projectors. But I knew nothing of how lasers work and if I could connect them to the web – Seb did explain some of the issues he ran into recreating the classic 1979 Asteroids game.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now the original game used a special display system called the QuadraScan, which used a CRT, but instead of pixels it directly steered the electron beam to draw these vector shapes.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now, before you get excited about me showing a laser clock… They are expensive. It’s not like my name is Seb Lee Delisle who has probably a couple of spare ones just stacked up behind couch.

# data[].attributes.slidedeck.data[].slides[].desc.html
But… That game of Asteroids kept lingering in the back of my head and I figured that an oscilloscope could function in the same way as that QuadraScan display.

# data[].attributes.slidedeck.data[].slides[].desc.html
And they are really cool too. I just love the aesthetic – it really feels you have wandered into the lab of a mad scientist with all of those buttons. And if you think of it. It literally is a small particle accelerator that fires electrons at about a quarter of the speed of light. And it is pointed right at your face. Only to be caught by a thin layer of phosphor.

# data[].attributes.slidedeck.data[].slides[].desc.html
But of course we want to use web technology to create this clock. So this part of the talk is titled: “How I used CSS Animations to draw a clock on an oscilloscope”

# data[].attributes.slidedeck.data[].slides[].desc.html
Eh… Wait… Actually, that is not really accurate.

# data[].attributes.slidedeck.data[].slides[].desc.html
How I used WebAudio to blow up an oscilloscope and almost caused a fire…. That would be more appropriate.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now before I tell that story… I want to take a minute and let’s think about what a clock really is… I don’t mean philosophically…

# data[].attributes.slidedeck.data[].slides[].desc.html
It is a circle and three lines. And on the web we can use SVG for that. Four simple shapes… Vectors… Just like that Asteroid game.

# data[].attributes.slidedeck.data[].slides[].desc.html
And now we can just use CSS to animate the hands and rotate them over time. Let’s take a look at the hour hand. It does a full rotation every 43200 seconds or better 12 hours. The default position of the hand is at the top. Midnight or noon. But we want the hand to be at 10.

# data[].attributes.slidedeck.data[].slides[].desc.html
So the animation starts at the to and it takes 10 hours to reach the position we want. But we want to start at 10. And we can do that by setting the animation delay to negative 10 hours. Then it no longer is a delay, but it pretends the animation started 10 hours ago. 10 hours ago it started at the top, so right now - ten hours later - it is at 10. I love negative animation delay. So useful.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now we need to draw these shapes on the scope. The face-melting electron beam is steered - or better deflected - by two electrically charged plates, called the X- and Y-plates. And by deflecting the beam vertically and horizontally we can trace the image that we want. And do that multiple times per second and our image appears.

# data[].attributes.slidedeck.data[].slides[].desc.html
So we need to generate two signals, one for X and one for Y and it needs to trace the image we want to draw. Let’s start with the clock face, the circle. This is the circle and to create this you need some high school math. Turns out my math professor was right all those years ago – I was going to need math later in life.

# data[].attributes.slidedeck.data[].slides[].desc.html
So this is the circle and if you look closely it actually starts to make sense. We just make a wave form directly from the X/Y coordinates of the circumference of the circle.

# data[].attributes.slidedeck.data[].slides[].desc.html
And we can make any shape with that method. Every shape can be expressed in that two channel signal.

# data[].attributes.slidedeck.data[].slides[].desc.html
Even triangles…. Which are three lines… And we need three lines for our clock. Just oriented slightly different.

# data[].attributes.slidedeck.data[].slides[].desc.html
So this is what our clock looks like. If you look closely at the red signal, we have a sine wave, that the circle, and three triangles, those are the clock hands that are drawn from the centre out and then back to the centre. That is one triangle. Then another back and forth for the second hand. And another one for the third. But how do we get those coordinates?

# data[].attributes.slidedeck.data[].slides[].desc.html
We start with our SVG and insert into the DOM with all of the CSS transforms and animation applied, and we can the getTotalLength() and getPointAtLength API to get the raw coordinates, then apply the computed transform matrix. And we end up with two arrays with numbers. And if we plot those numbers in a graph, you can see the sinus wave again, and the three triangles.

# data[].attributes.slidedeck.data[].slides[].desc.html
And we do this 30 times per second… And because the SVG exists in the DOM we just get the CSS animations for free and every time we sample the shapes, we capture the current state of the animation and so we get different wave that exactly captures every frame of the animation. CSS animations on a oscilloscope.

# data[].attributes.slidedeck.data[].slides[].desc.html
So I built a web app that does exactly this. It has a small editor which you can use to edit your SVG and CSS and it will just inject that into the DOM and sample the geometry 30 times per second. And it outputs the waveforms using WebAudio to the oscilloscope.

# data[].attributes.slidedeck.data[].slides[].desc.html
We’ve connected the computer’s audio output to the X and Y channel of the scope

# data[].attributes.slidedeck.data[].slides[].desc.html
Let see how this looks on the scope… And now let’s set it to XY mode. And there it is…

# data[].attributes.slidedeck.data[].slides[].desc.html
And at this point the oscilloscope decided to… Explode. Now this is just the aftermath… I was too busy finding the power plug, scrambling to find my phone to film it and at the same time a big column of smoke was rising up and sparks went flying everywhere. And then I realised — I should probably unplug my computer too. What if this thing sends a couple thousand volts back down the audio cable?

# data[].attributes.slidedeck.data[].slides[].desc.html
I was fine. Did not get electrocuted. My computer survived. But the scope was dead. Now what…

# data[].attributes.slidedeck.data[].slides[].desc.html
Redundancy!… I may have overreacted…

But I am getting ahead of myself. My scope has exploded. I have a signal generator to finish, but no way to actually continue until I get my scope repaired or find a replacement.

# data[].attributes.slidedeck.data[].slides[].desc.html
And I find myself on the train to Beyond Tellerrand in Berlin last November. It’s a five hour ride and I have nothing to do… So I decide to built my own oscilloscope simulator.

# data[].attributes.slidedeck.data[].slides[].desc.html
But what I really want a 1980s oscilloscope with all of its faults and limitations. We also want to replicate how the phosphor works in a real scope. And what we really want is a physics simulation of the electron beam and how it is deflected by the electrostatic x and y plates. But I have to do it from memory. I only had the scope working for an hour or so when it exploded.

# data[].attributes.slidedeck.data[].slides[].desc.html
And this is where things go off the rails. I did a deep dive into how scopes work. Electromagnetic Force, Acceleration, Velocity, Damping, Euler integration of how velocity moves the beam. Euler integrations. Overshoot amplitude decay…

# data[].attributes.slidedeck.data[].slides[].desc.html
And I was about to calculate how the electrons are exciting the phosphor and then I realised… That I have a life.

# data[].attributes.slidedeck.data[].slides[].desc.html
I actually don’t care about this. Apparently there was an end to the rabbit hole for me. It turns out I only care about how it looks. Does it look the same as a real scope? Yes. So great…

And with the simulator in place, it did allow me to create some other generators. Fully created and tested on the simulator… And when my scope got repaired, they just worked.

# data[].attributes.slidedeck.data[].slides[].desc.html
Great. But this isn’t the real DOOM game. We can move around and that is pretty much it. But we do have all of this data. We know where the walls are, where the floors and ceilings are. So I started thinking… We could project those walls with CSS 3D transforms. We could add textures. And within a day I had something where I could walk around. Eventually I starting adding more and more, such as doors, pickups, and the enemies, even fireballs.

# data[].attributes.slidedeck.data[].slides[].desc.html
And it isn’t fully CSS of course. There is a considerable bit of Javascript ported from the original game. But the renderer is almost 100% CSS.

# data[].attributes.slidedeck.data[].slides[].desc.html
But why?

# data[].attributes.slidedeck.data[].slides[].desc.html
First of all. JavaScript should only do what only Javascript can do. An CSS can do this. So… Now the next question is probably: “Are you crazy?”… Well… Eh..

# data[].attributes.slidedeck.data[].slides[].desc.html
So… There is just a small layer of JS that does basically nothing more than create DOM elements and sets classes and custom properties.

# data[].attributes.slidedeck.data[].slides[].desc.html
And every wall, floor, ceiling is a DIV. Simple DIVs that are 3D transformed.

# data[].attributes.slidedeck.data[].slides[].desc.html
We’re not using JavaScript to position every DIV, but instead the JavaScript extracts the raw coordinates from the DOOM game file - the WAD file - and those raw coordinates are passed to CSS as custom properties.

# data[].attributes.slidedeck.data[].slides[].desc.html
So these are all the DIVs we created for level 1. And this is one of the smaller levels. So we insert this in the DOM. And then we let CSS handle the rest. All the complicated math is done by CSS.

# data[].attributes.slidedeck.data[].slides[].desc.html
It’s actually not that bad. Especially compared to all the other math I needed for the scope simulator.

It basically boils down to the theorem of Pythagoras. And the reason why is that DOOM isn’t actually fully 3D, but 2,5 D. It is a flat map with heights. Not a full 3D scene - which is also why the translation to CSS works so well.

# data[].attributes.slidedeck.data[].slides[].desc.html
So this is our top view. We have our start coordinates and our end coordinates. What we want to do is position our wall on those start coordinates, and set a width and rotate it on the angle between the start and end coordinates.

# data[].attributes.slidedeck.data[].slides[].desc.html
And the width is just the longest side of the triangle, which is the square root of x squared + y squared. And the angle is the inverse tangent of y divided by x.

Great. Everybody with me? Just nod and pretend this makes sense. It’s fine.

# data[].attributes.slidedeck.data[].slides[].desc.html
In our CSS this looks like this. Thanks to the relatively new CSS trigonometry functions we can now just calculate the width and the angle that we need. No need for JavaScript to pre-calculate everything. No. We’re just setting the properties straight from the DOOM WAD file and CSS does the calculations.

# data[].attributes.slidedeck.data[].slides[].desc.html
And this is where we then simply set the width - and also the height which we can calculate from the height of the floor and ceiling.

And then we position the DIV in 3D space using translate3d with the start coordinates and finally rotate it using the angle we just calculated. And we do this for every wall in our scene. Which can be a couple of thousand.

# data[].attributes.slidedeck.data[].slides[].desc.html
And of course ceilings and floors too. They are also just square DIVs that use the same positioning calculations, except that we need to rotate it 90 degrees to place them flat “on the floor”.

Now this works… If you have a square “sector”. A sector is basically a group of walls, floors and ceilings that belong to each other.

# data[].attributes.slidedeck.data[].slides[].desc.html
But as you just seen, we can place wall at arbitrary angles, so rooms are not by definition squares. And DIVs are… So how are we dealing with these floors. Here we can see two sectors. One is a simple octagon. The other is an polygon with a hole cut out of it. And these are DIVs too…

# data[].attributes.slidedeck.data[].slides[].desc.html
DIVs with a clipping path. Great. This involves a bunch of extra math that we do in JavaScript - it isn’t really feasible to do this in CSS. That is that little bit of JavaScript that we need in our renderer. But we pre-calculate that and set it as a clipping path and then CSS does the actual rendering.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now, simple shapes like octagons have been possible for a while now. But the shape() function is relatively new and it uses a human readable format that basically describes the shape of the clipping path. And thanks to evenodd, we can basically describe two separate paths, so you can cut off the edges, and create a cut-out in the middle as well.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now if we turn around and only look at the lighting of the scene we can see two things. First of all we’re standing on a light platform looking at a dark room with another light room in the distance. Those are all static lights.

# data[].attributes.slidedeck.data[].slides[].desc.html
The light level is stored in the game wad file and not calculated on the spot. That makes our lives a lot easier. We can also see that the floors and the ceilings have the same shape and the same lighting level. Ceilings, floors and walls are grouped into sectors and the light is set on the whole sector.

# data[].attributes.slidedeck.data[].slides[].desc.html
But sectors are not always room sized. If you look at the stairs you can see the ceiling above the stairs have the same lighting as the stairs itself. That is because these are also sectors. Not the stairs together.

# data[].attributes.slidedeck.data[].slides[].desc.html
But every single step of the stairs is a sector with its own floor height and ceiling. And we set the light for the whole sector, so the ceiling is also lighter than the surrounding room, just like the step of the stairs.

# data[].attributes.slidedeck.data[].slides[].desc.html
How do we set the light level? We set a custom property on the sector itself and then that property is inherited down to the individual surfaces and we apply a brightness filter to it. And that is all we need to do. We are lighting our whole scene with a filter on every element.

# data[].attributes.slidedeck.data[].slides[].desc.html
But you’ll also notice that we have two pillars here that have a slow pulsating lighting effect. These effects are also per sector and are defined in the DOOM game file. And DOOM supports several types of lighting effects – which are little more than just animated brightness changes.

# data[].attributes.slidedeck.data[].slides[].desc.html
The pilar has a simple class on the sector which will run an infinite animation that changes the —light custom property. And now some of you will immediately say, you can’t do that. And that is true. Because CSS does not know what the light custom property is and how to transition between values.

# data[].attributes.slidedeck.data[].slides[].desc.html
So we can use at property to tell CSS that it is a number - which can have fluid transition and it inherits, which we need to give each child surface div the value we assign to the containing sector div.

# data[].attributes.slidedeck.data[].slides[].desc.html
And if we want to change the animation we only have to change one class and let CSS does what it does best.

# data[].attributes.slidedeck.data[].slides[].desc.html
But lets look one other very important part of DOOM. The textures.

# data[].attributes.slidedeck.data[].slides[].desc.html
So the textures are just the original image files that I extracted from the DOOM wad file, converted to PNGs. There are lots of them and they are pretty tiny according to modern standards

# data[].attributes.slidedeck.data[].slides[].desc.html
And every wall, ceiling or floor has the data-texture attribute which tells us which texture we are using. And we have a file that just loads the correct background image based on that texture value. I wish we could use attr() here, but as Kevin the Youtube guy showed, that is not allowed.

# data[].attributes.slidedeck.data[].slides[].desc.html
And then we got…. Eh. Yeah. That isn’t supposed to happen.

# data[].attributes.slidedeck.data[].slides[].desc.html
Sorry… Try again…

# data[].attributes.slidedeck.data[].slides[].desc.html
Ok, here we go.

# data[].attributes.slidedeck.data[].slides[].desc.html
Oh. Eh… This is of course CSS. So we could just override the textures with anything that we want.

# data[].attributes.slidedeck.data[].slides[].desc.html
Ok… We’re back. All this time we’ve been walking around this world… But how do we actually do that. Of course there is JavaScript involved here. We read keystrokes, mouse events or touch events, or the game pad buttons. And then… A lot more Javascript. We need collision detection. This is all running in the game loop.

# data[].attributes.slidedeck.data[].slides[].desc.html
But there is no camera in CSS that we can move around. Instead we move the world. So we don’t move through the world, the world moves around us. The game loop sets 4 simple custom properties. And that is all the renderer needs to know. Whenever the player moves, the properties are updated. And CSS moves the world using transform translate3d and rotateY. We don’t need to recalculate every wall floor or ceiling. The world itself is “static”.

# data[].attributes.slidedeck.data[].slides[].desc.html
If we zoom out a bit we can actually see this. The world rotates around us. If we move up the stairs, the world moves down. The player is static at exactly the same position. We go back down, the world moves back up.

# data[].attributes.slidedeck.data[].slides[].desc.html
And CSS gives us this for free. What you are seeing right here is spectator mode and it is build into cssDOOM. And this costs us literally nothing. It is just a small override of the scene transform where we add an additional translate to move the camera a bit back and up and rotate our camera to look down…

# data[].attributes.slidedeck.data[].slides[].desc.html
You’ve maybe also noticed that sprites, such as barrels and pick ups are always directed at player. This is called billboarding and from above that is clearly visible.

# data[].attributes.slidedeck.data[].slides[].desc.html
And that is again something that CSS will do for us. We already have the —player-angle custom property on the scene. And we can just use that to calculate the correct rotation to apply on the sprite.

# data[].attributes.slidedeck.data[].slides[].desc.html
This angle also gives us a good look the door. Let’s open it.

# data[].attributes.slidedeck.data[].slides[].desc.html
I wish I could show some interesting code here… But no. My apologies about the terrible joke here. Doors are actually really boring. They just transition between two heights… The offset is defined by the game data, so we just set that as a custom property when we generate the door.

# data[].attributes.slidedeck.data[].slides[].desc.html
The difficult part is all in the game loop. It needs to check collision detection and make sure that enemies cannot see through doors. But that is not interesting. The renderer actually does not need to know anything except when the door opens and closes and all it does is set the door-state attribute.

# data[].attributes.slidedeck.data[].slides[].desc.html
The next thing we should talk about is sprites. We have all kinds of objects that we can pick up, barrels that we can shoot and of course monsters. They are 2D animated images. For example for enemies we have images for walking from different viewpoints and we have shooting and dying. All of these images are combined in one large sprite sheet.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now that image is way larger than the div that contains it. Here you can see the whole sprite sheet, and the yellow box is the active sprite. You can see the selection of the sprite within that sheet constantly changing. So we use background position to show the correct angle or action.

# data[].attributes.slidedeck.data[].slides[].desc.html
And by clever orientation of the images and changing the background position we can have the sprites fully animated. One very important detail is that we are using stepped animations, which in case of this helmet, we are stepping over four possible positions.

# data[].attributes.slidedeck.data[].slides[].desc.html
So that gives us the appearance of the helmet pulsating, while in reality the image is just moving around.

# data[].attributes.slidedeck.data[].slides[].desc.html
And it looks really good. Again fully automatic and powered by CSS.

# data[].attributes.slidedeck.data[].slides[].desc.html
Fireballs are also sprites. They are animated and billboarded, just like the other sprites. But they are quite special… Because they are flying through space using a simple CSS animation.

# data[].attributes.slidedeck.data[].slides[].desc.html
Whenever an imp launches a fireball, a new DIV is added to the DOM with a couple of custom properties, such as the start point, the calculated endpoint and the time needed.

# data[].attributes.slidedeck.data[].slides[].desc.html
And then we let CSS run that animation. And it is incredibly effective. We don’t need to update the position from the game loop. CSS does it for free. And if you look closely here… We’re using the standalone translate property, not the transform property. And that is for a very good reason.

# data[].attributes.slidedeck.data[].slides[].desc.html
And that reason is billboarding. If we were to use the transform property we could not independently run the animation and have the billboarding effect. But by using the standalone transform properties we can set them without overriding the other. The animation controls the positioning. And the player angle controls the rotation.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now we do need to tell the renderer when there is an impact to remove it - perhaps even mid flight and then show the explosion sprite. By the way, explosions and bullet impacts are simple sprites created by the JavaScript layer that run for a couple of frames and then remove themselves again from the DOM by listening to the animationend event. So they are literally fire and forget.

# data[].attributes.slidedeck.data[].slides[].desc.html
So let’s talk a bit about another animation technique that is used throughout this project.

# data[].attributes.slidedeck.data[].slides[].desc.html
When the player is moving around you can see the weapon bob around. That is a nice little CSS animation.

# data[].attributes.slidedeck.data[].slides[].desc.html
And originally we just apply the animation when the player was moving using a class set on the viewport. And that works, but the illusion breaks whenever you stop moving. The weapon would just jerk back to the default position.

# data[].attributes.slidedeck.data[].slides[].desc.html
The solution is to always have the animation, but the default play state is set to paused. It is just paused somewhere in that animation and we don’t care where. And whenever the player is moving we set the state to running, so it will continue from where it last stopped. So now the transition between walking and stopping is seamless.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now, one of the last things I wanted to show you. Because this is the web, cssDOOM is responsive.

# data[].attributes.slidedeck.data[].slides[].desc.html
And that creates some other challenges, because the gun here needs to be aligned to the top of the status bar. And when the status bar wraps over two or even three rows, we need to move the gun upwards. Anchor positioning to the rescue. The status bar is the anchor and we just make sure the bottom of the gun is anchored to the top of the status bar. Probably not something that the spec authors thought about when they created Anchor positioning. But I love it.

# data[].attributes.slidedeck.data[].slides[].desc.html
So yeah. That is just some small details of how css DOOM works. It grew quite a bit since that first proof of concept. I’ve added multiplayer support - you can play it yourself upstairs - not now - later - which basically renders two full scenes in one browser window which is then split over two screens.

# data[].attributes.slidedeck.data[].slides[].desc.html
It is amazing that this actually works. We’re pretty close to the limit of what the browser can do…. WAIT Everything you’ve seen so far is just thousands of divs - probably less than a React app, but still. 1000s of divs and some CSS. But there is one exception…

# data[].attributes.slidedeck.data[].slides[].desc.html
This is a button. As it should be. And if I can do that in DOOM, what excuse do you have? A div is not a button.

# data[].attributes.slidedeck.data[].slides[].desc.html
So… Is this useful. No. Don’t do it. CSS was never intended to do this… But I am actually amazed how performant it is… CSS is awesome!

# data[].attributes.slidedeck.data[].slides[].desc.html
But not every browser does this well. We are definitely finding the limits. There are some issues in Chrome - which will hopefully be solved eventually. Some rendering issues in Safari, but it is definitely quite playable. The best browser for this kind of brutal rendering punishment is Firefox which has been not been flawless, but pretty close. Bugs will be filed. And I’ve been told that Jake and Bramus will personally fix them this afternoon… So…

# data[].attributes.slidedeck.data[].slides[].desc.html
So this was a very nice side-quest. A rabbit-hole inside of a rabbit-hole. But why stop here? Am I going to build a version of CSS DOOM that runs on Lyra’s CSS CPU emulator… No.

Is there more to this story? Well I got distracted for a bit.

# data[].attributes.slidedeck.data[].slides[].desc.html
I created a CSS flamethrower.

# data[].attributes.slidedeck.data[].slides[].desc.html
On the train to Beyond Tellerrand I also saw this on AliExpress. It’s really cheap. AliExpress, cheap and flamethrower. Not great to hear those words in the same sentence…. But there was this button. And I bought it… I figured I had weeks before it arrived and plenty of time to prepare my wife. And when I arrived home, it was there. On my coffee table. I had some explaining to do.

# data[].attributes.slidedeck.data[].slides[].desc.html
Yeah. I cannot show it here on stage, because * apparently a church burned down in Amsterdam earlier this year and I’ve been told that under no circumstances I am allowed to set fire to the stage.

# data[].attributes.slidedeck.data[].slides[].desc.html
We’re using the same technique as before. We’re sampling the styles applied to elements in the DOM. In this case getComputedStyle to get the values of custom properties. I build a whole web app for this that can control all kinds of devices, such as lights, smoke machines, laser projectors and flamethrowers. All using CSS and it can actually run animations on it.

# data[].attributes.slidedeck.data[].slides[].desc.html
And yes, I still want to make a clock using a laser projector… I figured that if I can buy 6 oscilloscopes and a flamethrower I could buy a laser projector as well… And as it turns out connecting it to the web is relatively simple using WebUSB. And it expects X and Y coordinates – which we already have for our oscilloscope…. So who wants to see CSS animations on a laser projector?