Pretty much all of the tile animations I've created so far have involved synchronous displacements. That is, all the tiles start and end their transits simultaneously, regardless of the distance travelled. In this article, I explored asynchronous timing of the tile animations.
The special effect chosen is morphing between various images. In the simplest case, one morphs a whole image into the next, as in
To convey the parameters of the animation, an XML file is used. To explain the required tags, let's examine the data file demo_morphs.xml ( [Download demo_morphs.xml] [MD5 checksum] ). It governs the creation of the latter animated GIF image:
| XML file "demo_morphs.xml" | Remarks |
|---|---|
| <animation> | start of XML declaration |
| <output>demo_morphs.gif</output> | name of the output file |
| <frames>5</frames> | the number of intermediate, interpolated frames for each morphing sequence |
| <delay>15</delay> | the number of milliseconds in delaying the image views |
| <loops>0</loops> | the number of times to cycle the animated GIF: 0 results in infinite looping |
| <tile> | start of tile specifications |
| <width>10</width> | in pixels |
| <height>10</height> | in pixels |
| </tile> | end of tile specifications |
| <imageDims> | all constituent images to be scaled initially to the following pixel dimensions |
| <width>100</width> | in pixels |
| <height>150</height> | in pixels |
| </imageDims> | end of image specifications |
| <image>Leonardo.jpg</image> | filename of the 1st image. Images are processed in the order of their declaration. |
| <image>Mona_Lisa.jpg</image> | filename of the 2nd image |
| <image>webpraxis.jpg</image> | filename of the 3rd image |
| <image>St_Anne.jpg</image> | filename of the 4th image |
| </animation> | end of XML declaration |
For the processing phase, Perl is used along with ImageMagick's drawing primitives accessed through its PerlMagick interface. The Perl script anim_morph.pl, displayed below, is the resulting code. It is released for personal, non-commercial and non-profit use only.
The listing includes the line numbers in order to reference them in the following general remarks.
perl anim_morphs.pl demo_morphs.xml
Here we are requesting that the XML file "demo_morphs.xml" be processed. A screen shot at the end of processing is:
If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.
001 use strict;
002 use warnings;
003 use POSIX qw(ceil);
004 use Image::Magick;
005 use Term::ANSIScreen qw/:color :cursor :screen/;
006 use Win32::Console::ANSI qw/ Cursor /;
007 use XML::Simple;
008 $XML::Simple::PREFERRED_PARSER = 'XML::Parser';
009 use constant FATAL => colored ['white on red'], "\aFATAL ERROR: ";
010 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
011 # anim_morphs.pl: Creates an animated GIF that morphs image tiles asynchronously.
012 #===============================================================================================================================
013 # Usage : perl anim_morphs.pl XmlFile
014 # Arguments : XmlFile = path of XML file for the animation specifications.
015 # Input Files : See arguments.
016 # Output Files : The animated GIF specified in the XML data file.
017 # Temporary Files : None.
018 # Remarks : See http://www.webpraxis.ab.ca/morphs/anim_morphs.shtml for details.
019 # History : v1.0.0 - October 30, 2009 - Original release.
020 #===============================================================================================================================
021 # 0) INITIALIZE:
022 $| = 1; #set STDOUT buffer to auto-flush
023 cls(); #clear screen
024 print colored ['black on white'], "$0\n\n\n", #display program name
025 colored ['reset'], 'Initializing... '; #report start of initialization
026 srand( time() ^ ($$ + ($$ << 15)) ); #seed the random number generator
027
028 my $XmlFile = shift || die FATAL, 'No XML file specified'; #get path of XML data file
029 my $Anim = XMLin( $XmlFile, ForceArray => [qw(image)] ); #read the XML data file
030 my $TileWidth = $$Anim{tile}{width}; #parameterize pixel width of a tile
031 my $TileHeight = $$Anim{tile}{height}; #parameterize pixel height of a tile
032 my $NoImages = @{ $$Anim{image} }; #get the number of source images
033 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner
034
035 my $NoTileCols = POSIX::ceil( $$Anim{imageDims}{width} / $TileWidth ); #compute integral number of tile columns
036 my $NoTileRows = POSIX::ceil( $$Anim{imageDims}{height} / $TileHeight ); #compute integral number of tile rows
037 my $TileCount = $NoTileCols * $NoTileRows; #compute total number of tiles
038
039 my $CanvasWidth = $NoTileCols * $TileWidth; #set canvas width
040 my $CanvasHeight = $NoTileRows * $TileHeight; #set canvas height
041 print colored ['bold green'], "XML data read\n\n"; #report end of initialization
042 #-------------------------------------------------------------------------------------------------------------------------------
043 # 1) LOAD AND SCALE THE IMAGES:
044 print 'Reading & scaling images... '; #report start of image processing
045 my $Images = Image::Magick->new( magick => 'JPG' ); #instantiate an object for the images
046
047 die FATAL, "At least 2 images required\n" if $NoImages < 2; #check for at least 2 images
048 for( 0..$NoImages-1 ) { #repeat for each source image
049 my $file = $$Anim{image}[$_]; # parameterize the file name
050 die FATAL, "Cannot locate image file '$file'\n" unless -e $file; # check image existence
051 $Images->Read( $file ); # read image file
052 } #until all images processed
053 $Images->Quantize( colors => 256, colorspace => 'RGB' ); #ensure uniform color space
054 $Images->Scale( geometry => "${CanvasWidth}x${CanvasHeight}!" ); #scale images to canvas dimensions
055 print colored ['bold green'], "Done\n\n"; #report end of image processing
056 #-------------------------------------------------------------------------------------------------------------------------------
057 # 2) CREATE TILE IMAGES AND RECORD THEIR LOCATIONS:
058 print 'Creating tile images... '; #report start of tile processing
059 my ( $CursorX, $CursorY ) = Cursor(); #record cursor position
060
061 my $Tiles = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the tiles
062 my @Tile_X; #array for tile top-left x-coordinates
063 my @Tile_Y; #array for tile top-left y-coordinates
064 { #start naked block as firewall
065 my $tile = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for a tile
066
067 for my $imgIdx ( 0..$NoImages-1 ) { # repeat for each source image
068 for ( my $y_topLeft = 0; # for each tile row: start at top and work down
069 $y_topLeft <= $CanvasHeight - $TileHeight;
070 $y_topLeft += $TileHeight
071 ) {
072 for ( my $x_topLeft = 0; # for each tile column: work left to right
073 $x_topLeft <= $CanvasWidth - $TileWidth;
074 $x_topLeft += $TileWidth
075 ) {
076 my $geometry = "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft"; # define tile geometry
077 $tile = $Images->[$imgIdx]->Clone(); # init tile with image
078 $tile->Crop( geometry => $geometry ); # crop tile area
079 $tile->Set( page => '0x0+0+0' ); # shrink canvas
080 push @$Tiles, $tile; # store the tile image
081 push @Tile_X, $x_topLeft; # store the tile coords
082 push @Tile_Y, $y_topLeft;
083 print locate( $CursorY, $CursorX ), clline, # report tile processing
084 colored ['bold yellow'], '(', $Spinners[$#$Tiles % 4], ')';
085 } # until all tile columns processed
086 } # until all tile rows processed
087 } # until all images processed
088 undef $tile; # destroy the tile image object
089 } #end naked block
090 print locate( $CursorY, $CursorX ), clline, #report end of tile processing
091 colored ['bold green'], scalar @Tile_X, " tiles\n\n";
092 #-------------------------------------------------------------------------------------------------------------------------------
093 # 3) CREATE MORPHED IMAGES FOR EACH TILE:
094 print 'Morphing each tile... '; #report start of morphing process
095 ( $CursorX, $CursorY ) = Cursor(); #record cursor position
096
097 my @Countdown; #countdowns for start of morphing display
098 my @Morphs; #morphed images for each tile
099 { #start naked block as firewall
100 my $tiles2morph = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for tile pairs
101
102 for my $imgIdx ( 0..$NoImages-1 ) { # repeat for each source image
103 for my $tileIdx ( $imgIdx*$TileCount..($imgIdx+1)*$TileCount - 1 ){ # repeat for each of the image's tiles
104 @$tiles2morph = (); # clear tile pairs
105 push @$tiles2morph, $Tiles->[$tileIdx], # set current tile as start image
106 $Tiles->[($tileIdx+$TileCount)%($NoImages*$TileCount)]; # and corresponding tile of next image as end image
107 $Morphs[$tileIdx] = $tiles2morph->Morph( frames => $$Anim{frames} ); # generate sequence of morphed images
108 $Countdown[$tileIdx] = int rand( int $TileCount / 10 ); # set random countdown
109 print locate( $CursorY, $CursorX ), clline, # report morphing process
110 colored ['bold yellow'], '(', $Spinners[$#Morphs % 4], ')';
111 } # until all image's tiles processed
112 } # until all images processed
113 undef $tiles2morph; # destroy the image object for tile pairs
114 } #end naked block
115 undef $Tiles; #destroy the image object for the tiles
116 print locate( $CursorY, $CursorX ), clline, #report end of morphing process
117 colored ['bold green'], scalar @Morphs, " morph sequences\n\n";
118 #-------------------------------------------------------------------------------------------------------------------------------
119 # 4) CREATE ANIMATION FRAMES:
120 print 'Creating animation frames... '; #report start of frame processing
121 ( $CursorX, $CursorY ) = Cursor(); #record cursor position
122
123 my $Canvas = $Images->[0]; #init canvas with zeroth image
124 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames
125 my $FrameNo = 0; #init no of animation frames
126 my $NoSequencesDone; #number of morph-sequence displays ended per image
127 undef $Images; #destroy the source image object
128 { #start naked block as firewall
129 my $morphedTile; # morphed tile to be displayed
130 for my $imgIdx ( 0..$NoImages-1 ) { # repeat for each source image
131 $NoSequencesDone = 0; # reset ended-sequence count for new image
132 until( $NoSequencesDone == $TileCount ) { # repeat
133 $NoSequencesDone = 0; # reset ended-sequence count for current image
134 for my $tileIdx ($imgIdx*$TileCount..($imgIdx+1)*$TileCount -1 ){ # repeat for each of the image's tiles
135 next if --$Countdown[$tileIdx] > 0; # decrement countdown & skip if greater than zero
136 $Canvas->Composite # add any morphed tile image to canvas
137 ( image => $morphedTile,
138 compose => 'Over',
139 geometry => "${TileWidth}x${TileHeight}+$Tile_X[$tileIdx]+$Tile_Y[$tileIdx]"
140 )
141 if $morphedTile = shift @{$Morphs[$tileIdx]};
142 ++$NoSequencesDone unless $Morphs[$tileIdx]->[0]; # increment sequence counter if no morphed images left
143 } # until all tiles processed
144 push @$Frames, $Canvas->Clone(); # add canvas to animation frames
145 print locate( $CursorY, $CursorX ), clline, # report frame processing
146 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
147 } # until all morph sequences displayed
148 } # until all images processed
149 undef $morphedTile; # destroy morphed tile object
150 } #end naked block
151 print locate( $CursorY, $CursorX ), clline, #report end of frame processing
152 colored ['bold green'], scalar @$Frames, " frames\n\n";
153 undef $Canvas; #destroy the canvas object
154 undef @Countdown; #destroy the array for the countdowns
155 undef @Morphs; #destroy the image object for the morph sequences
156 undef @Tile_X; #destroy the arrays for the tile coordinates
157 undef @Tile_Y;
158 #-------------------------------------------------------------------------------------------------------------------------------
159 # 5) CREATE ANIMATED GIF IMAGE:
160 print 'Creating animated GIF image... '; #report start of animation processing
161 $Frames->Write #output the animation
162 ( delay => $$Anim{delay},
163 loop => $$Anim{loops},
164 dispose => 'background',
165 filename => $$Anim{output}
166 );
167 print colored ['bold green'], $$Anim{output}, "\n"; #report end of animation processing
168 exit;
169 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
170 # end of anim_morphs.pl
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.