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
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:
| 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.
perl anim_grid_transits.pl demo.xml
Here we are requesting that the XML file "demo.xml" be processed. A screen shot at the end of processing is:
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:
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 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
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.