maanantai 14. toukokuuta 2018

Making of Wireframe World




For some time I've been trying to come up with a way to do a 3D wireframe landscape within the 140 byte size limit of Dwitter. This article details how I finally managed to pull it off.

1. The grid


Wireframe landscape is very tricky to do in limited space. The reason is the need to use the HTML5 Canvas API path drawing methods. The calls to x.beginPath(), x.lineTo() and x.stroke() already take up 33 bytes and that's not counting the statement separation. Also you need to call lineTo at least twice with different arguments to actually draw a line segment so you have to be really creative with the way you arrange the code. As usual lets start in 2D to keep things simple. Wireframe landscape can be thought of as a perfectly symmetrical grid but with height information stored for each vertex. So looking directly from above, it looks like a regular grid...




I know this might look boring unless you are really into Xs and Os. To draw a square we need four lines. With a grid we can cheat and just do two for each square...


With this simplification we've saved two calls to lineTo but our grid has two ugly unfinished edges. This is not a problem as we will make sure those are never visible in the demo.

So we are drawing only the left and top edges and the neighbors will take care of the rest. To do this efficiently the best solution I could come up with is to start from bottom left corner and then move to top left and top right corners. I've made a very important illustration to show this...


First we need to move to the right position. Normally this would be done with moveTo-method but lineTo works the same way when there is no previous points in the path. The movement sequence could be written X1,Y2 -> X1,Y1 -> X2,Y1. I'll just show you the full source code and then we'll break it into parts...

for(c.width=i=-9;++i<9;R=a=>x.lineTo(150+a*99/(z=j-t%1),z+90+(99+T(j+t&a+9|8)+a*a)/z))for(j=12;x.beginPath(x.stroke()),R(i),--j;R(i+1))R(i)

The grid points are done as expected with two nested loops. Outer loop with the variable i defines the x-coordinates and j defines the y-coordinates. The horizontal loop is centered around zero for simpler perspective projection and goes from left to right. The vertical loop goes backwards from 11 to 1. It's important to stop at one to avoid getting negative values for our perspective divider z. We create a function R that encapsulates the projection and path drawing. Arrow function syntax is naturally very suitable for code golfing. We'll use an argument a to pass an x-coordinate to the function and we can use j directly from global scope. Now we have our loops ready and a function that draws lines for us. Next we'll take a deeper look into the magic of the for-loop.

2. Understanding the for-loop


It is really important for any programmer to fully understand how a for-loop works. It is not necessarily obvious in which order statements and code are run so here is a quick reminder.

for(A;B;D){C}

  • Statement A is executed first and only once before the looping starts. It is used to initialize the loop
  • Statement B is executed before each repetition of the loop. It is the condition for running the loop. If it evaluates to false the loop stops, otherwise the loop will continue running
  • Code block C is executed if condition B was met
  • Statement D is executed after each repetition of code block C. Loop variable is often incremented here, but that is optional
Our inner loop looks like this. Lets go through what happens

for(j=12;x.beginPath(x.stroke()),R(i),--j;R(i+1))R(i)

In initialization we declare a variable j and set it to 12. For our conditional statement we are grouping statements in a way that some functions are called before the actual condition. This is something that is not normally done but allows us to run code before we change the value of j. The first function we call is x.stroke() which draws the current path. Since there is no path at first nothing is drawn, but we save one byte by placing it here as a useless argument. Then x.beginPath() gets called and starts a new path for us. Then we move to "X1,Y2" by doing our first call to R(). We pass our current x-coordinate i as an argument and the function uses our y-coordinate j directly. Then we decrement j and call R() again to draw our first segment to "X1,Y1". Notice here that the loop is now running the code block after evaluating the condition. Once --j evaluates to zero, the loop is stopped. Because we decrement first and then evaluate the condition, our code block will not run when j is zero. Noticing this optimization trick saved me 2 bytes and allowed me to fit the demo in 140 bytes after being stuck at 141 for several days. So why is this imporant. Well if we went to zero, we'd need to add 1 to our z calculation like this z=j-t%1+1After this the last statement is executed. Now we need to draw a line to "X2,Y1" so we pass i+1 as the argument. Now our path is complete and during the next repetition it gets drawn to canvas by a call to x.stroke().

The lesson here is that there are many ways you can use the for-loop. We are so used to sticking to the most common structures that it can be hard to notice when we benefit from a different approach.

