JS1k demo page v4

2015-01-10

I've updated the js1k demo pages. This is where the actual magic happens. The first visit of this page had just the shim but I quickly decided I wanted a header at the top. At that point a plethora of demos were already submitted so I had to watch my steps in rewriting that page.

In JS1k people do very odd things. It's the nature of the competition and it's what makes this awesome. Of course this also puts a burden on myself during maintenance. The v1 of the demo page was just the demo shim. With the v2 demo page rewrite I added a header. At this point the demo page consisted of three or four resources (main html, two frames, css). But near the end of the competition the site got slashdotted and I decided to make the demo page a single page resource. The demos overview page consisted of about 700 thumbs amounting to 1mb in total. That didn't help either. The website basically ddossed itself. It's all sprited and greatly reduced now.

So for the v3 demo page rewrite I ended up with a dynamic frameset that included everything. I generated the page by using the js-as-url hack. You can read more about that version here. It was beautiful as much as it was a hack. On the backend it required some triple encoding fu, which often caused submission encoding issues. Something I fixed last year by adding a base64 field.

As much love as that hack got, it did have a problem; it did not work in all browsers. None of these browsers were officially part of JS1k, but that doesn't mean I didn't want to support them. However, doing a v4 demo page was going to be tricky as the options were limited and I wasn't sure how many demos would cause problems if we changed the page to use an iframe rather than a frameset.

I even got into a little thing with the IE team over it. Hey it's not like I didn't want to change it, but keep in mind it wasn't my choice to put an arbitrary url limit in place for frames. People put the burden on me but seem to forget that I don't get paid for any of this. And this shit is hard. It takes time to get right.

In a nutshell, I figured the v4 demo page would look something like this:

Code:
<body>
<header></header>
<iframe>
<body>
<script>
submission
</script>
</body>
</iframe>
</body>

The iframe would be fullscreen, sans the header, and the header would do what it does now. And none of the demos should be affected. Easier said than done. But done, nonetheless.

This is the actual code of the body of the page:

Code:
<header>
<div>
<h1>
<a href="http://js1k.com/">JS1k</a>
<a href="http://js1k.com/2014-dragons/">2014</a>
<strong>webgl</strong> demo

"some title" by some author
</h1>
<p>
<em>
This is the first part of the desc and the second line of text in the header
</em>
</p>
<aside>

468 bytes

<a href="http://js1k.com/2014-dragons/details/123">demo details</a>

<a href="http://js1k.com/2014-dragons/demos">list of demos</a>

<a href="http://js.gd/dga">js.gd/dga</a>
<time datetime="2014-10-10" pubdate>2014-10-10</time>
</aside>
</div>

<a href="122.html" class="button p">↞</a>
<a href="124.html" class="button n">↠</a>
</header>

This is the "uncompressed" CSS to style that (at the time of writing this blog post...) for the 2014 demos:

