How to draw beautiful things in the browser

Default avatar.
October 13, 2014
How to draw beautiful things in the browser.

Beautiful code is a joy to write, but it is difficult to share that joy with other programmers, not to mention with non-programmers. In my freetime between my day job and family time I've been playing around with the idea of a programming poem using the canvas element to draw in the browser. There are a multitude of terms out there to describe visual experiments on the computer such as dev art, code sketch, demo, and interactive art, but ultimately I settled on programming poem to describe this process. The idea behind a poem is a polished piece of prose that is easily shareable, concise, and aesthetic. It's not a half-finished idea in a sketchbook, but a cohesive piece presented to the viewer for their enjoyment. A poem is not a tool, but exists to evoke an emotion.

For my own enjoyment I've been reading books on math, computation, physics, and biology. I've learned really quickly that when I ramble on an idea it bores people pretty quickly. Visually I can take some of these ideas which I find fascinating, and quickly give anyone a sense of wonder, even if they don't understand the theory behind the code and concepts that drives it. You don't need a handle on any hard philosophy or math to write a programming poem, just a desire to see something live and breathe on the screen.

The code and examples I've put together below will kickstart an understanding of how to actually pull off this quick and highly satisfying process. If you’d like to follow along with the code you can download the source files here.

The main trick when actually creating a poem is to keep it light and simple. Don't spend three months building one really cool demo. Instead, create 10 poems that evolve an idea. Write experimental code that is exciting, and don't be afraid to fail.

Intro to Canvas

For a quick overview, the canvas is essentially a 2d bitmap image element that lives in the DOM that can be drawn onto. Drawing can be done using either a 2d context or a WebGL context. The context is the JavaScript object that you use to get access to the drawing tools. The JavaScript events that are available for canvas are very barebones, unlike those available for SVG. Any event that is triggered is for the element as a whole, not anything drawn onto the canvas, just like a normal image element. Here's a basic canvas example:

var canvas = document.getElementById('example-canvas');
var context = canvas.getContext('2d');
 
//Draw a blue rectangle
context.fillStyle = '#91C0FF';
context.fillRect(
 100, // x
 100, // y
 400, // width
 200 // height
);
 
//Draw some text
context.fillStyle = '#333';
context.font = "18px Helvetica, Arial";
context.textAlign = 'center';
context.fillText(
 "The wonderful world of canvas", // text
 300, // x
 200 // y
);

It's pretty straightforward to get started. The only thing that could be a little confusing is that the context has to be configured with the settings such as fillStyle, lineWidth, font, and strokeStyle before the actual draw call is used. It's easy to forget to update or reset those settings and get some unintended results.

Making things move

The first example only ran once and drew a static image onto the canvas. That's OK, but when it really gets fun is when it's updated at 60 frames per second. Modern browsers have the built in function requestAnimationFrame that syncs custom drawing code to the draw cycles of the browser. This helps in terms of efficiency and smoothness. The target of a visualization should be code that hums along at 60 frames per second.

(A note on support: there are some simple polyfills available if you need to support older browsers.)

var canvas = document.getElementById('example-canvas');
var context = canvas.getContext('2d');
var counter = 0;
var rectWidth = 40;
var rectHeight = 40;
var xMovement;
//Place rectangle in the middle of the screen
var y = ( canvas.height / 2 ) - ( rectHeight / 2 );
context.fillStyle = '#91C0FF';
function draw() {
 //There are smarter ways to increment time, but this is for demonstration purposes
 counter++;
 
 //Cool math below. More explanation in the text following the code.
 xMovement = Math.sin(counter / 25) * canvas.width * 0.4 + canvas.width / 2 - rectWidth / 2;
 
 //Clear the previous drawing results
 context.clearRect(0, 0, canvas.width, canvas.height);
 
 //Actually draw on the canvas
 context.fillRect(
 xMovement,
 y,
 rectWidth,
 rectHeight
 );
 
 //Request once a new animation frame is available to call this function again
 requestAnimationFrame( draw );
}
draw();

Drawing with math

Now be careful, we were talking about programming and drawing on the screen and I snuck some math in there. Math is really where magic can happen on the screen. I admit that I've always loved math (and a lot of artists don't). Despite this love, I have some real frustration with the way that it was taught in school. It was all very formal, which is great for a mathematician, but what was missing was the excitement, exploration, and pragmatism of what can actually done with some really simple concepts. The great news is that programmers love to ignore the abstract proofs and concentrate on the interesting applications.

