anim_grid_transits.pl
Creates an animated GIF of images translating about a grid.

Whilst staring at a tiled floor, I was inspired to write a Perl script that would create an animated GIF where a group of images could be choreographed to translate to specified locations on a grid.

To make the images transit from one point to another is not technically demanding. All the required intermediate coordinates can be linearly interpolated using the parametric representation for a line segment. The parametric form has the advantage of not necessitating special attention to the case of a vertical line segment as would be the case with the more familiar Cartesian representation y = mx + b. Thus, for any point P(x,y) on the line segment between P(x0,y0) and P(x1,y1), we have

x = λx1 + (1 - λ)x0, and y = λy1 + (1 - λ)y0,
where λ ϵ [0,1]. Letting λ evolve as λ|0→1 results in the directed line segment from P(x0,y0) to P(x1,y1).

Two caveats to keep in mind here. Points on a line are real numbers and thus dense, whereas screen pixels are integers and thus discreet. So using the parametric representation for interpolating pixel coordinates will always require taking the integer part of the computational results. Consequently, slight discrepancies might occur between the actual and theoretical positioning of the image. Furthermore, roundoff often comes into play when incrementing λ by a given step size. For example, a step size of 0.1 does not have a finite binary representation. So, after 10 steps, λ will be slightly less than 1 and the image being transited might be off by one or two pixels from its theoretical final location. Hence, should these types of errors have a detrimental effect on the quality of the animation, certain counter-measures will be required.

The above is the easy part of my objective. The real crux is devising a simple way of describing the required image choreography. And by simple I mean that I want the following features:

Entering all the data on the command line is not practical. Instead, an external data file is called for. One possibility might be a spreadsheet. I opted instead to go with an XML file as it is a straightforward matter to import its content into a Perl script with the use of the CPAN modules XML::Simple and XML::Parser.

To explain the required XML tags, let's examine the data file demo.xml ( [Download demo.xml]   [MD5 checksum] ). It governs the creation of the following animated GIF image:

Leonardo+Mona+Anne+Vitruvius_Squirrel
XML file "demo.xml" Remarks Animation breakdown
<animation> Animation spanning a 3 x 3 grid
<output>demo.gif</output> name of the output file
<frames>14</frames> the number of intermediate, interpolated frames for each transit
<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
<imageDims> all constituent images to be scaled to the following pixel dimensions
<width>100</width>
<height>150</height>
</imageDims> end of image specifications
<transit> start of 1st transit declaration start of transit:


end of transit:

<image> 1st image: to remain at center (2,2)
<file>webpraxis.jpg</file> name of the source image file
<start>2,2</start> grid reference for the start location
<end>2,2</end> grid reference for the end location
</image>
<image> 2nd image: at top left corner (1,1) moving to top right corner (1,3)
<file>Leonardo.jpg</file>
<start>1,1</start>
<end>1,3</end>
</image>
<image> 3rd image: at top right corner (1,3) moving to top left corner (1,1)
<file>Mona_Lisa.jpg</file>
<start>1,3</start>
<end>1,1</end>
</image>
<image> 4th image: at bottom left corner (3,1) moving to bottom right corner (3,3)
<file>St_Anne.jpg</file>
<start>3,1</start>
<end>3,3</end>
</image>
<image> 5th image: at bottom right corner (3,3) moving to bottom left corner (3,1)
<file>Vitruve.jpg</file>
<start>3,3</start>
<end>3,1</end>
</image>
</transit> end of 1st transit declaration
<transit> start of 2nd transit declaration end of transit:
<image> 1st image: to continue remaining at center (2,2)
<file>webpraxis.jpg</file>
<end>2,2</end> the starting point is taken to be the end point in the previous transit
</image>
<image> 2nd image: to move to bottom right corner (3,3) from previous location
<file>Leonardo.jpg</file>
<end>3,3</end>
</image>
<image> 3rd image: to move to bottom left corner (3,1) from previous location
<file>Mona_Lisa.jpg</file>
<end>3,1</end>
</image>
<image> 4th image: to move to top right corner (1,3) from previous location
<file>St_Anne.jpg</file>
<end>1,3</end>
</image>
<image> 5th image: to move to top left corner (1,1) from previous location
<file>Vitruve.jpg</file>
<end>1,1</end>
</image>
</transit> end of 2nd transit declaration
<transit> start of 3rd transit declaration end of transit:
<image> 1st image: to continue remaining at center (2,2)
<file>webpraxis.jpg</file>
<end>2,2</end>
</image>
<image> 2nd image: to move to center (2,2)
<file>Leonardo.jpg</file>
<end>2,2</end>
</image>
<image> 3rd image: to move to center (2,2)
<file>Mona_Lisa.jpg</file>
<end>2,2</end>
</image>
<image> 4th image: to move to center (2,2)
<file>St_Anne.jpg</file>
<end>2,2</end>
</image>
<image> 5th image: to move to center (2,2)
<file>Vitruve.jpg</file>
<end>2,2</end>
</image>
</transit> end of 3rd transit declaration
<transit> start of 4th and final transit declaration end of transit:
<image> 1st image: to move to middle bottom (3,2) from previous location
<file>Vitruve.jpg</file> note the change in the image order which does not affect the outcome
<end>3,2</end>
</image>
<image> 2nd image: to move to middle right (2,3) from previous location
<file>St_Anne.jpg</file>
<end>2,3</end>
</image>
<image> 3rd image: to move to middle left (2,1) from previous location
<file>Mona_Lisa.jpg</file>
<end>2,1</end>
</image>
<image> 4th image: to move to middle top (1,2) from previous location
<file>Leonardo.jpg</file>
<end>1,2</end>
</image>
<image> 5th image: at center (2,2)
<file>squirrel.gif</file> new image being introduced, replacing the previous one at the center. And, yes, it's the "Banff Crasher Squirrel".
<start>2,2</start> starting point needs to be stated because it doesn't have an end point in the previous transit
<end>2,2</end>
</image>
</transit> end of 4th transit declaration
</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_grid_transits.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.

The script can be readily modified to include ancillary imagery. For example, instead of initializing each frame as just a blank canvas, a static image can be added as in the following animation:

plants
Here, the core "plants" component consists of two transits of 74 very small floral images. Once the basic animation was established, the urn was added to each frame via the supplementary statement
$Canvas->Composite( image => $Pot, compose => 'Over', geometry => '100x63+212+75' );

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