Code:
html, body, iframe { margin: 0; padding: 0; border: 0; width: 100%; height: 100%; }
/* iframe should be full screen with padding to leave a gap for the header. put it at 0x0 to use 100% */
iframe { position: absolute; top: 0; left: 0; padding-top: 50px; box-sizing: border-box; }
/* pos relative to use zindex to put it before the iframe, otherwise mouse wont work */
header { position: relative; z-index: 1; height: 47px; padding-top: 2px; border-bottom: 1px solid black; box-shadow: 0 -10px 25px #ccc inset; background-color: #eee; }
/* these are the two text lines in header */
p, div, h1, aside { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: center; font-size: 16px; font-weight: inherit; line-height: 22px; padding: 0; margin: 0; cursor: default; }
h1, aside { display: inline; }

a { color: black; text-decoration: none; border-bottom: 1px dashed black; }
a:hover { border-bottom: 1px solid red; }
a[href=".html"] { text-decoration: line-through; pointer-events: none; border-bottom: 0; color: #ccc; }

.button { float: left; width: 40px; height: 40px; line-height: 40px; text-align: center; padding: 0; margin: 2px 0 0 10px; border: 1px solid #888; border-color: #ddd #888 #888 #ddd; font-family: sans-serif; font-size: 30px; font-weight: bold; cursor: pointer; }
.button:hover { color: red; border-bottom-color: #888; }
.r { margin-right: 10px; }
time { display: none; }

CSS doesn't compress as well as JS does. It's a lot harder too except for obvious low hanging fruit. But that's besides the point. And I'm still wondering whether there isn't a more efficient way to define that border-color in .button.

The page needs to be in full screen mode (hence the 100% body and iframe). The iframe is put at top-left absolutely because I want a header above it.

The header is 50px high so the iframe has a 50px margin at the top. It gets box-sizing: border-box to make sure "100%" means "including border and padding". Otherwise you'd get a scrollbar. In some future we can maybe change this to a calc(100%-50px) rule, but now is not that time yet.

The header is also positioned relative to use the z-index and make sure it appears above the iframe. If we didn't, we couldn't click any of the fancy stuff in the header because the iframe would block it (due to being positioned absolutely).

The header starts with three content lines: inside the div there's a h1, p, and an aside. This makes sense to me because I want the main head (h1) to read "JS1k 2015 demo "some title" by some author". It won't matter that parts of it are wrapped in a tags. Then the desc goes on the second line but what about the meta data. I've put it in an aside, but obviously you're seeing it as if it's on the same line as the h1. Well... enter JS.

The JS on the page will first reorder the elements in the header a little. It will change it from:

Code:
<header>
<div>
<h1>AAA</h1>
<p>BBB</p>
<aside>CCC</aside>
</div>
<a>button</a>
<a>button</a>
</header>

To:

Code:
<header>
<a>button</a>
<a>button</a>
<div>
<h1>AAA</h1>
<aside>CCC</aside>
</div>
<p>BBB</p>
</header>

As you can see in the CSS, the h1 and aside get display: inline, causing them to collapse and appear as one single line. The p is marked up the same as the div and are given the appearance of two distinct lines.

The buttons are pushed to the top of the header and will float left. The floating will cause them to take up space and make centering in the content lines work as you'd expected it; they will now center such that left and right has the same amount of whitespace. If you'd position the buttons absolutely you would not get this and it would lead to ugly situations. As a bonus, sizing the window down such that the content no longer fits will degrade nicely, especially with the ellipsis rules in place.

Lastly the button CSS code should be pretty straight forward. I'm applying the pretty standard and ancient trick of a darker bottom-right border to give it the appearance of a shadow. It's an old trick but still works fine. The buttons have a margin, combined with floating it will space them out nicely. I could have used a fancy pseudo selector to give an extra padding to the last element, but I'm still worried about the support for that. Putting the padding elsewhere would lead to more code as well, to compensate for the extra padding in a 100% width. So this is fine.

The only catch is the disabled prev/next buttons when you've reached the first or last demo. In that case the link will only contain .html (yes, lazy). I use a CSS selector on that to disable the button: a[href=".html"] { ... }. Note also the pointer-events: none; there :)

The time tag is hidden because it's only meant for search engines. I'm hoping they ignore the hidden fact and I'm guessing they're oblivious to it.

Yeah I've put some effort into SEO listing. I really hope it pays off. Right now this is what google shows me for the actual demo pages (in order of competitions):

Code:
JS1k, 1k demo submission [635]
js1k.com/2010-first/demo/635
Vertaal deze pagina
demo submission by @marijnjh, 1023 bytes - view source and description - back to list of demos Next. Name: Marijn Haverbeke Twitter: @marijnjh. Website: ...

JS1k, 1k demo submission [856]
js1k.com/2010-xmas/demo/856
Vertaal deze pagina
demo submission by @romancortes, 1024 bytes - view source and description - back to list of demos Next. Name: Roman Cortes Twitter: @romancortes

JS1k, 1k demo submission [984]
js1k.com/2011-dysentery/demo/984
Vertaal deze pagina
demo submission by @keenblaze, 1023 bytes - view source and description - back to list of demos Next. Name: Taras Zlotnikov Twitter: @keenblaze. Website: ...

1K JavaScript Speech Synthesizer - JS1k
js1k.com/2012-love/demo/1274
Vertaal deze pagina
demo submission by @p01, 1020 bytes - view source and description ... using First Crush by @tpdown - http://js1k.com/2012-love/demo/1189 To go under 1K, ...

JS1k, 1k demo submission [1555]
js1k.com/2013-spring/demo/1555
Vertaal deze pagina
demo submission by @ehouais, 1017 bytes - view source and description (dropdown) - back to list of demos Next. Title: Strange crystals II By: Philippe ...

JS1k 2014 demo | DragonDrop
js1k.com/2014-dragons/demo/1903
Vertaal deze pagina
demo - "DragonDrop" by Felix Woitzel (@Flexi23) - 1021 bytes -. DragonDrop is a progressive image-based L-System type fractal renderer. view details - list of ...

The new header should at least get rid of the "view source and description" and "back to list of demos" part. It should now read something like "JS1k 2014 demo — "DragonDrop" by Felix Woitzel — DragonDrop is a progressive image-based L-System type fractal renderer.", which would be much better. Fingers crossed :) I may still put the byte count after the author. Oh and the title back into the <title>, I realized I've removed that.

The JS on the page generates the iframe dynamically and bootstraps the demo into shim. Let's go over the JS;

First we generate the iframe and attach it. We attach it immediately because otherwise you won't have a body.

Code:
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);

Cache the window and document objects of the iframe. They should be available to us immediately now.

Code:
var iwin = iframe.contentWindow;
var idoc = iframe.contentDocument;

Next we fix a problem for Firefox. It needs to be nudged to get access to innerHTML.

Code:
idoc.open();
idoc.close();

The idea here is that opening a closed document will erase it. So we first open the doc, in case it was closed (other browsers may). Then we close it. This suffices as a workaround.

Next we open it again because we need to update the doctype and write the start of the shim.

Code:
idoc.write('<!doctype html><head><meta charset="utf-8"><body>');

The doctype may look silly but was actually necessary for at least one demo. It relied on the proper doctype because it used "incomplete" shorthand css rules to save some bytes. But in quirksmode the borders wouldn't show.

The body was needed to trigger body.onload later. The charset supresses some "invalid character" warnings. The head is there for the sake of completion. And because I'm slightly worried document.head may otherwise not exist. That may just be overzealously defensive.

So this boilerplate is all the html I need for the shim. We do need to style it a little. At first I wanted to get rid of the stylesheet and use inline styles only. The reason is exactly why I couldn't do so in the end (but will so for 2015); some demo's relied on the style sheet to exist!

Code:
idoc.head.innerHTML = // must be in head because of 1720
'\n';

So you don't see document.head used very often, but it's real. And the stylesheet needs to be in the head because, yep, this demo relies on it. Or rather, it replaces document.body.innerHTML so the stylesheet disappears if you put it in there. So it must be in the head.

Removing even the second rule was a problem because these demos relied on it. They injected their own rules after the second. Removing the first means there was no second, causing the browser to throw up.

Best of all, I couldn't even adjust the rule because this demo hijacked it to style its own elements (I guess that was shorter?). I added 100% to it but it blew up the button so I had to put that on the canvas directly.

The canvas is pretty straightforward;

Code:
idoc.body.innerHTML =
' ' id="c"' + // up to 2014
' style="width:100%; height:100%;"' +
'>\n';

The canvas is blown up to full screen. Note that display: block; in the stylesheet will squash the scrollbars. Otherwise the element would get bars when put to 100%.

For the 2014 demos I'm keeping my Safari workaround. Again that may be overly defensive, but it shouldn't hurt either (let me know if there's still a canvas height problem!).

Code:
idoc.body.clientWidth;

Preset the canvas size to fullscreen (cs is canvas.style):

Code:
cs.width = (canvas.width = innerWidth) + 'px';
cs.height = (canvas.height = innerHeight-50) + 'px'; // 50 is for top header

Next we unprefix two interesting objects. At this time I'm not aware of other objects that need the same treatment. And I suppose requestAnimationFrame doesn't need it anymore either, but for the sake of completeness:

Code:
iwin.AudioContext = iwin.AudioContext || iwin.webkitAudioContext;
iwin.requestAnimationFrame = iwin.requestAnimationFrame || iwin.mozRequestAnimationFrame || iwin.webkitRequestAnimationFrame || iwin.msRequestAnimationFrame || function(f){ setTimeout(f, 1000/30); };

Next comes the autoresizer. I'm going to change it (make it configurable at submission time) but have to keep it as is for the 2014 compo. With the new iframe approach I had to add a check though since the iframe seems to trigger an extra resize which tripped up some demos that adjusted the size manually. These demo's indeed don't support the resizing at all currently, but I need to make sure it at least doesn't show onload.

Code:
var lastw;
var lasth;
(iwin.onorientationchange = iwin.onresize = function(){
var mw = Infinity;
var mh = Infinity;
var min = Math.min;

return function(w,h){
if (arguments.length === 2) {
mw = w;
mh = h;
}

// prevent iframe causing extra resize
w = min(mw, innerWidth);
h = min(mh, innerHeight-50);
if (w !== lastw || h !== lasth) {
lastw = w;
lasth = h;

cs.width = (canvas.width = w) + 'px';
cs.height = (canvas.height = h) + 'px';
}
};
}())();

The -50 is the header because innerHeight is that of the main document, not the iframe.

Like I said this code will change for 2015 by basically dropping the arguments. I think you'll be able to configure this default from the submission page and viewers will be able to change it in the header.

I realized that the resize code is actually suboptimal. In order to support it you would need to refresh the pixel size cache after a resize. Most demos won't even bother with that. I'm still considering how to go about the resize best. Right now I think a toggle at the submission page is best. There's no point in a resize shim if demos fail because of it.

Exposing the shim is trivial;

Code:
iwin.a = canvas;
iwin.b = idoc.body;
if (!webgl) iwin.c = canvas.getContext('2d');
iwin.d = function(){ canvas.parentNode.removeChild(canvas); };

I will make the whole canvas deal configurable from the submission page, so there's no longer need for d, and it will be dropped for 2015.

Before the webgl shim had its own template. But I've merged the two templates together and simply write in a true or false for using webgl. The webgl check is a token so it can be disabled/enabled through page generation. It hasn't changed so I won't put it here. I think I'll simply allow webgl in the main compo this year and drop the separate compo(s).

Injecting the actual demo is kind of trivial. I didn't want to have to encode it any more than I had to. So I'm putting the contents in a script tag on the main doc. But I'm setting the type attribute such that it won't be recognised as a script and will be completely ignored. It's an old trick, not my own. The only thing you must do is escape </script> because otherwise it will abrubtly close the script tag, wreaking havoc. (This is why you often see <\/script> in JS strings; the backslash prevents the problem but is ignored by JS otherwise in this case.)

So apart form preventing the closing tag, zero encoding is required. That's a huge breezer from v3 ;)

Code:
var demo = idoc.createElement('script');
demo.textContent = document.querySelector('script[type="demo"]').textContent;
idoc.body.appendChild(demo);

Create a script, get the contents from our fake script, as is. Inject the script into the iframe. The only trick is to create the new script tag using the document of the iframe, to retain ownership. Not super vital for our use case, but it prevents leakage. I think it's safe enough to use querySelector for this. In my mind there's still some browser versions in rotation that may not support it, but they probably don't support JS1k anyways.

Now an important hack. I did not find this on my own but:

Code:
idoc.close();

This triggers body.onload. Oh yes, it would otherwise not trigger. Some scripts require using the onload. God knows why because the script already ran inside the body tag so you wouldn't need it. But whatever, closing the document triggers the onload and fixes those demos.

One final piece to the demo puzzle is input. Iframes don't get focus by default so we need to fix that. The trick is not to focus on the canvas but on the window. The iframe.contentWindow is the window object of the iframe, so:

Code:
iframe.contentWindow.focus();

And now the keyboard events are immediately sent to the iframe, which makes demos work as you would expect. Yay!

Finally, I've added a reset button to the header which allows you to reload a demo without refreshing the whole page. Many demos don't bother with reload code causing people to refresh the page causing extra strain to the server. With this button I'm hoping to relieve that a little bit. All it does is remove the iframe (which destroys pretty much everything of the shim) and run the bootstrap code again like we do onload.

Code:
var r = document.createElement('div');
r.innerHTML = '↻';
r.className = 'button r';
r.title = 'restart just the demo (local, without remote fetch)';
r.onclick = function() {
document.body.removeChild(iframe);
r.parentElement.removeChild(r);

iframe = null;
r = null;
idoc = null;
header = null;

reload();
};
var firstLine = document.getElementsByTagName('div')[0];
header.insertBefore(r, firstLine);

As a bonus, you can enable audio in iOS Safari with it. If you use AudioContext mobile Safari requires audio to be used in a user driven event once. After that it gets unmuted permanently but you need the event. Obviously onload is not a user driven event, but the reload is (a tap) and so the audio will work after you reload the demo. I plan to add a one-time audio button to the header if I detect the audio api is used and the UA is on iOS. This button wouldn't reload the demo, but just trigger the unmute requirement. At the time of writing, for some reason the canvas in some demos (notably 1952 and 1953) don't work even when the sound does. I still have to debug this to figure out what's wrong. I'm sure I'll be able to fix it, if it's fixable.

One other thing I've done is add .htaccess rewrites to redirect http://js1k.com/XXXX to the right demo page in the right compo. So for example, http://js1k.com/1903 now leads to http://js1k.com/2014-dragons/demo/1903 and http://js1k.com/1854 leads to http://js1k.com/2014-dragons/demo/1854. It's intended to make mobile testing and loading a bit less of a pain. I also wanted to make a letter-only redirect work (by applying a base-26 encoding to the ID), but that gets more involved because there's only so much you can do with .htaccess. This redirect is already pushing it because it needs to redirect to different compo urls based on the number and .htaccess can only compare lexicographically. Letters only short url would make mobile even easier because you wouldn't need to switch the keyboard to numbers (at least on certain mobile OS versions). But I got it to work for numbers at least so yay.

And that's about it.

The new demo page works in the old browsers and now also in IE and iOS. Android already supported it. Fun fact; the webgl demos also work in iOS (and in all the others) :D (Ironically, this demo doesn't work on desktop anymore but does work on iOS, now, weiiird).

It properly converts all demos, except for (currently) this one because it puts the canvas at the bottom rather than the top. I will try to fix this soon, just need to debug it. *ahum*

The goal is to update the old demo's as well. This will be especially difficult for the first compo. I'm certain I'll encounter some cases that won't work out of the box, yet.

The JS1k hype is real!

(Continue reading for 4.1 where previous are also migrated)