614 words ~ 3-6 minsMathieu 'p01' Henri on March 13th, 2016


LRNZ SNGLRT is a minimalist and energetic entry for JS1k 2016 showing twisted Lorenz attractors with ambient occlusion, soft shadows, ... a strong beat & clean design.

After a long judging period, LRNZ SGNLRT ranked #10 at JS1k.

JS1k 2016: Let's get EleMental

As usual JS1k had an evasive theme to guide the participants. This year it was _Let's get EleMental_ which encompassed all things physics, and psychic.

This theme was spot on: Few days earlier the sound of gravitational waves broke the news. Also I wanted to play with Lorenz attractors, or coupled attractors, and better lighting since a while.

Unlike demoscene competitions, JS1k does not impose a maximum duration for the entries. This leaves two options: interactive or looping entries.

My first idea was to play with particles collapsing into a singularity, and again, using a Lorenz attractor as base shape. Hence the name: LRNZ SNGLRT

Lorenz Attractor

The attractor itself is a straight forward implementation of the formula:

dx = A * (y - x);
dy = x * (B - z) - y;
dz = x * y - C * z;
x += dx / dt;
y += dy / dt;
z += dz / dt;

Where A, B and C are the coefficients making the shape. This is so fast for 1024 particles that the attractor can be generated for each frame and rendered at 60fps. The rendering is by far most expensive operation.

The particles and the LRNZ SNGLRT title are placed in 3D, with the particles spinning around the y axis.

cos = Math.cos(angle);
sin = Math.sin(angle);
xNew = x * cos - z * sin;
yNew = y; // no change since we rotate around this axis
zNew = x * sin + z * cos;

Lorenz Attractor with a twist

Unfortunately Lorenz attractors have a very distinct shape that ressemble a butterfly and can feel a bit cliché. To make the shape more interesting, I added a simple twist to unravel the "wings of the butterfly" by adding

cos = Math.cos(angle + particleIndex * B);
sin = Math.sin(angle + particleIndex * B);
xNew = x * cos - z * sin;
yNew = y; // no change since we rotate around this axis
zNew = x * sin + z * cos;

Simple. Efficient.

Ambient occlusion, lighting and soft shadows

For the ambient occlusion and lighting, we jitter the position of the particles, accumulate them into a low resolution 3D texture, and spread them at increasingly lower opacity away from the global source of illumination. The render loop looks up in this 3D texture to occlude each particle.

Additionaly the particles are drawn in two halves, upper and lower, lit using the ambient occlusion 3D texture and the velocity of the particle, the dx, dy and dz values in the formula of the attractor, which map to the normal of the shape and give it more volume.

The soft shadow is done by projecting the particle to a fixed y coordinate and drawing a low opacity quad. The accumulation and movement complete the illusion.


The sound uses the Audio element and the ByteBeat technique to create a looping sound.

for(t=0,d="RIFFdataWAVEfmt "+atob("EAAAAAEAAQAAgAAAAIAAAAEACAA")+"data";t<32;t+=2/65536)
    + Math.random()/(t%1+.01)
    - (8*t&1333*t&15)
    + (8&t)*(8*t&655*t&7)/8);
C=new Audio("data:Audio/WAV;base64,"+btoa(d)),

This creates a WAVE PCM sound file that is loaded as a data: URI encoded in base64.

The snare is especially nifty and powerfull:


The other "instruments" are regular ByteBeat and provide a hint of melody

- (8*t&1333*t&15)
+ (8&t)*(8*t&655*t&7)/8);

Feedback ?

You can find the LRNZ SNGLRT at and JS1k where it ranked #10. Suggestions and comments are welcome.