anim_pad.pl
Creates an animated GIF that peels "3D" sheets off a pad.

Referring back to the Perl script anim_flipcard.pl, I discussed how to either flip or twirl a 3D "card" displaying multiple images. In this article, I discuss a simple variant where the images are hinged at their top side. The resulting illusion mimics the images being peeled a pad, as shown in the following animation:

Leonardo da Vinci, Mona_Lisa, St. Anne

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. The only new concept here is that the background of each animation frame is no longer transparent. Rather, it displays in a cyclic manner the image underlying the one being distorted. Thus the first image peeled off will also appear as the last background in the first loop of the animation.

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_pad.xml ( [Download demo_pad.xml]   [MD5 checksum] ) that governs the creation of the above animated GIF image:
XML file "demo_pad.xml" Remarks
<animation> start of XML declaration
<output>demo_pad.gif</output> name of the output file
<frames>16</frames> the number of intermediate, interpolated frames for each quarter turn
<delay>10</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
<imageDims> all constituent images to be scaled to the following pixel dimensions.
<width>100</width> in pixels
<height>150</height> in pixels
<stretch>5</stretch> in pixels. Horizontal distance along the axis of "rotation" of the theoretical corner end points from an image's vertical side.
</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_pad.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_pad.pl -- [Download latest version: v1.0.0 - October 1, 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_pad.pl: Creates an animated GIF that peels "3D" sheets off a pad.
011 #===============================================================================================================================
012 #           Usage : perl anim_pad.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_pad.shtml for details.
018 #         History : v1.0.0 - October 1, 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(image)] );                     #read the XML data file
028 my $ImgWidth        = $$Anim{imageDims}{width};                                         #parameterize the width of the images
029 my $ImgHeight       = $$Anim{imageDims}{height};                                        #parameterize the height of the images
030 my $ImgStretch      = $$Anim{imageDims}{stretch};                                       #parameterize the image distortion
031 my $NoImages        = @{ $$Anim{image} };                                               #get the number of source images
032 my @Spinners        = ( '-', '\\', '|', '/' );                                          #define symbols for spinner
033
034 my $CanvasWidth     = $ImgWidth + 2 * $ImgStretch;                                      # set canvas width
035 my $CanvasHeight    = $ImgHeight;                                                       # set frame 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 images required\n" if $NoImages % 2;                         #check for even number of images
043 for( 0..$NoImages-1 ) {                                                                 #repeat for each source image
044     my $file = $$Anim{image}[$_];                                                       # parameterize the file name
045     die FATAL, "Cannot locate image file '$file'\n" unless -e $file;                    # check image existence
046     $Images->Read( $file );                                                             # read image file
047 }                                                                                       #until all images processed
048 $Images->Quantize( colors => 256, colorspace => 'RGB' );                                #ensure uniform color space
049 $Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" );                              #scale images to stated dimensions
050 print colored ['bold green'], "Done\n\n";                                               #report end of image processing
051 #-------------------------------------------------------------------------------------------------------------------------------
052 # 2) CREATE ANIMATION FRAMES:
053 print 'Creating animation frames... ';                                                  #report start of frame processing
054 my($CursorX,$CursorY)   = Cursor();                                                     #record cursor position
055
056 my $Canvas              = Image::Magick->new( magick => 'GIF' );                        #instantiate an image object for the frame canvas
057 my $Parallelogram       = Image::Magick->new( magick => 'GIF' );                        #instantiate an image object for the distorted image
058 my $Frames              = Image::Magick->new( magick => 'GIF' );                        #instantiate an image object for animation frames
059 my $FrameNo             = 0;                                                            #init no of animation frames
060 my $Interpolate         = sub   {   my ( $lambda, $start, $end ) = @_;                  #anonymous sub for interpolating x,y-coordinates
061                                     int( $lambda * $end + ( 1. - $lambda ) * $start );
062                                 };
063 my $DeltaLambda         = 1. / ( $$Anim{frames} + 1. );                                 #set "rotation" step-size
064
065 my $ImgX_topLeft        = $ImgStretch;                                                  #top-left corner coords for 2D image
066 my $ImgY_topLeft        = 0;
067 my $ImgX_bottomRight    = $CanvasWidth  - $ImgStretch - 1;                              #bottom-right corner coords for 2D image
068 my $ImgY_bottomRight    = $CanvasHeight - 1;
069
070 my $x_left;                                                                             #apex coords for distorted image
071 my $x_right;
072 my $y_bottom;
073
074 for my $imgIdx ( 0..$NoImages - 1 ) {                                                   #repeat for each pairing of images
075     #QUARTER RIGHT-HAND-RULE ROTATION OF IMAGE HINGED AT THE TOP:
076     for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) {   # repeat for each rotation step
077         $x_left     = &$Interpolate( $lambda,   $ImgX_topLeft,      0                   );
078         $x_right    = &$Interpolate( $lambda,   $ImgX_bottomRight,  $CanvasWidth - 1    );
079         $y_bottom   = &$Interpolate( $lambda,   $ImgY_bottomRight,  0                   );
080         &drawFrame( $imgIdx );                                                          #  draw the animation frame
081     }                                                                                   # until all rotation steps processed
082 }                                                                                       #until all images processed
083 print   locate( $CursorY, $CursorX ), clline,                                           #report end of frame processing
084         colored ['bold green'], $FrameNo, " frames\n\n";
085 undef $Canvas;                                                                          #destroy the canvas object
086 undef $Parallelogram;                                                                   #destroy the distorted-image object
087 undef $Images;                                                                          #destroy the object for source images
088 #-------------------------------------------------------------------------------------------------------------------------------
089 # 3) CREATE ANIMATED GIF IMAGE:
090 print 'Creating animated GIF image... ';                                                #report start of animation processing
091 $Frames->Write                                                                          #output the animation
092             (   delay       => $$Anim{delay},
093                 loop        => $$Anim{loops},
094                 dispose     => 'background',
095                 filename    => $$Anim{output}
096             );
097 print colored ['bold green'], $$Anim{output}, "\n";                                     #report end of animation processing
098 exit;
099 #===== SUBROUTINES =============================================================================================================
100 #     Usage : &drawFrame( $IMGINDEX );
101 #   Purpose : Draw an animation frame with the specified image index
102 # Arguments : $IMGINDEX = index of the ImageMagick object $Images.
103 #      Subs : None.
104 #   Remarks : None.
105 #   History : v1.0.0 - October 1, 2009 - Original release.
106
107 sub drawFrame {                                                                         #begin sub
108     my $imgIdx = shift;                                                                 # parameterize the argument
109
110     @$Parallelogram = ();                                                               # clear the distorted-iamge canvas
111     $Parallelogram->Set( size => "${CanvasWidth}x${CanvasHeight}" );                    # set canvas size
112     $Parallelogram->ReadImage( 'xc:transparent' );                                      # set canvas background to transparent
113     $Parallelogram->Composite                                                           # init canvas with designated image
114                 (   image           => $Images->[$imgIdx],
115                     compose         => 'Over',
116                     geometry        => "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
117                 );
118     $Parallelogram->Distort                                                             # match image corners to perspective apexes:
119                 (   points          =>  [   $ImgX_topLeft,      $ImgY_topLeft,      $ImgX_topLeft,      $ImgY_topLeft, # top left
120                                             $ImgX_topLeft,      $ImgY_bottomRight,  $x_left,            $y_bottom,     # bottom left
121                                             $ImgX_bottomRight,  $ImgY_topLeft,      $ImgX_bottomRight,  $ImgY_topLeft, # top right
122                                             $ImgX_bottomRight,  $ImgY_bottomRight,  $x_right,           $y_bottom      # bottom right
123                                         ],
124                     type            => 'Bilinear',
125                     'virtual-pixel' => 'transparent',
126                     'best-fit'      => 1,
127                 );
128
129     @$Canvas = ();                                                                      # clear the frame canvas
130     $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );                           # set canvas size
131     $Canvas->ReadImage( 'xc:transparent' );                                             # set canvas background to transparent
132     $Canvas->Composite                                                                  # init canvas with next image as background
133                 (   image           => $Images->[( $imgIdx +  1 ) % $NoImages],
134                     compose         => 'Over',
135                     geometry        => "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
136                 );
137     $Canvas->Composite                                                                  # add parallelogram as foreground
138                 (   image           => $Parallelogram,
139                     compose         => 'Over',
140                     geometry        => "${CanvasWidth}x${CanvasHeight}+0+0"
141                 );
142     push @$Frames, @$Canvas;                                                            # add frame canvas to image sequence
143     print   locate( $CursorY, $CursorX ), clline,                                       # report frame processing
144             colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
145 }                                                                                       #end sub drawFlipFrame
146 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
147 # end of anim_pad.pl
			

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.