HTML Lauftexte basierend auf Canvas

Canvas Lauftexte
Canvas Lauftexte
Bildquelle: Herbert Weber, Kreuzgang, cropped, CC BY-SA 3.0

Im letzten Beitrag unseres Workshops über Digital Signage Newsletter lernten Sie die Erstellung einer CSS-Laufschrift mit CSS3-Keyframe Animationen kennen, welche Schlagzeilen aus einem RSS-Feed abspielt. In diesem Artikel stellen wir eine interessante Alternative für Lauftexte basierend auf dem HTML5-Element Canvas vor. Canvas für Lauftexte einzusetzen bringt Vorteile und Nachteile. Der offensichtlichste Unterschied: Canvas-Laufschriften benötigen im Gegensatz zu CSS-Lauftexten den Einsatz von JavaScript zur Darstellung.

Was ist ein HTML5-Canvas?

Ein Canvas (Leinwand) ist in HTML5 ein definierter Bereich in welchem mit JavaScript-Befehlen gezeichnet werden kann. Ursprünglich entwickelte Apple Canvas für ihren Browser Safari. Später wurde er als Bestandteil von HTML5 standardisiert. Canvas ermöglicht das Zeichnen von Kreisen, Linien, Polygonen und anderen geometrische Figuren. Eine Referenzkarte befindet sich bei Wikipedia.

Das Element ist sehr vielseitig und kann durch seine Bildfunktionen auch Videos abspielen. Diese können z.B. auch zusätzliche Effekte in Echtzeit enthalten. Alle aktuellen modernen Browser wie Chrome, Edge, Opera, Safari und Firefox sind in der Lage mit dem Canvas-Element umzugehen.
Im Body-Bereich des HTML wird er so eingesetzt:

<canvas id="myTicker" width="500" height="80">No Canvas</canvas>

Der Text „No Canvas“ ist ein sogenannter „Fallback“. Er wird nur von Browsern angezeigt, die kein Canvas darstellen können z.B. dem IE 8. Die Höhe setzen wir laufleistentypisch auf 80 Pixel und die Breite auf 500 Pixel.
Mit id geben wir dem Element eine eindeutigen Identifikation, um es über JavaScript korrekt anzusprechen.

Wie zeichnen wir einen Text in HTML Canvas?

Dazu fügen wir folgende Zeilen zwischen den body-Tags im HTML-Quelltext:

<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>

Da der Browser seine Inhalte von oben nach unten abarbeitet, ist es für dieses Beispiel wichtig die JavaScript-Befehle nach dem Canvas-Element einzugeben. Alternativ können wir natürlich den Canvas auch mittels onload im HTML-body Tag laden. Wir fügen zusätzlich mit style=“border:1px solid #ddd;“ der Übersicht halber einen grauen Rand hinzu. Der Befehl document.getElementById(„myTicker“); lädt das existierende Element in die Variable MyCanvas. Nun benötigt JavaScript einen Kontext zum Canvas.

Der Canvas-Kontext

Unter einem Kontext versteht der Programmierer eine Schnittstelle zu einem Objekt. Der Kontext stellt Funktionen und Datenstrukturen zur Verfügung, mit denen auf der Leinwand (Canvas) gezeichnet wird. Durch MyCanvas.getContext(„2d“) wird klargestellt, dass wir hier nur Funktionalität für zweidimensionale Vektorgrafik benötigen. Alternative Kontexte wären z.B. webgl/webgl2 für dreidimensionale Vektorgrafik oder bitmaprenderer, um pixeloriente Grafikfunktionen nutzen zu können. Da wir diese nicht benötigen initialisieren wir die Kontextvariable ctx mit 2d.

Als nächstes wird mittels ctx.font = „30px Arial“ dem Canvas-Kontext ein 30 Pixel großer Arial Font zugewiesen. Mit dem Befehl fillText geben wir den Text „Lorem ipsum dolor sit amet, consetetur“ aus.
Die beiden nachfolgenden Parameter setzen den Tickertext 10 Pixel rechts vom Rand und 50 Pixel von oben. Dabei wird vom Textanfang unten links ausgegangen. Da der Font 30 Pixel hoch ist bedeutet ein Wert von 50, dass der Text 20 Pixel unterhalb des Randes oben steht.

Klicken Sie auf canvas_animations_1.html, um zu das Ergebnis zu sehen.

Animation in JavaScript

Wir müssen jetzt den Text dazu bringen sich zu „bewegen“. Die grundlegende Herangehensweise für Canvas Lauftexte besteht darin, innerhalb von kurzen Zeitintervallen die Position der Inhalte zu verändern. Um für das menschliche Gehirn den Eindruck einer flüssigen Animation zu erzeugen, dürfen diese Intervalle auf einem digitalen Bildschirm höchstens eine 30stel Sekunde dauern. Wir sprechen dann von der sogenannten Framerate, die in diesem Fall 30fps (Frames pro Sekunde) beträgt. Um Ruckeleffekte bei schnellen Bewegungen wie zum Beispiel in Videos oder Spielen zu vermeiden werden sogar 60fps auf dem Bildschirm notwendig. Wir brauchen deshalb eine Funktionalität, die zeitgesteuert bestimmte Aktionen startet. Früher wurden dafür die JavaScript-Funktionen setTimeout oder setInterval eingesetzt. Diese Methode Animationen zu erstellen ist inzwischen veraltet und suboptimal.

Die Alternative heißt requestAnimationFrame

Seit ca. 2013 unterstützen alle aktuellen Browser eine effiziente Funktion namens requestAnimationFrame. Diese JavaScript-Funktion informiert den Browser, dass eine Animation ansteht. Worauf dieser selbstständig die Framerate auswählt und speziell für Animationen ausgelegte interne Routinen einsetzt. Dabei besteht die oberste Priorität darin, die Animation möglichst ruckelfrei ablaufen zu lassen. Es ist grundsätzlich besser, wenn der Browser die Framerate festlegt, da der optimale Wert abhängig von der Anzahl und der Art der Animationen auch während der Laufzeit variieren kann. Mit den alten statischen Funktionen werden schnell Grenzen erreicht. Außerdem stoppt die Animation wenn der Benutzer das Browsertab wechselt, während Animationen mittels setInterval() oder stTimeout() in dem Fall einfach weiterlaufen und sinnlos Ressourcen verbrauchen.

Der erste Lauftext

Wir erweitern Beispiel 1, um den Text zu bewegen. Dieser soll unendlich oft von rechts nach links scrollen. D.h. sobald der letzte Buchstabe am linken Rand raus läuft startet die Animation des Tickers vom rechten Rand aus neu. Der dafür notwendige Code für Canvas Lauftexte sieht so aus:

<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>

Als erstes setzen wir eine Konstante namens pixel_move. Dort legen wir fest um wie viele Pixel sich der Tickertext bei jeden Frame nach links verschiebt. Diese Variable steuert die Geschwindigkeit der animierten Canvas Lauftexte. Bei höheren Werten scrollt der Text schneller; bei niedrigen Werten z.B. 0.5 langsamer. Ein Wert von 1 sollte für unsere Zwecke reichen.

Die nächsten Zeilen sind identisch zum ersten Beispiel. Da wir den Text durch den Canvas „schieben“ wird die x-Position bei jedem Durchlauf neu kalkuliert. Also setzen wir die Variable x = 500 als Startwert, denn bei 500 Pixel befindet sich der rechte Rand des Canvas. Der Inhalt wird, weil mehrfach genutzt, zur Vereinfachung ebenfalls in eine Variable gesetzt.

Als nächstes muss das Skript die Weite des Textes des Arial-Fonts bi 30px kennen, um zu erkennen wann der Text komplett durchgelaufen ist. Danach wird die Animation bei Position 500px wieder neu gestartet. Diese Berechnung geschieht durch den Befehl ctx.measureText(ticker_content).width und wird in die Variable text_width geschrieben.

Auf einem System mit Linux und Chromium/Linux entspricht das beispielsweise ca. 519 Pixel. Mit der Funktion ctx.fillText(ticker_content, x, 50); setzen wir - wie oben beschrieben - den Beginn des Textes auf den rechten Rand des Canvas. Die nächste Zeile window.requestAnimationFrame(moveTicker) ruft die Funktion moveTicker auf. Programmierer bezeichnen eine Funktion, die als Parameter von einer anderen Funktion aufgerufen wird übrigens als sogenannte Rückruffunktion (Callback). In unserem Callback moveTicker passiert nun die „Magie“.

Der Algorithmus für Canvas Lauftexte

ctx.clearRect(0,0,x,80) löscht zunächst alle bisherigen Inhalte des Canvas-Kontextes. Als nächstes wertet das Skript mit der if..else-Bedingung den aktuellen Wert der x-Variablen aus und entscheidet über die neue Textposition. Entweder solle der Text einen Pixel nach links rücken oder wieder auf den Anfangszustand zurück.

Solange x größer ist als der negative Wert der Länge des Textes (also -519px), verschiebt sich der Textinhalt immer weiter nach links. Die Variable x wird irgendwann kleiner als -519. Das bedeutet, dass der Text nun vollständig aus dem sichtbaren Bereich gelaufen ist. Also muss seine Position wieder den Startwert annehmen (x = 500).

Um die Animation in einer Endlosschleife abspielen zu können, ruft sich die Callback-Funktion zum Schluss wieder mittels requestAnimationFrame selbst auf. Programmierer nennen so etwas eine rekursive Funktion.

Klicken Sie auf canvas_animations_2.html, um das Ergebnis zu sehen. Falls Sie gerade an einem PC sitzen, werden Sie vermutlich feststellen, dass dieser Ticker auf Anhieb ruckelfreier läuft als Lauftexte mit CSS3-Animationen .

Canvas Lauftexte RSS
Canvas Lauftexte mit RSS
Bildquelle: Herbert Weber, Hildesheim, Kreuzgang, cropped, CC BY-SA 3.0

Nutzen 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 über eine Url bereits in dem Beitrag über CSS-Laufschriften ausführlich erörtert wurde, gehen wir nur auf die spezifischen Unterschiede ein.

Der Body-Bereich

Der statische HTML-Bereich benötigt einige Änderungen, da mehr JavaScript hinzukommt und wir die Texte möglichst übersichtlich gestalten möchten.

<body onload="handleTicker()">
	<canvas id="myTicker" height="80">No Canvas</canvas>
</body>

Wir setzen hier den Canvas mit einer Höhe von 80px und geben ihm eine automatische Breite, denn er soll ja über den kompletten Anzeigebereich gehen. Als Identifikation wählen wir wieder myTicker. Das onload Ereignis wurde bereits in CSS Lauftexte beschrieben und verhält sich in diesem Beispiel natürlich identisch.

Im Stylesheet

<style>
	#myTicker
	{
		position: absolute;
		left: 0;
		top:0;	
	}
</style>

bekommt das HTML-Element myTicker eine absolute CSS-Position beginnend an der linken oberen Ecke. Das ist wichtig, weil dieses Konzept wegen der Berechnungen eine absolute Positionierung benötigt. Eine relative Positionierung im Style würde außerdem den Canvas nicht exakt an die linke und obere Seiten setzen. Das Elternelement body besitzt nämlich standardmäßig einen Abstand von 8px zu seinem Elternelement html (Browserfenster).

Das komplette 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/de/feed.rss");
}

Hier wird es etwas komplizierter. Der HTML-Canvas soll sich flexibel verhalten und sich bei einer Änderung der Breite des Browserfensters anpassen. Um das zu erreichen, muss in JavaScript ein sogenannter „Eventlistener“ deklariert werden. Dieser verweist bei seiner Aktivierung auf eine Funktion, in der die zur Größenänderung nötigen Befehle stehen. Das geschieht mit window.addEventListener(‚resize‘, resizeCanvas, false); D.h. bei einer Größenänderung (resize) wird immer die Callbackfunktion resizeCanvas() ausgeführt.

function resizeCanvas()
{
	canvas.width   = window.innerWidth;
	ctx.font       = "30px Arial";
	text_width     = ctx.measureText(ticker_content).width;
	x              = canvas.width;

}	

Die Canvas-Breite wird zunächst aus der neuen Fensterbreite ausgelesen. Da sich das Element dabei zurücksetzt, muss die Schrift wieder eingestellt und die Textbreite anhand des Fonts wieder neu berechnet werden. Der Lauftext soll in diesem Fall nochmal von vorn anfangen, also setzen wir den Wert die Position in der Variable x wieder auf das Maximum.

Die Konstante und globalen Variablen sind wie in canvas_animation_2.html. Allerdings mit dem Unterschied, dass die Variablen in diesem Beispiel mit Defaultwerten initialisiert werden.

Das RSS abrufen

Nachtrag: Die Yahoo-Api wurde eingestellt.Ich habe deshalb ein kleines Ersatzskript geschrieben und unter github veröffentlicht. Das Skript ist weitestgehend zur Yahoo-Api kompatibel und übernimmt die Feed-Url. Daher lässt es sich mit diesem Tutorial einsetzen.

Prinzipiell ist das Verfahren analog zu den Animationen in dem Beispiel CSS-Laufschrift. Die Funktion für eine Einsprung start() blieb bis auf die zusätzliche Initialisierung des Canvas und dessen Kontext in initCanvas() gleich. Der grobe Programmablauf besitzt jetzt fünf Schritte:

  1. Den Canvas Initialisieren
  2. Den RSS-Feed als JSON-Text abholen
  3. Den JSON-Textin ein JavaScript-Objekt konvertieren
  4. Aus dem JavaScript-Objekt den Text für den Ticker extrahieren
  5. Den Tickertext ausgeben

Es gibt zwei konzeptionelle Unterschiede zu den CSS3-Animationen

1. Die gute Nachricht: Die Dauer der Animation zu berechnen, um die Geschwindigkeit in Abhängigkeit zu der Textgröße zu steuern, ist nicht mehr nötig. Die Geschwindigkeit der Animation lässt sich über die Verschiebung der Pixel steuern und bleibt unabhängig von der Länge des Textes konstant. D.h. diese Funktionalität wurde entfernt.

2. Die schlechte Nachricht: Wir müssen dafür ein anderes Problem lösen! Der Canvas kann nämlich nicht beliebig breit werden. Die Limits sind bei jedem Browser unterschiedlich und können sich zukünftig verändern. D.h. Wir müssen für das jeweilige Zielgerät die Werte durch ausprobieren herausfinden. Firefox limitiert aktuell zum Beispiel auf 22528 x 20992 Pixel. Beim Chromium/Chrome liegt das Limit hingegen bei 16384 x 16384 Pixel. Das entspricht in etwa je nach Länge 10 bis 15 Schlagzeilen. Wenn das Limit überschritten wird, akzeptiert der Canvas-Kontext den über die Funktion fillText() einzufügenden Inhalt nicht und bleibt leer. D.h wir sind gezwungen, Vorkehrungen zu treffen, um diese Nachteile abzufangen.

Ansätze das Limit-Problem zu lösen

Es gibt mehrere Möglichkeiten an dieses Problem heranzugehen. Wir könnten z.B. den Text von 30px auf 20px verkleinern. Das kann bei Feeds, die knapp das Limit überschreiten noch funktionieren. Allerdings ist das keine wirkliche Lösung, sondern nur ein schlechter Workaround. Der Feed kann trotzdem noch zu lang werden.

Eine weitaus nachhaltigere Lösung wäre den sichtbaren Bereich des Canvas zirkulär zu befüllen. D.h. ein Zeichen, welches links im Off verschwindet aus dem Canvas-Kontext zu löschen und auf der rechten Seite ein neues Zeichen aus dem Inhalt nachzuschieben. Der Algorithmus dazu ist allerdings komplex und würde den Rahmen dieses Beitrages sprengen. Falls Sie eine solche Lösung umgesetzt haben möchten, können Sie sich gerne an uns wenden.

Wir haben uns für eine, aus unserer Sicht pragmatischere dritte Variante entschieden. Wir schneiden den Text einfach ab, bevor das Limit erreicht wird. Bei den meisten News-Feeds ist es in der Regel so, dass die neuesten Nachrichten am Anfang stehen, während ältere nach unten rutschen. Wenn wir davon ausgehen, dass 10 bis 15 Schlagzeilen aus einem Feed angezeigt werden können, sollte das für einen Newsticker in der Regel ausreichend aktuellen Inhalt darstellen. Wer will schon veraltete Nachrichten lesen?

Text vor dem Limit abschneiden

Das „Schneiden“ und Prüfen erfolgt optimalerweise in der Funktion, die den Tickerinhalt zusammenfügt:


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;
}

Bei jedem Feed-Item wird erst geprüft, ob sein Hinzufügen für ein Überschreiten des Schwellenwert von 16384 Pixel sorgt. Das geschieht in der Funktion isNewContentSizeValid():


function isNewContentSizeValid(txt)
{
	return (ctx.measureText(txt).width < _max_canvas_width);
}

Dort wird die Breite des neuen Textes untersucht. Wenn diese kleiner ist als 16384 Pixel (_max_canvas_width), liefert die Funktion ein true zurück. Somit bekommt die Variable ticker_text die zusätzliche Schlagzeile zugewiesen. Falls isNewContentSizeValid() false ausgibt, bedeutet das ein weiteres Hinzufügen würde das Limit überschreiten.

In dem Fall wird die Bedingung übersprungen, ticker_text bleibt unverändert und mit break verlassen wir vorzeitig die for-Schleife. Das können Sie prüfen, indem Sie den NTV-Feed (https://www.n-tv.de/rss) anstelle unseres Feed verwenden. Der NTV-Feed hat üblicherweise 20-22 Einträge und überschreitet damit definitiv das Limit.

Der Lohn der Mühe

function displayTicker(ticker_text)
{
	ticker_content = ticker_text;
	resizeCanvas();
	window.requestAnimationFrame(moveTicker);
}

Die globalen Variable ticker_content erhält den auszugebenden Text und das Canvas wird mit den entsprechenden Werten erstellt. Dafür nutzen wir die bereits für das Resizing erstellte Funktion resizeCanvas(). Am Ende starten wir die rekursiv arbeitende Animations über die Funktion 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);
}

welche sich gegenüber dem letzten Beitrag über CSS-Laufschriften nicht wesentlich geändert hat. Lediglich der Breitenwert wird jetzt anstelle des festen Wert 500 variabel durch die aktuelle Canvas-Breite bestimmt.

Rufen Sie der Datei canvas_animations_rss.html auf, um eine Live-Ansicht zu bekommen. Es kann je nach Geschwindigkeit Ihrer Verbindung 1-4 Sekunden dauern, bis der Feed gelesen und verarbeitet wird.

Wie geht es weiter?

Im nächsten Artikel Laufschrift mit SMIL, werden wir noch zwei weitere Techniken für horizontale Lauftexte vorstellen. Darunter ein Element, welches zum Urgestein des Internets gehört. Danach werden wir alle Konzepte miteinander vergleichen und die einzelnen Vor- und Nachteile zusammenfassen.


Gravatar Nikolaos Sagiadinos
Autor: Niko Sagiadinos
Open Source Entwickler & Co-Founder SmilControl – Digital Signage
Besuchen Sie mich auf: GitHub, LinkedIn oder Xing

Kontakt

Sie haben weitere Fragen?





Unsere Kontaktdaten

SmilControl GmbH
Niederaue 1a
D-30419 Hannover

☎ +49 (0) 511 – 96 499 560

Amtsgericht Hannover
HRB 221981
USt-Id: DE 281 780 194

Vertretungsberechtigter Geschäftsführer:
Nikolaos Sagiadinos