Building a Headline Viewer with Amino
This is part 3 of a series on Amino, a JavaScript graphics library for OpenGL on the Raspberry PI. You can also read part 1 and part 2.
Amino is built on Node JS, a robust JavaScript runtime married to a powerful IO library. That’s nice and all, but the real magic of Node is the modules. For any file format you can think of someone has probably written a Node module to parse it. For any database you might want use, someone has made a module for it. npmjs.org lists nearly ninety thousand packages! That’s a lot of modules ready for you to use.
For today’s demo we will build a nice rotating display of news headlines that could run in the lobby of an office using a flatscreen TV on the wall. It will look like this:
We will fetch news headlines as RSS feeds. Feeds are easy to parse using Node streams and the feedparser
module. Lets start by creating a parseFeed
function. This function takes a url. It will load the feed from the url, extract the title of each article, then call the provided callback function with the list of headlines.
var FeedParser = require('feedparser'); var http = require('http'); function parseFeed(url, cb) { var headlines = []; http.get(url, function(res) { res.pipe(new FeedParser()) .on('meta',function(meta) { //console.log('the meta is',meta); }) .on('data',function(article) { console.log("title = ", article.title); headlines.push(article.title); }) .on('end',function() { console.log("ended"); cb(headlines); }) }); }
Node uses _streams_. Many functions, like the `http.get()
` function, return a stream. You can pipe this stream through a filter or processor. In the code above we use the `FeedParser
` object to filter the HTTP stream. This returns a new stream which will produce events. We can then listen to the events as the data flows through the system, picking up just the parts we want. In this case we will watch for the data
event, which provides the article that was just parsed. Then we add just the title to the headlines
array. When the end
event happens we send the headlines array to the callback. This sort of streaming IO code is very common in Node programs.
Now that we have a list of headlines lets make a display. We will hard code the size to 1280 x 720, a common HDTV resolution. Adjust this to fit your own TV if necessary. As before, the first thing we do is turn the titles into a CircularBuffer (see previous blog ) and create a root group.
var amino = require('amino.js'); var sw = 1280; var sh = 720; parseFeed('http://www.npr.org/rss/rss.php?id=1001',function(titles) { amino.start(function(core, stage) { var titles = new CircularBuffer(titles); var root = new amino.Group(); stage.setSize(sw,sh); stage.setRoot(root); …
The RSS feed will be shown as two lines of text, so let’s create a text group then two text objects. Also create a background group to use later. Shapes are drawn in the order they are added, so we have to add the `bg
` group *before* the textgroup.
var bg = new amino.Group(); root.add(bg); var textgroup = new amino.Group(); root.add(textgroup); var line1 = new amino.Text().x(50).y(200).fill("#ffffff").text('foo').fontSize(80); var line2 = new amino.Text().x(50).y(300).fill("#ffffff").text('bar').fontSize(80); textgroup.add(line1,line2);
Each Text object has the same position, color, and size except that one is 100 pixels lower down on the screen than the other. Now we need to animate them.
The animation consists of three sections: set the text to the current headline, rotate the text in from the side, then rotate the text back out after a delay.
In the `setHeadlines` function; if the headline is longer than the max we support (currently set to 34 letters) then chop it into pieces. If we were really smart we’d be careful about not breaking words, but I’ll leave that as an exercise to the reader.
function setHeadlines(headline,t1,t2) { var max = 34; if(headline.length > max) { t1.text(headline.substring(0,max)); t2.text(headline.substring(max)); } else { t1.text(headline); t2.text(''); } }
The `rotateIn` function calls `setHeadlines` with the next title, then animates the Y rotation axis from 220 degrees to 360 over two seconds (2000 milliseconds). It also triggers `rotateOut` when it’s done.
function rotateIn() { setHeadlines(titles.next(),line1,line2); textgroup.ry.anim().from(220).to(360).dur(2000).then(rotateOut).start(); }
A quick note on rotation. Amino is fully 3D so in theory you can rotate shapes in any direction, not just in the 2D plane. To keep things simple the `Group` object has three rotation properties: `rx`, `ry`, and `rz`. These each rotate *around* the x, y, and z axes. The x axis is horizontal and fixed to the top of the screen, so rotating around the x axis would flip the shape from top to bottom. The y axis is vertical and on the left side of the screen. Rotating around the y axis flips the shape left to right. If you want to do a rotation that looks like the standard 2D rotation, then you want to go around the Z axis with `rz`. Also note that all rotations are in degrees, not radians.
The `rotateOut()` function rotates the text group back out from 0 to 140 over two seconds, then triggers `rotateIn` again. Since each function triggers the other they will continue to ping pong back and forth forever, pulling in a new headline each time. Notice the `delay()` call. This will make the animation wait five seconds before starting.
function rotateOut() { textgroup.ry.anim().delay(5000).from(0).to(140).dur(2000).then(rotateIn).start(); }
Finally we can start the whole shebang off back calling rotateIn the first time.
rotateIn();
What we have so far will work just fine but it’s a little boring because the background is pure black. Let’s add a few subtly moving rectangles in the background.
First we will create the three rectangles. They are each fill the screen and are 50% translucent, in the colors red, green, and blue.
//three rects that fill the screen: red, green, blue. 50% translucent var rect1 = new amino.Rect().w(sw).h(sh).opacity(0.5).fill("#ff0000"); var rect2 = new amino.Rect().w(sw).h(sh).opacity(0.5).fill("#00ff00"); var rect3 = new amino.Rect().w(sw).h(sh).opacity(0.5).fill("#0000ff"); bg.add(rect1,rect2,rect3);
Now let’s move the two back rectangles off the left edge of the screen.
//animate the back two rects rect1.x(-1000); rect2.x(-1000);
Finally we can slide them from left to right and back. Notice that these animations set `loop` to -1 and `autoreverse` to 1. The loop count sets how many times the animation will run. Using `-1` makes it run forever. The autoreverse property makes the animation alternate direction each time. Rather than going from left to right and starting over at the left again, instead it will go left to right then right to left. Finally the second animation has a five second delay. This staggers the two animations so they will always be in different places. Since all three rectangles are translucent the colors will continually mix and change as the rectangles slide back and forth.
rect1.x.anim().from(-1000).to(1000).dur(5000) .loop(-1).autoreverse(true).start(); rect2.x.anim().from(-1000).to(1000).dur(3000) .loop(-1).autoreverse(true).delay(5000).start();
Here’s what it finally looks like. Of course a still picture can’t do justice to the real thing.