The quest of the 🌍🔬🕸🇬🇱🎠 (World's Smallest WebGL Playground)


April 2016 - November 2019 - January 2022

Co-authors: Mathieu Henri, Martin Kleppe, Subzey, Anders Kaare, LiterallyLara, HellMood, innovati, Bálint Csala, Frank Force, Senokay, Román Cortés

See also: the smallest WebGL raytracing engine and my WebGL tutorial.



Introduction

This page presents the world's smallest WebGL shader playground and all the golfing steps that led us to it.

It is currently as short as 252 bytes, and looks like this:

<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|compileShader(S)|aS(p,S)),A`gl_Position.w=gl_PointSize=3e2`,A`[𝗬𝗢𝗨𝗥 𝗦𝗛𝗔𝗗𝗘𝗥 𝗛𝗘𝗥𝗘]`),ug(p),!dP(p))")'>

Example:

(DEMO / UNGOLFED VERSION)



How to use it

In the code above, replace [𝗬𝗢𝗨𝗥 𝗦𝗛𝗔𝗗𝗘𝗥 𝗛𝗘𝗥𝗘] with your GLSL demo. You can use the following inputs:

  • t: a frame counter
  • gl_FragCoord: the coordinates of the current pixel on the canvas (by default, x ∈ [0:300], y ∈ [0:150])
...and you need to set gl_FragColor, representing the color of each pixel. .



Tips

  • You can resize the canvas by editing its width and height attributes (in HTML: <canvas id=c width=600 height=600> / in JS: c.width=c.height=600)
  • You can make the time variable "t" count the run time in milliseconds instead of the number of frames by changing the way "NO" is incremented. Ex: NO+=.016
  • you can convert gl_FragCoord into clip space coordinates (where x,y ∈ [-1:1]) by prepending your shader with: vec4 p=vec4(gl_FragCoord.xy/vec2(150,75)-1.,0,1);
    (150 and 75 are half the canvas's width and height. If you resize the canvas, you'll have to change them)
  • If you resize your canvas, you'll also need to change the size of the point where everything is rendered (for example: gl_PointSize=6e2)
  • If your code contains more than 4 floats or vec2/3/4 vars (like p and t), it's shorter to remove "lowp" before each var and write precision lowp float; before "void main()"
  • On some devices/browsers, you can save 14 more bytes by removing gl_Position.w=. But we don't recommend it, as most people will just see a blank screen



Backstory

04/2016

After welcoming LiterallyLara and HellMood who won JS1K 2017 with a WebGL entry, the codegolf team gave itself a nice challenge: develop the smallest possible HTML/JS boilerplate that's able to run a WebGL fragment shader, à la Shadertoy. Our starting point was MiniShadertoy, that we simplified into MiniShadertoyLite, and after weeks of intense golfing, we managed reduce the code down to 349b!

01/2017

We made an entry for JS1k 2017 featuring all the tricks of this article, plus a nice UI and full Shadertoy compatibility. It was ranked #7! (ENTRY / COMMENTED CODE).

12/2019

We managed to remove the uniform time variable (by recompiling the program at each frame with a new value).
We also got rid of the verbose triangle generation (by rendering everything on a big square point). You can find more info about drawing points in this tutorial.
Our Raymarching / sdf post has been updated accordingly.

01/2022

Unfortunately, `g.ce` no longer matches `g.createShader` on Webkit browsers, so we have to use this method's full name until a better hash is found. This makes the golf score grow from 242 to 252 bytes.
Also, Safari support has been lost a long time ago, but this project by lostmachines.xyz supports it!



Commented source code

<canvas id=c>
<script>

// Canvas methods hashing:
// Creates tiny shortcuts for all the WebGL methods and constants we need:
// g.createProgram => g.cP
// g.shaderSource => g.sS
// g.createShader => g.cS
// g.compileShader => g.ce (no longer working on Webkit)
// g.attachShader => g.aS
// g.linkProgram => g.lo
// g.useProgram => g.ug
// g.deleteProgram => g.dP
// g.getUniformLocation => g.gf
// g.drawArrays => g.dr
// g.NO_ERROR => g.NO (= 0)
// g.FRAGMENT_SHADER => g.FN (= 35632)
// g.VERTEX_SHADER => g.V_ (= 35633)

for(i in g=c.getContext(`webgl`)){
  g[i[0]+i[6]]=g[i];
}

