/wakamoleguy

Here Be Dragons

For a few years now, the JS1K contest has been asking folks, "What can you do with just 1,024 bytes of JavaScript?" As it turns out, people can do some really amazing things. Just check out the winner from back in 2010 (Firefox seems to work best) which animates a decorated Christmas tree, or Strange Crystals II, which won in the spring of 2013. For the current contest, title 'Here Be Dragons', I decided to try my hand at it. What can I do with just 1K of JavaScript? Not much.

It's Cooler in my Mind

When I first set out, I had this grand vision of sailing a pirate ship through unknown waters, shooting cannonball at firebreathing dragons. I knew it would be a challenge to fit all of the features I wanted into 1K, but I opted for overshooting rather than trying to come up with an idea that sounded suitably bite-sized. Maybe I would surprise myself, or 1024 bytes would turn out to be more than it sounds.

It didn't. 1K is tiny. I wasn't familiar with the Canvas API, so I decided to go the DOM route. First of all, that was a huge mistake. DOM is not only harder to draw things with than a canvas, but also incredibly expensive to style. Who has room to type out 'background-color' or '-webkit-transition' when those precious bytes could be going to add clouds or random spawn points? But at this point, I was committed to making it work as best I could. There's always next time to try something new.

Let's Break It Down

Before I go further, you may want to check out the demo on JS1K to see what I'm talking about. It's not pretty. You should really look at some of the awesome things other people made as well. But anyways, if you've decided to stick around, below I'll walk through the entire source code. Don't worry, it's not too long.

The first part just removes the canvas using the provided function. I also included a block comment at the top of the file, to help myself keep track of different variables. The final portion of this chunk defines a few of those variables.

// Remove canvas
d();

/*
a
b - body
c - ship
d - dragon
e
f
g
h
i - mouse X
j - mouse Y
k
l
m
M - Math
n - setTimeout
o - setInterval
p - 'px'
q
r
s - 'style'
t - 1000
u - 400
v - 50
w
x
y
z
*/

n = setTimeout;
o = setInterval;
t = 1000;
u = 400;
v = 50;
M = Math;
p = 'px';
s = 'style';

As it turns out, CSS was a huge cost. I factored out all of the styling for the dragon, the pirate ship, and even the background into this one function setStyles. It uses a with statement to save precious bytes on property access. I was running the code through the Google Closure Compiler, so I wasn't worried about the long function name; it would get changed to a single character later.

function setStyles(q, l_background, l_height, l_width, l_top, l_left, l_transition, l_visibility) {
  with(q[s]) {
    position = 'absolute';
    background = l_background;
    height = l_height + p;
    width = l_width + p;
    top = (q.T = l_top) + p;
    left = (q.L = l_left) + p;
    transition = l_transition;
    visibility = l_visibility;
  }
}

Next, I create the pirate and the dragon. They live as DIV elements, styled appropriately. Note the expensive (read: long) names of the DOM methods. createElement, appendChild, and innerHTML are almost as painful as the CSS properties above. The colors are wasting bytes as well, but that was just laziness.

// Create pirate and dragon
function createBlock(isDragon) {
  var q = document.createElement('div');
  isDragon ?
    setStyles(q,'green',v,v,v,v) :
    setStyles(q,'brown',20,v,u,u);
  isDragon ? (d=q) : (c=q);
  b.appendChild(q);
  q.h = q.innerHTML = 5;
}
setStyles(b,'skyblue',t,t);
createBlock(0);
createBlock(1);
d.U = d.L;
c.U = c.L;

The bullet code was interesting. I had originally planned to fire the bullet to the mouse, or perhaps at a set distance from the boat. After I saw how little room I had to work, collision detection and aiming went out the window. Instead, the bullets animate from the ship to the dragon, or vise versa, using a CSS transition. When they are done, they are hidden. This code also includes the health tracking. Starting healths are cached on the objects, and if the target drops to 0, it is reset to its original health, as well as its position.

// Create or update a bullet (cannonball or fireball)
function bulletStyle(fromShip, start, u) {
  u || (u = document.createElement('div'));
  var t = (fromShip ^ start) ? d : c;

  setStyles(u,fromShip?'#000':'orange',5,5,t.T+25,t.L+25,
           'top 1s,left 1s,visibility 1s',start || 'hidden');
  return u;
}

// Fire a bullet
function bullet(fromShip,target) {
  var u = bulletStyle(fromShip,1);
  b.appendChild(u);
  n(bulletStyle.bind(0,fromShip,0,u),0);

  n(function () {
    if (!--target.h) {
      target.h = 5;
      target.T = target.U;
      target.L = target.U;
    }
    target.innerHTML = target.h;
  }, t);
}

// Fire cannon on click
this.onclick = bullet.bind(0,1,d);

This code rotates the ship with the mouse, moving the dragon in the opposite direction. I hoped this would give the impression of sailing across the open seas, although some imagination is definitely required. Updating the DOM on every mouse move proved too expensive, so I simply update the mouse position and update the DOM on an interval about 40 times per second. (Remember that o is setInterval.)

// Pirate follows mouse
b.parentNode.onmousemove = function (e) {
  i = e.x;
  j = e.y;
}
l=i=j=1;

o(function () {
  var dstyle = d[s];
  var cstyle = c[s];
  var a = M.PI/2-M.atan2(i-u,j-u);

  if (!--l) {
    l = 5;
    cstyle.webkitTransform = cstyle.transform = 'rotate(' + a + 'rad)';
  }
  dstyle.top = (d.T = (d.T - M.sin(a)+t) % t) + p;
  dstyle.left = (d.L = (d.L - M.cos(a)+t) % t) + p;
}, 24);

o(bullet.bind(0,0,c),3*t);

Room For Improvement

Just about every aspect of this could benefit from some love. From using requestAnimationFrame for better performance or avoiding strings for colors, all the way to massive overhauls such as, you know, not destroying the canvas and using the DOM instead, I could have done a lot better. There are still a couple weeks before the contest ends, so I may revisit it, though I don't expect I have a chance of winning.

I did, however, learn an immense amount from this challenge. Working under the 1K restriction forced me to think about cost differently. Day to day, I develop WebRTC applications designed for modern browsers on good networks. The size of the JavaScript file is almost never a factor in our technical decisions. Finding myself suddenly worrying about the length of CSS properties was an interesting twist. I would highly recommend giving it a try. Even if your final product isn't a masterpiece, you'll learn to think about your code in a completely different way. And hey, you'll probably still do better than I did.

Cheers!