Random circles

logo

No interactivity here. Just a bunch of random fading circles using webgl.

Almost all the action occurs in the fragment shader. The vertex shader is used only to draw a triangle strip over the entire viewport.

#version 300 es
in vec2 a_coords;
void main() {
gl_Position = vec4(a_coords, 0.0, 1.0);
}
function initGL() {
...
const a_coords = _gl.getAttribLocation(prog, 'a_coords');
const coords_buf = _gl.createBuffer();
const coords = new Float32Array([-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0]);
_gl.bindBuffer(_gl.ARRAY_BUFFER, coords_buf);
_gl.bufferData(_gl.ARRAY_BUFFER, coords, _gl.STREAM_DRAW);
_gl.enableVertexAttribArray(a_coords);
_gl.vertexAttribPointer(a_coords, 2, _gl.FLOAT, false, 0, 0);
...
}

function draw(center, radius, color) {
...
_gl.drawArrays(_gl.TRIANGLE_STRIP, 0, 4);
}

Now for the fragment shader. There is an uniform each for the center of the circle, its radius, and its colour.

#version 300 es
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
out vec4 out_color;
uniform vec3 u_circle_color;
uniform vec2 u_center;
uniform float u_radius;
void main() {
vec2 radius_vec = gl_FragCoord.xy - u_center;
float pct = smoothstep(u_radius - 6.0, u_radius, length(radius_vec)) - smoothstep(u_radius, u_radius + 6.0, length(radius_vec));
vec4 colr = mix(vec4(0, 0, 0, .2), vec4(u_circle_color, 1.0), pct);
out_color = colr;
}

By default, the x and y components of gl_FragCoord go from 0 to the width of the screen in pixels from left to right and from 0 to the height of the screen in pixels from bottom to top. Strictly speaking, though, by default, x and y go from 0.5 to width - 0.5 and 0.5 to height - 0.5. That is, by default, pixel centers are halfway along the size of a pixel. The line containing the smoothstep glsl function along with the next line with the mix glsl function ensures that the colour of a pixel goes from a nearly transparent black( vec4(0, 0, 0, .2) ) to the required colour of the circle as its distance from the center goes to u_radius from below u_radius - 6.0 or above u_radius + 6.0. Now how does that work.

Let ssss be smoothstep, rr, u_radius, and ll, length(radius_vec). From the defintion of smoothstep we have,

ss(r6.0,r,l)=0,l<r6.0=1,l>r=01,r6.0lr\begin{aligned}ss(r - 6.0, r, l) &= 0, l < r - 6.0\\&= 1, l > r\\&= 0-1, r - 6.0 \leq l \leq r \end{aligned}

Similarly,

ss(r,r+6.0,l)=0,l<r=1,l>r+6.0=01,rlr+6.0\begin{aligned}ss(r, r + 6.0, l) &= 0, l < r\\&= 1, l > r + 6.0\\&= 0-1, r \leq l \leq r + 6.0 \end{aligned}

Thus, when you subtract the second smoothstep from the first, you get the value pct to be

pct=0,l<r6.0,l>r+6.0=01,r6.0lr=10,rlr+6.0\begin{aligned}pct &= 0, l < r - 6.0, l > r + 6.0\\&= 0-1, r - 6.0 \leq l \leq r\\&= 1-0, r \leq l \leq r + 6.0 \end{aligned}

Th mix function uses pct to obtain a colour linearly interpolated between a transparent black( vec4(0, 0, 0, 0.2)) and the opaque circle color( vec4(u_circle_color, 1.0)). Since we do not clear the screen before each circle is drawn and instead use an option of preserveDrawingBuffer along with a blend function that blends the output of each circle drawing with the screen we get an effect in which the previous circles gradually fade away.

function resize() {
...
try {
_gl = _canvas.getContext('webgl2', {
alpha: false,
depth: false,
preserveDrawingBuffer: true,
});
if (!_gl) {
throw new Error('Browser does not support WebGL2');
}
} catch (e) {
_canvas_div.innerHTML =
'<p>Sorry, could not get a WebGL graphics context. Are you using a modern browser?</p>';
return;
}
...
}

function initGL() {
...
_gl.enable(_gl.BLEND);
_gl.blendFunc(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA);
...
}

Here is the entire code:

const _v_shader_src = `#version 300 es
in vec2 a_coords;
void main() {
gl_Position = vec4(a_coords, 0.0, 1.0);
}
`
;

