JS1K 2015 INVITATION

1165 words ~ 5-10 minsMathieu 'p01' Henri on January 28th, 2015

JS1k 2015 Invitation

JS1k 2015, the yearly 1kb JavaScript contest, is around the corner and kuvos asked a couple of optimizer extraordinaires to open the show. Hopefully this little invitation will tingle the spider sense of talented developers and code golfers in time for them to submit high quality entries to JS1k 2015.

JS1k 2015, the 1kb contest

JS1k 2015

The yearly 1kb JavaScript contest, JS1k is comming! Get ready for a few weeks of crazy code golfing-

Workflow

For this unexpected project I tried a slightly different workflow.

Normally I build several prototypes of the idea, figure the most compact one, optimize it by hand to the max, check in a JavaScript packer how things go and optimize again from there. This works. This is pretty much how I've done all my productions. But this can be stressing when the deadline approaches and the size limit remains far.

Instead this time, I built a prototype of the idea, refined it until the visuals were interesting enough, ran the code through a RegPack, optimized for the packer and kept iterating on the project, always keeping a close eye on the packed size.

The resulting code is verbose and I'm not sure if this approach is more size efficient but the whole experience was smoother.

Also I save and test the code constantly. In a work environment I would use Git, but this brings too much friction to my "demo coding" workflow. It's a shame because I often wish I had more tracks of the progress. This time, I regularly pushed the code to a private gist. This also made it simple to share the progress with others using bl.ocks.org.

Revisions

The gist is now public. You can check all the revisions of the JS1k invitation. To see individual revisions in actions, open the list of revisions on Github, view the gist at a given revision and replace the github.com by bl.ocks.org in the address field of your browser. I wish there was an easier way to do this. Although this might be an interesting pet project.

Gotchas

This workflow was smooth but I met two gotchas.

  1. SublimeText barfs some of the control characters used by the packer. Of course this breaks the code. So I had to use a Hex editor, again, to copy paste the packed code into the shim. I've got to look into this.
  2. RegPack does check the black list of variables when it preprocesses the code to do the hashing of the Canvas methods. It produced hashing code overwrite the variable holding the canvas context. Doh!

Together this two gotchas cost a day. Luckily there was hardly any time pressure for this release.

Fix

Trying to be a good citizen, I fixed the global variable overwriting issue in RegPack. As expected this was simple to fix. Glad this will benefit other developers.

Inspiration

When searching for images related to JS1k 2015, Emily Dove Gross's beautiful wedding stationeries stood out. The visuals are whismical and fitting. Not to mention I really wish Emily did our stationeries. Check out her artwork!

Technique

To render the train tracks and forest, it seemed obvious to treat the elements as particles and use a circular buffer to store their distance from the track. To make things easier and more compact, the x coordinate modulo 3 determines whether to draw the tracks, or a particle above/below the tracks. There are 3 kinds of particles: grass, trees & texts.

Also, since the file size was not a big problem this time, I could use the verbose performance.now() and requestAnimationFrame to be more mobile friendly, and add a nice intro with a custom cut-out typography. The first versions of the intro used the system font. This was very compact but gave zero control about the exact position, size and shape of the glyphs so I went on to craft a nice path spelling JS1k.

Source code

Here is the original, and commented, source code.

