Monday, October 31, 2011

Adventures in HTML5: An HTML5 Canvas Ticker

For my third experiment in JavaScript, I created a ticker that you could use to scroll stock information, news, or pretty much any textual content that has a label and a value. It looks like this and animates by scrolling to the left in a loop,. You can see a running version here. All the source is right there in the page, and there's a full listing at the end of this post as well.
I considered using CSS animation for this, but as it is not supported sufficiently in the widestream browsers yet I decided to go with HTML5 canvas and JavaScript. Let's start by looking at the HTML. In the body, we define a canvas named "tape" within a div container named "ticker". The canvas should be set to twice the width it needs to be for the widest size you expect to run it in--that's because we do some repetition of content in order to smoothly scroll it continuously.
<div id="ticker" style="width:1920px">
    <canvas id="tape" width="4096" height="40">Your browser doesn't support canvas</canvas>
</div>
 
A small amount of JavaScript in the onload event starts the ticker running with data--an array of objects each with a Symbol and Value property.
<body onload="OnLoad();">

<div id="ticker" style="width:1920px">
    <canvas id="tape" width="4096" height="40">Your browser doesn't support canvas</canvas>
</div>

...

<script>
    function OnLoad() {
        var items = [];
        items.push({ Symbol: 'AAA', Value: '1' });
        items.push({ Symbol: 'BBB', Value: '22' });
        items.push({ Symbol: 'CCC', Value: '3.3' });
        items.push({ Symbol: 'DDD', Value: '44 1/4' });
        items.push({ Symbol: 'EEE', Value: '5,000' });
        items.push({ Symbol: 'FFF', Value: '-66' });
        items.push({ Symbol: 'GGG', Value: '700' });
        items.push({ Symbol: 'HHH', Value: '88' });
        items.push({ Symbol: 'III', Value: '9999' });
        items.push({ Symbol: 'JJJ', Value: '10' });
        Ticker("ticker", "tape", items);
    }
</script>

</body>
 
In the JavaScript, there is a Ticker function that starts off the ticker, a MoveTicker function that continues the animation based on a timer, and a TickerState object that maintains housekeeping information for the animation between calls to MoveTicker.
 
In Ticker, the first order of business is to write out the symbols and values in the data array that was passed in. This is done, as many times as needed, to fill the canvas for the target width. That allows scrolling that looks smooth, even if the data doesn't fully fit the ticker width, and the animation can be reset back to its starting point without any visual roughage. Ticker's final act is to start a timer to call MoveTicker. The interval affects the scolling speed: 10 is fast, 50 is medium, 100 is slow.
 
In MoveTicker, we use CSS to move the ticker left--specifically by decrementing the canvas left margin(which is negative throughout) and counter-adjusting the ticker tape rectangle with CSS clip. This creates the illusion of a ticker rectangle that does not appear to move but has content that does. Once a full set of the data has been slid off the left of the screen, it is reset back to the starting offset. MoveTicker then sets a timer again so it will be called over and over. The result is a gliding ticker tape.
    var TickerState = {
        canvas: undefined,
        ctx: undefined,
        tickerX: 0,
        dataWidth: 0,
        interval: 10 /* Fast: 10 Medium: 50 Slow: 100 */
    };


    function Ticker(divId, canvasId, items) {

        ticker = document.getElementById(divId);
        maxWidth = parseInt(ticker.style.width);

        TickerState.canvas = document.getElementById(canvasId);
        TickerState.ctx = TickerState.canvas.getContext('2d');

        TickerState.ctx.font = "20px sans-serif";
        TickerState.ctx.font = "20px Nova Flat, 20px sans-serif";
        TickerState.ctx.textAlign = "left";

        var x = 10;
        var y = 20;

        var firstTime = true;
        while (x < maxWidth * 4) {

            for (var i = 0; i < items.length; i++) {

                TickerState.ctx.fillStyle = "Blue";
                TickerState.ctx.fillText(items[i].Symbol, x, y);
                x += TickerState.ctx.measureText(items[i].Symbol).width;

                TickerState.ctx.fillStyle = "Green";
                TickerState.ctx.fillText(items[i].Value, x, y + 10);
                x += TickerState.ctx.measureText(items[i].Value).width;
                x += 10 + 6;
            }

            if (firstTime) {
                TickerState.dataWidth = x;
                firstTime = false;
            }

        }
        setTimeout(MoveTicker, TickerState.interval);
    }

    function MoveTicker() {

        TickerState.tickerX += 1;

        if (TickerState.tickerX >= TickerState.dataWidth) {
            TickerState.tickerX = 10;
        }

        TickerState.canvas.style.marginLeft = "-" + TickerState.tickerX.toString() + "px";
        TickerState.canvas.style.clip = "rect(0px " + (maxWidth + TickerState.tickerX).toString() + "px 40px -" + TickerState.tickerX.toString() + "px)";

        setTimeout(MoveTicker, TickerState.interval);
    }
 
