torstai 19. huhtikuuta 2018

Making of Choo Choo


In this article I will explain how my Dwitter demo 'Choo Choo!' was created. It will hopefully show that the key to fitting interesting demos in 140 chars is to extremely simplify the logic. After that you can just use any and all dirty code golfing tricks to shave off unnecessary bytes. All aboard! :)

1. Thinking in angles


If you start to define XYZ-coordinates for anything in your dweet you'll have used up your space in no time. Luckily there are simpler ways to look at things. For this demo I chose to think of everything in angles. We'll be using rotation of axes in 2D. The mathematically correct formula to do this would (probably) be...
x = r cos α
y = r sin α

Let's first draw an overhead view of a track that is shaped like a circle. A full circle in radians is PI * 2 so normally we would loop from 0 to circa 6.283 in really small increments. Here we don't have to be that precise. We'll increment the angle by 1 and just go around so many times that we end up filling the gaps. In the code examples w is canvas width, a is current angle and r is radius. We are using the Dwitter boilerplate which gives us shorthands S and C for Math.sin and Math.cos. It also gives us t for elapsed time...

for(c.width = w = i = 500; i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r, 5, 5)
}

So our track is just a series of angles rendered with the rotation formula. Now let's think of a train as just another track, but positioned above the actual track. For presentation purposes we'll do this by using a slightly larger radius...

for(c.width = w = i = 500; i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r, 5, 5)
    x.fillRect(250 + S(a = i) * (r = 110), w/4 + C(a) * r, 5, 5)
}


Now we have a train but it's as long as the track which is not very useful. Let's squeeze it smaller by dividing i with 500. This means our train is 1 radians in length while the track is circa 6.283.

for(c.width = w = i = 500; i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r, 5, 5)
    x.fillRect(250 + S(a = i / 500) * (r = 110), w/4 + C(a) * r, 5, 5)
}

It would be nice to get the train in motion to make things more interesting. Since the train is just a series of angles, we can get it moving by adding time to each angle at each frame...

for(c.width = w = i = 500; i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r, 5, 5)
    x.fillRect(250 + S(a = i / 500 + t) * (r = 110), w/4 + C(a) * r, 5, 5)
}

While this long ass train is nice it would probably break from bending like that. Luckily someone invented railcars. We can get them by splitting our train into groups of angles. By experimenting we can find a suitable number of cars and make the locomotive itself a bit shorter. Note that we need to round to integers to get the gaps. In code golfing we love to do this with bitwise OR...

for(c.width = w = i = 500; i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r, 5, 5)
    x.fillRect(250 + S(a = i / 500 + t + (i / 90 | 0) / 9) * (r = 110) , w/4 + C(a) * r, 5, 5)
}


At this point it should be fairly obvious that we already have all the needed data and logic for our demo. We have successfully simplified the train and the track into angles. In the next step we will make the rendering more interesting by going 3D.

2. Rendering in 3D


The demo has perspective projection, but first we'll use parellel projection to achieve a 3D look with very little effort. All we need to do is make the radius used for the Y-axis rotation smaller by dividing it by 3. We'll do this to the track first.

for(c.width=w=i=500;i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r / 3, 5, 5)
}
Now comes another important simplification - we can render the track and train at the same coordinates, but with inverse height. When we give the train a negative height it is drawn above its position and thus ends up nicely on the tracks...


for(c.width=w=i=500;i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a) * r / 3, 5, 5)
    x.fillRect(250 + S(a = i / 500 + t + (i / 90 | 0) / 9) * r, w/4 + C(a) * r / 3, 5, -5)
}

3. Different tracks


I always want my demos to have a storyline. What I mean by that is that the visuals somehow evolve over time. A good demo will suprise you in the end just when you think you've seen it all. In 'Choo Choo!' I achieved this by changing two separate things at different time intervals. First we'll look at how I change the track shape. It turns out all I had to do was to multiply the angle used for the y rotation formula. This adds waves to the circle. The final demo uses 4 multipliers which doesn't make it look too busy. I'm tempted to do a remix of the demo with some other shapes though.

for(c.width=w=i=500;i--;){
    x.fillRect(250 + S(a = i) * (r = 100), w/4 + C(a * 3) * r / 3, 5, 5)
    x.fillRect(250 + S(a = i / 500 + t + (i / 90 | 0) / 9) * r, w/4 + C(a * 3) * r / 3, 5, -5)
}


4. Multiple perspectives


Well actually there's only two but since the track shape keeps changing we get the illusion of a much more complex camera work. Perspective projection makes things appear smaller the further away they are from the viewer. Since we are thinking in angles our distance divider can be taken from our y axis rotation formula. To get our Z for a point in the track we just use the cosine of the angle and zoom out by adding either 1 or 2 to it based on elapsed time. Here we benefit from Javascript logical expressions which evaluate to 1 or 0.

Z=C(a)+1+(t%6<3)



I get easily obsessed with small details that I just have to have in the demos. Instead of taking the easy way out and having the track and train be the same width, I wanted the train to be wider. With this comes a problem of centering the train on the track. It took several extra bytes but I think the demo benefits form this detail. Another 2 byte cost came from deciding to use strokeRect instead of fillRect. I felt the wireframe style rendering emphasized the 3D look better.

When the overall idea is really simple the implementation shouldn't take much space. Still 140 bytes is so little that I had to dig deep into my bag of golfing tricks to get this all in. Below is the final source code and it looks like a mess. The most important thing was to create a function for drawing. It's rarely advisable on Dwitter but it works here cause we can reuse the drawing code to the extent where the track is basically added with just 6 more chars - F(i,4).

for(c.width=i=714;i--;F(t+i/714+(i>>7)/9,-9))(F=(a,o)=>x.strokeRect(357+S(a)*290-o/(Z=C(a)+1+(t%6<3)),99+C(a*(t/6%4|0))*99/Z,o/=Z/2,o))(i,4)

That's the 140 bytes of JavaScript that on a Dwitter boilerplate gives you a fun train ride. I hope this article has inspired you to have a go at code golfing yourself. Choo choo!

Follow me on Twitter if you want to hear of new demos and blog updates - @jylikangas

Ei kommentteja:

Lähetä kommentti