setInterval(() => {             // Repeat the following code 60 times per second:
                                // In the golfed code, the function is replaced with a string: setInterval("with(g)...")
                                // Also, the second parameter of setInterval is omitted. It defaults to ~60fps on modern browsers
                                

  with(g){                      // Make g implicit (no need to use the prefix "g.")
                                // In the golfed code, the curly braces are removed
                                // This is possible because all the following statements are separated by "," instead of ";"

    dr(                         // g.drawArrays(0,0,1): draws a single point on the canvas (mode = g.POINTS, start = 0, count = 1)
    
      lo(                       // - g.linkProgram(p): links the program p and returns "undefined", which is equivalent to "0"

        p = cP(                 // -- p = createProgram(): creates the WebGL program "p"
        
        
        
          A = s => {            // --- Declare the function A that will be used twice per frame. It creates, compiles and attaches a shader to the program
                                // In the golfed code, the curly braces are removed
                                // This is possible because all the following statements are separated by "|" instead of ";"
                                
            sS(                 // g.shaderSource(S = g.createShader(FN ^= 1), template) creates the shader "S" and sets its type and source code
              S = cS(FN ^= 1),  // The first time A() is called, FN is equal to g.FRAGMENT_SHADER (36532), then it is XOR'd with 1 to become 35633
                                // The second time A() is called, FN is equal to g.VERTEX_SHADER (36533), then it is XOR'd with 1 to become 35632
        
              `                         // This template is common to both vertex and fragment shaders:
              void main(){              // * Declare the main function of the shader
                lowp float t=${NO++}.;  // * Declare the time variable "t", increased by JS at each frame. g.NO is used because its value on load is 0
                ${s};                   // * Append the code of the current shader
              }
              `
            )
            | compileShader(S)  // - g.compileShader(S)
            | aS(p, S)          // - g.attachShader(p, S)
          }
        ),
    
        A(`                     // Call A() a first time to set up the vertex shader
							    // the point's size is set to 300px (the size of the canvas)
								// At the same time, the 4th value of the point's position is set to 300
								// This 300 is not important, but it forces the three values x, y and z to default to 0, so the point is centered
          gl_Position.w = gl_PointSize = 3e2
        `),

        A(`[SHADER]`),          // Call A() a second time to set up the fragment shader (your code goes here)    
      ),
      
      ug(p),                    // - g.useProgram(p) returns "undefined", which is equivalent to "0"
      !dP(p)                    // - g.deleteProgram(p) returns "undefined". It becomes "true" with a "!" at the beginning, which is equivalent to "1"
    )
  }
}                               // The content of this script tag is placed in the onload attribute of a <svg> element to save bytes
</script>




Golf progress

Here are all our golfing steps, based on the source code of MiniShadertoyLite:

// Step 1 (April 2016, 308 bytes): start from MiniShadertoyLite, remove the textarea (hardcoded shader), minify, apply RegPack's webgl method hashing, replace all the constants with their numeric value
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P;${V="void main(){gl_"}Position=vec4(P,0,1);}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T;${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),35044)'><canvas id=a>

// Step 2: simplify `gl_Position=vec4(P,0,1)` to `gl_Position.xy=P`
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P;${V="void main(){gl_"}Position.xy=P;}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T;${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),35044)'><canvas id=a>

// Step 3: place the `;` in the template, reuse B at the end
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P${V=";void main(){gl_"}Position.xy=P;}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),B+82)'><canvas id=a>

// Step 4: `with(g)` including the setInterval, arrow function inside setInterval, `int8array.of()`
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),so(S=ch(35633),`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),(A=x=>cS(S)+ah(P,S))(),so(S=ch(35632),`uniform lowp float T${V}FragColor=[SHADER]}`),A(),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'><canvas id=a>

// Step 5: move some reused code inside the `A()` function, and reuse B better
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),B=35633,(A=s=>so(S=ch(B--),s)+cS(S)+ah(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'><canvas id=a>

// Step 6: replace "body onload=..." with "svg onload=..."
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),B=35633,(A=s=>so(S=ch(B--),s)+cS(S)+ah(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>

// Step 7: use a simpler hashing (`g[i[0]+i[6]]` instead of `g[i[0]+i[7]+[i[13]]]`)
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),T++)+dr(6,0,3),9),P=cP(),B=35633,(A=s=>sS(S=cS(B--),s)+ce(S)+aS(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lo(P),ug(P),bf(B=34962,cB()),eV(T=0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>

// Step 8: replace the first usages of `B` (36532, 36533) with hashed properties (`g.FN` and `g.FN+1`, simplified to `g.FN++` called twice)
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),T++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=34962,cB()),eV(T=0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>

// Step 9: replace `T` with `g.NO`, which is already set to 0
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),NO++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=34962,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>

// Step 10: simplify the constant `34962` with `g.ET-3`
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),NO++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=ET-3,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>

// Step 11: Subzey joins the game... remove `getContext` parenthesis, replace the second param of `g.dr` (0) with the result of the neighbour function `uniform1f()`. Renamem c into p and T to t
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=p=P;}`),lo(P),ug(P),bf(B=ET-3,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>

// Step 12: get rid of some more zeros
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),vA(A(`attribute vec4 P${V}Position=p=P;}`),2,5120,lo(P),ug(P),eV(bf(B=ET-3,cB()))),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>

