WebGL Fake CRT Effect for HTML5 Games

published:
2012.08.17
topics:
games
javascript
webgl

I created a realtime fake CRT effect with screen bulge, vignette, and scanlines using a quick little hack. WebGL scrapes a 2D game's HTML5 canvas and applies effects. Never have your JavaScript games been more "indie" ... lulz. It looks like this — and I made the bulging subtle so that it wouldn't make players seasick.

Screenshot of fake CRT effect applied to game.

Play around with the game to get the best feel for it. Usually works, although I should warn you that Safari 6 managed to freeze my entire system for a minute at least a couple times... but not every time. You might be happier in Chrome. There's a bit of a flicker caused by a bug (although it kind of adds to the CRT effect!) I also applied the effect to this RPG game that doesn't have the same flicker issues. And here's a screen capture of the effect on YouTube.

When I discovered Evan Wallace's glfx.js lib, this is actually the first thing I thought of. It is a library meant to apply realtime WebGL effects to an image, but the <canvas> tag can actually be addressed as an image source... so, viola!

Here's the code I used to quickly hack together a working demo of the idea. Certainly not optimized, but usually runs fine on my hardware, especially in Chrome.

// Make sure you've included the glfx.js script in your code!

// Here I load a PNG with scanlines that I overwrite onto the 2D game's canvas.
// This file happens to be customized for the demo game, so to make this a
// general solution we'll need a generic scanline image or we'll generate them
// procedurally.
// Start loading the image right away, not after the onload event.
var lines = new Image();
lines.src = 'media/scanlines-vignette-4gl.png';

window.addEventListener('load', fakeCRT, false);

function fakeCRT() {
    var glcanvas, source, srcctx, texture, w, h, hw, hh, w75;
    
    // Try to create a WebGL canvas (will fail if WebGL isn't supported)
    try {
        glcanvas = fx.canvas();
    } catch (e) {return;}
    
    // Assumes the first canvas tag in the document is the 2D game, but
    // obviously we could supply a specific canvas element here.
    source = document.getElementsByTagName('canvas')[0];
    srcctx = source.getContext('2d');
    
    // This tells glfx what to use as a source image
    texture = glcanvas.texture(source);
    
    // Just setting up some details to tweak the bulgePinch effect
    w = source.width;
    h = source.height;
    hw = w / 2;
    hh = h / 2;
    w75 = w * 0.75;

    // Hide the source 2D canvas and put the WebGL Canvas in its place
    source.parentNode.insertBefore(glcanvas, source);
    source.style.display = 'none';
    glcanvas.className = source.className;
    glcanvas.id = source.id;
    source.id = 'old_' + source.id;
    
    // It is pretty silly to setup a separate animation timer loop here, but
    // this lets us avoid monkeying with the source game's code.
    // It would make way more sense to do the following directly in the source
    // game's draw function in terms of performance.
    setInterval(function () {
        // Give the source scanlines
        srcctx.drawImage(lines, 0, 0, w, h);
        
        // Load the latest source frame
        texture.loadContentsOf(source);
        
        // Apply WebGL magic
        glcanvas.draw(texture)
            .bulgePinch(hw, hh, w75, 0.12)
            .vignette(0.25, 0.74)
            .update();
    }, Math.floor(1000 / 40));
}

The other thing that would be interesting to try in terms of performance would be to render the source game via WebGL into the same WebGL canvas that the effects are being applied to. I'm curious if it could be as simple as using this WebGL implementation of the 2D Canvas API? That's something to explore.