3. Going 3D


Lets assume our grid lies flat on the floor. From where we look at it the horizontal axis is X and vertical axis is Y. Since it's flat, Y is constant. Lets have an axis perpendicular to X on the same plane and call it Z. This can approximately be thought of as our distance from any of the grid points. To achieve perspective projection we just need to divide both X and Y by Z. In our demo we could do it like this...

R=a=>x.lineTo(150+a*40/(z=j),90+90/z) 




Now we could use some movement. We want to go forward which means our distance should decrease. We do this by subtracting time from z. Our grid is limited in size so we will do a loop by using modulus operator...

R=a=>x.lineTo(150+a*40/((z=j-t%1)),90+90/z)





At this point my small 3D wireframe engine is working, but it's not very interesting. I tried to do various random terrains but wasn't happy with them. Then I remembered a cool unfinished JS1K demo by Bálint Csala and decided to do a small planet that spins around and has little spiky mountains. I like spruce trees so the end results also reminds me of Taiga landscapes. Anyway to make it seem like we are on a small planet we need to make the grid round both horizontally and vertically. Here's how I did it...

R=a=>x.lineTo(150+a*80/((z=j-t%1)),z+90+90/z+a*a/z)




This also effectively hides the ugly right edge of our grid. Now all we need is those spikes. It really is not easy to add any type of recognicable architecture to a 3D model this small. Often the best solution is to use trigonometric functions. This time I experimented with tangent planes until I found something I liked...

R=a=>x.lineTo(150+a*80/((z=j-t%1)),z+90+90/z+a*a/z+T(j+t&a+9|8)/z)


A bit more code golfing trickery and that's all of Wireframe World. I just wish there was room to use some 80s colors on black background.

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

tiistai 24. huhtikuuta 2018

Making of Neon City Wakes Up


I love retro pixel art and 80s graphics in general. I also like to program in an old school way with bitwise operations and all that glam rock. I'm just telling this so you all would understand why this demo is so incredibly last season. Here we go again on our own...

1. Pixel array approach


When I do raycasting I often manipulate the Canvas ImageData array directly. For Dwitter demos that approach is not feasible because the calls to getImageData and putImageData take a lot of space. There is a way to simulate it though. We can use fillRect to draw each pixel on the screen separately and then control them either by setting color with fillStyle or by simply switching pixel width between 1 and 0 and doing black and white. This changes the way we look at drawing on screen as we are drawing every pixel by default and the logic will either define what not to draw or at what color. For the sake of simplicity, lets first look at the case of each pixel on screen being either drawn or not drawn and build our city skyline this way. We have the Dwitter boilerplate and all we need in addition is a tight loop that draws the screen full of small rects...

for(c.width=i=110;i--;)for(j=64;j--;)x.fillRect(i,j,1,1)

That should paint our whole screen black. Basically i is our screen x and j is screen y. If you change the width in fillRect to 0, the screen is left white. 0.5 will make it grey. Adding a conditional statement there lets us control the image. Just to show how it works here's how you'd do a flat horizon...

x.fillRect(i,j,j>32,1)

So if our current screen y coordinate is larger than 32 the width of the rect will be 1, otherwise 0. This results in only the lower half of the screen being black. Now lets get busy and do the silhouette of the buildings using this method...

2. Adding conditions




x.fillRect(i,j,j/9>3+S(i|3),1)

I'm quite proud of this little formula that gives us the city in 12 bytes. It's easier to read as  j>27+S(i|3)*9 but we save an extra byte by scaling things down. The condition basically tells the browser to only draw when the current pixel is below our little sine wave. The blockiness is achieved with the bitwise OR operation. Now to something much more complex - adding the reflections on water...



x.fillRect(i,j,j/8<6+C(i-j-t)&j/9>3+S(i|3),1)

The idea is the same but we don't need the OR operation and we make the reflections angled by adding j to the mix. Well actually subtracting it but you get the idea. Variable t is elapsed time and gives us movement. We use the AND operator to make sure both conditions are met before we draw pixels. Seems to be working nicely and only 82 bytes spent in total. Time for details!

Many 80s computer graphics had wireframe grids vanishing to the horizon and a sun that had horizontal lines going over it. We will simplify this by just doing horizontal lines on the sky - again by adding a condition...


x.fillRect(i,j,j/8<6+C(i-j-t)&(j/9>3+S(i|3)|j%7<1),1)

