In the article for the Perl script anim_flipcard.pl, I discussed how to either flip or twirl a 3D "card" displaying multiple images. Whereas for anim_pad.pl, I showed how to peel a "3D" sheet off a pad, whilst using the next image in the sequence as the canvas background. In this article, I discuss a combined variant where the images are hinged in the middle of the canvas. The resulting illusion mimics a book where the images act as self-turning pages:
For those unfamiliar with the principles governing the illusion and the methods used to create it, the article regarding anim_flipcard.pl should be consulted.
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_book.xml
( [Download demo_book.xml]
[MD5 checksum] )
that governs the creation of the above animated GIF image:
XML file "demo_book.xml" | Remarks |
---|---|
<animation> | start of XML declaration |
<output>demo_book.gif</output> | name of the output file |
<frames>16</frames> | the number of intermediate, interpolated frames for each quarter turn |
<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 |
<pageDims> | all constituent page images to be scaled to the following pixel dimensions. |
<width>100</width> | in pixels |
<height>150</height> | in pixels |
<stretch>20</stretch> | in pixels. Vertical distance along the axis of "rotation" of the theoretical corner end points from an image's horizontal side. |
</pageDims> | end of page image specifications |
<page>scrapbook004.jpg</page> | filename of the 1st image. Images are processed in the order of their declaration. This image, courtesy of The Graphics Fairy, acts as the front cover. |
<page>1floralpaper006.jpg</page> | filename of the 2nd image. This image, also courtesy of The Graphics Fairy, acts as an end paper to the front cover. |
<page>Leonardo.jpg</page> | filename of the 3rd image. |
<page>Mona_Lisa.jpg</page> | filename of the 4th image |
<page>St_Anne.jpg</page> | filename of the 5th image |
<page>Vitruve.jpg</page> | filename of the 6th image |
<page>Study_of_horse.jpg</page> | filename of the 7th image |
<page>The_Lady_with_an_Ermine.jpg</page> | filename of the 8th image |
<page>1floralpaper006.jpg</page> | filename of the 9th image. Same image as before, but now acting as the end paper to the back cover. |
<page>scrapbook_backcover.jpg</page> | filename of the 10th image, acting as the back cover. |
</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_book.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_book.pl demo_book.xml
Here we are requesting that the XML file "demo_book.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 Image::Magick; 004 use Term::ANSIScreen qw/:color :cursor :screen/; 005 use Win32::Console::ANSI qw/ Cursor /; 006 use XML::Simple; 007 $XML::Simple::PREFERRED_PARSER = 'XML::Parser'; 008 use constant FATAL => colored ['white on red'], "\aFATAL ERROR: "; 009 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================ 010 # anim_book.pl: Creates an animated GIF that turns "3D" pages of a book. 011 #=============================================================================================================================== 012 # Usage : perl anim_book.pl XmlFile 013 # Arguments : XmlFile = path of XML file for the animation specifications. 014 # Input Files : See arguments. 015 # Output Files : The animated GIF specified in the XML data file. 016 # Temporary Files : None. 017 # Remarks : See http://www.webpraxis.ab.ca/flips/anim_book.shtml for details. 018 # History : v1.0.0 - October 6, 2009 - Original release. 019 #=============================================================================================================================== 020 # 0) INITIALIZE: 021 $| = 1; #set STDOUT buffer to auto-flush 022 cls(); #clear screen 023 print colored ['black on white'], "$0\n\n\n", #display program name 024 colored ['reset'], 'Initializing... '; #report start of initialization 025 026 my $XmlFile = shift || die FATAL, 'No XML file specified'; #get path of XML data file 027 my $Anim = XMLin( $XmlFile, ForceArray => [qw(page)] ); #read the XML data file 028 my $ImgWidth = $$Anim{pageDims}{width}; #parameterize the width of the images 029 my $ImgHeight = $$Anim{pageDims}{height}; #parameterize the height of the images 030 my $ImgStretch = $$Anim{pageDims}{stretch}; #parameterize the image distortion 031 my $NoPages = @{ $$Anim{page} }; #get the number of specified images 032 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner 033 034 my $CanvasWidth = 2 * $ImgWidth; #set canvas width 035 my $CanvasHeight = $ImgHeight + 2 * $ImgStretch; #set canvas height 036 print colored ['bold green'], "XML data read\n\n"; #report end of initialization 037 #------------------------------------------------------------------------------------------------------------------------------- 038 # 1) LOAD AND SCALE THE IMAGES: 039 print 'Reading & scaling images... '; #report start of image processing 040 my $Images = Image::Magick->new( magick => 'JPG' ); #instantiate an object for the images 041 042 die FATAL, "Even number of pages required\n" if $NoPages % 2; #check for even number of images 043 die FATAL, "At least 4 pages required\n" if $NoPages < 4; #check for minimum of 4 images 044 for( 0..$NoPages-1 ) { #repeat for each page image 045 my $file = $$Anim{page}[$_]; # parameterize the file name 046 die FATAL, "Cannot locate page file '$file'\n" unless -e $file; # check page image existence 047 $Images->Read( $file ); # read page image file 048 } #until all images read 049 $Images->Quantize( colors => 256, colorspace => 'RGB' ); #ensure uniform color space 050 $Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" ); #scale images to stated dimensions 051 print colored ['bold green'], "Done\n\n"; #report end of image processing 052 #------------------------------------------------------------------------------------------------------------------------------- 053 # 2) CREATE ANIMATION FRAMES: 054 print 'Creating animation frames... '; #report start of frame processing 055 my($CursorX,$CursorY) = Cursor(); #record cursor position 056 057 my $Canvas = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the canvas 058 my $CanvasBkgrnd = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the canvas background 059 my $Page = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the distorted page 060 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames 061 my $FrameNo = 0; #init no of animation frames 062 my $Interpolate = sub { my ( $lambda, $start, $end ) = @_; #anonymous sub for interpolating x,y-coordinates 063 int( $lambda * $end + ( 1. - $lambda ) * $start ); 064 }; 065 my $DeltaLambda = 1. / ( $$Anim{frames} + 1. ); #set "rotation" step-size 066 067 my $ImgX_VERSOtopLeft = 0; #top-left corner coords for 2D verso page 068 my $ImgY_VERSOtopLeft = $ImgStretch; 069 my $ImgX_VERSObottomRight = $ImgWidth - 1; #bottom-right corner coords for 2D verso page 070 my $ImgY_VERSObottomRight = $CanvasHeight - $ImgStretch - 1; 071 072 my $ImgX_RECTOtopLeft = $ImgX_VERSObottomRight + 1; #top-left corner coords for 2D recto page 073 my $ImgY_RECTOtopLeft = $ImgY_VERSOtopLeft; 074 my $ImgX_RECTObottomRight = $CanvasWidth - 1; #bottom-right corner coords for 2D recto page 075 my $ImgY_RECTObottomRight = $ImgY_VERSObottomRight; 076 077 my $x_left; #apex coords for distorted page 078 my $x_right; 079 my $y_top; 080 my $y_bottom; 081 082 for( my $imgIdx = 0; $imgIdx <= $NoPages - 2; $imgIdx += 2 ) { #repeat for each pairing of pages 083 &drawBackground( $imgIdx ); # draw corresponding verso and recto pages 084 #QUARTER LEFT-HAND-RULE ROTATION OF RECTO IMAGE: 085 for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) { # repeat for each rotation step 086 $x_right = &$Interpolate( $lambda, $ImgX_RECTObottomRight, $ImgX_RECTOtopLeft ); 087 $y_top = &$Interpolate( $lambda, $ImgY_RECTOtopLeft, 0 ); 088 $y_bottom = &$Interpolate( $lambda, $ImgY_RECTObottomRight, $CanvasHeight - 1 ); 089 &turnRectoImage( $imgIdx ); # draw the distorted page 090 &drawPage(); # draw the animation frame 091 } # until all rotation steps processed 092 093 #QUARTER LEFT-HAND-RULE ROTATION OF VERSO IMAGE: 094 for(my $lambda=$DeltaLambda; $lambda < 1.+$DeltaLambda/2.; $lambda+=$DeltaLambda) { # repeat for each rotation step 095 $x_left = &$Interpolate( $lambda, $ImgX_VERSObottomRight, $ImgX_VERSOtopLeft ); 096 $y_top = &$Interpolate( $lambda, 0, $ImgY_VERSOtopLeft ); 097 $y_bottom = &$Interpolate( $lambda, $CanvasHeight - 1, $ImgY_VERSObottomRight ); 098 &turnVersoImage( $imgIdx + 1 ); # draw the distorted page 099 &drawPage(); # draw the animation frame 100 } # until all rotation steps processed 101 } #until all image pairs processed 102 103 undef $CanvasBkgrnd; #destroy image object for background 104 #QUARTER RIGHT-HAND-RULE ROTATION OF "BACK COVER": 105 for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) { #repeat for each rotation step 106 $x_left = &$Interpolate( $lambda, $ImgX_VERSOtopLeft, $ImgX_VERSObottomRight ); 107 $y_top = &$Interpolate( $lambda, $ImgY_VERSOtopLeft, 0 ); 108 $y_bottom = &$Interpolate( $lambda, $ImgY_VERSObottomRight, $CanvasHeight - 1 ); 109 &turnVersoImage( $NoPages - 1 ); # draw the distorted back cover 110 &drawPage(); # draw the animation frame 111 } #until all rotation steps processed 112 113 #QUARTER RIGHT-HAND-RULE ROTATION OF "FRONT COVER": 114 for( my $lambda=$DeltaLambda; $lambda < 1.+$DeltaLambda/2.; $lambda+=$DeltaLambda ) { # repeat for each rotation step 115 $x_right = &$Interpolate( $lambda, $ImgX_RECTOtopLeft, $ImgX_RECTObottomRight ); 116 $y_top = &$Interpolate( $lambda, 0, $ImgY_RECTOtopLeft ); 117 $y_bottom = &$Interpolate( $lambda, $CanvasHeight - 1, $ImgY_RECTObottomRight ); 118 &turnRectoImage( 0 ); # draw the distorted front cover 119 &drawPage(); # draw the animation frame 120 } #until all rotation steps processed 121 122 print locate( $CursorY, $CursorX ), clline, #report end of frame processing 123 colored ['bold green'], $FrameNo, " frames\n\n"; 124 undef $Canvas; #destroy image objects no longer needed 125 undef $Images; 126 undef $Page; 127 #------------------------------------------------------------------------------------------------------------------------------- 128 # 3) CREATE ANIMATED GIF IMAGE: 129 print 'Creating animated GIF image... '; #report start of animation processing 130 $Frames->Write #output the animation 131 ( delay => $$Anim{delay}, 132 loop => $$Anim{loops}, 133 dispose => 'background', 134 filename => $$Anim{output} 135 ); 136 print colored ['bold green'], $$Anim{output}, "\n"; #report end of animation processing 137 exit; 138 #===== SUBROUTINES ============================================================================================================= 139 # Usage : &drawBackground( $IMGINDEX ); 140 # Purpose : Draws the verso and recto background images corresponding the specified image index. 141 # Arguments : $IMGINDEX = index of the ImageMagick object $Images. 142 # Subs : None. 143 # Remarks : Background is common to the recto and verso sides of the current page pairs. 144 # History : v1.0.0 - October 5, 2009 - Original release. 145 146 sub drawBackground { #begin sub 147 my $imgIdx = shift; # parameterize the argument 148 149 @$CanvasBkgrnd = (); # clear the background canvas 150 $CanvasBkgrnd->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size 151 $CanvasBkgrnd->ReadImage( 'xc:transparent' ); # set canvas background to transparent 152 $CanvasBkgrnd->Composite # draw verso image of previous sheet 153 ( image => $Images->[$imgIdx - 1], 154 compose => 'Over', 155 geometry => "${ImgWidth}x${ImgHeight}+$ImgX_VERSOtopLeft+$ImgY_VERSOtopLeft" 156 ) 157 if $imgIdx; # only if it exists 158 $CanvasBkgrnd->Composite # draw recto image of next sheet 159 ( image => $Images->[$imgIdx + 2], 160 compose => 'Over', 161 geometry => "${ImgWidth}x${ImgHeight}+$ImgX_RECTOtopLeft+$ImgY_RECTOtopLeft" 162 ) 163 if $imgIdx < $NoPages-2; # only if it exists 164 } #end sub drawBackground 165 #------------------------------------------------------------------------------------------------------------------------------- 166 # Usage : &turnRectoImage( $IMGINDEX ); 167 # Purpose : Distorts the specified recto image into the required parallelogram. 168 # Arguments : $IMGINDEX = index of the ImageMagick object $Images. 169 # Subs : None. 170 # Remarks : None. 171 # History : v1.0.0 - October 5, 2009 - Original release. 172 173 sub turnRectoImage { #begin sub 174 my $imgIdx = shift; # parameterize the argument 175 176 @$Page = (); # clear the distorted-image canvas 177 $Page->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size 178 $Page->ReadImage( 'xc:transparent' ); # set canvas background to transparent 179 $Page->Composite # init canvas with designated image 180 ( image => $Images->[$imgIdx], 181 compose => 'Over', 182 geometry => "${ImgWidth}x${ImgHeight}+$ImgX_RECTOtopLeft+$ImgY_RECTOtopLeft" 183 ); 184 $Page->Distort # match image corners to perspective apexes: 185 ( points => [ # top left 186 $ImgX_RECTOtopLeft, $ImgY_RECTOtopLeft, $ImgX_RECTOtopLeft, $ImgY_RECTOtopLeft, 187 # bottom left 188 $ImgX_RECTOtopLeft, $ImgY_RECTObottomRight, $ImgX_RECTOtopLeft, $ImgY_RECTObottomRight, 189 # top right 190 $ImgX_RECTObottomRight, $ImgY_RECTOtopLeft, $x_right, $y_top, 191 # bottom right 192 $ImgX_RECTObottomRight, $ImgY_RECTObottomRight, $x_right, $y_bottom 193 ], 194 type => 'Bilinear', 195 'virtual-pixel' => 'transparent', 196 'best-fit' => 1, 197 ); 198 } #end sub turnRectoImage 199 #------------------------------------------------------------------------------------------------------------------------------- 200 # Usage : &turnVersoImage( $IMGINDEX ); 201 # Purpose : Distorts the specified verso image into the required parallelogram. 202 # Arguments : $IMGINDEX = index of the ImageMagick object $Images. 203 # Subs : None. 204 # Remarks : None. 205 # History : v1.0.0 - October 5, 2009 - Original release. 206 207 sub turnVersoImage { #begin sub 208 my $imgIdx = shift; # parameterize the argument 209 210 @$Page = (); # clear the distorted-image canvas 211 $Page->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size 212 $Page->ReadImage( 'xc:transparent' ); # set canvas background to transparent 213 $Page->Composite # init canvas with designated image 214 ( image => $Images->[$imgIdx], 215 compose => 'Over', 216 geometry => "${ImgWidth}x${ImgHeight}+$ImgX_VERSOtopLeft+$ImgY_VERSOtopLeft" 217 ); 218 $Page->Distort # match image corners to perspective apexes: 219 ( points => [ # top left 220 $ImgX_VERSOtopLeft, $ImgY_VERSOtopLeft, $x_left, $y_top, 221 # bottom left 222 $ImgX_VERSOtopLeft, $ImgY_VERSObottomRight, $x_left, $y_bottom, 223 # top right 224 $ImgX_VERSObottomRight, $ImgY_VERSOtopLeft, $ImgX_VERSObottomRight, $ImgY_VERSOtopLeft, 225 # bottom right 226 $ImgX_VERSObottomRight, $ImgY_VERSObottomRight, $ImgX_VERSObottomRight, $ImgY_VERSObottomRight 227 ], 228 type => 'Bilinear', 229 'virtual-pixel' => 'transparent', 230 'best-fit' => 1, 231 ); 232 } #end sub turnVersoImage 233 #------------------------------------------------------------------------------------------------------------------------------- 234 # Usage : &drawPage(); 235 # Purpose : Draws the current page atop any canvas background and adds the result to the animation frame sequence. 236 # Arguments : None. 237 # Subs : None. 238 # Remarks : None. 239 # History : v1.0.0 - October 6, 2009 - Original release. 240 241 sub drawPage { #begin sub 242 243 @$Canvas = (); # clear the frame canvas 244 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size 245 $Canvas->ReadImage( 'xc:transparent' ); # set canvas background to transparent 246 $Canvas->Composite # add any background images to canvas 247 ( image => $CanvasBkgrnd, 248 compose => 'Over', 249 geometry => "${CanvasWidth}x${CanvasHeight}+0+0" 250 ) 251 if defined $CanvasBkgrnd; 252 $Canvas->Composite # add foreground page to canvas 253 ( image => $Page, 254 compose => 'Over', 255 geometry => "${CanvasWidth}x${CanvasHeight}+0+0" 256 ); 257 push @$Frames, @$Canvas; # add frame canvas to animation sequence 258 print locate( $CursorY, $CursorX ), clline, # report frame processing 259 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')'; 260 } #end sub drawPage 261 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================ 262 # end of anim_book.pl
© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.