JS1K first attempt !

by Vincent Thibault, posted Feb 20, 2012 in Blog (No Comments)

javascript contest love 2012

Yesterday I learned the start of the 4th edition of js1k with the love theme.
The goal is simple: build an amazing demo with only 1k of javascript (1024 bytes).

For this king of contest you have to compress/obfuscate your code a maximum to not exceed the size limit. So you can divide your work in two parts:

  • Build your awesome demo.
  • Compress it with some weird stuffs.

I submit a little demo, a beating heart with animating sex particles, you can see it here.

 

 

First step : build an amazing demo !

Building a demo it’s not really difficult, you just have to make it awesome.
Don’t forget that you are in a 1K contest so you must know that you are limited in space.
Here my source script:

(function JS1K_love(ctx,canvas,body){
 
 
	var // Global variables and settings
		width  = canvas.width  = 600,
		height = canvas.height = 450,
		list   = ["♂","♀","#6371b7","#db7189"],
		particles = [],
		love_str,
		love_tick=-1,
		tick   = (new Date).getTime(),
		random = Math.random,
		floor  = Math.floor,
		round  = Math.round
	;
 
 
	// Canvas in the center, define the background color.
	body.style.background = "#724567";
	body.style.textAlign  = "center";
 
 
	// Function to draw a heart shape.
	function draw_heart() {
		ctx.lineWidth = 30;
		ctx.beginPath();
		ctx.moveTo( width/2, height/3);
		ctx.bezierCurveTo( width/10,       0,         0, height*0.6, width/2, height*0.9);
		ctx.bezierCurveTo( width, height*0.6, width*0.9,          0, width/2, height/3 );
		ctx.closePath();
		ctx.stroke();
	}
 
 
	// Return particles from canvas pixels.
	function get_particles_from_heart_pixels() {
		var pixels = ctx.getImageData( 0, 0, width, height).data;
		var p = [];
		// Put a particle with a chance of 1.5% where there is the heart shape.
		for ( var i=0; i<pixels.length; i+=4 )
			if ( pixels[i+3] && random()<.015 )
				p.push({ sex:round(random()), x:i/4%width, y:i/4/width }); // randomly select the particle sex.
		return p;
	}
 
 
	// Generate the animation
	function process() {
 
		// Generate settings
		var
			time     =  (new Date).getTime() - tick,
			radius   =  Math.cos(time*3E-4),
			gradient =  ctx.createRadialGradient( width/2, height/2, 0, width/2, height/2, width/2 ),
			i,
			particle,
			angle,
			_x,
			_y
		;
 
		// Generate the dradiant effect (move with time)
		gradient.addColorStop( 0, "#c68fb8" );
		gradient.addColorStop( 0.6-Math.sin(time*3E-4)/10, "#724567" );
 
		// Blur effect, just erase with 0.3 opacity
		ctx.globalAlpha = 0.3;
		ctx.fillStyle   = gradient;
		ctx.fillRect( 0, 0, width, height );
 
		// Draw particles
		for ( i in particles ) {
 
			particle = particles[i];
			angle     = Math.PI * 5 * Math.sin(time*3E-4) + i * Math.PI/30;
			_x        = Math.sin(angle);
			_y        = Math.cos(angle);
 
			// Change position
			particle.x += radius * _x;
			particle.y += radius * _y;
 
			// Change size and opacity and generate particle
			ctx.font  = floor( _x*5 + 17) + "px Arial";
			ctx.globalAlpha = 1.5 - ( _x + 1.5 ) / 2;
			ctx.strokeStyle = list[particle.sex + 2];
			ctx.strokeText( list[particle.sex], particle.x, particle.y );
		}
 
		// Restore opacity for the LOVE, and find size.
		ctx.globalAlpha = 1;
		ctx.font = "bold " + floor( _x * 2 + 55 ) + "px Arial";
		ctx.fillStyle   = "rgba(255,255,255,0.1)";
		ctx.strokeStyle = "rgba(255,255,255,0.2)";
		ctx.textAlign   = "center";
 
		// Change sex in LOVE, every 4secs.
		if ( floor(time/4000) !== love_tick ) {
			love_tick = floor(time/4000);
			love_str  = list[ round(random()) ] + " LOVE " + list[ round(random()) ];
		}
 
		// Draw Love text.
		ctx.fillText( love_str, width/2, height/2 + 30 );
		ctx.strokeText( love_str, width/2, height/2 + 30);
	}
 
 
	// start the contest
	(function init() {
		draw_heart(); // Draw the heart
		particles = get_particles_from_heart_pixels(); // Generate particles from heart pixels
		ctx.clearRect( 0, 0, width, height ); // clear the canvas
		ctx.lineWidth = 2; // restore the line width
		setInterval( process, 30 ); // animation.
	})()
 
})(a,c,b);

It’s a clean script of 3323 bytes. A little too much for the contest…
I don’t really need to explain the script, all things are already explains in comments and it’s not difficult to understand how it works.
So now we can start to reduce the size…

 

 

Second step : Optimize the size