Only 8 more bytes thanks to the modulus operator! We don't want the lines on water so we need to combine our new condition with our building condition. This requires adding parenthesis. We use the bitwise OR to draw if either condition is true. Notice what a big difference these horizontal lines make in making the image recognizable. Up next, the stars...


x.fillRect(i,j,j/8<6+C(i-j-t)&(j/9>3+S(i|3)|j%7<1)|!(1+j*i&79),1)

12 bytes for a starry sky that reflects on the ocean isn't all that bad. We multiply our x and y coordinates and use some bitwise magic to scatter the stars all over the screen. The logical NOT operator turns the result of our formula into 1 if it's anything other than 0. You might wonder why we add one in the beginning - that's so we don't get a vertical black line on our left edge when we are multiplying zero with zero. Now we have added 4 layers of fairly short conditions. With these rules the end results is a 102 byte black and white version of the demo. I used the remaining space to add animated neon colors and higher resolution. I also changed the order of conditions slightly to separate layers into 3 groups of base color.

3. Code golfing tip


To finish I'll point out one code golfing trick that helped me fit this demo inside the Dwitter limit of 140 byte...

x.fillRect(i*T,j*T,T=18,T)

To get larger pixels for the high resolution version I needed to multiply each argument by 18. The first time I need it is not the best place to define a variable cause I'd have to use parenthesis. What we can do is use a variable that has already been declared in the boilerplate. We don't need Math.tan in our demo so I chose the provided shorthand T. When fillRect is first called it doesn't draw anything cause T is not a number. The second time and from then on the value of T is 18.

I hope you enjoyed this article. Would love to see your work on Dwitter too!

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




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.

1. Start with 2D


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.

3. Fake shading


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)

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

keskiviikko 14. maaliskuuta 2018

Making of Tuulian Minecraft






The idea for this very short demo came from trying to fit my  old JS1k demo "3D City Tour" floorcasting engine into 140 bytes. While working on it I thought it would be cool to try and mimic Minecraft landscapes. Our family's youngest daughter Tuulia is a big fan of the game which not only gave me inspiration for the challenge but also meant that I had a boss who wouldn't settle for anything less than the original. Eventually I told her she would have to live without the lambs.

1. Floorcasting engine


How do you do a Minecraft landscape in 140 bytes of JavaScript on Dwitter?
Well, you first do it in 100 bytes and then just have fun with all the remaining space. :)





Here is the source code for the 100 byte version which I'll use to explain how the landscape generation and rendering work...

for(c.width=i=W=99;i--;)for(j=W;j--;)x.fillRect(i,j,2/(d=700/j),~(d*S(A=i/W+S(t))&d*C(A)+t*9&5)*W/d)

It really is that short. We are using a nested loop. First loop goes through each column of the screen (variable i is basically screen x-coordinate) and the second goes through each row (variable j is screen y).

In the heart of the loop is a call to HTML5 canvas fillRect method. It takes four parameters - x, y, width, height. This is all we need for rendering. The loop goes through each pixel on the screen and we need to decide on two things - the height and the opacity of the rect to draw at that position. The default color is black so we don't need to set it and we can fake greyscale by using rects that are less than 1 in width. You can think of the width as the alpha channel.

I've explained the process of floorcasting in the past so I won't go into detail about it here. In this demo we aren't really doing anything differently, we just do things super efficiently. I'll break it down next.

2. Variables


Variable A is current ray angle. It is calculated by dividing the screen x by total width and adding the camera direction (yaw). Camera direction could be anything but we use the sine of elapsed time for smooth and exciting movement. We could save three bytes here by settling for less pleasing camera path.

A = i / W + S(t)

Variable d is a distant factor. We will need it to figure out where our current ray lands in the world coordinates. We also use it for the fog effect. It is calculated by defining a large number that we divide by the current screen y. 700 was selected because Toni Kukoc wore number 7. For different landscapes another number could work better. We are just doing scaling here.

d = 700 / j;

For our world coordinates we use basic trigonometry. Below are the lines that calculate world X and world Y. We add elapsed time to Y coordinate to achieve movement. It's multiplied by nine cause we like it fast and we have the bytes for it.

X = d * S(A); // world X
Y = d * C(A) + t * 9; // world Y

Since we are in code-golfing mode things can be a little tricky to spot from the source code. The landscape is the result of a very simple height function using the bitwise AND operator. 5 creates a lego city landscape while 7 gives a mountain terrain as used in the color version of the demo.

