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 be smoothstep
, , u_radius
, and , length(radius_vec)
. From the defintion of smoothstep
we have,
Similarly,
Thus, when you subtract the second smoothstep
from the first, you get the value pct
to be
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);