This part is a pain to ass; you have to drastically reduce your script length by using some sort of weird things :

  • Remove function if you can
  • Remove “var”, use only global variable
  • Change variables name, use for example “w”, instead of “width”
  • Optimize numbers : use “.9″ instead of “0.9″n, use 5E3 instead of 5000
  • Use with() to access object members
  • Rewrite your code differently…

Doing this by hand is better than using a javascript packer in a lot of case.

So here my script after this changes:

w=c.width=600,
h=c.height=450,
k=["♂","♀","#6371b7","#db7189"],
p=[],
t=0,
l=m=1,
n=(new Date).getTime(),
r=Math.random,
f=Math.floor,
o=Math.round;
 
with(a)
	lineWidth=30,
	beginPath(),
	moveTo(w/2,h/3),
	bezierCurveTo(w/10,0,0,h*.6,w/2,h*.9),
	bezierCurveTo(w,h*.6,w*.9,0,w/2,h/3),
	closePath(),
	stroke(),
	d=getImageData(0,0,w,h).data,
	clearRect(0,0,w,h),
	lineWidth=2;
 
for(i=0;i<w*h*4;i+=4)
	if(d[i+3]&&r()<.015)
		p.push({t:o(r()),x:i/4%w,y:i/4/w});
 
setInterval(
	'with(a){\
		t+=Math.max((new Date).getTime()-n,30);\
		n=(new Date).getTime();\
		z=Math.cos(t*3E-4),\
		globalAlpha=.3;\
		g=createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);\
		g.addColorStop(0,"#c68fb8");\
		g.addColorStop(.6-Math.sin(t*3E-4)/10,b.style.background="#724567");\
		fillStyle=g;fillRect(0,0,w,h);\
		\
		for(i in p)\
			q=p[i],\
			g=Math.PI*5*Math.sin(t*3E-4)+i*Math.PI/30,\
			u=Math.sin(g),\
			v=Math.cos(g),\
			q.x+=z*u,\
			q.y+=z*v,\
			font=f(u*5+17)+"px Arial",\
			globalAlpha=1.5-(u+1.5)/2,\
			strokeStyle=k[q.t+2],\
			strokeText(k[q.t],q.x,q.y);\
		\
		font="bold "+f(u*2+55)+"px Arial",\
		globalAlpha=1,\
		fillStyle="rgba(255,255,255,.1)";\
		strokeStyle="rgba(255,255,255,.2)",\
		b.style.textAlign=textAlign="center";\
		if((s=f(t/5E3))!==l)\
			l=s,m=k[o(r())]+" LOVE "+k[o(r())];\
		fillText(m,w/2,h/2+30);\
		strokeText(m,w/2,h/2+30)\
	}',1)

1354 bytes, a lot better ! But need to be 1024 max !
Now we can remove spaces and indent to gain some bytes.

w=c.width=600,h=c.height=450,k=["♂","♀","#6371b7","#db7189"],p=[],t=0,l=m=1,n=(new Date).getTime(),r=Math.random,f=Math.floor,o=Math.round;with(a)lineWidth=30,beginPath(),moveTo(w/2,h/3),bezierCurveTo(w/10,0,0,h*.6,w/2,h*.9),bezierCurveTo(w,h*.6,w*.9,0,w/2,h/3),closePath(),stroke(),d=getImageData(0,0,w,h).data,clearRect(0,0,w,h),lineWidth=2;for(i=0;i<w*h*4;i+=4)if(d[i+3]&&r()<.015)p.push({t:o(r()),x:i/4%w,y:i/4/w});setInterval('with(a){t+=Math.max((new Date).getTime()-n,30);n=(new Date).getTime();z=Math.cos(t*3E-4),globalAlpha=.3;g=createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);g.addColorStop(0,"#c68fb8");g.addColorStop(.6-Math.sin(t*3E-4)/10,b.style.background="#724567");fillStyle=g;fillRect(0,0,w,h);for(i in p)q=p[i],g=Math.PI*5*Math.sin(t*3E-4)+i*Math.PI/30,u=Math.sin(g),v=Math.cos(g),q.x+=z*u,q.y+=z*v,font=f(u*5+17)+"px Arial",globalAlpha=1.5-(u+1.5)/2,strokeStyle=k[q.t+2],strokeText(k[q.t],q.x,q.y),font="bold "+f(u*2+55)+"px Arial",globalAlpha=1,fillStyle="rgba(255,255,255,.1)";strokeStyle="rgba(255,255,255,.2)",b.style.textAlign=textAlign="center";if((s=f(t/5E3))!==l)l=s,m=k[o(r())]+" LOVE "+k[o(r())];fillText(m,w/2,h/2+30);strokeText(m,w/2,h/2+30)}',1)

Result: 1174 bytes. It’s not enough !
We are not forced to think about a packing method, a compressor.

 

 

Step 3 : Pack it

You can see in this code that a lot of same words are repeating: globalAlpha, stroke, fill, text, Math… We can think about keep one reference for each words and use a specify byte to replace them.

So I wrote a little compression tool that search for this words and replace them with one byte. Here the result:

for(l in $='w=c.wੂ60ྺh=c.height=45ྺk=["♂ಚ♀ಚ#6371b7ಚ#db7189"ෆp=[ෆt=ྺx=m=1,n=â,r=દrandom,f=દfloor,y=દ৞;׶Ξ3ྺbegส֒moॺࡎ3Ȏ/1ྺྺྺܢశh*.9Ȏ,ܢ*.9,ྺࡎ3བclose֒ߪ(བd=getImageD໲aЂ.d໲a,clearɲ,Ξ2ھ=0;i<w*h*4;i+=4)iൢd[i+3]&&r()<.015)p.push({t:ކ,x:i/4%w,y:i/4/w});setInterval(\'׶{t+=દmax(â-n,30);n=â;z=દcosԮ˖.3;g=cre໲eRadialGradient(ࡎ2,ྺࡎ2,w/2ƪྺ"#c68fb8"ƪ.6-દsสԮ/1ྺѦbackg৞="#724567");୮ٚg;୮ɲھ ส p)q=p[iෆg=દPI*5*દsสԮ+i*દPI/3ྺu=દsส(gབ_=દcos(gབຎx+=z*u,ຎy+=z*_,खൢu*5+17.5-(u+1.5)శߪٚk[ຎt+2ෆߪࢲk[ຎtෆຎx,ຎyབख"bold "+ൢu*2+55,୮~1)";ߪ~2)",Ѧ̺̺"center";iൢ(s೾t/5E3))!==x)x=s,m=k[ކ]+" LOVE "+k[ކ];୮ņ;ߪņ}\',1)',_=')+"px Arial"˖1|ٚ"rgba(ӊଊ.|(new D໲e).getTime()|ࢲm,ࡎ2+30)|);g.addColorStop(|བbezierCurॺw|RectЂ|,globalAlpha=|textAlign=|lสeWੂ|(ྺྺw,h)|b.s௒.|ଊଊ|(t*3E-4)|P໲h(བ|with(a)|S௒=|;for(i|h*.6,w|y(r())|stroke|wశh/|Text(|font=|veTo(|round|idth=|M໲h.|255,|fill|tyle|/2,|","|=ൢ|f(|],|in|q.|at|),|0,'.split("|"))$=$.split(String.fromCharCode(l+26)).join(_[l]);eval($)

925 bytes ! Here we go !

So now… Maybe we can add a little header, just for fun; we have 99 free bytes.

(function JS1K(l,o,v,e){ <code> })(2,0,1,2)

And finally, the result:

(function JS1K(l,o,v,e){for(l in $='w=c.wੂ60ྺh=c.height=45ྺk=["♂ಚ♀ಚ#6371b7ಚ#db7189"ෆp=[ෆt=ྺx=m=1,n=â,r=દrandom,f=દfloor,y=દ৞;׶Ξ3ྺbegส֒moॺࡎ3Ȏ/1ྺྺྺܢశh*.9Ȏ,ܢ*.9,ྺࡎ3བclose֒ߪ(བd=getImageD໲aЂ.d໲a,clearɲ,Ξ2ھ=0;i<w*h*4;i+=4)iൢd[i+3]&&r()<.015)p.push({t:ކ,x:i/4%w,y:i/4/w});setInterval(\'׶{t+=દmax(â-n,30);n=â;z=દcosԮ˖.3;g=cre໲eRadialGradient(ࡎ2,ྺࡎ2,w/2ƪྺ"#c68fb8"ƪ.6-દsสԮ/1ྺѦbackg৞="#724567");୮ٚg;୮ɲھ ส p)q=p[iෆg=દPI*5*દsสԮ+i*દPI/3ྺu=દsส(gབ_=દcos(gབຎx+=z*u,ຎy+=z*_,खൢu*5+17.5-(u+1.5)శߪٚk[ຎt+2ෆߪࢲk[ຎtෆຎx,ຎyབख"bold "+ൢu*2+55,୮~1)";ߪ~2)",Ѧ̺̺"center";iൢ(s೾t/5E3))!==x)x=s,m=k[ކ]+" LOVE "+k[ކ];୮ņ;ߪņ}\',1)',_=')+"px Arial"˖1|ٚ"rgba(ӊଊ.|(new D໲e).getTime()|ࢲm,ࡎ2+30)|);g.addColorStop(|བbezierCurॺw|RectЂ|,globalAlpha=|textAlign=|lสeWੂ|(ྺྺw,h)|b.s௒.|ଊଊ|(t*3E-4)|P໲h(བ|with(a)|S௒=|;for(i|h*.6,w|y(r())|stroke|wశh/|Text(|font=|veTo(|round|idth=|M໲h.|255,|fill|tyle|/2,|","|=ൢ|f(|],|in|q.|at|),|0,'.split("|"))$=$.split(String.fromCharCode(l+26)).join(_[l]);eval($)})(2,0,1,2)

960 bytes.
 

 

Step 4 : Submit !

Now submit your script at js1k.com and share your knowledge !





Leave a comment