HTML Running Texts based on Canvas
In the last post of our Digital Signage Newsletter workshop, we showed you how to create a CSS scrolling text with CSS3 keyframe animations that plays headlines from an RSS feed. In this article, you learn an interesting alternative based on the HTML5 element Canvas. Using Canvas scrolling text brings advantages and disadvantages.
What is an HTML5 canvas?
A canvas is a defined area in HTML5 in which you can draw with Javascript commands. Apple developed Canvas originally for their browser Safari. It was standardized as part of HTML5 sometime later. Canvas allows drawing circles, lines, polygons and other geometric forms. A reference map can be found at
Wikipedia.
This element is multi-purpose and can also play videos due to its image functions. These may, for example, include additional effects in real time.
All current browsers (April 2018) are able to handle the canvas element.
This is how it is used in the body area of HTML:
<canvas id="myTicker" width="500" height="80">No Canvas</canvas>
The text “No Canvas” is a so-called “Fallback”. Only browsers that can’t render a canvas, e.g. Internet Explorer 8 display this text. We apply 80 pixels for the height and 500 pixels for the width.
With the attribute id we give the element a unique identification to be able to address it correctly via Javascript.
How do we draw a Text in a Canvas?
For this purpose, we add the following lines between the body tags in the HTML source code:
<canvas id="myTicker" width="500" height="80" style="border:1px solid #ddd;">
No Canvas
</canvas>
<script>
var MyCanvas = document.getElementById("myTicker");
var ctx = MyCanvas.getContext("2d");
ctx.font = "30px Arial";
ctx.fillText("Lorem ipsum dolor sit amet, consetetur", 10, 50);
</script>
Since the browser processes its contents from top to bottom, it is important for this example to enter the Javascript commands after the canvas element. Alternatively, we can of course load the canvas using onload in the body tag. We additionally add a grey border with style=”border:1px solid #ddd;”. The command document.getElementById(“myTicker”); loads the existing element into the variable MyCanvas. Now Javascript needs a context to the canvas.
The Canvas Context
The programmer defines a context as an interface to an object. The context provides functions and data structures. which we can apply to draw on the canvas. MyCanvas.getContext(“2d”) clarifies, that we need only functionality for two-dimensional vector graphics here. Alternative contexts would be e.g. webgl/webgl2 for three-dimensional vector graphics or bitmaprenderer to use pixel-oriented graphics functions. Since we do not need this, we initialize the context variable ctx with 2d.
Now the canvas context is assigned a 30 pixel Arial font using ctx.font = “30px Arial”. Finally, the command fillText() outputs the text “Lorem ipsum dolor sit amet, consetetur”.
The following two parameters set the ticker text 10 pixels to the right of the border and 50 pixels from above. It starts from the beginning of the text at the bottom left. Since the font is 30 pixels high, a value of 50 means that the text is 20 pixels below the edge at the top.
You can click on canvas_animations_1.html to see the result.
Javascript Animation
We must now make the text “move”. The basic approach for canvas tickers is to change the position of the content at very short time intervals. To give the human brain the impression of a fluid movement, these intervals on a digital screen must not exceed a 30th of a second. We are then talking about the so-called frame rate, which in this case is 30fps (frames per second).
To avoid jerk effects during fast movements, e.g. in videos or games, even 60fps are required. We therefore need Javascript functionality that starts certain actions on a time-controlled basis. In the past, the functions setTimeout or setInterval were used for this purpose. This method of creating animations has become obsolete and suboptimal.
The alternative is requestAnimationFrame
All current browsers support an efficient function called requestAnimationFrame since 2013. This function informs the browser that there is a pending animation. The user selects the frame rate and uses internal routines specially designed for animations. The top priority is to animate as smoothly as possible. It is generally better if the browser sets the frame rate, as the optimal value can also vary during runtime depending on the number and type of animations.
With the old static functions, we will reach the limits quickly. Furthermore, the animation stops when the user changes the browser tab, while animations using setInterval() or stTimeout() simply continue to run in this case and consume resources without any sense.
The first Running Text
We extend example 1 to move the text. It should scroll infinitely often from right to left. I.e. as soon as the last letter at the left edge runs out, the ticker starts again from the right edge. The necessary code for canvas running texts looks like this:
<canvas id="myTicker" width="500" height="80" style="border:1px solid #d3d3d3;">
No Canvas
</canvas>
<script>
const move_pixel = 1;
var MyCanvas = document.getElementById("myTicker");
var ctx = MyCanvas.getContext("2d");
ctx.font = "30px Arial";
var x = 500;
var ticker_content = "Lorem ipsum dolor sit amet, consetetur";
var text_width = ctx.measureText(ticker_content).width;
ctx.fillText(ticker_content, x, 50);
window.requestAnimationFrame(moveTicker);
function moveTicker()
{
ctx.clearRect(0,0,x,80);
if (x > -text_width)
x = x - move_pixel;
else
x = 500;
ctx.fillText(ticker_content, x, 50);
window.requestAnimationFrame(moveTicker);
}
</script>
First we set a constant called pixel_move. This defines how many pixels the ticker text scrolls to the left for each frame. This constant controls the speed of the canvas running texts. With higher values, the text scrolls faster; with smaller values, e.g. 0.5 slower. A value of 1 would be fine for our purposes.
The next lines are identical to the first example. Since we “push” the text through the canvas, the x-position recalculates every time we run. So we set x = 500 as start value, because the right edge of the canvas is at 500 pixels. The content is also put into a variable because we will use it several times for simplification
Now the script needs to know the width of the text in 30px Arial to see when the text has run completely. Then, it restart at position 500px. This calculation is done by the ctx. measureText(ticker_content).width command and is written to the text_width variable. On a system with Linux and Chromium/Chrome, for example, this corresponds to approx. 519 pixels. With ctx.fillText(ticker_content, x, 50); set the beginning of the text to the right edge of the canvas as described above.
The next line window.requestAnimationFrame(moveTicker) calls the function moveTicker. Programmers characterize a function that is called as a parameter from another function as a so-called callback function. Now the “magic” happens in our callback moveTicker().
The Algorithm for Canvas running texts
At first ctx.clearRect(0,0,x,80) deletes all previous contents of the canvas context. The script then uses the if…else condition to evaluate the current x value and decide on the new text position. Either the text should move one pixel to the left or back to its initial state. As long as x is greater than the negative value of the text length (i.e. -519px), the text content moves further and further to the left. The variable x will eventually become smaller than -519, which means that the text is now completely out of the visible area. So its position must return to the starting value (x = 500).
To be able to playback the animation in an endless loop, the callback function finally calls itself again using requestAnimationFrame. Programmers call this a recursive function.
Click canvas_animations_2.html, to see the result. If you are sitting in front of a PC, you will probably notice that this ticker runs more smoothly than scrolling texts with CSS3-Animations .
Nutzen utzen wir die erlernte Technik und befüllen den Canvas-Kontext mit Inhalten aus einem RSS-Feed. Da der Algorithmus zum Abholen und Verarbeiten des RSS-Feeds bereits in dem Beitrag über CSS-Laufschriften ausführlich erörtert wurde, gehen wir nur auf die spezifischen Unterschiede ein.
The Body-Region
The static HTML part needs some modifications, because we add more Javascript and want to make it as clear as possible.
<body onload="handleTicker()">
<canvas id="myTicker" height="80">No Canvas</canvas>
</body>
Let's use the technique we learned and fill the Canvas context with content from an RSS feed. Since the algorithm for fetching and processing the RSS feed has already been discussed in detail in the CSS running texts post, we'll only cover the specific differences.
In the style sheet
<style>
#myTicker
{
position: absolute;
left: 0;
top:0;
}
</style>
the element takes an absolute position starting at the upper left corner. That is important because this concept requires absolute positioning due to the calculations. A relative positioning would also not place the canvas exactly on the left and upper sides. The parent element body hash by default a margin of 8px to its parent element html (browser window).
The complete Javascript
window.addEventListener('resize', resizeCanvas, false);
const _move_pixel = 1;
const _max_canvas_width = 16384;
var MyCanvas = {};
var ctx = {};
var x = 0;
var ticker_content = "";
var text_width = 0;
function initCanvas()
{
MyCanvas = document.getElementById("myTicker");
ctx = MyCanvas.getContext("2d");
}
function resizeCanvas()
{
MyCanvas.width = window.innerWidth;
ctx.font = "30px Arial";
text_width = ctx.measureText(ticker_content).width;
x = MyCanvas.width;
}
function isNewContentSizeValid(txt)
{
return (ctx.measureText(txt).width < _max_canvas_width);
}
function moveTicker()
{
ctx.clearRect(0,0, MyCanvas.width, 80);
if (x > -text_width)
x = x - _move_pixel;
else
x = MyCanvas.width;
ctx.fillText(ticker_content, x, 50);
window.requestAnimationFrame(moveTicker);
}
function displayTicker(ticker_text)
{
ticker_content = ticker_text;
resizeCanvas();
window.requestAnimationFrame(moveTicker);
}
function createTickerOutput(feed_obj)
{
var ticker_text = " +++ ";
var tmp_text = "";
for (var i = 0; i < feed_obj.query.count; i++)
{
tmp_text = feed_obj.query.results.item[i].title+ " +++ ";
if (isNewContentSizeValid(ticker_text + tmp_text))
ticker_text += tmp_text;
else
break;
}
return ticker_text;
}
function handleTicker(response)
{
var feed_obj = JSON.parse(response);
var ticker_text = createTickerOutput(feed_obj);
displayTicker(ticker_text, feed_obj.query.count);
}
function getRSS(url)
{
var request_url = 'https://smil-control.de/beispiele/fetch-rss.php?feed_url='+url;
var MyRequest = new XMLHttpRequest(); // a new request
MyRequest.open("GET", request_url, true);
MyRequest.onload = function (e)
{
if (MyRequest.readyState === 4)
{
if (MyRequest.status === 200)
{
handleTicker(MyRequest.responseText);
}
else
{
console.error(MyRequest.statusText);
}
}
};
MyRequest.onerror = function (e)
{
console.error(MyRequest.statusText);
};
MyRequest.send(null);
return;
}
function start()
{
initCanvas();
getRSS("https://smil-control.com/feed.rss");
}
Things get a little more complicated here. The canvas should behave flexibly and must adjust automatically when the width of the browser window changes. To achieve this, we must declare a so-called “event listener” in Javascript. When activated, it refers to a function that contains the commands required for resizing. We do this with window.addEventListener(‘resize’, resizeCanvas, false); This means that the callback function resizeCanvas() is always executed when resizing.
function resizeCanvas()
{
canvas.width = window.innerWidth;
ctx.font = "30px Arial";
text_width = ctx.measureText(ticker_content).width;
x = canvas.width;
}
First, the canvas width get the size value from the new window width. Since the element resets itself, the font needs to be reset and the text width must be recalculated. In this case, the scrolling text starts again from the beginning. So we set the value of the position variable x to maximum again.
The constant and global variables are the same as in canvas_animation_2.html. However, with the difference that we initialize the variables for this sample with default values.
Fetch the RSS
In principle, the procedure is similar to the animations described in CSS running Texts. The entry function start() has remained the same, except for the additional initialization of the canvas and its context in initCanvas(). The general program sequence now has five steps:
- Initialize the canvas
- Get the RSS feed as JSON text
- Convert the JSON text into a Javascript object
- Extract the text for the ticker from the Javascript object
- Print the ticker text
Update: This Yahoo-Api has been discontinued. So I wrote a small replacement script and published it under Github . The script is mostly compatible to the Yahoo-Api. Therefore it can be used with this tutorial.
There are two conceptual differences to the CSS3 animations
1. The good news: It is no longer necessary to calculate an animation time to control the reading speed depending on the text size. We determine the tempo of the running text by the number of pixels which the text should move with each frame. It therefore remains constant regardless of the text length. That means we removed the calculation function of the CSS3 animations.
2. The bad news: We have to solve another problem! The canvas width can’t expand to any size. The limits are different for each browser and may change in the future. This means, that we have to find out the values for the respective target device by trying them out. For example, Firefox currently limit to 22528 x 20992 pixels.
With Chromium/Chrome the limit is 16384 x 16384 pixels. Depending on the length, this corresponds to 10 – 15 headlines. If our text exceeds the limit, the canvas context does not accept the content fillText() wants to insert and remains empty. This means that we must take precautions to deal with this problem.
Approaches to solve the limit issue
There are several ways to address this problem. For example, we could reduce the text from 30px to 20px. This can still work for feeds that are slightly over the limit. However, this is not a real solution, but only a bad workaround. The feed can still be too long.
A much more sustainable solution would be to fill the visible area of the canvas circularly. This means to delete a character that disappears from the canvas context on the left and to move a new character from the content on the right side. However, the algorithm for this is complex and would go outside the scope of this article. If you would like to have such a solution implemented, you are welcome to contact us.
We have decided to select what we consider to be a more pragmatic third option. We simply cut off the text before it reaches the limit. Most news feeds usually start with the latest news, while older ones slide down. If we assume that 10 – 15 headlines can be displayed from a feed, this should usually be up-to-date enough for a news ticker. Who wants to read outdated news?
Crop text before limit
Optimally, “cutting” and examining takes place in the function that merges the ticker content:
function createTickerOutput(feed_obj)
{
var ticker_text = " +++ ";
var tmp_text = "";
for (var i = 0; i < feed_obj.query.count; i++)
{
tmp_text = feed_obj.query.results.item[i].title+ " +++ ";
if (isNewContentSizeValid(ticker_text + tmp_text))
ticker_text += tmp_text;
else
break;
}
return ticker_text;
}
For each feed item, the system first verifies whether its addition exceeds the threshold value of 16384 pixels. The function isNewContentSizeValid() does this by examining the text length.
function isNewContentSizeValid(txt)
{
return (ctx.measureText(txt).width < _max_canvas_width);
}
If it is smaller than 16384 pixels (_max_canvas_width), the function returns true. Thus, the headline is finally added to the ticker_text variable. If isNewContentSizeValid() returns false, this means that the addition will exceed the limit. In this case the condition is skipped and ticker_text remains unchanged. With break we also leave the for loop prematurely. You can check this by using the NTV feed (https://www.n-tv.de/rss) instead of https://smil-control.com/blog/feed The NTV feed usually has 20-22 entries and thus definitely exceeds the limit.
Wage of trouble
function displayTicker(ticker_text)
{
ticker_content = ticker_text;
resizeCanvas();
window.requestAnimationFrame(moveTicker);
}
The global variable ticker_content receives the output text and the canvas is formed with the corresponding values. For this we use the resizeCanvas() function already created for resizing. At the end we launch the recursive animation function moveTicker(),
function moveTicker()
{
ctx.clearRect(0,0, canvas.width, 80);
if (x > -text_width)
x = x - move_pixel;
else
x = canvas.width;
ctx.fillText(ticker_content, x, 50);
window.requestAnimationFrame(moveTicker);
}
which has not changed significantly since the last post about CSS-running texts. Merely instead of the fixed value 500, the width value is now determined variably by the current canvas width.
Open the file canvas_animations_rss.html to get a live view. Depending on the connection speed, you may have to wait 1-4 seconds until the process ends.
What happens next?
In the next article Running texts with SMIL, you will introduced to two more techniques for horizontal scrolling texts. One of these is an element that belongs to the prehistoric age of the Internet. Then we will compare all concepts and summarize the individual advantages and disadvantages.
If you have any questions or comments, please do not hesitate to contact me.
Open Source Developer & Co-Founder SmilControl – Digital Signage Visit me on: GitHub or LinkedIn