Abusing the Canvas – Lesson 2: Marquee

Lesson 2: Keep on moving, text

This article is part of a series. Part 1 and table of contents.

Here’s a surprising fact, for some of you this text will scroll.

We have to assume that the marquee’s days are numbered, though. The marquee tag started its life in Internet Explorer, back in the bad old days. If you search for ‘marquee tag’ on Google, the results are in a marquee tag. But the Wikipedia page describing it basically just spends the entire time saying “Don’t”

If you remember the GeoCities days, almost every page (every page that was worth reading, anyways) had at least one element where someone did <blink><marquee>This text is important!</marquee></blink> so that it would stand out regardless of which browser you use.

By the end of this lesson, we’ll make this monstrosity a reality. The blinking marquee.

Alright, if you remember last time, when we made blinking text, we just used setInterval to keep updating the canvas. We’re going to do something like that again.

First thing’s first, let’s use the same HTML as last time (more or less):

<html>
<head>
</head>
<body>
<button onclick="startScrolling()">Start Scrolling!</button>
<canvas id="canvas1" width="500px" height="100px" />
</body>
</html>

I made the height smaller, since the 500px height was probably too much. You with me so far? Easy, right? Good. Let’s move on to making a simple class called Scroller that will handle the canvas and our scrolling text. For now, let’s just pass in a reference to the canvas, the text we want to scroll, and have our drawtext method. We’re going to make that startScrolling() function as well. Here’s the code:

function startScrolling()
{
    var c = document.getElementById("canvas1");
    var scroller = new Scroller(c, "Welcome to my homepage! Sign my guestbook!");
    scroller.drawText();
}

function Scroller(canvas, scrollingText)
{
    this.canvas = canvas;
    this.text = scrollingText;
    this.yloc = 10;
    this.xloc = 0;
    this.drawText = function()
    {
        var context = this.canvas.getContext("2d");
        context.fillText(this.text, this.xloc, this.yloc);
    };
}

OK, let’s go over the code, though if you did part one of this series, you everything will be familiar. The startScrolling() function is what we’re calling when we press the button. From there we create an instance of our Scroller and manually call the drawText() method.

The Scroller takes a reference to the canvas and some text, saves it interally, and has default x and y values. The y value is set to 10 so we can see the text, since unlike regular HTML the text doesn’t draw from a box starting on the upper left, it starts drawing from the text’s baseline. So we have to adjust for that. Here’s a fiddle with the working code so far: http://jsfiddle.net/kadono/QLSGJ/

Sweet, we drew some text, let’s keep going and make it move. The canvas has a few different ways we can do this, but we’re going to use the window’s requestAnimFrame method. Actually, before we move on, let’s talk about requestAnimFrame a bit.

requestAnimationFrame

I’m going to pass on the sarcastic tone for this part of the guide. When we made our dumb blinking text, it was kind of funny to make it use too many resources. However, we were only calling the method with setInterval every 500ms, and it took almost no time to run, so it wasn’t really that big of a deal. If we want to have nice, buttery animation we’re going to want to run way more often than twice per second, though. So we should think a little more about the resources we’re using.

requestAnimationFrame is a relatively new method added to the DOM. Until very recently, it wasn’t available on any mobile platforms, though now it’s on the lastest versions of the iOS and Android default browsers. Still, before we go any further, if you’re going to use the requestAnimationFrame method (which you should), then you use use Paul Irish’s gist, located here: https://gist.github.com/paulirish/1579671 Just throw that code in at the top of your Javascript, and now you have requestAnimationFrame, even on older browsers. There are other shims that add the method or extend on it, but we’re not going to cover those here.

Ok, so why do we want to use requestAnimationFrame? Well, we could just use setInterval(someFunction, 1000/60) to get 60fps and it’ll look good, right? That works, but it’s like trying to pull a tooth with a sledgehammer. Too much tool, too much collateral damage. We need to use some finesse.