const _f_shader_src = `#version 300 es
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
out vec4 out_color;
uniform vec3 u_circle_color;
uniform vec2 u_center;
uniform float u_radius;
void main() {
vec2 radius_vec = gl_FragCoord.xy - u_center;
float pct = smoothstep(u_radius - 6.0, u_radius, length(radius_vec)) - smoothstep(u_radius, u_radius + 6.0, length(radius_vec));
vec4 colr = mix(vec4(0, 0, 0, .2), vec4(u_circle_color, 1.0), pct);
out_color = colr;
}
`
;

let _canvas_div;
let _canvas;
let _gl;

let _u_circle_color;
let _u_center;
let _u_radius;

let _anim_request;

function createProgram() {
const v_shader = _gl.createShader(_gl.VERTEX_SHADER);
_gl.shaderSource(v_shader, _v_shader_src);
_gl.compileShader(v_shader);
if (!_gl.getShaderParameter(v_shader, _gl.COMPILE_STATUS)) {
throw new Error(
`Error in vertex shader: ${_gl.getShaderInfoLog(v_shader)}`
);
}
const f_shader = _gl.createShader(_gl.FRAGMENT_SHADER);
_gl.shaderSource(f_shader, _f_shader_src);
_gl.compileShader(f_shader);
if (!_gl.getShaderParameter(f_shader, _gl.COMPILE_STATUS)) {
throw new Error(
`Error in fragment shader: ${_gl.getShaderInfoLog(f_shader)}`
);
}
const prog = _gl.createProgram();
_gl.attachShader(prog, v_shader);
_gl.attachShader(prog, f_shader);
_gl.linkProgram(prog);
if (!_gl.getProgramParameter(prog, _gl.LINK_STATUS)) {
throw new Error(`Link error in program:${_gl.getProgramInfoLog(prog)}`);
}
return prog;
}

function initGL() {
const prog = createProgram();
const a_coords = _gl.getAttribLocation(prog, 'a_coords');
const coords_buf = _gl.createBuffer();
const coords = new Float32Array([-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0]);
_gl.bindBuffer(_gl.ARRAY_BUFFER, coords_buf);
_gl.bufferData(_gl.ARRAY_BUFFER, coords, _gl.STREAM_DRAW);
_gl.enableVertexAttribArray(a_coords);
_gl.vertexAttribPointer(a_coords, 2, _gl.FLOAT, false, 0, 0);
_u_circle_color = _gl.getUniformLocation(prog, 'u_circle_color');
_u_center = _gl.getUniformLocation(prog, 'u_center');
_u_radius = _gl.getUniformLocation(prog, 'u_radius');

_gl.viewport(0, 0, _canvas.width, _canvas.height);
_gl.enable(_gl.BLEND);
_gl.blendFunc(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA);
_gl.useProgram(prog);
}

function draw(center, radius, color) {
_gl.uniform2fv(_u_center, center);
_gl.uniform1f(_u_radius, radius);
_gl.uniform3fv(_u_circle_color, color);
_gl.drawArrays(_gl.TRIANGLE_STRIP, 0, 4);
}

const rnd = (n1, n2) => Math.floor(Math.random() * (n2 - n1 + 1)) + n1;

function animate(delay) {
let then;
function tick(now) {
if (!then) {
then = now;
}
const elapsed = now - then;
if (elapsed > delay) {
then = now - (elapsed % delay);
const center = [
rnd(10, _canvas.width - 10),
rnd(10, _canvas.height - 10),
];
const smaller_coord = Math.min(
center[0],
_canvas.width - center[0],
center[1],
_canvas.height - center[1]
);
const radius = rnd(10, smaller_coord);
const color = [Math.random(), Math.random(), Math.random()];
draw(center, radius, color);
}
_anim_request = requestAnimationFrame(tick);
}
_anim_request = requestAnimationFrame(tick);
}

function resize() {
if (_anim_request) {
cancelAnimationFrame(_anim_request);
}
_canvas.width = _canvas_div.clientWidth;
_canvas.height = _canvas_div.clientHeight;
try {
_gl = _canvas.getContext('webgl2', {
alpha: false,
depth: false,
preserveDrawingBuffer: true,
});
if (!_gl) {
throw new Error('Browser does not support WebGL2');
}
} catch (e) {
_canvas_div.innerHTML =
'<p>Sorry, could not get a WebGL graphics context. Are you using a modern browser?</p>';
return;
}
try {
initGL();
} catch (e) {
_canvas_div.innerHTML = `<p>Sorry, could not initialize the WebGL graphics context: ${e.message}. Are you using a modern browser?</p>`;
return;
}
animate(120);
}

function init() {
_canvas_div = document.getElementById('canvas_div');
_canvas = document.getElementById('canvas');
resize();
}

window.addEventListener('load', init);
window.addEventListener('resize', resize, false);
window.addEventListener('orientationchange', resize, false);