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