When we use requestAnimationFrame, a few things happen.

  1. The code will run immediately before the page redraws, not accidentally during the redraw or in some other non-ideal situation.
  2. The code won’t run if the tab isn’t visible, saving battery on laptops and things. It’s not like we need it to run then anyways.
  3. We get something much closer to an update/draw loop we can use.

Overkill for our project? Probably, but it’s good to use good coding practices, even if we’re doing awful things in the process. Let’s talk about this method a little more generally before we move into our actual implementation.

The recommended usage of requestAnimationFrame moves our animation code from this:

setInterval(someFunction, 1000/60);

To this:

(function mainloop()
{
    requestAnimationFrame(mainloop);
    someFunction();
})()

So you can see, that once you call mainloop() once, it will continue to run recursively forever. So, requestAnimFrame takes a callback function, then calls that function at the appropriate time. Using this type of layout, we have an infinite loop, except it only continues when requestAnimationFrame decides it’s time to call the callback function. We get another benefit as well. requestAnimFunction actually returns a reference we can use to cancel the animation with another method: cancelAnimationFrame

So we can have the following:

var animId;

function mainloop()
{
    animId = requestAnimationFrame(mainloop);
    someFunction();
};
function stopAnimating()
{
    cancelAnimationFrame(animId);
};

So you can see what’s going on here. When we call cancelAnimationFrame(animId) it tells the system not to do the callback we previously asked it to do. That breaks the recursion in our function and stops the animation.

Back to the Marquee

Now that we have some idea how we can do smooth animation without killing the performance, let’s start working on this, shall we?

Last time we had to do some trickery to change the relative value of “this” when a function was called as the setInterval callback. We’re going to have to do the same thing with requestAnimationFrame. Since I like to add one thing at a time, we can start by getting the requestAnimationFrame working. We’re going to add our new callback method called doAnimation, which for now will just update a frame counter and draw the text. We won’t be keeping the frame counter around for too long, it’s just a way to confirm our animation is working. Here’s the code:

function startScrolling()
{
    var c = document.getElementById("canvas1");
    var scroller = new Scroller(c, "Welcome to my homepage! Sign my guestbook! Frame count:");
    scroller.doAnimation();
}

function Scroller(canvas, scrollingText)
{
    this.canvas = canvas;
    this.text = scrollingText;
    this.yloc = 10;
    this.xloc = 0;
    var self = this;
    this.animId;
    this.counter = 0;
    this.doAnimation = function()
    {
        this.animId = requestAnimationFrame(function() { self.doAnimation(); });
        this.counter++;
        this.drawText();
    }
    this.drawText = function()
    {
        var context = this.canvas.getContext("2d");
        context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        context.fillText(this.text + this.counter, this.xloc, this.yloc);
    };
}

You can see we’re keeping around a reference so we can stop the animation later. Pretty simple change, huh? You can see the code working here: http://jsfiddle.net/kadono/Lh8jj/ and if you start the frame counter and flip over to another tab and come back, you’ll see that it hasn’t been increasing while it was invisible. Nice, huh? And we didn’t even have to do anything except use requestAnimationFrame to get that nice little piece of functionality.

Also, we added a clearRect call to clear out the canvas before we draw the text, otherwise we’ll just be stacking the text on top of itself over and over forever, and that’s no good. We’re going to need to do that when we animate the text anyways, so we might as well get it in now.

So what should we be doing to move the text? Let’s consider how a marquee tag works:

  1. Text should move from right to left
  2. The entire text should move out of sight before it starts scrolling in from the right again
  3. Since there isn’t a standard, we can eyeball the rate it moves.
  4. There are a number of behaviors and modifiers we can emulate if we want

Out of extreme laziness, we’re going to go with the default scroll rate being 1 pixel per frame, which ends up being 60 pixels per second if we’re running at 60fps. Regardless of the rate, it’ll look smooth since it only moves one pixel at a time. So we can do that just by decrementing the xloc every frame. But what about when the text has move entirely off the frame? That’s simple, we can measure the text using the canvas method measureText and move the text off the right side of the canvas after the -xloc is bigger than the measureText length.

Just look at the code, it’ll make sense:

function startScrolling()
{
    var c = document.getElementById("canvas1");
    var scroller = new Scroller(c, "Welcome to my homepage! Sign my guestbook!");
    scroller.doAnimation();
}

function Scroller(canvas, scrollingText)
{
    this.canvas = canvas;
    this.text = scrollingText;
    this.textWidth = 0;
    this.yloc = 10;
    this.xloc = this.canvas.width;
    this.font = "12px Arial";
    var self = this;
    this.animId;
    this.doAnimation = function()
    {
        this.animId = requestAnimationFrame(function() { self.doAnimation(); });
        if(this.textWidth == 0)
        {
            var context = this.canvas.getContext("2d");
            context.font = this.font;
            this.textWidth = context.measureText(this.text).width;
        }
        if(this.textWidth + this.xloc < 0)
        {
            this.xloc = this.canvas.width;
        }
        else
        {
            this.xloc--;
        }
        this.drawText();
    }
    this.drawText = function()
    {
        var context = this.canvas.getContext("2d");
        context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        context.font = this.font;
        context.fillText(this.text, this.xloc, this.yloc);
    };
}

OK, so major changes:

  • We’re setting a font, this is to make sure that measureText() behaves predictably
  • We got rid of the frame counter
  • We’re starting the text off the right side of the canvas so it will scroll in
  • We’re measuring the text, but only if it hasn’t been measured before
  • If the text is entirely off the canvas, we reset it to the right side, otherwise we move it left.

Sweet, we have a basic, working marquee. Here’s the fiddle if you want to play with it: http://jsfiddle.net/kadono/N692p/

Now, we could add a bunch of options on here, like adding support for different speeds, colors and directions. So, let’s just do it. The only canvas-type code we’ll be adding here is we’re going to use the fillStyle. All we need to do is set the context’s fillStyle property to a hex value string to set the color. The rest of the code is just simple switches and math, so I won’t go into detail on that. I’ll just show you the code:

function startScrolling()
{
    var c = document.getElementById("canvas1");
    var scroller = new Scroller(c, "Welcome to my homepage! Sign my guestbook!", 1, "down", "#CC0000");
    scroller.doAnimation();
}

function Scroller(canvas, scrollingText, rate, direction, textColor)
{
    this.canvas = canvas;
    this.text = scrollingText;
    this.textWidth = 0;
    this.textHeight = 12;
    this.yloc = 10;
    this.xloc = this.canvas.width;
    this.rate = rate;
    this.direction = direction;
    this.textColor = textColor;
    this.font = "12px Arial";
    var self = this;
    this.animId;
    if(this.direction == "up")
    {
        this.xloc = 0;
        this.yloc = this.canvas.height;
    }
    else if(this.direction == "down")
    {
        this.xloc = 0;
        this.yloc = 0;
    }
    this.doAnimation = function()
    {
        this.animId = requestAnimationFrame(function() { self.doAnimation(); });
        if(this.textWidth == 0)
        {
            var context = this.canvas.getContext("2d");
            context.font = this.font;
            this.textWidth = context.measureText(this.text).width;
        }
        if(this.direction == "left")
        {
             if(this.textWidth + this.xloc < 0)
             {
                 this.xloc = this.canvas.width;
             }
             else
             {
                 this.xloc -= this.rate;
             }
         }
         else if(this.direction == "right")
         {
            if(this.xloc > this.canvas.width)
            {
                this.xloc = - this.textWidth;
            }
            else
            {
                this.xloc += this.rate;
            }
        }
        else if(this.direction == "up")
        {
             if(this.yloc < 0)
             {
                 this.yloc = this.canvas.height + this.textHeight;
             }
             else
             {
                 this.yloc -= this.rate;
             }
        }
        else if(this.direction == "down")
        {
             if(this.yloc > this.canvas.height + this.textHeight)
             {
                this.yloc = 0;
             }   
             else
             {
                this.yloc += this.rate;
             }
        }
        this.drawText();
    }
    this.drawText = function()
    {
        var context = this.canvas.getContext("2d");
        context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        context.font = this.font;
        context.fillStyle = this.textColor;
        context.fillText(this.text, this.xloc, this.yloc);
    };
}

