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