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
© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.