Simple WebGL Background Animation

When I created this site, I wanted to add an animated background. Here’s how I did it. First, here’s the javascript in full.

/**
 * A simple WebGL Background.
 */

function startAnimatedBackground() {

	// ========================================================================
	// OPTIONS
	// ========================================================================
	
	var vertex_count = 12; // Number of vertices on object.
	var animation_speed = 0.015; // Animation speed.
	var min_animation_speed = 0.005; // Minimum animation speed.
	var line_width = 4; // Width of the lines drawn.
	var echo_count = 12; // Number of echoed lines of the main object.
	var canvas_id = 'background_canvas'; // The canvas to draw to.
	var clear_color = [ 0.2, 0.2, 0.2 ]; // Background clear color.
	
	// ========================================================================
	// SETUP OPENGL
	// ========================================================================
	
	// Get the browser ready to use webgl in the canvas.
	var background_canvas = document.getElementById(canvas_id);
	webgl = background_canvas.getContext('experimental-webgl');
	
	// GLSL vertex shader code.
	var vertex_code = 'attribute vec2 coordinates;'+
	   'attribute vec3 color;'+
	   'varying vec3 vColor;'+
	   'void main(void) {' +
	      ' gl_Position = vec4(coordinates, 0.0, 1.0);' +
	      'vColor = color;'+
	   '}';
	   
	// Compile the vertex shader.
	var vertex_shader = webgl.createShader(webgl.VERTEX_SHADER);
	webgl.shaderSource(vertex_shader, vertex_code);
	webgl.compileShader(vertex_shader);
	
	// GLSL fragment shader code.
	var fragment_code = 'precision mediump float;'+
	   'varying vec3 vColor;'+
	   'void main(void) {'+
	      'gl_FragColor = vec4(vColor, 1.0);'+
	   '}';
	   
	// Compile the fragment shader.
	var fragment_shader = webgl.createShader(webgl.FRAGMENT_SHADER);
	webgl.shaderSource(fragment_shader, fragment_code);
	webgl.compileShader(fragment_shader);
	
	// Link the vertex and fragment shaders into a shader program.
	var shader_program = webgl.createProgram();
	webgl.attachShader(shader_program, vertex_shader);
	webgl.attachShader(shader_program, fragment_shader);
	webgl.linkProgram(shader_program);
	webgl.useProgram(shader_program);
	
	// ========================================================================
	// SETUP EFFECT DATA
	// ========================================================================
	
	// Create the vertices for the object.
	var vertices = [ ];
	for(var i = 0; i < vertex_count; i++) {
		vertices.push(randomCoordinate());
		vertices.push(randomCoordinate());
	}
	
	// Create vectors to describe how the vertices move.
	var vectors = [ ];
	for(var i = 0; i < vertex_count; i++) {
		vectors.push(randomVector());
		vectors.push(randomVector());
	}
	
	// Create the colors for each vertex.
	var colors = [ ];
	for(var i = 0; i < vertex_count; i++) {
		colors.push(0.0);
		colors.push(0.3 * Math.random() + 0.3);
		colors.push(0.0);
	}
	
	// Create the OpenGL color buffer.
	var colors_float32 = new Float32Array(colors);
	var color_buffer = webgl.createBuffer ();
	webgl.bindBuffer(webgl.ARRAY_BUFFER, color_buffer);
	webgl.bufferData(webgl.ARRAY_BUFFER, colors_float32, webgl.STATIC_DRAW);
	
	// Tell the shader program to use this color data.
	var color = webgl.getAttribLocation(shader_program, "color");
	webgl.vertexAttribPointer(color, 3, webgl.FLOAT, false,0,0) ;
	webgl.enableVertexAttribArray(color);
	
	// Create the vertex buffer for the main object.
	var vertices_float32 = new Float32Array(vertices);
	var vertex_buffer = webgl.createBuffer();
	
	// Create vertex buffers for each echoed line.
	var echo_vertices_float32 = [ ];
	var echo_buffers = [ ];
	for(var i = 0; i < echo_count; i++) {
		echo_vertices_float32.push(new Float32Array(vertices_float32));
		echo_buffers.push(webgl.createBuffer());
	}
	
	// ========================================================================
	// ANIMATION LOOP
	// ========================================================================
	
	var animate_background = function(time) {
		
		// Resize the canvas as needed.
		background_canvas.width = window.innerWidth;
		background_canvas.height = window.innerHeight;
		
		// Copy the current object into the last vertex array.
		if(echo_vertices_float32.length > 0) {
			echo_vertices_float32[echo_vertices_float32.length -1].set(vertices_float32);
		}
		
		// Do the animation. 
		for(var i = 0; i < vertices_float32.length; i++) {
			vertices_float32[i] += vectors[i];
			
			// Reverse the direction of vectors if a coordinate goes out of bounds.
			if(vertices_float32[i] >= 1.0) {
				vertices_float32[i] = 0.9999;
				vectors[i] = vectors[i] * -1.0;
			} else if(vertices_float32[i] <= -1.0) {
				vertices_float32[i] = -0.9999;
				vectors[i] = vectors[i] * -1.0;
			}
		}
	
		// Begin updating coordinates.
	    var coord = webgl.getAttribLocation(shader_program, "coordinates");
	    webgl.enableVertexAttribArray(coord);
		
		// Clear the drawing canvas.
	    webgl.clearColor(clear_color[0], clear_color[1], clear_color[2], 1.0);
	    webgl.viewport(0.0, 0.0, background_canvas.width, background_canvas.height);
	    webgl.clear(webgl.COLOR_BUFFER_BIT);
	    
	    // Draw all the echo lines.
	    for(var i = 0; i < echo_count; i++) {
	    	webgl.bindBuffer(webgl.ARRAY_BUFFER, echo_buffers[i]);
	    	webgl.bufferData(webgl.ARRAY_BUFFER, echo_vertices_float32[i], webgl.STATIC_DRAW);
	    	webgl.vertexAttribPointer(coord, 2, webgl.FLOAT, false, 0, 0);
	    	
	    	webgl.lineWidth(line_width);
	        webgl.drawArrays(webgl.LINE_LOOP, 0, vertex_count);
	        
	        // Copy the current echo vertices to the previous one.
	        if(i > 0) {
	        	echo_vertices_float32[i - 1].set(echo_vertices_float32[i]);        		
	        }
	    }
	    
	    // Draw the main vertices.
	    webgl.bindBuffer(webgl.ARRAY_BUFFER, vertex_buffer);
		webgl.bufferData(webgl.ARRAY_BUFFER, vertices_float32, webgl.STATIC_DRAW);
		webgl.vertexAttribPointer(coord, 2, webgl.FLOAT, false, 0, 0);
	    
	    webgl.lineWidth(line_width);
	    webgl.drawArrays(webgl.LINE_LOOP, 0, vertex_count);
	
	    // We are done.
	    window.requestAnimationFrame(animate_background);
	 }
	
	// Start the animation.
	animate_background(0);
	
	// ========================================================================
	// UTILITY FUNCTIONS.
	// ========================================================================
	
	// Generate a random coordinate for the effect.
	function randomCoordinate() {
		return (Math.random() * 2.0) - 1.0;
	}
	
	// Generate a random movement vector.
	function randomVector() {
		var vec = ((Math.random() * 2.0) - 1.0) * animation_speed;
		
		if(vec >= 0 && vec < min_animation_speed){
			vec = min_animation_speed;
		} else if(vec < 0 && vec > (min_animation_speed * -1.0)) {
			vec = min_animation_speed * -1.0;
		}

		return vec;
	}
}

And to add the background to the site, add the following CSS code:

.bg_canvas {
    position:fixed;
	z-index:-1;
}

Finally, this HTML.

 <canvas id="background_canvas" width="100%" height="100%" class="bg_canvas"></canvas>
 <script type="text/javascript" src="webgl_background.js"></script>
 <script>startAnimatedBackground();</script>

How does this work

Much of this javascript sets up the environment. At the top, I placed arguments to tweak the effect. After that, I set up the OpenGL environment. This includes some simple GLSL shaders for working with 2D color lines. I then set up the geometry.

There’s a primary set of vertices that make up the leading or main set of lines. In the animation loop, previous values for the leading vertices are stored into “echo” vertices which gives it a bit of an orderly effect to the chaos. All the buffers are drawn to the screen, and the the animation loops indefinitely.

I optimized the animation by, of course, using WebGL, but also making sure I was not calling new statements in the animation loops. In general, I find it’s best to minimize the number of allocations done while a program runs.