In this article, I discuss a Perl script that simulates either flipping or twirling a 3D "card". However, being a digital simulation, we are not confined to only having two images on a "card" as demonstrated by the following animated GIF image, consisting of four images:
The principle behind the illusion is a particular sequence of distorted views of the source images. Each distortion for the recto image is a parallelogram with its dimension further increased on one side and decreased on the opposite side by the same amount. Concurrent to this, the two sides in question move symmetrically towards a central line. The overall effect is to have this central line appear as the axis of rotation. Finally the whole process is reverse to bring the verso image to the recto position.
Generating the sequence requires specifying the proper coordinates for the apexes of each parallelogram. The process for twirling an image during the first-quarter turn is illustrated in the following diagram:
As indicated in the above diagram, all displacements occur along directed line segments. The required intermediate coordinates can be linearly interpolated using the parametric representation for a line segment. For any point P(x,y) on the line segment between P(x0,y0) and P(x1,y1), we have
Two caveats to keep in mind here. Points on a line are real numbers and thus dense, whereas screen pixels are integers and thus discreet. So using the parametric representation for interpolating pixel coordinates will always require taking the integer part of the computational results. Consequently, slight discrepancies might occur between the actual and theoretical positioning of a parallelogram. Furthermore, roundoff often comes into play when incrementing λ by a given step size. For example, a step size of 0.1 does not have a finite binary representation. So, after 10 steps, λ will be slightly less than 1. Thus, additional discrepancies in a parallelogram's apexes can be expected.
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_twirlcard.xml
( [Download demo_twirlcard.xml]
[MD5 checksum] ).
It governs the creation of the following animated GIF image:
| XML file "demo_twirlcard.xml" | Remarks |
|---|---|
| <animation> | start of XML declaration |
| <simulation>twirl</simulation> | the type of simulation. The other valid option is flip. |
| <output>demo_twirlcard.gif</output> | name of the output file |
| <frames>20</frames> | the number of intermediate, interpolated frames for each quarter turn |
| <delay>5</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>10</stretch> | in pixels. Vertical (twirl case) or horizontal (flip case) distance along the axis of "rotation" of the theoretical corner end points from a source-image side (see above diagram). |
| </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>St_Anne.jpg</image> | filename of the 3rd image |
| <image>webpraxis.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_flipcard.pl, displayed below, is the resulting code. It is released for personal, non-commercial and non-profit use only. Note in passing that the aforementioned discussion clearly implies that there are many coordinates to track and displace. 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_flipcard.pl demo_twirlcard.xml
Here we are requesting that the XML file "demo_twirlcard.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_flipcard.pl: Creates an animated GIF that flips or twirls a 3D "card" but with multiple images.
011 #===============================================================================================================================
012 # Usage : perl anim_flipcard.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_flipcard.shtml for details.
018 # History : v1.0.0 - September 24, 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 $Simulation = $$Anim{simulation}; #parameterize the simulation request
032 my $NoImages = @{ $$Anim{image} }; #get the number of specified images
033 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner
034 my $CanvasWidth = $ImgWidth; #canvas width in pixels
035 $CanvasWidth += 2 * $ImgStretch if $Simulation eq 'flip';
036 my $CanvasHeight = $ImgHeight; #canvas height in pixels
037 $CanvasHeight += 2 * $ImgStretch if $Simulation eq 'twirl';
038 print colored ['bold green'], "XML data read\n\n"; #report end of initialization
039 #-------------------------------------------------------------------------------------------------------------------------------
040 # 1) LOAD AND SCALE THE IMAGES:
041 print 'Reading & scaling images... '; #report start of image processing
042 my $Images = Image::Magick->new( magick => 'JPG' ); #instantiate an object for the images
043
044 die FATAL, "Even number of images required\n" if $NoImages % 2; #check for even number of images
045 for( 0..$NoImages-1 ) { #repeat for each source image
046 my $file = $$Anim{image}[$_]; # parameterize the file name
047 die FATAL, "Cannot locate image file '$file'\n" unless -e $file; # check image existence
048 $Images->Read( $file ); # read image file
049 } #until all images processed
050 $Images->Quantize( colors => 256, colorspace => 'RGB' ); #ensure uniform color space
051 $Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" ); #scale images to specified dimensions
052 print colored ['bold green'], "Done\n\n"; #report end of image processing
053 #-------------------------------------------------------------------------------------------------------------------------------
054 # 2) CREATE ANIMATION FRAMES:
055 print 'Creating animation frames... '; #report start of frame processing
056 my ($CursorX, $CursorY) = Cursor(); #record cursor position
057
058 my $Canvas = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the canvas
059 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames
060 my $FrameNo = 0; #init no of animation frames
061 my $Interpolate = sub { my ( $lambda, $start, $end ) = @_; #anonymous sub for interpolating x,y-coordinates
062 int( $lambda * $end + ( 1. - $lambda ) * $start );
063 };
064 my $DeltaLambda = 1. / ( $$Anim{frames} + 1. ); #set transit step-size
065
066 my $ImgX_topLeft; #top-left corner coords for 2D image
067 my $ImgY_topLeft;
068 my $ImgX_bottomRight; #bottom-right corner coords for 2D image
069 my $ImgY_bottomRight;
070 my $ImgX_mid; #mid coord along x-axis
071 my $ImgY_mid; #mid coord along y-axis
072 if( $Simulation eq 'twirl' ) {
073 $ImgX_topLeft = 0;
074 $ImgY_topLeft = $ImgStretch;
075 $ImgX_bottomRight = $CanvasWidth - 1;
076 $ImgY_bottomRight = $CanvasHeight - 1 - $ImgStretch;
077 $ImgX_mid = $ImgX_bottomRight / 2;
078 } elsif( $Simulation eq 'flip' ) {
079 $ImgX_topLeft = $ImgStretch;
080 $ImgY_topLeft = 0;
081 $ImgX_bottomRight = $CanvasWidth - 1 - $ImgStretch;
082 $ImgY_bottomRight = $CanvasHeight - 1;
083 $ImgY_mid = $ImgY_bottomRight / 2;
084 } else {
085 die FATAL, "Invalid simulation request '$Simulation'\n";
086 }
087
088 my $x_left; #perspective apex coords when twirling
089 my $x_right;
090 my $y_topLeft;
091 my $y_bottomLeft;
092 my $y_topRight;
093 my $y_bottomRight;
094
095 my $y_top; #perspective apex coords when flipping
096 my $y_bottom;
097 my $x_topLeft;
098 my $x_bottomLeft;
099 my $x_topRight;
100 my $x_bottomRight;
101
102 for my $imgIdx ( 0..$NoImages - 1 ) { #repeat for each pairing of images
103 #QUARTER RIGHT-HAND-RULE TURN OF RECTO IMAGE:
104 for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) { # repeat for each transit step
105 if( $Simulation eq 'twirl' ) { # interpolate twirl apex coords & draw frame
106 $x_left = &$Interpolate( $lambda, $ImgX_topLeft, $ImgX_mid );
107 $x_right = &$Interpolate( $lambda, $ImgX_bottomRight, $ImgX_mid );
108 $y_topLeft = &$Interpolate( $lambda, $ImgY_topLeft, 0 );
109 $y_bottomLeft = &$Interpolate( $lambda, $ImgY_bottomRight, $ImgY_bottomRight + $ImgStretch );
110 $y_topRight = &$Interpolate( $lambda, $ImgY_topLeft, $ImgY_topLeft + $ImgStretch );
111 $y_bottomRight = &$Interpolate( $lambda, $ImgY_bottomRight, $ImgY_bottomRight - $ImgStretch );
112 &drawTwirlFrame( $imgIdx );
113 } else { # interpolate flip apex coords & draw frame
114 $y_top = &$Interpolate( $lambda, $ImgY_topLeft, $ImgY_mid );
115 $y_bottom = &$Interpolate( $lambda, $ImgY_bottomRight, $ImgY_mid );
116 $x_topLeft = &$Interpolate( $lambda, $ImgX_topLeft, $ImgX_topLeft + $ImgStretch );
117 $x_bottomLeft = &$Interpolate( $lambda, $ImgX_topLeft, 0 );
118 $x_topRight = &$Interpolate( $lambda, $ImgX_bottomRight, $ImgX_bottomRight - $ImgStretch );
119 $x_bottomRight = &$Interpolate( $lambda, $ImgX_bottomRight, $ImgX_bottomRight + $ImgStretch );
120 &drawFlipFrame( $imgIdx );
121 } # end if-else
122 } # until all steps processed
123 #QUARTER RIGHT-HAND-RULE TURN OF VERSO IMAGE:
124 for(my $lambda=$DeltaLambda; $lambda < 1.-$DeltaLambda/2.; $lambda+=$DeltaLambda) { # repeat for each transit step
125 if( $Simulation eq 'twirl' ) { # interpolate twirl apex coords & draw frame
126 $x_left = &$Interpolate( $lambda, $ImgX_mid, $ImgX_topLeft );
127 $x_right = &$Interpolate( $lambda, $ImgX_mid, $ImgX_bottomRight );
128 $y_topLeft = &$Interpolate( $lambda, $ImgY_topLeft + $ImgStretch, $ImgY_topLeft );
129 $y_bottomLeft = &$Interpolate( $lambda, $ImgY_bottomRight - $ImgStretch, $ImgY_bottomRight );
130 $y_topRight = &$Interpolate( $lambda, 0, $ImgY_topLeft );
131 $y_bottomRight = &$Interpolate( $lambda, $ImgY_bottomRight + $ImgStretch, $ImgY_bottomRight );
132 &drawTwirlFrame( ( $imgIdx + 1 ) % $NoImages );
133 } else { # interpolate flip apex coords & draw frame
134 $y_top = &$Interpolate( $lambda, $ImgY_mid, $ImgY_topLeft );
135 $y_bottom = &$Interpolate( $lambda, $ImgY_mid, $ImgY_bottomRight );
136 $x_topLeft = &$Interpolate( $lambda, 0, $ImgX_topLeft );
137 $x_bottomLeft = &$Interpolate( $lambda, $ImgX_topLeft + $ImgStretch, $ImgX_topLeft );
138 $x_topRight = &$Interpolate( $lambda, $ImgX_bottomRight + $ImgStretch, $ImgX_bottomRight );
139 $x_bottomRight = &$Interpolate( $lambda, $ImgX_bottomRight - $ImgStretch, $ImgX_bottomRight );
140 &drawFlipFrame( ( $imgIdx + 1 ) % $NoImages );
141 } # end if-else
142 } # until all steps processed
143 } #until all images processed
144 print locate( $CursorY, $CursorX ), clline, #report end of frame processing
145 colored ['bold green'], $FrameNo, " frames\n\n";
146 undef $Canvas; #destroy the canvas object
147 undef $Images; #destroy the source image object
148 #-------------------------------------------------------------------------------------------------------------------------------
149 # 3) CREATE ANIMATED GIF IMAGE:
150 print 'Creating animated GIF image... '; #report start of animation processing
151 $Frames->Write #output the animation
152 ( delay => $$Anim{delay},
153 loop => $$Anim{loops},
154 dispose => 'background',
155 filename => $$Anim{output}
156 );
157 print colored ['bold green'], $$Anim{output}, "\n"; #report end of animation processing
158 exit;
159 #===== SUBROUTINES =============================================================================================================
160 # Usage : &drawTwirlFrame( $IMGINDEX );
161 # Purpose : Draw a "twirl" animation frame with the specified image index
162 # Arguments : $IMGINDEX = index of the ImageMagick object $Images.
163 # Subs : None.
164 # Remarks : None.
165 # History : v1.0.0 - September 24, 2009 - Original release.
166
167 sub drawTwirlFrame { #begin sub
168 my $imgIdx = shift; # parameterize the argument
169
170 @$Canvas = (); # clear the canvas
171 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size
172 $Canvas->ReadImage( 'xc:transparent' ); # set canvas background to transparent
173 $Canvas->Composite # init canvas with designated image
174 ( image => $Images->[$imgIdx],
175 compose => 'Over',
176 geometry => "${ImgWidth}x${ImgHeight}+0+$ImgStretch"
177 );
178 $Canvas->Distort # match image corners to perspective apexes:
179 ( points => [ $ImgX_topLeft, $ImgY_topLeft, $x_left, $y_topLeft, # top left
180 $ImgX_topLeft, $ImgY_bottomRight, $x_left, $y_bottomLeft, # bottom left
181 $ImgX_bottomRight, $ImgY_topLeft, $x_right, $y_topRight, # top right
182 $ImgX_bottomRight, $ImgY_bottomRight, $x_right, $y_bottomRight # bottom right
183 ],
184 type => 'Bilinear',
185 'virtual-pixel' => 'transparent',
186 'best-fit' => 1,
187 );
188 push @$Frames, @$Canvas; # add canvas to image sequence
189 print locate( $CursorY, $CursorX ), clline, # report frame processing
190 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
191 } #end sub drawTwirlFrame
192 #-------------------------------------------------------------------------------------------------------------------------------
193 # Usage : &drawFlipFrame( $IMGINDEX );
194 # Purpose : Draw a "flip" animation frame with the specified image index
195 # Arguments : $IMGINDEX = index of the ImageMagick object $Images.
196 # Subs : None.
197 # Remarks : None.
198 # History : v1.0.0 - September 24, 2009 - Original release.
199
200 sub drawFlipFrame { #begin sub
201 my $imgIdx = shift; # parameterize the argument
202
203 @$Canvas = (); # clear the canvas
204 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size
205 $Canvas->ReadImage( 'xc:transparent' ); # set canvas background to transparent
206 $Canvas->Composite # init canvas with designated image
207 ( image => $Images->[$imgIdx],
208 compose => 'Over',
209 geometry => "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
210 );
211 $Canvas->Distort # match image corners to perspective apexes:
212 ( points => [ $ImgX_topLeft, $ImgY_topLeft, $x_topLeft, $y_top, # top left
213 $ImgX_topLeft, $ImgY_bottomRight, $x_bottomLeft, $y_bottom, # bottom left
214 $ImgX_bottomRight, $ImgY_topLeft, $x_topRight, $y_top, # top right
215 $ImgX_bottomRight, $ImgY_bottomRight, $x_bottomRight, $y_bottom # bottom right
216 ],
217 type => 'Bilinear',
218 'virtual-pixel' => 'transparent',
219 'best-fit' => 1,
220 );
221 push @$Frames, @$Canvas; # add canvas to image sequence
222 print locate( $CursorY, $CursorX ), clline, # report frame processing
223 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
224 } #end sub drawFlipFrame
225 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
226 # end of anim_flipcard.pl
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.