// Step 13: move `A` declaration into `cP()`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S)),A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),vA(A(`attribute vec4 P${V}Position=p=P;}`),2,5120,lo(P),ug(P),eV(bf(B=ET-3,cB()))),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>

// Step 14: move `vA`, replace the ones in `Int8Array.of()` with neighbor functions
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),!vA(P=cP(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S)),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(-3,1,!A(`attribute vec4 P${V}Position=p=P;}`),-3,!lo(P),!ug(P)),B+82)))'>

// Step 15: move `vA(P=cP(...))` around the `setInterval`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(-3,1,!A(`attribute vec4 P${V}Position=p=P;}`),-3,!lo(P),!ug(P)),B+82))'>

// Step 16: change the Int8Array's vertices to `3,-1,-1,3,-1,-1` and use `~` to turn `null` into `-1`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(3,-1,~A(`attribute vec4 P${V}Position=p=P;}`),3,~lo(P),~ug(P)),B+82))'>

// Step 17: Make `A()` always return `-1`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|~aS(P,S))),2,5120,0,eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(3,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=p=P;}`),3,~lo(P),~ug(P)),B+82))'>

// Step 18: change the shape of the triangle, using the return value of `setInterval` (usually between 1 and 127) as first param of `Int8Array`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P${V}Position=p=P;}`)),B+82),lo(P),ug(P))'>

// Step 19: introduce C and T as attributes, set `t.x=T.x`, and use `vA` (vertexAttrib1f) instead of `uniform1f`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t.x=T.x;}`)),B+82),lo(P),ug(P))'>

// Step 20: Set `t=T` directly
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),lo(P),ug(P))'>

// Step 21: Set 5th parameter of vA (i.e. `vertexAttribPointer`), stride, to 1, to  make the coordinate pairs overlap: `int8Array.of(x1 = 1, y1 = x2 = -3, y2 = x3 = 1, y3 = 1)`. Also, the setTimeout is moved into `cP` (i.e. `createProgram`)
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>

// Step 22: Make `A` return 1 in order to avoid calling `!A()` with a `!` twice
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>

// Step 23: Rearrange `Int8Array.of()` arguments to place `!eV()` first, and move `V` declaration into `cb()`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(!eV(bf(B,cB(V="varying lowp vec4 p,t;void main(){gl_"))),B,A(V+`FragColor=[SHADER]}`),A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>

// Step 24: Use the svg's id (which is an empty string by default)
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),id+"varying lowp vec4 p,t;void main(){gl_"+s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(A`FragColor=[SHADER]}`,B,!eV(bf(B,cB(id="attribute vec4 P,T;"))),A`Position=p=P;t=T;}`),B+82),!lo(P),ug(P))'>

// Step 25 (11/2019): Replace the triangle with a single, huge point. No need to use a buffer object anymore! (we lose Safari compatibility here)
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)p=cP(setInterval(z=>dr(uniform1f(gf(p,"t"),NO++),0,1),A=s=>sS(S=cS(FN++),`precision lowp float;uniform float t;void main(){${s};}`)|ce(S)|aS(p,S))),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,lo(p),ug(p)'>

// Step 26: Get rid of the uniform time variable by recompiling the program at each frame. Also, replace the parameters of drawArrays (0,0,1) with "lo(),ug(p),!dp(p)" (warning: perf drop may happen at this point)
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)A=s=>sS(S=cS(h++),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S),setInterval(z=>{p=cP(h=FN),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,dr(lo(p),ug(p),!dP(p))})'>

// Step 27: Use a string parameter for setInterval instead of a function
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)A=s=>sS(S=cS(h++),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S),p=cP(h=FN),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,dr(lo(p),ug(p),!dP(p))")'>

// Step 28: Remove h, use FN^=1 to alternate between 35632 and 35633 at each frame instead. Put A into cP().
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)p=cP(A=s=>sS(S=cS(FN^=1),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`,dr(lo(p),ug(p),!dP(p))")'>

// Step 29: Rearrange the code: "dr(lo(p=cP(A=...),A(...),A(...)),ug(p),!dP(p)" instead of "p=cP(A=...),A(...),A(...),dr(lo(p),ug(p),!dP(p)))"
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`),ug(p),!dP(p))")'>

// Step 30: Remove "precision lowp float", and write "lowp" before each float/vec2/vec3/vec4 instead
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`),ug(p),!dP(p))")'>

// Step 31: Only set gl_Position.w to 300, which makes the values x, y and z default to 0
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position.w=gl_PointSize=3e2`,A`[SHADER]`),ug(p),!dP(p))")'>

// Step 32 (01/2022): rename "ce" to "createShader" to fix Webkit browsers
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|createShader(S)|aS(p,S)),A`gl_Position.w=gl_PointSize=3e2`,A`[SHADER]`),ug(p),!dP(p))")'>




Cheers!

The Codegolf team