anim_grid_transits.pl -- [Download latest version: v1.0.0 - September 15, 2009]   [MD5 checksum]
001 use strict;
002 use warnings;
003 use Image::Magick;
004 use List::AllUtils qw(max);
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_grid_transits.pl: Creates an animated GIF of images translating about a grid.
012 #===============================================================================================================================
013 #           Usage : perl anim_grid_transits.pl XmlFile
014 #       Arguments : XmlFile = path of XML file describing the animation sequence.
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/transits/anim_grid_transits.shtml for details.
019 #         History : v1.0.0 - September 15, 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
027 my $XmlFile     = shift || die FATAL, 'No XML file specified';                          #get path of XML data file
028 my $Anim        = XMLin( $XmlFile, ForceArray => [qw(transit image)] );                 #read the XML data file
029 my $ImgWidth    = $$Anim{imageDims}{width};                                             #parameterize the width of the images
030 my $ImgHeight   = $$Anim{imageDims}{height};                                            #parameterize the height of the images
031 my $ImgDims     = "${ImgWidth}x${ImgHeight}";                                           #set dimension declaration of the images
032 my %ImgFile2Idx;                                                                        #hash for matching image files to ImageMagick indexes
033 print colored ['bold green'], "XML data read\n\n";                                      #report end of initialization
034 #-------------------------------------------------------------------------------------------------------------------------------
035 # 1) LOAD AND SCALE THE IMAGES:
036 print 'Reading & scaling images... ';                                                   #report image processing
037 my $Images = Image::Magick->new( magick => "JPG" );                                     #instantiate an object for the images
038 for my $transitIdx ( 0..$#{$$Anim{transit}} ) {                                         #repeat for each transit declaration
039     for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {                     # repeat for each corresponding image
040         my $file = $$Anim{transit}[$transitIdx]{image}[$imgIdx]{file}                   #  parameterize the file name
041                     || die FATAL, "Missing filename declaration\n";
042         next if defined $ImgFile2Idx{$file};                                            #  skip image if already processed
043         die FATAL, "Cannot locate image file '$file'\n" unless -e $file;
044         $Images->Read( $file );                                                         #  read image file
045         $ImgFile2Idx{$file} = $#{$Images};                                              #  correlate file name to image array index
046     }                                                                                   # until all images processed
047 }                                                                                       #until all transits processed
048 $Images->Quantize( colors => 256, colorspace => 'RGB' );                                #ensure uniform color space
049 $Images->Scale( geometry => "$ImgDims!" );                                              #scale images to specified dimensions
050 print colored ['bold green'], scalar keys %ImgFile2Idx, " unique images\n\n";           #report image processing
051 #-------------------------------------------------------------------------------------------------------------------------------
052 # 2) COMPUTE THE DIMENSIONS OF THE ANIMATION CANVAS:
053 print 'Computing canvas size... ';                                                      #report canvas processing
054 my $CanvasWidth;                                                                        #canvas width in pixels
055 my $CanvasHeight;                                                                       #canvas height in pixels
056 {                                                                                       #start naked block
057     my $maxCol  = undef;                                                                # maximum canvas column number
058     my $maxRow  = undef;                                                                # maximum canvas row number
059
060     for my $transitIdx ( 0..$#{$$Anim{transit}} ) {                                     # repeat for each transit declaration
061         for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {                 #  repeat for each corresponding image
062             my $refImage = $$Anim{transit}[$transitIdx]{image}[$imgIdx];                #   define a reference to the current image
063             unless( defined $$refImage{start} ) {                                       #   if missing start location
064                 my $match = ( grep  {   $$refImage{file}                                #    search for image index in previous transit
065                                         eq
066                                         $$Anim{transit}[$transitIdx-1]{image}[$_]{file}
067                                     }
068                                     0..$#{$$Anim{transit}[$transitIdx-1]{image}}
069                             )[0];
070                 $$refImage{start} = $$Anim{transit}[$transitIdx-1]{image}[$match]{end}  #    set start location to prev end location
071                  if defined $match;                                                     #     if match found
072             }                                                                           #   end if
073             for my $where ( 'start', 'end' ) {                                          #   repeat for each extreme image position
074                 die FATAL, "Missing $where location for image '$$refImage{file}'\n"     #    check that position is defined
075                  unless defined $$refImage{$where};
076                 my ( $row, $col )   = split /,/, $$refImage{$where};                    #    parse location data
077                 $maxRow             = max grep { defined $_ } $maxRow, $row;            #    update maximum canvas row number
078                 $maxCol             = max grep { defined $_ } $maxCol, $col;            #    update maximum canvas column number
079             }                                                                           #   until both positions processed
080         }                                                                               #  until all images processed
081     }                                                                                   # until all transits processed
082     $CanvasWidth    = $maxCol * $ImgWidth;                                              # compute canvas width
083     $CanvasHeight   = $maxRow * $ImgHeight;                                             # compute canvas height
084 }                                                                                       #end naked block
085 print colored ['bold green'], "${CanvasWidth}x${CanvasHeight} px\n\n";                  #report canvas processing
086 #-------------------------------------------------------------------------------------------------------------------------------
087 # 3) CREATE ANIMATION FRAMES:
088 print 'Creating animation frames... ';                                                  #report start of frame processing
089 my ( $CursorX, $CursorY )   = Cursor();                                                 #record cursor position
090 my $Canvas                  = Image::Magick->new( magick => "GIF" );                    #instantiate an image object for the canvas
091 my $Frames                  = Image::Magick->new( magick => "GIF" );                    #instantiate an image object for animation frames
092 my $FrameNo                 = 0;                                                        #init no of animation frames
093 my $Interpolate             = sub   {   my ( $lambda, $start, $end ) = @_;              #anonymous sub for interpolating x,y-coordinates
094                                         int( $lambda * $end + ( 1. - $lambda ) * $start );
095                                     };
096 my $DeltaLambda             = 1. / ( $$Anim{frames} + 1. );                             #set transit step-size
097 my @Spinners                = ( '-', '\\', '|', '/' );                                  #define symbols for spinner
098
099 for my $transitIdx ( 0..$#{$$Anim{transit}} ) {                                         #repeat for each transit declaration
100     for( my $lambda = 0.; $lambda < 1. + $DeltaLambda/2.; $lambda += $DeltaLambda ) {   # repeat for each transit step
101         @$Canvas = ();                                                                  #  clear the canvas
102         $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );                       #  set canvas size
103         $Canvas->ReadImage( 'xc:transparent' );                                         #  set canvas background to transparent
104         for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {                 #  repeat for each transit image
105             my $refImage            = $$Anim{transit}[$transitIdx]{image}[$imgIdx];     #   define a reference to the current image
106             my ($r_start,$c_start)  = split /,/, $$refImage{start};                     #   parse the start location
107             my ($r_end,  $c_end  )  = split /,/, $$refImage{end};                       #   parse the end location
108             my $x_topLeft           = &$Interpolate                                     #   compute top-left image x-coordinate:
109                                         (   $lambda,
110                                             ( $c_start - 1 ) * $ImgWidth,               #    translate start column no to coordinate
111                                             ( $c_end   - 1 ) * $ImgWidth                #    translate end column no to coordinate
112                                         );
113             my $y_topLeft           = &$Interpolate                                     #   compute top-left image y-coordinate:
114                                         (   $lambda,
115                                             ( $r_start - 1 ) * $ImgHeight,              #    translate start row no to coordinate
116                                             ( $r_end   - 1 ) * $ImgHeight               #    translate end row no to coordinate
117                                         );
118             $Canvas->Composite                                                          #   blend image onto canvas
119                         (   image       => $Images->[$ImgFile2Idx{$$refImage{file}}],
120                             compose     => 'Blend',
121                             geometry    => "$ImgDims+$x_topLeft+$y_topLeft"
122                         );
123         }                                                                               #  until all transit images processed
124         push @$Frames, @$Canvas;                                                        #  add canvas to image sequence
125         print   locate( $CursorY, $CursorX ), clline,                                   #  report frame processing
126                 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
127     }                                                                                   # until all steps processed for transit
128 }                                                                                       #until all transits processed
129 print   locate( $CursorY, $CursorX ), clline,                                           #report end of frame processing
130         colored ['bold green'], $FrameNo, " frames\n\n";
131 undef $Canvas;                                                                          #destroy the canvas object
132 #-------------------------------------------------------------------------------------------------------------------------------
133 # 4) CREATE ANIMATED GIF IMAGE:
134 print 'Creating animated GIF image... ';                                                #report start of animation processing
135 $Frames->Write                                                                          #output the animation
136             (   delay       => $$Anim{delay},
137                 loop        => $$Anim{loops},
138                 dispose     => 'background',
139                 filename    => $$Anim{output}
140             );
141 print colored ['bold green'], $$Anim{output}, "\n";                                     #report end of animation processing
142 exit;
143 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
144 # end of anim_grid_transits.pl
			

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.