h = (X & Y & 5) * 300 / d; // world X & world Y & 5

We multiply the output of this formula to get more height and then scale it with d for perspective correction.

That's really all there is to this massive rendering engine.

3. Adding colors


For the colored version we need to set RGB channels with fillStyle. Dwitter has a handy R(r,g,b,a) function for this purpose. For the red channel we use the height function value which is h. This makes the high areas more yellowish. For the green channel we use the screen y which in the code is variable j. This channel dominates the scene. It makes distant areas darker adding a nice feel of depth. We use the bitwise OR operator to add fake texture that costs us 2 bytes. Last but not least is the blue channel. This brings the scene alive and is perhaps the most clever part of the demo. First we check if height (h) is false (0). If it is not then zero is returned as value and things stay green. If h is indeed false we need to make the pixel blue. W is set to 300 earlier and we subtract the screen y from it. This means that the pixels higher up the screen will appear more blue than the ones near the bottom. It makes the visuals more interesting than using screen y by itself but also costs two more bytes.

Red  = h, Green = j | 2, Blue = !h && W - j;

The rest of the bytes go into optimization and refining the camera path. The colored version doesn't use opacity so the drawing has to be done with painter's algorithm. It's trivial to just loop screen Y from top to bottom but takes more bytes than the other way around.

4. Conclusion


While it looks like a lot for 140 bytes it really is quite simple demo. And simplicity of course is the key to creating things with limited bytes. Some of my other Dwitter demos have more complex code in them. I'll keep posting more and writing about them. Here are all the demos I've released so far: www.dwitter.net/u/jylikangas

Special thanks to Mathieu 'p01' Henri for saving me 3 bytes with the 100 byte version!





perjantai 9. maaliskuuta 2018

Making of Cyber Auroras



Update - I'm happy to annouce the demo got 3rd place in the JS1k 2018 competition!

1. Changing the process


For JS1k 2018 I took a new approach to creating a small demo. In the past I had come up with some fancy rendering techniques and built the demos on top of those ideas over several iterations. When looking back at them I always had some regrets and felt that some of the early versions might have been better than the final release. Also I didn't feel like the programming was much fun after the initial PoC versions were done. Since I'm always seeking to improve it was time to change this process. Thanks to my partner's suggestion I started using Pinterest to search and collect inspiration. Through this fun period of just browsing through images and videos I got several ideas and I started to think how they could be implemented with limited bytes. I created a Padlet page to write small notes and store all kinds of related stuff. The demo started to become alive even though not a single line of code existed. I narrowed the ideas down to two which eventually were combined into one.

2. Julia fractal auroras


I wanted to try rendering a Julia fractal since I've never done anything with them before. I quickly programmed a small demo that would floorcast the fractal and then move it around to paint a landscape. Mirroring the shadow of the fractal to the top of the image reminded me a lot of northern lights aka auroras. As a nature photographer this concept really hit me.



3. 80s wireframes


I always get inspired by retro style art with simple wireframes and went through a ton of them on Pinterest. I then came up with a scanline rendering technique which could do those landscapes in small size. I wrote down the logic and then did a test implementation of a Taiga forest. Again working from sources of inspiration towards my personal interests.




4. Implementation


Since I had done a lot of background work I was really anxious and motivated to start programming the demo. I had a clear vision of the end product and had achieved a personal connection to it on many levels. I was so excited I basically did the whole thing over one weekend. I code-golfed the PoC implementations as small as I could so I would have room to add a storyline and add some bonus effects. As usual the biggest improvements came from simplifying the visuals rather than working on the code itself:

  • I dropped the vertical lines from the wireframe engine and made the height function really simple
  • I limited the number of total colors
  • I rendered the fractal with really large pixels and only two colors. This not only saved space but made the demo run nicely on mobile phones
  • I reused my old implementation of a starfield for a cheap night sky

Now I had a solid demo and a lot of bytes still left. It was time to go back to Pinterest to look for those little things that make a huge difference...


5. Adding the storyline


I think the reason that my Highway at Night demo became somewhat popular was because it not only created a small world but also had a story. Events followed each other and got crazier towards the end just when you thought you'd seen it all. I wanted to do the same thing this time and because of the auroras theme it was obvious how it all should go...
  • Start with a nice landscape view with the camera following the path of a river
  • Sun sets changing the water color and revealing the stars in the sky
  • Scene opens up for the upcoming light show when we arrive at a lake
  • Auroras fade in and approach the viewer
  • Water starts reflecting the sky adding more interest to the scene
