Photo Slideshow in Amino

This is the second blog in a series about Amino, a Javascript OpenGL library for the Raspberry Pi. The first post is here.

This week we will build a digital photo frame. A Raspberry PI is perfect for this task because it plugs directly into the back of a flat screen TV through HDMI. Just give it power and network and you are ready to go.

Last week I talked about the new Amino API built around properties. Several people commented that I didn’t say how to actually get and run Amino. Very good point! Let’s kick things off with an install-fest. These instructions assume you are running Raspbian, though pretty much any Linux distro should work.

Amino is fundamentally a Node JS library so first you’ll need Node itself. Fortunately, installing Node is far easier than it used to be. In brief, update your system with `apt-get`, download and unzip Node from `nodejs.org`, and add `node` and `npm` to your path. Verify the installation with `npm —version`. I wrote full instructions here

Amino uses some native code, so you’ll need Node Gyp and GCC. Verify GCC is installed with gcc —version. Install node-gyp with npm install -g node-gyp.

Now we can checkout and compile Amino. You’ll also need Git installed if you don’t have it.

git clone git@github.com:joshmarinacci/aminogfx.git
cd aminogfx
node-gyp clean configure --OS=raspberrypi build
npm install
node build desktop
export NODE_PATH=build/desktopnode tests/examples/simple.js

This will get the amino source, build the native parts, then build the Javascript parts. When you run node tests/examples/simple.js you should see this:

Now let’s build a photo slideshow. The app will scan a directory for image, then loop through the photos on screen. It will slide the photos to the left using two ImageViews, one for the outgoing image and one for the incoming, then swap them. First we need to import the required modules.

var amino = require('amino.js');
var fs = require('fs');
var Group = require('amino').Group;
var ImageView = require('amino').ImageView;

Technically you could call `amino.Group()` instead of importing `Group` separately, but it makes for less typing later on.

Now let’s check that the user specified an input directory. If so, then we can get a list of images to load.

if(process.argv.length < 3) {
    console.log("you must provide a directory to use");
    return;
}

var dir = process.argv[2];
var filelist = fs.readdirSync(dir).map(function(file) {
    return dir+'/'+file;
});

So far this is all straight forward Node stuff. Since we are going to loop through the photos over and over again we will need an index to increment through the array. When the index reaches the end it should wrap around to the beginning, and handle the case when new images are added to the array. Since this is a common operation I created a utility object with a single function: `next()`. Each time we call `next` it will return the next image, automatically wrapping around.

function CircularBuffer(arr) {
    this.arr = arr;
    this.index = -1;
    this.next = function() {
        this.index = (this.index+1)%this.arr.length;
        return this.arr[this.index];
    }
}

//wrap files in a circular buffer
var files = new CircularBuffer(filelist);

Now lets set up a scene in Amino. To make sure the threading is handled properly you must always pass a setup function to `amino.start()`. It will set up the graphics system then give you a reference to the `core` object and a `stage`, which is the window you can draw in. (Technically it’s the contents of the window, not the window itself).

amino.start(function(core, stage) {
    stage.setSize(800,600);

    var root = new Group();
    stage.setRoot(root);


    var sw = stage.getW();
    var sh = stage.getH();

    //create two image views
    var iv1 = new ImageView().x(0);
    var iv2 = new ImageView().x(1000);

    //add to the scene
    root.add(iv1,iv2);
…
};

The setup function above sets the size of the stage and creates a Group to use as the root of the scene. Within the root it adds two image views, `iv1` and `iv2`.

The images may not be the same size as the screen so we must scale them. However, we can only scale once we know how big the images will be. Furthermore, the image view will hold different images as we loop through them, so we really want to recalculate the scale every time a new image is set. To do this, we will watch for changes to the image property of the image view like this.

    //auto scale them with this function
    function scaleImage(img,prop,obj) {
        var scale = Math.min(sw/img.w,sh/img.h);
        obj.sx(scale).sy(scale);
    }
     // call scaleImage whenever the image property changes
    iv1.image.watch(scaleImage);
    iv2.image.watch(scaleImage);

    //load the first two images
    iv1.src(files.next());
    iv2.src(files.next());

Now that we have two images we can animate them. Sliding images to the left is as simple as animating their `x` property. This code will animate the x position of `iv1` over 3 seconds, starting at `0` and going to `-sw`. This will slide the image off the screen to the left.

iv1.x.anim().delay(1000).from(0).to(-sw).dur(3000).start();

To slide the next image onto the screen we do the same thing for iv2,

iv2.x.anim().delay(1000).from(sw).to(0).dur(3000)

However, once the animation is done we want to swap the images and move them back, so let’s add a `then(afterAnim)` function call. This will invoke `afterAnim` once the second animation is done. The final call in the chain is to the `start()` function. Until `start` is called nothing will actually be animated.

    //animate out and in
    function swap() {
        iv1.x.anim().delay(1000).from(0).to(-sw).dur(3000).start();
        iv2.x.anim().delay(1000).from(sw).to(0).dur(3000)
            .then(afterAnim).start();
    }
     //kick off the loop
    swap();

The `afterAnim` function moves the ImageViews back to their original positions and moves the image from `iv2` to `iv1`. Since this happens between frames the viewer will never notice anything has changed. Finally it sets the source of `iv2` to the next image and calls the `swap()` function to loop again.

    function afterAnim() {
        //swap images and move views back in place
        iv1.x(0);
        iv2.x(sw);
        iv1.image(iv2.image());
        // load the next image
        iv2.src(files.next());
        //recurse
        swap();
    }

A note on something a bit subtle. The `src` of an image view is a string, either a url of file path, which refers to the image. The `image` property of an ImageView is the actual in memory image. When you set the `src` to a new value the ImageView will automatically load it, then set the `image` property. That’s why we added a watch function to the `iv1.image` not `iv1.src`.

Now let’s run it, the last argument is the path to a directory containing some JPGs or PNGs.

node demos/slideshow/slideshow.js demos/slideshow/images

If everything goes well you should see something like this:

By default, animations will use a cubic interpolator so the images will start moving slowly, speed up, then slow down again when they reach the end of the transition. This looks nicer than a straight linear interpolation.

So that’s it. A nice smooth slideshow in about 80 lines of code. By removing comments and utility functions we could get it under 40, but this longer version is easier to read.

Here is the final complete code. It is also in the git repo under demos/slideshow.

var amino = require('amino.js');
var fs = require('fs');
var Group = require('amino').Group;
var ImageView = require('amino').ImageView;

if(process.argv.length < 3) {
    console.log("you must provide a directory to use");
    return;
}

var dir = process.argv[2];
var filelist = fs.readdirSync(dir).map(function(file) {
    return dir+'/'+file;
});


function CircularBuffer(arr) {
    this.arr = arr;
    this.index = -1;
    this.next = function() {
        this.index = (this.index+1)%this.arr.length;
        return this.arr[this.index];
    }
}

//wrap files in a circular buffer
var files = new CircularBuffer(filelist);


amino.start(function(core, stage) {
    stage.setSize(800,600);

    var root = new Group();
    stage.setRoot(root);


    var sw = stage.getW();
    var sh = stage.getH();

    //create two image views
    var iv1 = new ImageView().x(0);
    var iv2 = new ImageView().x(1000);

    //add to the scene
    root.add(iv1,iv2);

    //auto scale them
    function scaleImage(img,prop,obj) {
        var scale = Math.min(sw/img.w,sh/img.h);
        obj.sx(scale).sy(scale);
    }
    iv1.image.watch(scaleImage);
    iv2.image.watch(scaleImage);

    //load the first two images
    iv1.src(files.next());
    iv2.src(files.next());



    //animate out and in
    function swap() {
        iv1.x.anim().delay(1000).from(0).to(-sw).dur(3000).start();
        iv2.x.anim().delay(1000).from(sw).to(0).dur(3000)
            .then(afterAnim).start();
    }
    swap();

    function afterAnim() {
        //swap images and move views back in place
        iv1.x(0);
        iv2.x(sw);
        iv1.image(iv2.image());
        iv2.src(files.next());
        //recurse
        swap();
    }

});

Thank you and stay tuned for more Amino examples on my blog.

Amino repoundefined

Talk to me about it on Twitter

Posted August 11th, 2014

Tagged: amino nodejs