// JS1k 2015 Invitation by Mathieu 'p01' Henri
// JavaScript code to use along with the shim of JS1k
// Compresses down to ~1020 bytes with Uglify + RegPack
(u=function(){
  requestAnimationFrame(u);
  a.width=x=1023;
  a.height=h=x*innerHeight/innerWidth|1;
  H=performance.now()/15|1;
  c.textAlign='center';
  c.font='2em cursive';
 
  q=170;
  c.fillStyle='hsl('+q+',20%,55%)';
  c.fillRect(0,0,1023,h);
  w=555;
  for(;x--;){
    s=x%3?H++&1||-1:0;
    // exisiting || new portion of tracks + distance of the "particle"
    c[H&1023]=x&&c[H&1023]||8+Math.random()*h/2;
 
    // Y coordinate & parallax
    Y=Math.sin(H/511-Math.sin(H/511*3)*3)*48+s*c[H&1023]+h/2;
    X=1023-x+(511-x)*Y/h;
    c.globalAlpha=1;
    q=170;
    // "particles"
    if (s) {
      // Adjust the color based on the Y coordinate
      q+=Y&31;
      c.fillStyle='hsl('+q+',55%,200%)';
      if (H%511) {
        // GRASS "particle"
        // Adjust the color based on the Y coordinate, again
        if (Y&1)
          c.fillStyle='hsl('+q+',55%,20%)';
        // vertical strands of grass
        c.fillRect(X+Math.sin(q++)*3,Y+Math.sin(q++),1,5);
        c.fillRect(X+Math.sin(q++)*3,Y+Math.sin(q++),1,5);
        c.fillRect(X+Math.sin(q++)*3,Y+Math.sin(q++),1,5);
 
        // little bits of grass
        c.fillRect(X+Math.sin(q++)*48,Y+Math.sin(q++)*3,1,1);
        c.fillRect(X+Math.sin(q++)*48,Y+Math.sin(q++)*3,1,1);
        c.fillRect(X+Math.sin(q++)*48,Y+Math.sin(q++)*3,1,1);
 
        // TREE "particle"
        // wih varying intensity of the forest
        if (!(H%(9+Math.sin(H/511/3)*3|1))) {
          // Get the height of the tree
          z=Y&15;
          z+=6;
          Y-=z*6;
          c.globalAlpha=.3;
          // Draw multiple triangles to make up a pine tree
          for (;z--;) {
            c.beginPath();
            c.lineTo(X,Y);
            c.lineTo(X-2*z+Math.sin(q++)*3,Y+6*z+Math.sin(q++)*3);
            c.lineTo(X+2*z+Math.sin(q++)*3,Y+6*z+Math.sin(q++)*3);
            c.fill();
          }
        }
      } else {
        // TEXT "particle"
        c.fillText(['Cross browser','1kb demos','February 2015','JS1k 2015'][H/511&3],X,Y);
      }
    } else {
      // tracks
      c.fillStyle='hsl('+q+',55%,2%)';
      c.fillRect(X,Y,1,5);
      c.fillRect(X,Y,5,1);Y+=2;
      c.fillRect(X,Y,5,1);
 
      // TRAIN
      if(x==w){
        // wagons
        X-=10;
        c.fillRect(X,Y,20,-10);
        // locomotive
        if(x<511){
          c.fillRect(X,Y,10,-15);X+=15;
          c.fillRect(X,Y,2,-15);
          // steam
          c.fillStyle='hsl('+q+',55%,200%)';
          c.globalAlpha=.3;
          q=H;
          z=9;
          for(;z--;){
            c.fillRect(X+Math.sin(q++)*3,Y-6*z+Math.sin(q++)*3,z,z);
          }
        } else {
          w-=15;
        }
      }
    }
  }
 
  // Cut out JS1k typography
  c.globalCompositeOperation='destination-in';
  H=Math.sin(H/511/2+10)*31+31;
  H=H<3?9:H*H;
  c.translate(511,h/2);
  c.rotate(H/511+.1);
  c.scale(H,H);
  X=Y=-3;
  c.beginPath();
  // J
  c.moveTo(X,Y);
  c.arc(X-2,Y+3,2,0,Math.PI/2,0);
  // S
  X+=2;
  Y+=4;
  c.moveTo(X,Y);
  c.arc(X,Y-1,1,Math.PI/2,Math.PI/2*3,1);
  c.arc(X,Y-3,1,Math.PI/2,Math.PI/2*3,0);
  // 1
  X+=3;
  c.moveTo(X,Y);
  c.arc(X-2,Y-5,2,0,Math.PI/2,0);
  // k
  X+=2;
  c.moveTo(X,Y);
  c.lineTo(X,Y-6);
  X+=3;
  c.moveTo(X,Y);
  c.arc(X-1,Y-2,1,Math.PI/2*3/2,Math.PI/2*3,0);
  c.stroke();
})()

Feedback ?

For now my 4 years old daughter likes the little train and forest once the JS1k logo has moved out of the way, but she wondered why she couldn't hear the train. I would appreciate any help to explain why daddy didn't manage to make a proper 'choo-choo' train in 1kb.

Also you can find the JS1k 2015 invitation at Pouet.net. Suggestions and comments are welcome. But above all, go and make an entry for JS1k.