anim_morphs.pl
Creates an animated GIF that morphs image tiles asynchronously.

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

Leonardo da Vinci
Here instead, after slicing and dicing the images into tiles, each tile is individually morphed to its corresponding number on the next image using a uniform number of intermediate frames. Then the display of each resulting sequence of morphed tiles starts randomly. But to avoid total mayhem, the transition between the next pairing of images can only begin once the previous one has terminated. This creates the illusion of each image fading away in a piecemeal fashion:
Leonardo da Vinci, Mona Lisa, Saint Anne

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.

If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.


anim_morphs.pl -- [Download latest version: v1.0.0 - October 30, 2009]   [MD5 checksum]
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
			

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.