Skip to content

Instantly share code, notes, and snippets.

@tlack
Created July 3, 2012 11:41
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save tlack/3039247 to your computer and use it in GitHub Desktop.
Save tlack/3039247 to your computer and use it in GitHub Desktop.
p01's Matraka javascript demo decoded
// 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))
@p01
Copy link

p01 commented Jul 4, 2012

Interesting and funny notes :)

Don't worry I'll explain it all on http://www.p01.org/releases/MATRAKA/ in the coming week(s). It's just that I'm ridiculously busy these days.

@tlack
Copy link
Author

tlack commented Jul 4, 2012 via email

@p01
Copy link

p01 commented Jul 7, 2012 via email

@tlack
Copy link
Author

tlack commented Jul 7, 2012

Mathieu, care to explain the SetInterval/M.sort trick?

@p01
Copy link

p01 commented Jul 7, 2012

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 setIntervaland 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 ?

@tlack
Copy link
Author

tlack commented Jul 7, 2012

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..

@p01
Copy link

p01 commented Jul 8, 2012

I wrote this code for my JS1k #3 entry, the Particle Carriage [1], and did it this way for two reasons:

  1. 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 the M.sort(...);. I haven't fiddled much around this. Surely all three approaches must be a couple of bytes apart after compression.
  2. 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.

[1] http://js1k.com/2011-dysentery/demo/993

@ottonascarella
Copy link

Ingenious Mathieu.

Respect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment