Animating Photos from Flickr

By Anton Tarasov, November 17, 2008

The sample fetches pictures from the Flickr web service, based on the tag you type. It animates the pictures in such a way that those which have common tags “flock” together. You can select a picture with a mouse. The picture will be double-sized and you will be able to see tags assosiated with it. The tag list can be scrolled. Clicking a tag from the list puts the tag in the search field and the sample starts fetching new pictures. When you select a picture and click it, the picture is expanded to full screen and a large image is loaded. Press any key or click the large picture to return it to the smaller image size again.

The sample should not be run on a mobile platform, nor with pre-6u10 JDK.

Understanding the Code

The Scene node of the sample contains a child PhotoFlockr node. It's the root node for all other nodes used in the sample. PhotoFlockr is an instance of the Group class with the following content:

  • A sequence of Sprite nodes. Each of the sprites displays a fetched picture.
  • A text field in which a user types search tags.
  • CrossHair custom node, which renders a cursor.
  • A transparent Rectangle node used to intersect mouse movement.
  • A node used to manage a close control.

Note that the CrossHair node has the attributes cx and cy bound to the mouse coordinates set in the callback functions of the glass pane. See Figure 1.

Source Code
    override function create(): Node {
        ...
        return Group {
            content:
                //
                // A sequence of nodes displaying fetched pictures.
                //
                [spriteGroup = Group {
                    content: sprites
                    visible: bind spriteGroupVisible
                },
                //
                // A tag search field.
                //
                Group {
                    translateX: 10
                    translateY: 160
                    content: tagSearchField
                },
                //
                // A node rendering crosshair cursor.
                //
                CrossHair {
                    w: width
                    h: height
                    cx: bind mouseX
                    cy: bind mouseY
                },
                //
                // A "glass pane".
                //
                Rectangle {
                    cursor: Cursor.NONE
                    height: height
                    width: width
                    fill: Color.rgb(0, 0, 0, 0)
                    onMouseMoved: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    onMouseDragged: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    smooth: false
                },
                //
                // A close control.
                //
                Group {
                    translateX: width - 20
                    content:
                        [ImageView {
                            translateX: 5
                            translateY: 5
                            image: Image {
                                url: "{__DIR__}images/close.png"
                            }
                            cache: true
                        },
                        Rectangle {
                            width: 20
                            height: 20
                            fill: Color.rgb(0, 0, 0, 0)
                            cursor: Cursor.HAND
                            cache: true
                            onMouseClicked: function(e) {
                                FX.exit();
                            }
                        }]
                }]
    }

Figure 1: Main.fx Script: PhotoFlockr.create Function

When a mouse hovers over a Sprite node, the picture is selected and the function reorderSprites shown in Figure 2 is called. The function reorders the sequence of the sprites in such a way that the selected sprite moves to the end of the sequence. The content of spriteGroup is then replaced and the selected sprite appears on top (closer to the screen) of the hierarchy.

Source Code
    package function reorderSprites() {
        var _sprites = [ sprites[s | not s.boid.selected], sprites[s | s.boid.selected] ];
        spriteGroup.content = _sprites;
    }

Figure 2: Main.fx Script: PhotoFlockr.reorderSprites Function

Figure 3 shows the Sprite custom node and its child nodes hierarchy. Two levels of transforms are used here. The topmost Transform.translate(spriteX, spriteY) transform is applied to the whole hierarchy and is used to move the sprite over the sample screen. A sequence of tranforms is located one level below. That sequence is applied only to the ImageView containing a small picture and a black foreground Rectangle. These transforms are used to scale the picture when it is selected or expanded to full screen and to rotate it when it "flocks". Note that all the transforms are bound, as their parameters are mutable values.

Two Groups on top of the hierarchy (the buttom of the code fragment) contain largeImage and a list of the picture's tags, respectively. The opacity attribute of the large picture Group is bound to the largeImageFade variable that is changed by fadeTimeline on expanding the picture to full screen. Also note that the ImageView that is initialized with the image variable has its opacity attribute bound in such a way that the image being expanded to full screen fades as the largeImage appears.

Source Code
    override public function create(): Node {
        Group {
            transforms: bind Transform.translate(spriteX, spriteY)
            content:
                [Group {
                    transforms: bind [ Transform.scale(alpha, alpha),
                                       Transform.rotate(if (not selected) heading
                                                        else spriteHeading, radius, radius) ]
                    content: [
                        //
                        // The picture's background black rectangle.
                        //
                        Rectangle {
                            height: radius*2
                            width: radius*2
                            fill: Color.BLACK
                            smooth: false
                        },
                        //
                        // The small image of the picture.
                        //
                        ImageView {
                            opacity: bind 1.0 - largeImageFade
                            image: bind image
                            smooth: true
                            onMouseClicked: function(e) {
                                this.doSelect();
                            }
                        },
                        //
                        // The picture's gray frame.
                        //
                        Rectangle {
                            transforms: bind Transform.scale(1.0/alpha, 1.0/alpha);
                            stroke: Color.GRAY
                            height: bind radius*2*alpha
                            width: bind radius*2*alpha
                            fill: Color.color(0, 0, 0, 0);
                            strokeWidth: 1
                       }
                    ]
                },
                //
                // The large image of the picture.
                //
                Group {
                    opacity: bind largeImageFade
                    visible: bind largeImage != null
                    content:
                        [Rectangle {
                            height: screenHeight
                            width: screenWidth
                            fill: Color.BLACK
                            visible: bind largeImageFade == 1.0
                        },
                        ImageView {
                            image: bind largeImage
                            smooth: true
                        }]
                },
                //
                // The tag list.
                //
                Group {
                    visible: bind alpha == 2.0
                    content: bind tagList
                }]
        }
    }

Figure 3: Sprite.create Method

The implementation of the large image-fading effect is shown in Figure 4. The largeImageProgress> variable is bound to the largeImage.progress attribute and the trigger set on it starts playing fadeTimeline when the loading of largeImage is completed.

Source Code
    // Start the picture-fading effect when a large image loading is completed.
    var largeImageProgress = bind largeImage.progress on replace {
        if (largeImage != null and largeImageProgress == 100) {
            fadeTimeline.play();
        }
    }

    var fadeTimeline = Timeline {
        keyFrames:
            [ at (0s) { largeImageFade => 0.0 },
              at (1s) { largeImageFade => 1.0 } ]
    }

Figure 4: Sprite.fx Script: the Large Image-Fading Effect

Listening for selection of the sprite node is easy. It's enough to override the Node.hover attribute and set a trigger on it (Figure 5). Note also that the Node.blocksMouse attribute is also overriden and set to true. This setting prevents mouse events from being sent to other sprite nodes behind this node in the scene hierarchy.

Source Code
    override var blocksMouse = true;
    override var hover on replace {
        if (hover and not fullScreen) {
            onEnter();
        } else {
            onLeave();
        }
    }

Figure 5: Sprite.fx Script With Node's Attributes Overriden

The Boid class manages the logic by which the sprites are moving across the sample screen. Several forces affect the rate and direction of movement (see Figure 6):

  • Separation – Two nearby sprites tend to steer away.
  • Cohesion – Two nearby sprites with some particular count of common tags tend to move alongside each other.
  • Alignment – The rates of two nearby sprites are averaged.

The Vector2D class is used to perform those calculations.

Source Code
    // Calculate new cordinates and heading of the sprite
    public function run(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        if (not selected) {
            flock(boids, cohere);
            update();
            borders();
        }
    }

    // Accumulate a new acceleration each time based on three rules
    function flock(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        var sep: Vector2D = separate(boids);           // Separation
        var ali: Vector2D = align(boids);              // Alignment
        var coh: Vector2D = cohesion(boids, cohere);   // Cohesion
        // Arbitrarily weight these forces
        sep.mult(8.0);
        ali.mult(1.0);
        coh.mult(1.0);
        // Add the force vectors to acceleration
        acc.add(sep);
        acc.add(ali);
        acc.add(coh);
    }

    // Update location
    function update(): Void {
        vel.add(acc);           // Update velocity
        vel.limit(maxspeed);    // Limit speed
        vel.updateHeading2D();  // Update direction
        loc.add(vel);           // Finally update location
        acc.setXY(0, 0);        // Reset accelertion
    }

    // Wraparound
    function borders(): Void {
        if (loc.x < -radius*2) loc.x = width + radius*2;
        if (loc.y < -radius*2) loc.y = height + radius*2;
        if (loc.x > width + radius*2) loc.x = -radius*2;
        if (loc.y > height + radius*2) loc.y = -radius*2;
    }

Figure 6: Boid.fx Script: Vector Calculations

Model.fx is used to fetch XML data from Flickr, in particular: javafx.io.http.HttpRequest, which performs asynchronous http requests, and javafx.data.pull.PullParser, which parses XML data. The PhotoModel class encapsulates URLs of small and large variants of a particular photo and a string list of tags associated with it.

And finally, the TagList class extends CustomNode and shows a list of photo tags when it is selected (Figure 7). The transparent background rectangle intersects mouse wheel events and scrolls the list. Another rectangle contained in listGroup listens for a mouse click on a particular tag. A click triggers a new search for photos corresponding the selected tag.

Source Code
    override public function create(): Node {
        Group {
            var margin = 5;
            var spacing = 6;
            clip: Rectangle {
                height: height
                width: width
            }
            content: [
                // The rectangle manages scrolling.
                Rectangle {
                    var h = bind sizeof tags * (font.size + spacing);
                    var dif = bind h - height
                    height: bind h
                    width: bind width;
                    fill: Color.color(0, 0, 0, 0);
                    smooth: false
                    onMouseWheelMoved: function(e) {
                        if ((scrollY >= 0 and e.wheelRotation < 0) or
                            (scrollY + dif <= 0 and e.wheelRotation > 0))
                        {
                            return;
                        }
                        if (scrollY - e.wheelRotation*5 > 0) {
                            scrollY = 0;
                        } else if (scrollY + dif - e.wheelRotation*5 < 0) {
                            scrollY = -dif;
                        } else {
                            scrollY -= e.wheelRotation*5;
                        }
                    }
                },
                // A list of tags.
                listGroup = Group {
                    translateY: bind scrollY
                    content: bind for (tag in tags)
                        Group {
                            translateY: indexof tag * (font.size + spacing)
                            var r: Rectangle;
                            content: [
                                // The rectangle manages tag selection.
                                r = Rectangle {
                                    height: 20
                                    width: 150
                                    smooth: false
                                    fill: Color.color(0, 0, 0, 0)
                                    onMouseClicked: function(e) {
                                        clickAction(tag);
                                    }
                                },
                                // A single tag.
                                Text {
                                    fill: bind if (r.hover) selectionColor else color
                                    translateX: 5
                                    translateY: 12
                                    content: tag
                                    font: font
                                }]
                        }
                }
            ]
        }
    }

Figure 7: TagList.create Method