Let me take a few seconds to explain how the sin function works. This will be helpful to start thinking about how mathematical functions can create movement. The thumbnail below links to an interactive graph of sin. Notice that there are a few more variables than just the x in sin( x ). The function I created is sin( a * x + b ) * c + d. Play around with the sliders on the graph and you can see how changing any of those values can adjust the graph's position and scale.

Now I'll rewrite my formula from the previous code example as a more broken down version that is easier to read.

var a = 1 / 25, //Make the oscillation happen a lot slower
 x = counter, //Move along the graph a little bit each time draw() is called
 b = 0, //No need to adjust the graph up or down
 c = width * 0.4, //Make the oscillation as wide as a little less than half the canvas
 d = canvas.width / 2 - rectWidth / 2; //Tweak the position of the rectangle to be centered

xMovement = Math.sin( a * x + b ) * c + d;

If you want to play around with the code so far, I would suggest adding some movement in the y direction. Try changing the values in the sin function, or switch to some other kind of function to play around and see what happens.

User input

Beyond driving motion with math, take a moment to imagine what you can do with different user input devices to move a square around a page. There are all kinds of options available in the browser including the microphone, webcam, mouse, keyboard and gamepad. Additional plugin-driven options are available with something like the Leap Motion or Kinect. Using WebSockets and a server you could hook up a visualization to home-built hardware. Hook up a microphone to the Web Audio API and drive your pixels with sound. You can even build a motion sensor out of a Webcam and scare a school of virtual fish (ok I did the last one in Flash five or so years ago.)

So now that you have your big idea let's jump back into some more examples. One square is boring, let's up the ante. First off, let's create a square function that can do a lot. We'll call it a Dot. One thing to that helps when working with moving objects is to use vectors rather than separate x and y variables. In these code samples I've pulled in the three.js Vector2 class. It's easy to use right away with vector.x and vector.y, but it also has a bunch of handy methods to work with them. Take a look at the docs for a deeper dive.

The almighty dot

This example's code gets a bit more complex because it's interacting with objects, but it will be worth it. Check out the example code to see a new Scene object that manages the basics of drawing to the canvas. Our new Dot class will get a handle to this scene to access any variables like the canvas context that it will need.

function Dot( x, y, scene ) {
 
 var speed = 0.5;
 
 this.color = '#000000';
 this.size = 10;
 
 this.position = new THREE.Vector2(x,y);
 this.direction = new THREE.Vector2(
 speed * Math.random() - speed / 2,
 speed * Math.random() - speed / 2
 );
 
 this.scene = scene;
 
}

To begin with the constructor for the Dot sets up the configuration of its behavior, and sets some variables to use. Again, this is using the three.js vector class. When rendering at 60fps, it's important to pre-initialize your objects, and not create new ones while animating. This eats into your available memory and can make your visualization choppy. Also, notice how the Dot is passed a copy of the scene by reference. This keeps things clean.

Dot.prototype = {
 update : function() {
 ...
 },
 draw : function() {
 ...
 }
}

All of the rest of the code will be set on the Dot's prototype object so that each new Dot that gets created has access to these methods. I'll go function by function in the explanation.

update : function( dt ) {
 this.updatePosition( dt );
 this.draw( dt );
},

I'm separating out my draw code from my update code. This makes it much easier to maintain and tweak your object, much like the MVC pattern separates out your control and view logic. The dt variable is the change in time in milliseconds since the last update call. The name is nice and short and comes from (don't be scared) calculus derivatives. What this does is separates out your motion from the speed of the frame rate. This way you don't get NES style slowdowns when things get too complicated. Your motion will drop frames if it's working hard, but it will stay at the same speed.

updatePosition : function() {

 //This is a little trick to create a variable outside of the render loop
 //It's expensive to allocate memory inside of the loop.
 //The variable is only accessible to the function below.

 var moveDistance = new THREE.Vector2();
 //This is the actual function
 return function( dt ) {

 moveDistance.copy( this.direction );
 moveDistance.multiplyScalar( dt );
 this.position.add( moveDistance );

 //Keep the dot on the screen
 this.position.x = (this.position.x + this.scene.canvas.width) % this.scene.canvas.width;
 this.position.y = (this.position.y + this.scene.canvas.height) % this.scene.canvas.height;
 }

}(), //Note that this function is immediately executed and returns a different function

This function is a little odd in its structure, but handy for visualizations. It's really expensive to allocate memory in a function. The moveDistance variable is set once, and re-used any time the function is called.

This vector is only used to help calculate the new position, but not used outside of the function. This is the first vector math that's being used. Right now the direction vector is being multiplied against the change in time, then added to the position. At the end there is a little modulo action going on to keep the dot on the screen.

