-
-
Save tlack/3039247 to your computer and use it in GitHub Desktop.
// Matraka's source code decoded and beautified | |
// by @tlack | |
// | |
// Matraka is a 1005 byte Javascript "demo" by p01. It includes an 'evolving animation' | |
// and great dirty synth music. View here: | |
// | |
// http://www.p01.org/releases/MATRAKA/matraka.png.html | |
// | |
// I fondly recall the demo scene of my youth, puzzling over the work of Future | |
// Creators and those guys. I was puzzled by this worked so I had to figure it | |
// out. | |
// | |
// First of all, the page seems to manipulate a semi-bug: if the browser gets | |
// confused by the Content-type of a page, it seems to attempt to interpret it | |
// as HTML - even if it actually contains image data. In this case, a strange | |
// mix of behaviors will occur.. the image data will be decoded, but content on | |
// the page will be evaluated as Javascript as well. Not what I would have | |
// expected. | |
// | |
// Here's the "loader" part of the script, which interprets the decoded image | |
// data, pulls it out of the screen's canvas, and then eval()s it: | |
// | |
// NB. I've reworked this code by hand: | |
// | |
// <canvas id=c> | |
// <img src=# onload="for( | |
// a=c.getContext( '2d'),i=e='' ,S=String.fromCharCode; | |
// a.drawImage(this,i--,0),t=a.getImageData(0,0,1,1).data[0];){ | |
// e+=S(t); | |
// }(1,eval)(e)> | |
// | |
// This appears to step through the decoded bytes of the PNG file and then eval | |
// the result. I don't quite understand the (1,eval) bit though - what does 'for' | |
// return that's being invoked here? | |
// | |
// In Chrome's web console, you can kind of do the same thing: | |
// | |
// > for(i=e='';i--;){}(1,eval) | |
// function eval() { [native code] } | |
// > for(i=e='';i--;){}(1,eval)('100*3') | |
// 300 | |
// | |
// Gotta figure out how this works. | |
// | |
// Anyway, so I fetched the page content, removed the 'loader' bits, and then decoded | |
// the resulting png into a bmp file. bmps are basically raw pixel-by-pixel image data, | |
// so I knew this would make it easy to decode the rest. | |
// | |
// Once I examined that bmp, I saw the javascript. Of course there was a 64 byte header | |
// that I wrote a quick script to trim off. | |
// | |
// This left me with the following (od -c output here, including header): | |
// | |
// 0000000 B M " 017 \0 \0 \0 \0 \0 \0 6 \0 \0 \0 ( \0 | |
// 0000020 \0 \0 371 004 \0 \0 001 \0 \0 \0 001 \0 030 \0 \0 \0 | |
// 0000040 \0 \0 354 016 \0 \0 # . \0 \0 # . \0 \0 \0 \0 | |
// 0000060 \0 \0 \0 \0 \0 001 M M M = = = [ [ [ R | |
// 0000100 R R = = = M M M a a a t t t h h | |
// 0000120 h . . . r r r a a a n n n d d d | |
// 0000140 o o o m m m , , , Q Q Q = = = M | |
// 0000160 M M a a a t t t h h h . . . c c | |
// 0000200 c o o o s s s , , , c c c . . . | |
// 0000220 s s s t t t y y y l l l e e e . | |
// 0000240 . . c c c s s s s s s T T T e e | |
// 0000260 e x x x t t t = = = ' ' ' p p p | |
// 0000300 o o o s s s i i i t t t i i i o | |
// 0000320 o o n n n : : : f f f i i i x x | |
// 0000340 x e e e d d d ; ; ; t t t o o o | |
// 0000360 p p p : : : 0 0 0 ; ; ; l l l e | |
// 0000400 e e f f f t t t : : : 0 0 0 ; ; | |
// 0000420 ; w w w i i i d d d t t t h h h | |
// 0000440 : : : 1 1 1 0 0 0 0 0 0 % % % ; | |
// 0000460 ; ; h h h e e e i i i g g g h h | |
// 0000500 h t t t : : : 1 1 1 0 0 0 0 0 0 | |
// 0000520 % % % ; ; ; b b b a a a c c c k | |
// | |
// You'll note that each pixel in the image is represented by a 3 byte (r,g,b) | |
// combination. p01 encoded each character of Javascript as a single pixel, or | |
// 3 bytes. Why didn't they use every byte of the data and make their bootstrap | |
// code a bit more complicated, but the png data much simpler? I don't know. | |
// Maybe it compressed much worse? Hard to say. They also left in a lot of | |
// optional semicolons it seems | |
// | |
// It was a simple task to pull out every third byte of the file. | |
// | |
// I ran the result through jsbeautifier and did some annotating and the result | |
// is below. I still don't understand how the real "meat" of it works. | |
// | |
M = [ | |
R = Math.random, Q = Math.cos, c.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:#000', e = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAwF0AAMBdAAABAAgAZGF0YQAA']; | |
for (t = 2560; t--;) M[t] = { | |
t: t, | |
u: t % 48 / 7.64, | |
v: t / 366.7, | |
x: 128 - R() * 256, | |
y: 64 - R() * 256, | |
z: 128 - R() * 256 | |
}; | |
// | |
// note, S = String.fromCharCode, assigned in the bootstrapping phase | |
// btoa() decodes base64 data | |
// | |
// what he appears to be doing here is indexing into that odd string to build a decodable | |
// base64 string | |
// | |
for (t = 0; t++ < 8e5;) | |
e += btoa( | |
S('13107103135701314204' [(t >> 10 & 15) + (t >> 13 & 4)] * t & 96, R() * 127 * (Math.pow(t / 144000 % 1, 16) / 4 + Math.pow(1 - t / 144000 % 1, 64)), t >> 10 & 7 ^ 5 || R() * 127)); | |
console.log(e); | |
Z = new Audio(e); | |
Z.play(setInterval(function b(v, w) { | |
// note that setInterval calls its callback with no parameters - so what | |
// are we doing here? | |
if (v) return w.W - v.W; | |
// sorting as a way to find the data we need for the next iteration? | |
M.sort(b); | |
// here, c refers to the original canvas | |
h = c.height = 0 | 300 * innerHeight / innerWidth; | |
// Q = Math.cos | |
C = Q(r = Z.currentTime / 2); | |
S = Q(r - 8); | |
a.rotate((r & 13) / 64 - .1); | |
B = r / 9; | |
A = Math.pow(B % 1, 64); | |
d = [1 - A, 0, A][0 | B++ % 3]; | |
f = [1 - A, 0, A][0 | B++ % 3]; | |
e = [1 - A, 0, A][0 | B++ % 3]; | |
for (t = 2560; t--;) { | |
v = M[t]; | |
if (v.W > 0) a.fillRect(150 - v.W * v.U, v.W * (v.y + Math.max(16 * Q(r), 150 - r * 42)) + h / 2, v.W * 7, v.W * 7, a.fillStyle = 'hsl(' + [r * 17 - v.y + R() * 48, (16 + R() * 48) + '%', v.W * 7 + 32 - 32 * Q(v.u * 2 - 8) * Q(v.v * 3 - 8)] + '%)'); | |
if (v.t < 2304) Y = 96 - 30 * v.v, D = 32 + 8 * Q(v.u * 2) * Q(v.v * 3), v.x = (96 - 30 * v.u) * d + D * (Q(v.u) * e + Q(v.u) * Q(v.v / 2 - 8) * f), v.z = Y * d + D * (Q(v.u - 8) * e + Q(v.u - 8) * Q(v.v / 2 - 8) * f), v.y = D * d + Y * e + D * Q(v.v / 2) * f; | |
v.W = 128 / (v.z * C - v.x * S + 96); | |
v.U = v.x * C + v.z * S | |
} | |
a.fillText(['P01 4MAT', 'MATRAKA'][B & 1].substr(a.drawImage(c, 0, r * h % h, 32, h / 8 * (3 + A + Q(B + A)), 0, r * h % h, 300, h / 8 * (3 + A + Q(B + A))), r * 8 - 48) + '|', 32, h / 2) | |
}, Z.loop = 9)) |
The setInterval(function b(v,w){if(v)return w.W-v.W;M.sort(b);/* ... */},9);
is actually pretty simple.
The function b
has two purposes: it is the main loop of the animation AND the sort method of the array of particles, M
. The earlier is called by the setInterval
and takes no argument, the latter is called by the main loop of the animation takes 2 arguments ( the elements of the array to compare ).
First thing I do in b
is check if it's call with some arguments, in which case I compare the arguments and return the result of the comparisons. Everything after that is the main loop. And the first thing I do in the main loop is to sort the array of particles ( using M.sort(b);
) before displaying them and processing their new position and so on,
Does that make sense ?
did you write it that way for performance reasons or just to 'hotdog' (show off, be quirky, etc)? seems like it would have been easier to define a typical sort function, and separate that from the visualization code, but i know you did this very intentionally..
I wrote this code for my JS1k #3 entry, the Particle Carriage [1], and did it this way for two reasons:
- It saves one function declaration. I know the compression would chew most of that code, also I could pass a string to the
setInterval(...,9)
and a function to theM.sort(...);
. I haven't fiddled much around this. Surely all three approaches must be a couple of bytes apart after compression. - This pattern fit very well in the (code) flow of Matraka and the Particle Carriage: sort, display, compute next frame. As opposed to the classical (code) flow: compute frame, sort, display. With this approach the display and compute fit in a single for loop, instead of two, thus saving some more bytes.
Ingenious Mathieu.
Respect.
Mathieu, care to explain the SetInterval/M.sort trick?