And here’s the fiddle: http://jsfiddle.net/kadono/zpnru/ This is probably not the most efficient way of making this method, but hey, we’re already doing some pretty nasty anti-design patterns, so what’s some not-completely-efficient code, eh?

This is probably enough, right? Yeah, this is definitely enough. We have a working marquee element that has EVEN WORSE USABILITY than the actual marquee tag. We can do other things like add a “stop” function and put that in the mouseover, and other things, but I’ll leave that exercise to the reader.

The monstrosity

I promised this, so let’s get that blinking marquee text going. We’ll put a blinkrate into the previous method, and count frames to keep track of if we should be showing the text or not.

I’m really not proud of this, but in an odd way I am proud of this. The realization of that horrible nested tag from the 90’s:

function startScrolling()
{
    var c = document.getElementById("canvas1");
    var scroller = new Scroller(c, "Welcome to my homepage! Sign my guestbook!", 1, "left", "#CC0000", 30);
    scroller.doAnimation();
}

function Scroller(canvas, scrollingText, rate, direction, textColor, blinkRate)
{
    this.canvas = canvas;
    this.text = scrollingText;
    this.textWidth = 0;
    this.textHeight = 12;
    this.yloc = 10;
    this.xloc = this.canvas.width;
    this.rate = rate;
    this.direction = direction;
    this.textColor = textColor;
    this.blinkRate = blinkRate;
    this.showText = true;
    this.frameCount = 0;
    this.font = "12px Arial";
    var self = this;
    this.animId;
    if(this.direction == "up")
    {
        this.xloc = 0;
        this.yloc = this.canvas.height;
    }
    else if(this.direction == "down")
    {
        this.xloc = 0;
        this.yloc = 0;
    }
    this.doAnimation = function()
    {
        this.animId = requestAnimationFrame(function() { self.doAnimation(); });
        if(this.textWidth == 0)
        {
            var context = this.canvas.getContext("2d");
            context.font = this.font;
            this.textWidth = context.measureText(this.text).width;
        }
        if(this.blinkRate > 0)
        {
            this.frameCount++;
            if(this.frameCount > this.blinkRate)
            {
                this.showText = !this.showText;
                this.frameCount = 0;
            }
        }
        if(this.direction == "left")
        {
             if(this.textWidth + this.xloc < 0)
             {
                 this.xloc = this.canvas.width;
             }
             else
             {
                 this.xloc -= this.rate;
             }
         }
         else if(this.direction == "right")
         {
            if(this.xloc > this.canvas.width)
            {
                this.xloc = - this.textWidth;
            }
            else
            {
                this.xloc += this.rate;
            }
        }
        else if(this.direction == "up")
        {
             if(this.yloc < 0)
             {
                 this.yloc = this.canvas.height + this.textHeight;
             }
             else
             {
                 this.yloc -= this.rate;
             }
        }
        else if(this.direction == "down")
        {
             if(this.yloc > this.canvas.height + this.textHeight)
             {
                this.yloc = 0;
             }   
             else
             {
                this.yloc += this.rate;
             }
        }
        this.drawText();
    }
    this.drawText = function()
    {
        var context = this.canvas.getContext("2d");
        context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        if(this.showText)
        {
            context.font = this.font;
            context.fillStyle = this.textColor;
            context.fillText(this.text, this.xloc, this.yloc);
        }
    };
}

And the fiddle: http://jsfiddle.net/kadono/4eVKb/

So there we have it, blinking scrolling text.

Future Developments?

Well, if you want, you could implement all of the options the marquee element has/had. You could add the stop() function, allow for more dynamic speeds, allow images in the marquee, and so on. I’m not going to do any of that in this tutorial, but you should be able to do most of those pretty easily from what we’ve done here.

If you wanted to go even farther, you could add diagonal scrolling, gradients instead of colors, or even moving/changing gradients. Amazing, don’t you think!

I hope you’ve learned more about the canvas today, and more about website usability!


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *