Fit starry sky on WebGL in 1009 bytes JavaScript

Two things always fill the soul with new and ever stronger surprise and awe, the more often and longer we think about them - this is the starry sky above me and the moral law in me. Immanuel Kant

JS1k is an annual competition where you need to accommodate a demo, a game or anything, in 1024 characters in JavaScript. This year, my demo took fourth place (until the third there were not enough any two points). You can watch the demo on the JS1k website . Who does not open or does not work, should look like this:









The minified and complete source code is on github . And under the cut is an analysis of how JavaScript is now being minified for such contests.







Disclaimer



The main beauty of the demo is a fragment shader by Pablo Roman Andrioli. Pablo is an artist who works with fractals, and on the fractalforums forum he gives some details of the calculations. My task was to pack a shader and WebGL code of 1024 bytes.







WebGL Initialization



The JS1k wrapper at the start of the demo provides a WebGL context in the global variable g . Despite this, working with WebGL is very verbose. For example, to add a vertex shader to a program, 159 characters are needed:







// Define a new program p=g.createProgram(); // Basic vertex shader s=g.createShader(VERTEX_SHADER); g.shaderSource(s,"attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}"); // Compile and attach it to the program g.compileShader(s); g.attachShader(p,s);
      
      





To solve this problem, all JS1k solutions of the last few years use a trick with synonyms of functions:







 for(i in g){ g[i[0] + i[6]] = g[i]; }
      
      





The loop adds a synonym for each function (and for any member) of the WebGL context, which consists of the first and 7 letters. For example, c reate P rogram becomes cP , s hader S ource - sS , etc. Additionally, framing the entire code with(g)



construct (which cannot be used in these projects), we get:







 with(g){ p=cP(); sS(s=cS(35633),'attribute vec2 p;void main(){gl_Position=vec4(p,1,1);}'); ce(s); aS(p,s); }
      
      





Shader minification



The original shader takes 1100 characters. The main abbreviations: removing unnecessary variables, and combining similar fragments. After all, I passed the code through the online minifier . As a result, a little more than 500 bytes remained from the shader.







JSCrush



JSCrush is the de facto standard for compressing code in such competitions. The utility turns the code into approximately the following sequence:







_ = '(i a.style = ...
 _='(i a.style="widMj%;hEjvh;:left",g)g[i[0]+i[6]]=g[i];wiMO.u=g.G1f,x=y=k=g)p=cP(35633"tribute 2 p gl_Posit=4?FN"precis mediump ;G Zt,a,x,y Uf`ord.rg/64!-.f.=a;Zc=+xz,v=+yz;m2 m$cc-cc)s$vv-vv)fJf#Ur`Q,,r+`t*2.,t,-2.rJr#Zg=.1,b=Q;Ui`!Kl=Rl<2Rl++){Uo=r+f*;oQ)-mod(o,2.))Ze,n=e=!;Kd=Rd<2Rd++)oo)/dot(o,o)-3,n+o)-ee=oif(l>6)Q-max(!,.3-i+=b+g,g,g)*n*5*b;.73;g+=.1;}i=mix(i)i,.85lor=4(i*.01.lo?ug?bfO=34962,cB()eV(0vA(2,5120bDO,Tw Int8Array([|,|]35044o=,(Lt@-oa@TrHE/TrWidMx@xy@ydr(6,3requestAnimFrame(L)})(down=upk^=1},movek&&(xX,yY)};),3=funct(e){uOf?,"flo}@ce(saS?,slengM(onmouse ;void ma(){Tw De/1e5);incos(for=abs(gl_FragCo,1g*(sS(s=cS(n*n*.001at.5vecionb*=s(=e.page0,!0.#.r=s;$=m2(?(p@"EeightGunimJ.rm;K(t MthO(gQ1.R0;TneU Z `=j:100z/50!|-3';for(Y in $='|zj`ZUTRQOMKJGE@?$#! ')with(_.split($[Y]))_=join(pop());eval(_)
      
      





JSCrush working principle can be visually viewed in the tool for reverse code conversion . Or read in detail in the article . In general, this is encoding with a dictionary:







  1. We find a character that is not used in the code
  2. We find repeating fragments in the code that we replace with the character from the first paragraph
  3. Replace the string with a character
  4. Repeat 1-3 until the result is smaller than the source code.


Optimization



After all the operations, I still had about 30 characters left, which could be used to optimize performance. When loading the demo, you can specify the canvas size: full screen or fixed size. Launching in full screen is beautiful, but the fragment shader is called for each pixel and works slowly. The solution was to request a fixed size from JS1k canvas (I chose 640x640), and then increase it to full screen in the code:







 a.style='width:100%;height:100vh;float:left';
      
      





Then the image occupies the entire screen, but the shader is executed only for each pixel of the original canvas size.








All Articles