At this point I had pretty much ran out of space but I needed a grand finale. I got the idea for a meteor shower from a GIF on the internet showing objects hitting water. It was time to do some more code-golfing because it was clear I wouldn't be happy without getting it all in. Luckily my scanline rendering engine allowed to do the splashes with small cost and the meteors could be drawn as a simple chain of growing rectangles.


After some more heavy size optimizations I could actually get lazy and just do the background mountains with regular canvas lineTo-function and add shadowBlur to them. I know I could have done it differently to save space for even more things but I felt the demo was ready. I released it two weeks before the deadline. I was and still am very happy with it. I was left with many more ideas in my head but luckily found my way to Dwitter. I spent the next two weeks releasing several 140 bytes demos there and you can check them all out HERE. I'll be writing about them later.

6. Conclusions and source code


Inspiration and personal connection to the subjects will drive you to better performance and leave you a much happier programmer! Thanks for reading and see you on Twitter & Dwitter. :)




X=0;
S=50;
w=S*6;
setInterval(()=>{
X||(Z=T=0); // intialize counters
X=X>999?Z=0:X+.2; // loop back to start
a.height=a.width=W=w*2;
c.fillRect(0,0,W,W);
c.fillStyle="#A4A";
c.globalAlpha=(Z++-W)/W;
for(x=W;x&&Z>W;x-=6)// Julia
for(y=W;y>w;y-=6){
i=W*S/(y-w);
k=(X+i*Math.cos((x-w)/W)-S)/W;
l=i*Math.sin((x-w)/W)/W;
for(i=0;k*k+l*l<4&&i<40;i++)z=k*k-l*l+Math.cos(Z/w)/20-.8,l=2*k*l+.2,k=z;
if(i>25){
c.fillStyle=i>38?"#A4A":"#0C4";
c.fillRect(x,W-y,3,5);
X>420&&c.fillRect(x,y,3,1)// reflection
}
}
c.globalAlpha=1;

s=Z<w?0:s+.3; // sunset
for(x=7;x--&&s<S;)c.fillRect(250+x*x,337+s-x*9,99-x*x*2,6);
c.fillStyle="#0AC"; // stars
for(x=w;x&&s>S;x-=6)c.fillRect(x*x%W,x,1,1);
c.fillStyle="#112"; // mountains
c.strokeStyle=c.shadowColor="#0FF";c.shadowBlur=S;
c.lineTo(-99,350);c.lineTo(90,220);c.lineTo(140,270);c.lineTo(170,250);c.lineTo(w,360);c.lineTo(500,220);c.lineTo(700,350);c.fill();
c.fillStyle="#000"; // mask
c.fillRect(c.shadowBlur=0,340,W,S);

p=Z/S|0;
for(y=p*S;y<W*4+p*S;y+=S){ // scanlines
c.beginPath();
for(x=W*4;x-=S;){
z=w/(y-Z+w); // scaling
k=Math.abs(W*2-x)/(W+y)*9-1|0; // terrain height
i=w+(x-W*2)*z;j=w-(k*90-w)*z; // project to screen coordinates
y<w*7&&y%80<S&&x!=W*2&&c.lineTo(i,j);
k=y>1900?y-1900:0; // river flows into a lake
if(x==W*2){
c.fillStyle="#000";
y<W*4&&c.fillRect(i+Math.cos(y)*S*z-(S+k)*z,w+w*z,(150+k*2)*z,S*z);
c.fillStyle=s>S?"#0AC":"#A4A";
c.fillRect(i+Math.cos(y)*S*z-(S+k)*z,w+w*z+70*z,(150+k*2)*z,.5)
}
T>S&&( // meteor hit
k=(Q-120)*-4+x,
l=y-p*S-W,
j=Math.sqrt(k*k+l*l),
T>S&&j<T*4&&j>T*4-70&&c.fillRect(i,w+w*z,4,2) 
)
}
c.stroke()
}
// meteor shower
if(T){
for(x=20;x--&&T<S;)c.fillRect(Q-T*2-x,T*7+x*3,x/4,x/2);
T=T>120?0:T+.9
}
!T&&Z>W*6&&(Q=w+Z%w,T=1)
},9)