draw : function(dt) { 
 
 //Get a short variable name for convenience
 var ctx = this.scene.context;
 
 ctx.beginPath();
 ctx.fillStyle = this.color;
 ctx.fillRect(this.position.x, this.position.y, this.size, this.size);
}

Finally the easy stuff. Get a copy of the context from the scene object, and then draw a rectangle (or whatever you want). Rectangles are probably the quickest thing you can draw on the screen.

At this point I add a new Dot by calling this.dot = new Dot(x, y, this) in the main scene constructor, and then in the scene update method I add a this.dot.update( dt ) and there is a dot zooming around the screen. (Check out the source code for the full code in context.)

That's nice that there is a little bit more internal structure to the code, but it doesn't really do anything that much more interesting. That's where a loop comes in. In the scene object we'll create a new DotManager object. It's handy to collect this functionality in a separate object, as it's easier and cleaner to reason with as more and more complexity gets added to the simulation.

var DotManager = function( numberOfDots, scene ) {
 
 this.dots = [];
 this.numberOfDots = numberOfDots;
 this.scene = scene;
 
 for(var i=0; i < numberOfDots; i++) {
 this.dots.push( new Dot(
 Math.random() * this.canvas.width,
 Math.random() * this.canvas.height,
 this.scene
 ));
 }
 
};

DotManager.prototype = {
 update : function( dt ) {
 for(var i=0; i < this.numberOfDots; i++) {
 this.dots[i].update( dt );
 }
 }
};

Now in the scene, rather than creating and updating a Dot, we create and update the DotManager. We'll create 5000 dots to get started.

function Scene() {
 ...
 this.dotManager = new DotManager(5000, this); 
 ...
};
 
Scene.prototype = {
 ...
 update : function( dt ) {
 this.dotManager.update( dt );
 }
 ...
};

Ok, this is getting better than just a single dot. Now is a good time to start adding more to the update method to the individual Dot. Anything changed on the object's code will affect every single dot on the screen. This is where things start to feel magical. Remember the sin function from above? How about adding a little wave action to the movement? We'll create a wavePosition variable to the Dot object. In the final draw we'll add this variable to the Y position.

updateWave : function( dt, time ) {
 this.wavePosition = Math.sin( this.scene.currTime / 500 + this.position.x / this.scene.canvas.width * 4 ) * 20;
}

It's a little confusing in one line, so here it is broken down like the sin function from earlier.

var a = 1 / 500, //Make the oscillation happen a lot slower
x = this.scene.currTime, //Move along the graph a little bit each time draw() is called
b = this.position.x / this.scene.canvas.width * 4, //No need to adjust the graph up or down
c = 20, //Make the oscillation as wide as a little less than half the canvas
d = 0; //Tweak the position of the rectangle to be centered
xMovement = Math.sin( a * x + b ) * c + d;

Getting groovy…

One more little tweak. Monochrome is a little drab, so let's add some color.

var hue = this.position.x / this.scene.canvas.width * 360;
this.color = Utils.hslToFillStyle(hue, 50, 50, 0.5);

For each new Dot created, take its initial position, and set its hue to where it is along the width of the canvas. The Utils.hslToFillStyle function is a little helper function I added to transform some input variables into the correctly formatted fillStyle string. Already things are looking more exciting. The dots will eventually merge together and lose their rainbow effect after they have time to disperse. Again, this is an example of driving visuals with a little bit of math or variable inputs. I really enjoy doing colors with the HSL color model with generative art rather than RGB due to the ease of use. RGB is a little abstract.

User interaction using a mouse

There hasn't been any real user interaction up to this point.

var Mouse = function( scene ) {
 this.scene = scene;
 this.position = new THREE.Vector2(-10000, -10000);
 $(window).mousemove( this.onMouseMove.bind(this) );
};

Mouse.prototype = {
 onMouseMove : function(e) {
 if(typeof(e.pageX) == "number") {
 this.position.x = e.pageX;
 this.position.y = e.pageY;
 } else {
 this.position.x = -100000;
 this.position.y = -100000;
 }
 }
};

This simple object encapsulates the logic of the mouse updates from the rest of the scene. It only updates the position vector on a mouse move. The rest of the objects can then sample from the mouse's position vector if they are passed a reference to the object. One caveat that I'm ignoring here is if the canvas's width isn't one to one with the pixel dimensions of the DOM i.e. a responsively resized canvas or a higher pixel density (retina) canvas or if the canvas isn't located at the top left. The coordinates of the mouse will need to be adjusted accordingly.

var Scene = function() {
 ...
 this.mouse = new Mouse( this );
 ...
};

The only thing left for the mouse was to create the mouse object inside the scene. Now that we have a mouse, let's attract the dots to it.

