seismograph.js

#

Copyright 2011, jsr@malamute.dk, released under the WTFPL

Introduction

This script produces a – scientifically broken – seismograph, using data from one of three events:

By the time of writing (March 2011), the two first are supported by Chrome and Mobile Safari, and the latter in Firefox.

How to use

Create any sort of HTML element – eg a div – and make sure it has width, height, foreground and background color defined somewhere (inline or in a style sheet). Now, add a script element within the container, loading http://isthisanearthquake.com/seismograph.js.

That's it! Have a look at the source code in this sample:

Sorry, but your device/browser doesn't support this.

If you add additional content within the container – besides the script element – this will be shown as a fall-back when no Javascript available.

Annotated source code

#

Wrap everything in a function, to avoid polluting the global namespace.

(function () {
#

Setup phase

#

Use a dirty old trick to dump an anchor where the script is called.

    document.write('<a id="itae-anchor" href="http://isthisanearthquake.com"></a>');
#

Grab a reference to the DOM, and create additional elements.

    var anchor = document.getElementById('itae-anchor'),
        container = anchor.parentNode,
        viewport = document.createElement('div');
#

Make sure our elements are alone in the container. This will remove any default content.

    while (container.hasChildNodes()) {
        container.removeChild(container.firstChild);
    }
    container.appendChild(anchor);
    container.appendChild(viewport);
#

Put the anchor and the seismograph viewport on top of each other. Let's use getComputedStyle() to figure out how this should look.

    var style = window.getComputedStyle(container, null);
    anchor.style.position = 'absolute';
    anchor.style.width = viewport.style.width = style.width;
    anchor.style.height = viewport.style.height = style.height;
    anchor.style.zIndex = 1;
    anchor.style.zIndex = 0;
#

We are going to be painting on a canvas wider than our container. Tell the viewport to hide overflow, so that we can auto-scroll it.

    viewport.style.overflow = 'hidden';
#

The following might look scary, but it actually isn't that bad. It's all about defining some constants for later use.

    var width = viewport.clientWidth,
        height = viewport.clientHeight,
        pan = Math.round(width / 200),
        totalWidth = pan * 1000,
        zero = height / 2,
        refreshRate = 100,
        threshold = 50;
#

Ok – we will need a few variables as well.

    var x, y = zero,
        deflection = 0,
        axesPrev = [],
        canvas, ctx;
#

Getting a fresh canvas

    var freshCanvas = function () {
#

Create a new canvas, and make it pretty wide.

        var newCanvas = document.createElement('canvas');
        newCanvas.width = totalWidth;
        newCanvas.height = height;
#

Add it to our viewport, and scroll this all the way to the left. As we paint on the canvas, we will scroll the viewport rigtwards.

        viewport.appendChild(newCanvas);
        viewport.scrollLeft = 0;
#

Prepare to paint on the canvas, using a 2D context.

        ctx = newCanvas.getContext('2d');
        ctx.strokeStyle = style.color;
#

If an old canvas exists, copy the last screenfull of pixels over. This trick gives the illusion of an infinate scrolling canvas.

        if (canvas) {
            ctx.drawImage(canvas, width - x, 0);
            viewport.removeChild(canvas);
        }

        canvas = newCanvas;
        x = width;
    };
#

Scrolling the canvas, one pixel at the time

    var scrollCanvas = function() {
        viewport.scrollLeft++;
#

As we run out of space, get a fresh canvas.

        if (viewport.scrollLeft >= totalWidth - width) {
            freshCanvas();
        }
    };
#

Drawing the line

    var drawTheLine = function () {
#

Move the pencil to where we left off, and put it to the canvas.

        ctx.beginPath();
        ctx.moveTo(x, y);
#

Compute and move pencil to next location, stroking a line.

        x += pan;
        y = zero + (height * deflection / 25);
        deflection = Math.random() * .2 - .1;
        ctx.lineTo(x, y);
        ctx.stroke();
#

Each invocation of scollCanvas moves by one pixel. Schedule enough invocations to animate until next refresh.

        for (var i = 0; i < pan; i++) {
            setTimeout(scrollCanvas, i * refreshRate / pan);
        }
    };
#

Feeling the ground shake

    var tilt = function (axes) {
#

We need to compare the current state with the previous state. To do this, we need the axesPrev variable to be set.

        if (axesPrev) {
#

Depending on the type of event, any number of axis may exists. Find the one which has changed the most since last time.

            for (var i = 0; i < axes.length; i++) {
                var delta = axes[i] - axesPrev[i];
                if (Math.abs(delta) > Math.abs(deflection)) {
                    deflection = delta;
                }
            }
        }

        axesPrev = axes;
    };
#

Putting everything together

#

Grab the first canvas, and strike a flat line across it.

    freshCanvas();
    ctx.moveTo(0, y);
    ctx.lineTo(x, y);
    ctx.stroke();
#

Hook up to the best available event. Use magic constants to keep results somewhat similar.

    if (window.DeviceOrientationEvent) {
        window.addEventListener('deviceorientation', function(event) {
            tilt([event.beta, event.gamma]);
        }, true);
    } else if (window.DeviceMotionEvent) {
        window.addEventListener('devicemotion', function(event) {
            tilt([event.acceleration.x * 2, event.acceleration.y * 2]);
        }, true);
    } else {
        window.addEventListener('MozOrientation', function(orientation) {
            tilt([orientation.x * 50, orientation.y * 50]);
        }, true);
    }
#

Hook up a timer to draw new data on the seismograph.

    setInterval(drawTheLine, refreshRate);
})();