anim_book.pl
Creates an animated GIF that turns "3D" pages of a book.

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:

Leonardo da Vinci, Mona_Lisa, St. Anne, horse, ermine

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.

If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.


anim_book.pl -- [Download latest version: v1.0.0 - October 6, 2009]   [MD5 checksum]
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.