Create a randomly falling image animation
From SpinetiX Support Wiki
This page is related to jSignage.
Contents
Introduction
This tutorial extends the "Display images at random positions" tutorial to add a fly-down effect on the images.
In the second part of the tutorial a zoom effect is added on some of the images. When the image reaches the middle of the screen, it is zoomed up to fill the screen.
The images are located in a specific folder.
Tutorial
- Difficulty: High, programming skills required.
- Total duration: 15-20 minutes.
- Requirements:
- First follow this tutorial: Display images at random positions.
- HMP200 or later, preferably using the latest firmware;
- Elementi (Elementi X recommended);
- A text editor such as Notepad++
Adding motion
The first step is to rename the Project created in Create custom image display to "Tutorial Image Fly". We can do this directly from the Project tab of the Browse panel. Note that we will also need to change the location of the explorer window to shell:Personal\SpinetiX\My Projects\Tutorial Image Fly
.
Create a random path
As we want to animate the images along a path, we will need to create a function to create such path.
function random_path( startPoint, endPoint, nbStops, maxHorizontal ){
var deltaX = ( endPoint.x - startPoint.x ) / ( nbStops + 1 );
var deltaY = ( endPoint.y - startPoint.y ) / ( nbStops + 1 );
maxHorizontal = maxHorizontal || [ -deltaX/2, deltaX/2 ];
var path = new $.pathData();
path.moveTo( startPoint.x, startPoint.y );
var current = startPoint;
for ( var i=0; i<nbStops; i++ ){
var incX = deltaX + Math.round( Math.random()*(maxHorizontal[1]-maxHorizontal[0]) + maxHorizontal[0] );
current = { x: current.x + incX, y: current.y + deltaY };
path.lineTo( current.x, current.y );
}
path.lineTo( endPoint.x, endPoint.y );
return path;
}
Let's explain the code in details:
-
function random_path( startPoint, endPoint, nbStops, maxHorizontal ){ ... }
- We create a function, taking a few user parameters: The starting and ending points of the path, the number of stops, and we also allow the user to specify a maximum horizontal motion between each stops. There are no parameters for the vertical direction as we want all image to fly down at the same speed.
-
var deltaX = ( endPoint.x - startPoint.x ) / ( nbStops + 1 );
- We compute the average increment along the x direction in order to go from the starting point to the ending point in the horizontal direction.
-
var deltaY = ( endPoint.y - startPoint.y ) / ( nbStops + 1 );
- Same for the vertical direction.
-
maxHorizontal = maxHorizontal || [ -deltaX/2, deltaX/2 ];
- If the user provided a maximum motion for the horizontal direction, we use it. Otherwise we constrain the random part of the motion to half of the average step motion.
-
var path = new $.pathData();
- We create a new path object using the jSignage $.pathData().
-
path.moveTo( startPoint.x, startPoint.y );
- We use the startPoint as the first element of our path.
-
var current = startPoint;
- We create a variable to store the current coordinate of our path.
-
for ( var i=0; i<nbStops; i++ ){ ... }
- We can now create a loop for each one of the stops we need to add to our path.
-
var incX = deltaX + Math.round( Math.random()*(maxHorizontal[1]-maxHorizontal[0]) + maxHorizontal[0] );
- This is the random part of the path, we compute an increment on the X direction by adding a random value between maxHorizontal[0] and maxHorizontal[1], i.e. the two maximum motion provided by the user.
-
current = { x: current.x + incX, y: current.y + deltaY };
- We update the value of the current point using the random increment in the X direction and the constant motion in the Y direction.
-
path.lineTo( current.x, current.y );
- We can now add this new point to our path.
-
path.lineTo( endPoint.x, endPoint.y );
- Finally we add the end point to our path.
-
return path;
- We can now return the resulting path to the called to be used in the animation.
Modify the display_image() function
Instead of adding the image at a random position in the screen as this was done in the Create custom image display tutorial, we need to animate it from the top to the bottom of the screen.
function display_image( href ) {
var x = Math.round( Math.random() * (1920-config.width) );
var beginTime = $('svg')[0].getCurrentTime();
var media = $.image( {
href: href,
height: config.height, width: config.width,
begin: beginTime,
dur: config.duration
});
var start = { x: x, y: -config.height };
var end = { x: x, y: 1080+config.height };
var path = random_path( start, end, 10, [-100, 100], [0,0] );
$.svgAnimation( media[0], 'animateMotion', {
path: path,
begin: beginTime,
dur: config.duration,
calcMode: 'paced'
});
media.removeAfter();
media.addTo('#layers');
}
There are a few modification compared to the previous code.
Two lines have disappeared:
- Generating a random y positions as this is no longer necessary
- The top, and left position of the image. As we will animate the position, it is no longer necessary to specify it when creating the media.
A few new lines have been added:
-
var beginTime = $('svg')[0].getCurrentTime();
- As we need to sync both the apparition of the image, and the animation, we need to know the current time. We use the jSignage getCurrentTime() for this.
-
begin: beginTime,
- We now explicitly declare the begin time of the media to insure that it will be done at the same time as the animation starts.
-
var start = { x: x, y: -config.height };
- We create the starting point of our path. The X position was chosen at random, whereas, the Y position is located outside the top of the screen.
-
var end = { x: x, y: 1080+config.height };
- We create the ending point of our path. The X position is the same as the starting position (so that our image fall in average along a straight line). The Y position is located outside the bottom of the screen.
var path = random_path( start, end, 10, [-100, 100], [0,0] );
- This is the creation of the random path itself using the function from the previous section. We are using 10 intermediate points, and a maximum random motion of 100 pixel on each direction. This will insure a smooth and not too fast motion of our images.
-
$.svgAnimation( media[0], 'animateMotion', { ... } );
- We create the animation itself using the jSignage svgAnimation() function. The first argument is our media, the second is the parameter of the animation itself.
-
path: path,
- This is the path along which our image will move.
-
begin: beginTime,
- This is the begin time of the animation, which is the same as the one used for the display of the media.
-
dur: config.duration,
- This is the duration of our animation. It is the same as the one used to display our media.
-
calcMode: 'paced'
- This options enforce a constant speed of the animation.
Putting it all together
Finally, we can create the full script putting all component together:
var config = $.parseJSON( $('#jsonConfig').text() );
function random_path( startPoint, endPoint, nbStops, maxHorizontal ){
var deltaX = ( endPoint.x - startPoint.x ) / ( nbStops + 1 );
var deltaY = ( endPoint.y - startPoint.y ) / ( nbStops + 1 );
maxHorizontal = maxHorizontal || [ -deltaX/2, deltaX/2 ];
var path = new $.pathData();
path.moveTo( startPoint.x, startPoint.y );
var current = startPoint;
for ( var i=0; i<nbStops; i++ ){
var incX = deltaX + Math.round( Math.random()*(maxHorizontal[1]-maxHorizontal[0]) + maxHorizontal[0] );
current = { x: current.x + incX, y: current.y + deltaY };
path.lineTo( current.x, current.y );
}
path.lineTo( endPoint.x, endPoint.y );
return path;
}
function display_image( href ) {
var x = Math.round( Math.random() * (1920-config.width) );
var beginTime = $.getCurrentTime();
var media = $.image( {
href: href,
height: config.height, width: config.width,
begin: beginTime,
dur: config.duration
});
var start = { x: x, y: -config.height };
var end = { x: x, y: 1080+config.height };
var path = random_path( start, end, 10, [-100, 100], [0,0] );
$.svgAnimation( media[0], 'animateMotion', {
path: path,
begin: beginTime,
dur: config.duration,
calcMode: 'paced'
});
media.removeAfter();
media.addTo('#layers');
}
function data_display( data ) {
var freq = config.duration / config.nbImage;
$.setInterval( function() {
var imgIdx = Math.floor( Math.random() * data.length );
var href = data[imgIdx].href;
display_image( href );
}, freq*1000 );
}
$(function(){
$.getData( config.dataSource, data_display );
});
Adding zoom
We now want to add a zoom effect on some image. The idea is that one of the falling down images, stops in the middle of the screen and starts zooming on top of the other images.
Adding options
The first step is to add more options to our main svg file
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" dur="indefinite" viewBox="0 0 1920 1080">
<defs xml:id="jsonConfig"><![CDATA[{
"dataSource": { "parser": { "resourcetype": "file", "type": "dir" }, "src": "media", "type": "uri" },
"duration": 10,
"height": 216, "width": 384,
"nbImage": 20,
"zoom": 3,
"zoomDur": 5
}]]></defs>
<spx:properties xmlns:spx="http://www.spinetix.com/namespace/1.0/spx">
<spx:json-data name="Data source" propertyName="dataSource" type="uri" xlink:href="#jsonConfig"/>
<spx:json-number name="Nb images" propertyName="nbImage" xlink:href="#jsonConfig"/>
<spx:json-number name="Image duration" propertyName="duration" xlink:href="#jsonConfig"/>
<spx:json-number name="Zoom duration" propertyName="zoomDur" xlink:href="#jsonConfig"/>
<spx:json-number name="Image width" propertyName="width" xlink:href="#jsonConfig"/>
<spx:json-number name="Image height" propertyName="height" xlink:href="#jsonConfig"/>
<spx:json-number name="Zoom factor" propertyName="zoom" xlink:href="#jsonConfig"/>
</spx:properties>
<script xlink:href="http://download.spinetix.com/spxjslibs/jSignage.js"/>
<script xlink:href="anim.js"/>
<g id="layers"/>
<g id="zoomed"/>
</svg>
We add 2 new options (zoom factor and duration), with their interface. We also add one more place older:
-
<g id="zoomed"/>
- Placeholder for the zoomed image. This will insure that the zoomed image is in front of the other images.
Modifying the data_display()
function data_display( data ) {
var cnt = 0;
var delay = config.duration / config.nbImage;
$.setInterval( function() {
var imgIdx = Math.floor( Math.random() * data.length );
var href = data[imgIdx].href;
if ( config.zoomDur>0 && cnt === 0 ){
display_image_zoom( href );
cnt = Math.ceil( config.zoomDur/delay );
} else {
display_image( href );
cnt --;
}
}, delay*1000 );
}
We add an extra code, to call a new function ( display_image_zoom()
) for some of the images. The frequency at which the new function is called depends on the duration of the zoom effect. The purpose is that each time a image has finished zooming a new is starts.
Note that setting config.zoomDur
to zero will remove the zoom effect.
New display_image_zoom() function
We now need to create a new function to handle images that will zoom in the middle of the flying down.
First we need to compute a few variable that will help us in the rest of the function
var zoomHeight = config.height * config.zoom;
var zoomWidth = config.width * config.zoom;
var zoomInDur = config.zoomDur * 0.4;
var zoomOutDur = config.zoomDur * 0.1;
var zoomStopDur = config.zoomDur - zoomInDur - zoomOutDur;
var totDur = config.duration + config.zoomDur;
var x = zoomWidth/2 + Math.round( Math.random() * (1920-zoomWidth) );
var beginTime = $.getCurrentTime();
We compute the height and width of the zoomed images, and we setup to zoom in and zoom out time. In this example, the zoom in time corresponds to 40% of the total zoom, whereas the zoom out time is only 10%. The X still random, but we make sure that the zoomed image will be inside the screen.
var g = $.g( { begin: beginTime, dur: config.duration + config.zoomDur });
g.removeAfter();
g.addTo( '#layers' );
var g2 = $.g( { begin: 0 });
g2.addTo( g );
var media = $.image( {
href: href,
top: -zoomHeight/2, left: -zoomWidth/2,
height: zoomHeight, width: zoomWidth,
begin: 0,
dur: totDur
});
media.addTo( g2 );
Instead of creating a single media in our rendering tree, we need to use additional groups. The purpose of those groups, is to allow the zoom animation to be centered on the image. let us explain the following details:
var g = $.g( { begin: beginTime, dur: config.duration + config.zoomDur });
- This is the main group, responsible for the timing and the position of the media. As we will see below, this is the group whose position will be animated.
var g2 = $.g( { begin: 0 });
- This group will be used for the zoom. We must insure that it starts at 0, relatively to it's parent (g).
var media = $.image( { top: -zoomHeight/2, left: -zoomWidth/2, begin: 0, ... }
- This is the media itself. We add some top and left coordinate, so that the media is centered in the middle. We also make sure it start at 0 relatively to its parent (g2)
-
height: zoomHeight, width: zoomWidth,
- the full zoomed size is used for the media. We will use an animation to reduce it size while flying down. The reason we use the full size when creating the media is to make sure that the rendering engine will use the beste quality possible for this media when it is displayed in its large size.
var start = { x: x, y: -config.height };
var middle = { x: x, y: 1080/2 };
var end = { x: x, y: 1080+config.height };
var path = random_path( start, middle, 5, [-100, 100], [0,0] );
$.svgAnimation( g[0], 'animateMotion', {
path: path,
begin: beginTime,
dur: config.duration/2,
calcMode: 'paced',
fill: "freeze"
});
var path2 = random_path( middle, end, 5, [-100, 100], [0,0] );
$.svgAnimation( g[0], 'animateMotion', {
path: path2,
begin: beginTime + config.duration/2 + config.zoomDur,
dur: config.duration/2,
calcMode: 'paced'
});
As we are going to zoom the image in the middle of the fly down path, we need to add a new point in the middle, and split the path in 2. We then create 2 animations, one from the top of the screen to the middle, and one from the middle of the screen to the bottom. The duration of the animation is set to dur: config.duration/2
, and the second annimation starts once the zoom is finished (begin: beginTime + config.duration/2 + config.zoomDur
).
$.svgAnimation( g2[0], 'animateTransform', {
attributeName: 'transform',
type: 'scale',
values: 1/config.zoom + ";" +1/config.zoom +";1;1;" + 1/config.zoom + ";" + 1/config.zoom,
keyTimes: "0;" + config.duration/2/totDur + ";"
+ (config.duration/2+zoomInDur)/totDur + ";"
+ (config.duration/2+zoomInDur+zoomStopDur)/totDur + ";"
+ (config.duration/2+config.zoomDur)/totDur + ";1",
begin: beginTime,
dur: totDur,
additive: "sum"
});
We can now add the zooming animation. The zooming animation will be active durring all the time the image is visible on the screen. It has 6 keyTimes.
- Start of the display of the image
- Start of the zooming in
- Begining of the display of the image in full resolution
- End of the display of the full resolution image
- End of zooming out
- End of the display of the image.
It is interesting to to note that the target of the animation is g2
and not the media itself. This will insure the zoom is centered on the media.
$.setTimeout( function () { g.addTo( '#zoomed' ); }, config.duration/2 * 1000 );
Finally, we need to insure that the zoomed media is in front of the other images. To do so, we move the media from the "layers" placehoder, to the "zoomed" placeholder at the begining ot the zoom. To do so, we use a timer.
The complete function can be found bellow.
function display_image_zoom( href ) {
var zoomHeight = config.height * config.zoom;
var zoomWidth = config.width * config.zoom;
var zoomInDur = config.zoomDur * 0.4;
var zoomOutDur = config.zoomDur * 0.1;
var zoomStopDur = config.zoomDur - zoomInDur - zoomOutDur;
var totDur = config.duration + config.zoomDur;
var x = zoomWidth/2 + Math.round( Math.random() * (1920-zoomWidth) );
var beginTime = $.getCurrentTime();
var g = $.g( { begin: beginTime, dur: config.duration + config.zoomDur });
g.removeAfter();
g.addTo( '#layers' );
var g2 = $.g( { begin: 0 });
g2.addTo( g );
var media = $.image( {
href: href,
top: -zoomHeight/2, left: -zoomWidth/2,
height: zoomHeight, width: zoomWidth,
begin: 0,
dur: totDur
});
media.addTo( g2 );
var start = { x: x, y: -config.height };
var middle = { x: x, y: 1080/2 };
var end = { x: x, y: 1080+config.height };
var path = random_path( start, middle, 5, [-100, 100], [0,0] );
$.svgAnimation( g[0], 'animateMotion', {
path: path,
begin: beginTime,
dur: config.duration/2,
calcMode: 'paced',
fill: "freeze"
});
var path2 = random_path( middle, end, 5, [-100, 100], [0,0] );
$.svgAnimation( g[0], 'animateMotion', {
path: path2,
begin: beginTime + config.duration/2 + config.zoomDur,
dur: config.duration/2,
calcMode: 'paced'
});
$.svgAnimation( g2[0], 'animateTransform', {
attributeName: 'transform',
type: 'scale',
values: 1/config.zoom + ";" +1/config.zoom +";1;1;" + 1/config.zoom + ";" + 1/config.zoom,
keyTimes: "0;" + config.duration/2/totDur + ";"
+ (config.duration/2+zoomInDur)/totDur + ";"
+ (config.duration/2+zoomInDur+zoomStopDur)/totDur + ";"
+ (config.duration/2+config.zoomDur)/totDur + ";1",
begin: beginTime,
dur: totDur,
additive: "sum"
});
$.setTimeout( function () { g.addTo( '#zoomed' ); }, config.duration/2 * 1000 );
}
Putting it all together
The final version of the widget can be downloaded using the link on the left. A set of food images are use as images, but you can edit the content of the media folder using Elementi.