In my previous article regarding the Perl script anim_flipcard.pl, I discussed how to either flip or twirl a 3D "card" displaying multiple images. Time now to move to the next level of complexity where the images are sliced into slats. These in turn are made to rotate, sequentially revealing the constituent images, as if mimicking a set of either horizontal or vertical 3D "blinds". Using our usual set of four reference images, the following animation is an example:
The coding required to pull this off is truly an extension of anim_flipcard.pl. For those unfamiliar with the principles governing the illusion and the methods used to create it, that article should be consulted first before proceeding further. For those in the know, I'll simply say that the key here is that each slat must be treated in the same manner as a source image in anim_flipcard.pl. The reason follows from the definition of ImageMagick's "Distort" command: one can only distort a whole image. Consequently, one cannot simply lay the slats flat on a canvas and mapped each one to the requisite parallelogram. Rather, for each slat, one must initialize an image object with a slat source image, distort it and then place the result at the appropriate location on the frame canvas.
As with all our previous animation scripts,
an XML file conveys the animation specifications.
To explain the required XML tags, let's examine the data file demo_horizontalblinds.xml
( [Download demo_horizontalblinds.xml]
[MD5 checksum] ).
It governs the creation of the following animated GIF image:
| XML file "demo_horizontalblinds.xml" | Remarks |
|---|---|
| <animation> | start of XML declaration |
| <simulation>horizontal</simulation> | the type of simulation. The other valid option is vertical. |
| <output>demo_horizontalBlinds.gif</output> | name of the output file |
| <frames>10</frames> | the number of intermediate, interpolated frames for each quarter turn |
| <delay>25</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 |
| <slats> | start of specifications for all the slats |
| <width>25</width> | in pixels. Measurement of the shorter dimension, be it for horizontal or vertical blinds |
| <stretch>5</stretch> | in pixels. Vertical or horizontal distance along the axis of "rotation" of the theoretical corner end points from a slat-image side. |
| </slats> | end of slat specifications |
| <imageDims> | all constituent images to be scaled to the following pixel dimensions. They are applied to the images prior to the final dimension adjustments for an integral number of slats. |
| <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_blinds.pl, displayed below, is the resulting code. It is released for personal, non-commercial and non-profit use only. Note in passing that, as with the aforementioned previous script, no attempt has been made to render the code more compact or increase its computational efficiency. The emphasis is strictly on clarity where repetition has been favored over abstraction.
The listing includes the line numbers in order to reference them in the following general remarks.
perl anim_blinds.pl demo_verticalBlinds.xml
Here we are requesting that the XML file "demo_verticalBlinds.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_blinds.pl: Creates an animated GIF that rotates horizontal/vertical 3D "blinds" but with multiple images.
012 #===============================================================================================================================
013 # Usage : perl anim_blinds.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/flips/anim_blinds.shtml for details.
019 # History : v1.0.0 - September 28, 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(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 $SlatWidth = $$Anim{slats}{width}; #parameterize the width of a slat
032 my $SlatStretch = $$Anim{slats}{stretch}; #parameterize the slat distortion
033 my $Simulation = $$Anim{simulation}; #parameterize the simulation request
034 my $NoImages = @{ $$Anim{image} }; #get the number of source images
035 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner
036
037 my $NoSlats; #number of slats per source image
038 my $CanvasWidth; #frame canvas width in pixels
039 my $CanvasHeight; #frame canvas height in pixels
040 if( $Simulation eq 'vertical' ) { #if vertical slats requested
041 $NoSlats = POSIX::ceil( $ImgWidth / $SlatWidth ); # compute integral number of slats
042 $ImgWidth = $NoSlats * $SlatWidth; # adjust source image width
043 $CanvasWidth = $ImgWidth; # set frame canvas width
044 $CanvasHeight = $ImgHeight + 2 * $SlatStretch; # set frame canvas height
045 } elsif( $Simulation eq 'horizontal' ) { #else if horizontal slats requested
046 $NoSlats = POSIX::ceil( $ImgHeight / $SlatWidth ); # compute integral number of slats
047 $ImgHeight = $NoSlats * $SlatWidth; # adjust source image height
048 $CanvasWidth = $ImgWidth + 2 * $SlatStretch; # set frame canvas width
049 $CanvasHeight = $ImgHeight; # set frame canvas height
050 } else { #else unknown/undefined simulation
051 die FATAL, "Invalid simulation request '$Simulation'\n";
052 } #end if-elsif-else
053 print colored ['bold green'], "XML data read\n\n"; #report end of initialization
054 #-------------------------------------------------------------------------------------------------------------------------------
055 # 1) LOAD AND SCALE THE IMAGES:
056 print 'Reading & scaling images... '; #report start of image processing
057 my $Images = Image::Magick->new( magick => 'JPG' ); #instantiate an object for the images
058
059 die FATAL, "Even number of images required\n" if $NoImages % 2; #check for even number of images
060 for( 0..$NoImages-1 ) { #repeat for each source image
061 my $file = $$Anim{image}[$_]; # parameterize the file name
062 die FATAL, "Cannot locate image file '$file'\n" unless -e $file; # check image existence
063 $Images->Read( $file ); # read image file
064 } #until all images processed
065 $Images->Quantize( colors => 256, colorspace => 'RGB' ); #ensure uniform color space
066 $Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" ); #scale images to adjusted dimensions
067 print colored ['bold green'], "Done\n\n"; #report end of image processing
068 #-------------------------------------------------------------------------------------------------------------------------------
069 # 2) CREATE SLAT IMAGES:
070 print 'Creating slat images... '; #report start of slat processing
071 my ( $CursorX, $CursorY ) = Cursor(); #record cursor position
072
073 my $Slats = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the slats
074 { #start naked block as firewall
075 my $slat = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for a slat
076
077 if( $Simulation eq 'vertical' ) { # if vertical slats requested
078 for my $imgIdx ( 0..$NoImages-1 ) { # repeat for each source image
079 for ( my $x_topLeft = 0; # for each slat: work left to right
080 $x_topLeft <= $ImgWidth - $SlatWidth;
081 $x_topLeft += $SlatWidth
082 ) {
083 my $geometry = "${SlatWidth}x${ImgHeight}+$x_topLeft+0"; # define slat geometry
084 $slat = $Images->[$imgIdx]->Clone(); # init slat with image
085 $slat->Crop( geometry => $geometry ); # crop slat area
086 $slat->Set( page => '0x0+0+0' ); # shrink canvas
087 push @$Slats, @$slat; # store the slat image
088 print locate( $CursorY, $CursorX ), clline, # report slat processing
089 colored ['bold yellow'], '(', $Spinners[$#$Slats % 4], ')';
090 } # until all slats created
091 } # until all images processed
092 } else { # else horizontal slats requested
093 for my $imgIdx ( 0..$NoImages-1 ) { # repeat for each source image
094 for ( my $y_topLeft = 0; # for each slat: work top to bottom
095 $y_topLeft <= $ImgHeight - $SlatWidth;
096 $y_topLeft += $SlatWidth
097 ) {
098 my $geometry = "${ImgWidth}x${SlatWidth}+0+$y_topLeft"; # define slat geometry
099 $slat = $Images->[$imgIdx]->Clone(); # init slat with image
100 $slat->Crop( geometry => $geometry ); # crop slat area
101 $slat->Set( page => '0x0+0+0' ); # shrink canvas
102 push @$Slats, @$slat; # store the slat image
103 print locate( $CursorY, $CursorX ), clline, # report slat processing
104 colored ['bold yellow'], '(', $Spinners[$#$Slats % 4], ')';
105 } # until all slats created
106 } # until all images processed
107 } # end if-else
108 undef $slat; # destroy the slat image object
109 } #end naked block
110 print locate( $CursorY, $CursorX ), clline, #report end of slat processing
111 colored ['bold green'], scalar @$Slats, " slats\n\n";
112 undef $Images; #destroy the source image object
113 #-------------------------------------------------------------------------------------------------------------------------------
114 # 3) CREATE ANIMATION FRAMES:
115 print 'Creating animation frames... '; #report start of frame processing
116 ( $CursorX, $CursorY ) = Cursor(); #record cursor position
117
118 my $Canvas = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the frame canvas
119 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames
120 my $FrameNo = 0; #init no of animation frames
121 my $Interpolate = sub { my ( $lambda, $start, $end ) = @_; #anonymous sub for interpolating x,y-coordinates
122 int( $lambda * $end + ( 1. - $lambda ) * $start );
123 };
124 my $DeltaLambda = 1. / ( $$Anim{frames} + 1. ); #set turn step-size
125
126 my $SlatX_topLeft; #top-left corner coords for 2D slat
127 my $SlatY_topLeft;
128 my $SlatX_bottomRight; #bottom-right corner coords for 2D slat
129 my $SlatY_bottomRight;
130 my $SlatX_mid; #mid coord along x-axis
131 my $SlatY_mid; #mid coord along y-axis
132 if( $Simulation eq 'vertical' ) { #if vertical slats requested
133 $SlatX_mid = ( $SlatWidth - 1 ) / 2; # set mid coord along x-axis
134 $SlatX_topLeft = 0; # set top-left corner coords
135 $SlatY_topLeft = $SlatStretch;
136 $SlatX_bottomRight = $SlatWidth - 1; # set bottom-right corner coords
137 $SlatY_bottomRight = $CanvasHeight - $SlatStretch - 1;
138 } else { #else horizontal slats requested
139 $SlatY_mid = ( $SlatWidth - 1 ) / 2; # set mid coord along y-axis
140 $SlatX_topLeft = $SlatStretch; # set top-left corner coords
141 $SlatY_topLeft = 0;
142 $SlatX_bottomRight = $CanvasWidth - $SlatStretch - 1; # set bottom-right corner coords
143 $SlatY_bottomRight = $SlatWidth;
144 } #end if-else
145
146 my $x_left; #perspective apex coords for vertical slats
147 my $x_right;
148 my $y_topLeft;
149 my $y_bottomLeft;
150 my $y_topRight;
151 my $y_bottomRight;
152
153 my $y_top; #perspective apex coords for horizontal slats
154 my $y_bottom;
155 my $x_topLeft;
156 my $x_bottomLeft;
157 my $x_topRight;
158 my $x_bottomRight;
159
160 for my $imgIdx ( 0..$NoImages - 1 ) { #repeat for each pairing of images
161 #QUARTER RIGHT-HAND-RULE TURN OF RECTO SLATS:
162 for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) { # repeat for each turn step
163 if( $Simulation eq 'vertical' ) { # interpolate apex coords
164 $x_left = &$Interpolate( $lambda, $SlatX_topLeft, $SlatX_mid );
165 $x_right = &$Interpolate( $lambda, $SlatX_bottomRight, $SlatX_mid );
166 $y_topLeft = &$Interpolate( $lambda, $SlatY_topLeft, 0 );
167 $y_bottomLeft = &$Interpolate( $lambda, $SlatY_bottomRight, $SlatY_bottomRight + $SlatStretch );
168 $y_topRight = &$Interpolate( $lambda, $SlatY_topLeft, $SlatY_topLeft + $SlatStretch );
169 $y_bottomRight = &$Interpolate( $lambda, $SlatY_bottomRight, $SlatY_bottomRight - $SlatStretch );
170 &drawVerticalSlats( $imgIdx ); # draw all the image's slats
171 } else { # interpolate apex coords
172 $y_top = &$Interpolate( $lambda, $SlatY_topLeft, $SlatY_mid );
173 $y_bottom = &$Interpolate( $lambda, $SlatY_bottomRight, $SlatY_mid );
174 $x_topLeft = &$Interpolate( $lambda, $SlatX_topLeft, $SlatX_topLeft + $SlatStretch );
175 $x_bottomLeft = &$Interpolate( $lambda, $SlatX_topLeft, 0 );
176 $x_topRight = &$Interpolate( $lambda, $SlatX_bottomRight, $SlatX_bottomRight - $SlatStretch );
177 $x_bottomRight = &$Interpolate( $lambda, $SlatX_bottomRight, $SlatX_bottomRight + $SlatStretch );
178 &drawHorizontalSlats( $imgIdx ); # draw all the image's slats
179 } # end if-else
180 } # until all turn steps processed
181 #QUARTER RIGHT-HAND-RULE TURN OF VERSO SLATS:
182 for(my $lambda=$DeltaLambda; $lambda < 1.-$DeltaLambda/2.; $lambda+=$DeltaLambda) { # repeat for each turn step
183 if( $Simulation eq 'vertical' ) { # interpolate apex coords
184 $x_left = &$Interpolate( $lambda, $SlatX_mid, $SlatX_topLeft );
185 $x_right = &$Interpolate( $lambda, $SlatX_mid, $SlatX_bottomRight );
186 $y_topLeft = &$Interpolate( $lambda, $SlatY_topLeft + $SlatStretch, $SlatY_topLeft );
187 $y_bottomLeft = &$Interpolate( $lambda, $SlatY_bottomRight - $SlatStretch, $SlatY_bottomRight );
188 $y_topRight = &$Interpolate( $lambda, 0, $SlatY_topLeft );
189 $y_bottomRight = &$Interpolate( $lambda, $SlatY_bottomRight + $SlatStretch, $SlatY_bottomRight );
190 &drawVerticalSlats( ( $imgIdx + 1 ) % $NoImages ); # draw all the image's slats
191 } else { # interpolate apex coords
192 $y_top = &$Interpolate( $lambda, $SlatY_mid, $SlatY_topLeft );
193 $y_bottom = &$Interpolate( $lambda, $SlatY_mid, $SlatY_bottomRight );
194 $x_topLeft = &$Interpolate( $lambda, 0, $SlatX_topLeft );
195 $x_bottomLeft = &$Interpolate( $lambda, $SlatX_topLeft + $SlatStretch, $SlatX_topLeft );
196 $x_topRight = &$Interpolate( $lambda, $SlatX_bottomRight + $SlatStretch, $SlatX_bottomRight );
197 $x_bottomRight = &$Interpolate( $lambda, $SlatX_bottomRight - $SlatStretch, $SlatX_bottomRight );
198 &drawHorizontalSlats( ( $imgIdx + 1 ) % $NoImages ); # draw all the image's slats
199 } # end if-else
200 } # until all turn steps processed
201 } #until all images processed
202 print locate( $CursorY, $CursorX ), clline, #report end of frame processing
203 colored ['bold green'], $FrameNo, " frames\n\n";
204 undef $Canvas; #destroy the canvas object
205 undef $Slats; #destroy the slats object
206 #-------------------------------------------------------------------------------------------------------------------------------
207 # 4) CREATE ANIMATED GIF IMAGE:
208 print 'Creating animated GIF image... '; #report start of animation processing
209 $Frames->Write #output the animation
210 ( delay => $$Anim{delay},
211 loop => $$Anim{loops},
212 dispose => 'background',
213 filename => $$Anim{output}
214 );
215 print colored ['bold green'], $$Anim{output}, "\n"; #report end of animation processing
216 exit;
217 #===== SUBROUTINES =============================================================================================================
218 # Usage : &drawHorizontalSlats( $IMGINDEX );
219 # Purpose : Draw an animation frame with horizontal slats for the specified image index
220 # Arguments : $IMGINDEX = image index as was defined for the ImageMagick object $Images.
221 # Subs : None.
222 # Remarks : None.
223 # History : v1.0.0 - September 28, 2009 - Original release.
224
225 sub drawHorizontalSlats { #begin sub
226 my $imgIdx = shift; # parameterize the argument
227 my $slat = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for the slat canvas
228
229 @$Canvas = (); # clear the frame canvas
230 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set frame canvas size
231 $Canvas->ReadImage( 'xc:transparent' ); # set frame canvas background to transparent
232
233 for my $slatIdx ( $imgIdx*$NoSlats..($imgIdx+1)*$NoSlats - 1 ) { # repeat for each of the image's slats
234 @$slat = (); # clear the slat canvas
235 $slat->Set( size => "${CanvasWidth}x${SlatWidth}" ); # set slat canvas size
236 $slat->ReadImage( 'xc:transparent' ); # set slat canvas background to transparent
237 $slat->Composite # init slat canvas with designated slat image
238 ( image => $Slats->[$slatIdx],
239 compose => 'Over',
240 geometry => "${ImgWidth}x${SlatWidth}+$SlatStretch+0"
241 );
242 $slat->Distort # match slat corners to perspective apexes:
243 ( points => [ $SlatX_topLeft, $SlatY_topLeft, $x_topLeft, $y_top, # top left
244 $SlatX_topLeft, $SlatY_bottomRight, $x_bottomLeft, $y_bottom, # bottom left
245 $SlatX_bottomRight, $SlatY_topLeft, $x_topRight, $y_top, # top right
246 $SlatX_bottomRight, $SlatY_bottomRight, $x_bottomRight, $y_bottom # bottom right
247 ],
248 type => 'Bilinear',
249 'virtual-pixel' => 'transparent',
250 'best-fit' => 1,
251 );
252 $Canvas->Composite # add slat to frame canvas
253 ( image => $slat,
254 compose => 'Over',
255 x => 0,
256 y => ( $slatIdx % $NoSlats ) * $SlatWidth
257 );
258 } # until all image's slats processed
259 push @$Frames, @$Canvas; # add frame canvas to animation sequence
260 print locate( $CursorY, $CursorX ), clline, # report frame processing
261 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
262 } #end sub drawHorizontalSlats
263 #-------------------------------------------------------------------------------------------------------------------------------
264 # Usage : &drawVerticalSlats( $IMGINDEX );
265 # Purpose : Draw an animation frame with vertical slats for the specified image index
266 # Arguments : $IMGINDEX = image index as was defined for the ImageMagick object $Images.
267 # Subs : None.
268 # Remarks : None.
269 # History : v1.0.0 - September 28, 2009 - Original release.
270
271 sub drawVerticalSlats { #begin sub
272 my $imgIdx = shift; # parameterize the argument
273 my $slat = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for the slat canvas
274
275 @$Canvas = (); # clear the frame canvas
276 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set frame canvas size
277 $Canvas->ReadImage( 'xc:transparent' ); # set frame canvas background to transparent
278
279 for my $slatIdx ( $imgIdx*$NoSlats..($imgIdx+1)*$NoSlats - 1 ) { # repeat for each of the image's slats
280 @$slat = (); # clear the slat canvas
281 $slat->Set( size => "${SlatWidth}x${CanvasHeight}" ); # set slat canvas size
282 $slat->ReadImage( 'xc:transparent' ); # set slat canvas background to transparent
283 $slat->Composite # init slat canvas with designated slat image
284 ( image => $Slats->[$slatIdx],
285 compose => 'Over',
286 geometry => "${SlatWidth}x${ImgHeight}+0+$SlatStretch"
287 );
288 $slat->Distort # match slat corners to perspective apexes:
289 ( points => [ $SlatX_topLeft, $SlatY_topLeft, $x_left, $y_topLeft, # top left
290 $SlatX_topLeft, $SlatY_bottomRight, $x_left, $y_bottomLeft, # bottom left
291 $SlatX_bottomRight, $SlatY_topLeft, $x_right, $y_topRight, # top right
292 $SlatX_bottomRight, $SlatY_bottomRight, $x_right, $y_bottomRight # bottom right
293 ],
294 type => 'Bilinear',
295 'virtual-pixel' => 'transparent',
296 'best-fit' => 1,
297 );
298 $Canvas->Composite # add slat to frame canvas
299 ( image => $slat,
300 compose => 'Over',
301 x => ( $slatIdx % $NoSlats ) * $SlatWidth,
302 y => 0
303 );
304 } # until all image's slats processed
305 push @$Frames, @$Canvas; # add frame canvas to animation sequence
306 print locate( $CursorY, $CursorX ), clline, # report frame processing
307 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
308 } #end sub drawVerticalSlats
309 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
310 # end of anim_blinds.pl
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.