sunnuntai 22. huhtikuuta 2018

Making of White Room Traps You

This time we'll try to create a 3D wireframe cube with the smallest possible charcount. I think that you should always put extra effort into simplifying the object or rendering technique that is the center of your demo. If we can manage to do the cube with circa 100 bytes that leaves us plenty of (white) room to build something special. In the case of my Dwitter demo "White Room Traps You" the box gets some nice shading and becomes alive to trap you inside. Spooky.

Before trying anything in 3D it can help to do a 2D version first. Cube in two dimensions is a square. Since it's perfectly symmetrical we can define it as the largest square that fits inside a circle of a given radius. So we only need a radius - no vertices! We have four corners so we go around a circle in steps of 90 degrees and draw lines between those points like in the animation below...

When we have a flat object parallel to the x-axis we can imagine creating another copy with different y-axis position. When we connect the corners we end up with a 3D wireframe version...

Now we have visualized in a simple way what a cube essentially is. Next we'll look at an efficient way to render it in a single loop.

2. Rendering the cube with a single loop

I often approach a problem with pen and paper before programming anything. To solve this puzzle I drew a 3D cube and followed the edges with a pen to find a simple way to visit all the edges. Since I'm a programmer I can automatically think of the required logic for different paths. It seemed the easiest way was to draw a square wave going around the cube once and then another but with inverted phase. Let's first draw a square wave...

Then do it in 3D around a center point...

Then do the same inverted...

Cube is what happens when we put the two together. But instead of inverting the phase and doing another round we can start to go backwards when we meet our own tail. This keeps the pattern of consecutive vertical and horizontal movements going thus making our JavaScript implementation smaller.

Of course we are drawing some edges twice but dirty tricks are what code golfing is all about. Plus it adds character to the box. Lets look at some code already!

That's the smallest and ugliest rotating wireframe box I could do with the Dwitter boilerplate. I'll try to explain what's going on in the 87 bytes. First we define our canvas size which also will clean it. We will use 64 for the loop repetition count even though 16 would be enough. Again it doesn't matter that we are drawing edges multiple times and we save space when we don't need to define another number. There's also no need to use functions like moveTo, beginPath or closePath usually associated with path drawing. We just keep calling lineTo without worrying about performance or super clean lines. Here's what's going on with our line drawing coordinates if we clean it up...

X = 32 + S(t += (7 - i) % 2 * 1.57) * 20

The first number, 32, is just an offset that places us nicely on the center of the canvas. The radius of the bounding circle is 20 and for x-coordinate we need to multiply that with the sine of our current angle. The angle t is incremented every other round in steps of 1.57 (approximation of PI / 2) with the help of the modulus operator. Manipulating the variable t as our angle is a nice code golfing trick because it's predefined by the framework and resets to elapsed time before each call to u. It gives us rotation animation with only 1 extra byte versus a static cube. A byte well spent I'd say. However the most important part to note is that we turn backwards after 8 iterations by using 7 - i.

Y = 9 + (i % 4 >> 1) * 20 + C(t) * 5

For y-coordinate we multiply the radius by 0 or 1 based on if we are drawing the top or bottom edges of the cube. We need to alternate every other round which is a bit more tricky. The more typical case, which we already had with the angle change, would be to alternate each time by using i % 2. We need to manipulate this by taking the remainder of i divided by 4, then diving it by 2 and rounding down. In the last part we add the cosine of current angle multiplied by 5 to get nice parallel projection.

That's a lot of text and images to explain 87 bytes but we did it! I feel this was the most important part of the demo. Without getting the cube down to this size it would be impossible to add exciting visual additions and a storyline which we'll discuss next.

Having a lot of space left allows us to use more of the Canvas API possibilities. I'd seen other Dweets use drawImage function for cool effects and wanted to try some things with it myself. The function can be used to create a copy of the current canvas or a part of it and draw it in another position. This can be useful for a mirror effect by combining it with scale or rotate function. What I did here was to adjust globalAlpha to 0.1 and copy the canvas onto itself at each iteration but with incrementing y-position. Since the stroke-function is drawing the lines several times at the same position they look dense. But the copies are moved so they remain transparent.

drawImage(c,globalAlpha=.1,i)

Below you can see the difference this subtle shading trick makes. I think it adds a sense of atmosphere and presence to a simple wireframe object...

At this point I was very excited how the demo was evolving and knew I had to fit a storyline to it.

4. Animating with perspective projection manipulation

When you want to manipulate how an object looks you have two options. Either you actually change the shape of the object by adjusting the data or you just change the way it is rendered. For our cube we could change the radius over time to make it larger. While this would otherwise work for the demo it would require more bytes than adjusting the projection variables. What we will do is change our perspective divider Z when the combination of elapsed time and current angle gets past 6.

Z=(t<6)+1+C(t)

What this does is zoom us inside the box over time. Because we do this to one corner at a time the sides of the box appear to open and wrap around us. I hope people actually let the demo run long enough to notice this. :) So that's my white room (without black curtains) which I personally think is among my nicest dweets. I leave you with the full 140 byte source code. Follow me on Twitter if you want to hear of new demos and blog updates - @jylikangas

with(x)for(i=16,c.width=198;i--;stroke()&drawImage(c,globalAlpha=.1,i))
lineTo(99+S(t+=(7-i)%2/.64)*99/(Z=(t<6)+1+C(t)),49+((i%4/2<<6)-25)/Z)