function Dot( x, y, scene ) {
 ...
 this.attractSpeed = 1000 * Math.random() + 500;
 this.attractDistance = (150 * Math.random()) + 180;
 ...
}

I added some scalar values to the dot so that each one behaves a little bit differently in the simulation to give it a bit of realism. Play around with these values to get a different feel. Now on to the attract mouse method. It's a little long with the comments.

attractMouse : function() {

 //Again, create some private variables for this method
 var vectorToMouse = new THREE.Vector2(),
 vectorToMove = new THREE.Vector2();

 //This is the actual public method
 return function(dt) {
 var distanceToMouse, distanceToMove;

 //Get a vector that represents the x and y distance from the dot to the mouse
 //Check out the three.js documentation for more information on how these vectors work
 vectorToMouse
 .copy( this.scene.mouse.position )
 .sub( this.position );

 //Get the distance to the mouse from the vector
 distanceToMouse = vectorToMouse.length();

 //Use the individual scalar values for the dot to adjust the distance moved
 moveLength = dt * (this.attractDistance - distanceToMouse) / this.attractSpeed;

 //Only move the dot if it's being attracted
 if( moveLength > 0 ) {

 //Resize the vector to the mouse to the desired move length
 vectorToMove
 .copy( vectorToMouse )
 .divideScalar( distanceToMouse )
 .multiplyScalar( moveLength );

 //Go ahead and add it to the current position now, rather than in the draw call
 this.position.add(
 vectorToMove
 );
 }
 };
}()

This method could be a bit confusing if you're not up to date on your vector math. Vectors can be very visual and can help if you draw some scribbles out on a coffee stained scrap of paper. In plain English, this function is getting the distance between the mouse and the dot. It's then moving the dot a little closer to the dot based on how close it already is to the dot and the amount of time that has elapsed. It does this by figuring out the distance to move (a normal scalar number), and then multiplying that by the normalized vector (a vector with length 1) of the dot pointing to the mouse. Ok, that last sentence wasn't necessarily plain English, but it's a start.

For me the bottom line of creating a programming poem and generative art is license to explore. I would recommend not to get too bogged down in the details and big projects. Instead iterate, experiment, succeed and occasionally fail. I had one piece that ended up looking like a piece rainbow poop. I had high hopes for my recursive circles, but I didn't let the results get me down in the dumps. Ultimately the code here was iteratively written as an exploration in a very object oriented style, as that's what I'm most comfortable with. Adapt these ideas to your own process, tools, and workflow. Poems aren't large integrated systems, so it's easy to sketch and experiment without the normal encumbrances of a big web project.

Treat the canvas like a pencil, find some cool ideas to play around with, and let your creations fly.

Greg Tatum

With a background in contemporary sculpture, aquarium exhibit design, marketing, animation, and web development, Greg is all over the place. Currently he is a code artist and web developer in Tulsa, Oklahoma. Check out his work at GregTatum.com and drop him a line on Twitter.

Read Next

3 Essential Design Trends, May 2024

Integrated navigation elements, interactive typography, and digital overprints are three website design trends making…

20 Best New Websites, April 2024

Welcome to our sites of the month for April. With some websites, the details make all the difference, while in others,…

Exciting New Tools for Designers, April 2024

Welcome to our April tools collection. There are no practical jokes here, just practical gadgets, services, and apps to…

14 Top UX Tools for Designers in 2024

User Experience (UX) is one of the most important fields of design, so it should come as no surprise that there are a…

What Negative Effects Does a Bad Website Design Have On My Business?

Consumer expectations for a responsive, immersive, and visually appealing website experience have never been higher. In…

10+ Best Resources & Tools for Web Designers (2024 update)

Is searching for the best web design tools to suit your needs akin to having a recurring bad dream? Does each…

3 Essential Design Trends, April 2024

Ready to jump into some amazing new design ideas for Spring? Our roundup has everything from UX to color trends…

How to Plan Your First Successful Website

Planning a new website can be exciting and — if you’re anything like me — a little daunting. Whether you’re an…

15 Best New Fonts, March 2024

Welcome to March’s edition of our roundup of the best new fonts for designers. This month’s compilation includes…

LimeWire Developer APIs Herald a New Era of AI Integration

Generative AI is a fascinating technology. Far from the design killer some people feared, it is an empowering and…

20 Best New Websites, March 2024

Welcome to our pick of sites for March. This month’s collection tends towards the simple and clean, which goes to show…

Exciting New Tools for Designers, March 2024

The fast-paced world of design never stops turning, and staying ahead of the curve is essential for creatives. As…