Animating Photos from Flickr
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.
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.
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.
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.
// 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.
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.
// 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.
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