Here's the HTML page in its entirety:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Ticker</title>

    <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Nova+Flat">


    <style>

    #ticker {
        font-family: 'Nova Flat', cursive;
        font-size: 20px;
    }

    #tape {
        font-family: 'Nova Flat', cursive;
        font-size: 20px;
        position: absolute; 
        clip: rect(0px 2048px 40px 0px); 
        vertical-align: top; 
        margin-left: 0px; 
        background-color: LightYellow;
    }
    </style>


<script>

    var TickerState = {
        canvas: undefined,
        ctx: undefined,
        tickerX: 0,
        dataWidth: 0,
        interval: 10 /* Fast: 10 Medium: 50 Slow: 100 */
    };


    function Ticker(divId, canvasId, items) {

        ticker = document.getElementById(divId);
        maxWidth = parseInt(ticker.style.width);

        TickerState.canvas = document.getElementById(canvasId);
        TickerState.ctx = TickerState.canvas.getContext('2d');

        TickerState.ctx.font = "20px sans-serif";
        TickerState.ctx.font = "20px Nova Flat, 20px sans-serif";
        TickerState.ctx.textAlign = "left";

        var x = 10;
        var y = 20;

        var firstTime = true;
        while (x < maxWidth * 4) {

            for (var i = 0; i < items.length; i++) {

                TickerState.ctx.fillStyle = "Blue";
                TickerState.ctx.fillText(items[i].Symbol, x, y);
                x += TickerState.ctx.measureText(items[i].Symbol).width;

                TickerState.ctx.fillStyle = "Green";
                TickerState.ctx.fillText(items[i].Value, x, y + 10);
                x += TickerState.ctx.measureText(items[i].Value).width;
                x += 10 + 6;
            }

            if (firstTime) {
                TickerState.dataWidth = x;
                firstTime = false;
            }

        }
        setTimeout(MoveTicker, TickerState.interval);
    }

    function MoveTicker() {

        TickerState.tickerX += 1;

        if (TickerState.tickerX >= TickerState.dataWidth) {
            TickerState.tickerX = 10;
        }

        TickerState.canvas.style.marginLeft = "-" + TickerState.tickerX.toString() + "px";
        TickerState.canvas.style.clip = "rect(0px " + (maxWidth + TickerState.tickerX).toString() + "px 40px -" + TickerState.tickerX.toString() + "px)";

        setTimeout(MoveTicker, TickerState.interval);
    }
</script>

</head>
<body onload="OnLoad();">

<div id="ticker" style="width:1920px">
    <canvas id="tape" width="4096" height="40">Your browser doesn't support canvas</canvas>
</div>

&nbsp;<br />
&nbsp;<br />
&nbsp;<br />
Hello

<script>

    var loaded = false;

    function OnLoad() {
        loaded = true;
        var items = [];
        items.push({ Symbol: 'AAA', Value: '1' });
        items.push({ Symbol: 'BBB', Value: '22' });
        items.push({ Symbol: 'CCC', Value: '3.3' });
        items.push({ Symbol: 'DDD', Value: '44 1/4' });
        items.push({ Symbol: 'EEE', Value: '5,000' });
        items.push({ Symbol: 'FFF', Value: '-66' });
        items.push({ Symbol: 'GGG', Value: '700' });
        items.push({ Symbol: 'HHH', Value: '88' });
        items.push({ Symbol: 'III', Value: '9999' });
        items.push({ Symbol: 'JJJ', Value: '10' });
        Ticker("ticker", "tape", items);
    }
</script>

</body>
</html>
 

11 comments:

Olivier said...

Seems great, though, you may update your live demo link: I doubt I can reach your localhost:50005 from my home ;)

Erebus Bat said...

David,

Your link to the running version points to your localhost so it won't load on the web :(

Looks good though once you download it.

David Pallmann said...

Whoops, sorry about the bad link to the onlinet icker demo (now fixed).

Tomahawk1955 said...

It Flickers, any way to make it sliding smoother ??
Thanks

David Pallmann said...

Tomahawk1955, leveraging CSS transitions and animation would likely be the way to make this really smooth. Perhaps I'll do an update using that method down the road once I'm a little more savvy in that area.

Tomahawk1955 said...

Thanks Sir :)

Anonymous said...

Any possibility of eliminating the horizontal scroll bar it enables at the bottom of the screen?

David Pallmann said...

Anonymous, I'm looking to eliminate that horizontal scroll bar haven't hit upon the answer yet.

Anonymous said...

Hello,

thank you for this code, really saves me lot of time.
But it is possible to display the ticker only in an area of the screen (the area defined by the div ticker)?
Lets say in the upper right quarter (top:45% left:50%).
The canvas will move straight through the div 'ticker' and the text is shown on the left side.

David Pallmann said...

Yes, I don't like the horizontal scroll bar either. I think I've learned enough over the last few months to re-do this ticker letting CSS do a lot more of the work. When I have an updated version, I'll be sure to post it.

Kiran said...

excellent. thank you.