HTML5 Canvas and the Web Audio API
Posted 7th June, 2015 in Animation, Audio Visualization, Design, Programming, Web Development
This is an older project I always intended to document when time allowed. A couple of years back I took an introductory Computer Science and Programming course provided by MIT via edX. Introducing a notion of computation and algorithm design with the Python language, the course was something of a challenge for me at that time but with a little hard work and perseverance I made my way through it with the equivalent of an A grade. Keen to apply some of what I’d learnt in a more creative context, I began to explore how I might create a simple abstract animation capable of responding to an accompanying audio track in real-time.
Choosing Javascript meant I could create something browser-based and take the opportunity to familiarise myself with both the Web Audio API and HTML5 canvas element. Disappointingly both Firefox and Safari still have somewhat limited support for the Web Audio API even now, so at the time of writing this support for the resulting demo remains limited to the desktop version of Google’s Chrome browser. The links below launch the real-time demo and an accompanying page where the complete source code can be viewed with Google prettify. There’s an additional video clip available here for readers on mobile devices, or anyone simply not wanting to download and install Chrome to their machine for any reason. Vimeo’s compression has resulted in a rather blurred video though so it’s really no substitute for running the demo itself.
I began by writing a drawPolygon() function to automate the drawing of individual paths. creating the outline of a single shape. Calling this function over and over simply draws multiple shape instances one on top of another. In order to create discrete frames of animation, the entire canvas can be repainted with it’s original background colour at regular intervals to effectively ‘wipe clean’ a previous frame. By exposing various parameters used in the drawing of each frame, these variables can be altered over time to animate the resulting image. Both the Width and Shade (color) of shape paths can be set directly via properties of the current Canvas context. In addition to these, the logic in drawPolygon() also takes values for both the Size and NumberOfSides in a given shape instance. Using trigonometric functions, it then ascertains the required length of each path along with the angles between them.
function drawPolygon (Width, NumberOfSides, Size, Shade) { var Index; $.Visualiser.Context.beginPath(); $.Visualiser.Context.moveTo($.Visualiser.XCenter + Size * Math.cos(0), $.Visualiser.YCenter + Size * Math.sin(0)); for (Index = 1; Index <= NumberOfSides; Index += 1) { $.Visualiser.Context.lineTo($.Visualiser.XCenter + Size * Math.cos(Index * 2 * Math.PI / NumberOfSides), $.Visualiser.YCenter + Size * Math.sin(Index * 2 * Math.PI / NumberOfSides)); } $.Visualiser.Context.lineWidth = Width; $.Visualiser.Context.strokeStyle = Shade; $.Visualiser.Context.closePath(); $.Visualiser.Context.stroke(); }
The Web Audio API includes a handy Audio Analyser node, providing real-time access to amplitude data from an accompanying audio track. Having setup an animation loop at a rate of 24fps, to both (re)paint the black background and draw a single shape to the canvas, that same loop could also be used to read the current amplitude value of the Analyser node then scale and map it to the Width parameter of drawPolygon(). At this point I had the outline of a single animated shape instance, blinking in time to the accompanying music. Basic but very much progressing in the right direction.
In an attempt to develop an element of composition, I tried drawing multiple instances of the same shape on each animated frame, distributed initially in a tiled formation. This made for an interesting grid like pattern but in the process I’d lost the hypnotic effect of a single focal point. I returned to a fixed centre position and this time drew multiple shape instances of steadily increasing sizes on each frame, reaching out to the perimeter of the canvas. Within seconds of watching the concentric outlines blinking in unison, the next step became clear.
Until now I’d been mapping the same audio amplitude value to the path width of every shape instance I drew on a single frame. That amplitude value was updated with new real-time data once per frame, prior to the new frame being drawn. My composition involved a set number of concentric shape instances (enough to reach the edge of my canvas), so rather than updating a single amplitude value for use in each frame, why not keep an array of the same set length, to hold a history of recent values? Now at the start of each frame, I could push the latest amplitude data onto one end of my array and trim the oldest value from the other, creating a kind of conveyor belt or real-time values. The path width of each concentric outline could then be mapped in sequence to it’s associated value in the array so that the inner most shape instance blinked in time with the latest data from the Audio Analyser node while the data used for the next instance was delayed by a single frame, and so on. The resulting visual effect was a dynamic ripple, emitted from the centre of the canvas and travelling out towards it’s edges.
By this stage I’d made use of both the path Width and overall Size parameters exposed by my drawPolygon function but as you’ve likely noticed, the animated image example above also features an element of variation in colour. The idea was to create a second rolling data array with the same sequential relationship to shapes instances described above. This time however, rather than using real-time amplitude data, I would update the array with the output of synchronised Sine functions to modulate the four channels of an RGBA value instead.
For the uninitiated, here’s a (very brief) primer on the additive colour system in use here… RGBA stands for Red, Green, Blue and Alpha, which is three composite colour channels and an additional alpha channel for transparency. While the alpha channel takes a value between 0.0 – 1.0 (0.0 being transparent and 1.0 being fully opaque), each of the three colour channels are set in the range of 0 – 255. With this model we represent an opaque black as rgba(0,0,0,1.0) which is zero in each of the colour channels but the maximum value for the alpha channel. An opaque white is found at rgba(255,255,255,1.0), the maximum value for each colour and the alpha. A mid-point grey can found between the previous two at rgba(128,128,128,0.5), the alpha in this example is set to 0.5 meaning that the resulting tone will have a transparency of 50%. Colours are represented by shifting the values of the colour channels independently of each other. An opaque red would be found at rgba(255,0,0,1.0) with green at rgba(0,255,0,1.0) and blue at rgba(0,0,255,1.0). The result of mixing values across the three colour channels should correlate roughly with the mixing equivalent intensities of red, green and blue light in the real world. A more detailed description can be found here.
While modulating three colour channels (in unison) with a single sine wave results in a greyscale cycle from black to white, modulation of a single channel (in isolation) results in a shifting hue but within a rather limited range of the entire colour spectrum. To achieve the loop of colour hues seen in the final demo, the three colour channels simultaneously receive modulation from their own discrete Sine waves, whose phases are adequately shifted in respect to each other. After some further experimentation I settled on a constant alpha value of 0.5, allowing the transparency effect to further deepen the composition. The drawing of every shape was then proceeded by a duplicate instance in white, with a path width value only 25% of the original and an alpha of 0.6, thus creating a layered effect where sufficient sized outlines are highlighted with a bright inner stroke. The specific implementation of this in my animation cycle can be found in the accompanying source code but for anyone wanting to get a quick start on some experiments of their own, a simplified example of the colour cycle logic described here is available below.
// Initialize Variables var Index, ColorFreq, ColorPhase1, ColorPhase2, ColorPhase3, ColorCenter, ColorWidth, Color, ColorCounter; // Cycle frequency ColorFreq = 0.036; // Alternate phase for each Sine wave ColorPhase1 = 2; ColorPhase2 = 4; ColorPhase3 = 0; // Center point (baseline) and modulation depth for Sine wave ColorCenter = 128; ColorWidth = 127; // Initialize color to white and create counter for cycling Color = "rgba(255,255,255,1)"; ColorCounter = 0; // Function to generate each new step in the color cycle function getColor (frequency, phase1, phase2, phase3, center, width) { var r = Math.sin(frequency*ColorCounter + phase1) * width + center; var g = Math.sin(frequency*ColorCounter + phase2) * width + center; var b = Math.sin(frequency*ColorCounter + phase3) * width + center; ColorCounter++; return "rgba("+Math.round(r)+","+Math.round(g)+","+Math.round(b)+",0.5)"; } // Then on each animation frame... Color = getColor(ColorFreq, ColorPhase1, ColorPhase2, ColorPhase3, ColorCenter, ColorWidth);
I’d originally exposed the NumberOfSides variable in drawPolygon() to explore the use of different shapes but found that modulated changes in this value somewhat compromised the overall composition, making it a less than ideal candidate for automation. However, further experimentation did lead to a nice effect whereby the drawing of each shape instance described so far was followed by a complimentary instance with an alternate number of sides, essentially doubling the entire composition. Combined with the previously mentioned transparency this makes for some much more interesting geometry in general.
Lastly, I also wanted to provide an element of interaction to the end user by allowing them to tweak various aspects of the animation as it played. The colours were set to cycle by default so I added the ability to pause on a particular set of tones with an accompanying ‘Hue’ button. I next created an independent button set for each of the two shape layers, enabling their visibility to be toggled on/off and their number of sides to be modified. All along I’d been drawing concentric shape instances at equal intervals in scale so I went ahead and added a ‘Zoom’ button to enable the option of exponentially increasing intervals instead (as illustrated in the initial purple image at the top of this post). The final button activates a ‘Hyper’ mode where the amplitude value mapped to the path width of each concentric shape, is then multiplied by the inverse of it’s sequential position in the array, resulting in an emphasising effect that diminishes from the middle out. If you’re curious, you can find the simple logic for that last part inside of animateFrame() in the source code.
View other posts tagged with: HTML5 Canvas, Javascript, Web Audio API.