use strict;
use warnings;
use Image::Magick;
use Term::ANSIScreen qw/:color :cursor :screen/;
use Win32::Console::ANSI qw/ Cursor /;
use XML::Simple;
	$XML::Simple::PREFERRED_PARSER  = 'XML::Parser';
use constant FATAL => colored ['white on red'], "\aFATAL ERROR: ";
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# anim_flipcard.pl: Creates an animated GIF that flips or twirls a 3D "card" but with multiple images.
#===============================================================================================================================
#           Usage : perl anim_flipcard.pl XmlFile
#       Arguments : XmlFile = path of XML file for the animation specifications.
#     Input Files : See arguments.
#    Output Files : The animated GIF specified in the XML data file.
# Temporary Files : None.
#         Remarks : See http://www.webpraxis.ab.ca/flips/anim_flipcard.shtml for details.
#         History : v1.0.0 - September 24, 2009 - Original release.
#===============================================================================================================================
# 0) INITIALIZE:
$| = 1; 																				#set STDOUT buffer to auto-flush
cls();																					#clear screen
print colored ['black on white'], "$0\n\n\n",											#display program name
	  colored ['reset'], 'Initializing... ';											#report start of initialization

my $XmlFile			= shift || die FATAL, 'No XML file specified';						#get path of XML data file
my $Anim			= XMLin( $XmlFile, ForceArray => [qw(image)] );						#read the XML data file
my $ImgWidth		= $$Anim{imageDims}{width};											#parameterize the width of the images
my $ImgHeight		= $$Anim{imageDims}{height};										#parameterize the height of the images
my $ImgStretch		= $$Anim{imageDims}{stretch};										#parameterize the image distortion
my $Simulation		= $$Anim{simulation};												#parameterize the simulation request
my $NoImages		= @{ $$Anim{image} };												#get the number of specified images
my @Spinners		= ( '-', '\\', '|', '/' );											#define symbols for spinner
my $CanvasWidth		= $ImgWidth;														#canvas width in pixels
	$CanvasWidth   += 2 * $ImgStretch if $Simulation eq 'flip';
my $CanvasHeight	= $ImgHeight;														#canvas height in pixels
	$CanvasHeight  += 2 * $ImgStretch if $Simulation eq 'twirl';
print colored ['bold green'], "XML data read\n\n";										#report end of initialization
#-------------------------------------------------------------------------------------------------------------------------------
# 1) LOAD AND SCALE THE IMAGES:
print 'Reading & scaling images... ';													#report start of image processing
my $Images = Image::Magick->new( magick => 'JPG' );										#instantiate an object for the images

die FATAL, "Even number of images required\n" if $NoImages % 2;							#check for even number of images
for( 0..$NoImages-1 ) {																	#repeat for each source image
	my $file = $$Anim{image}[$_];														# parameterize the file name
	die FATAL, "Cannot locate image file '$file'\n" unless -e $file;					# check image existence
	$Images->Read( $file );																# read image file
}																						#until all images processed
$Images->Quantize( colors => 256, colorspace => 'RGB' );								#ensure uniform color space
$Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" );								#scale images to specified dimensions
print colored ['bold green'], "Done\n\n";												#report end of image processing
#-------------------------------------------------------------------------------------------------------------------------------
# 2) CREATE ANIMATION FRAMES:
print 'Creating animation frames... ';													#report start of frame processing
my ($CursorX, $CursorY)	= Cursor();														#record cursor position

my $Canvas				= Image::Magick->new( magick => 'GIF' );						#instantiate an image object for the canvas
my $Frames				= Image::Magick->new( magick => 'GIF' );						#instantiate an image object for animation frames
my $FrameNo				= 0;															#init no of animation frames
my $Interpolate			= sub	{	my ( $lambda, $start, $end ) = @_;					#anonymous sub for interpolating x,y-coordinates
									int( $lambda * $end + ( 1. - $lambda ) * $start );
								};
my $DeltaLambda			= 1. / ( $$Anim{frames} + 1. );									#set transit step-size

my $ImgX_topLeft;																		#top-left corner coords for 2D image
my $ImgY_topLeft;
my $ImgX_bottomRight;																	#bottom-right corner coords for 2D image
my $ImgY_bottomRight;
my $ImgX_mid;																			#mid coord along x-axis
my $ImgY_mid;																			#mid coord along y-axis
if( $Simulation eq 'twirl' ) {
	$ImgX_topLeft		= 0;
	$ImgY_topLeft		= $ImgStretch;
	$ImgX_bottomRight	= $CanvasWidth  - 1;
	$ImgY_bottomRight	= $CanvasHeight - 1 - $ImgStretch;
	$ImgX_mid			= $ImgX_bottomRight / 2;
} elsif( $Simulation eq 'flip' ) {
	$ImgX_topLeft		= $ImgStretch;
	$ImgY_topLeft		= 0;
	$ImgX_bottomRight	= $CanvasWidth  - 1 - $ImgStretch;
	$ImgY_bottomRight	= $CanvasHeight - 1;
	$ImgY_mid			= $ImgY_bottomRight / 2;
} else {
	die FATAL, "Invalid simulation request '$Simulation'\n";
}

my $x_left;																				#perspective apex coords when twirling
my $x_right;
my $y_topLeft;
my $y_bottomLeft;
my $y_topRight;
my $y_bottomRight;

my $y_top;																				#perspective apex coords when flipping
my $y_bottom;
my $x_topLeft;
my $x_bottomLeft;
my $x_topRight;
my $x_bottomRight;

for my $imgIdx ( 0..$NoImages - 1 ) {													#repeat for each pairing of images
	#QUARTER RIGHT-HAND-RULE TURN OF RECTO IMAGE:
	for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) {	# repeat for each transit step
		if( $Simulation eq 'twirl' ) {													#  interpolate twirl apex coords & draw frame
			$x_left			= &$Interpolate( $lambda,	$ImgX_topLeft,		$ImgX_mid							);
			$x_right		= &$Interpolate( $lambda,	$ImgX_bottomRight,	$ImgX_mid							);
			$y_topLeft		= &$Interpolate( $lambda, 	$ImgY_topLeft,		0									);
			$y_bottomLeft	= &$Interpolate( $lambda,	$ImgY_bottomRight,	$ImgY_bottomRight	+ $ImgStretch	);
			$y_topRight		= &$Interpolate( $lambda, 	$ImgY_topLeft,		$ImgY_topLeft		+ $ImgStretch	);
			$y_bottomRight	= &$Interpolate( $lambda,	$ImgY_bottomRight,	$ImgY_bottomRight	- $ImgStretch 	);
			&drawTwirlFrame( $imgIdx );
		} else {																		#  interpolate flip apex coords & draw frame
			$y_top			= &$Interpolate( $lambda,	$ImgY_topLeft,		$ImgY_mid							);
			$y_bottom		= &$Interpolate( $lambda,	$ImgY_bottomRight,	$ImgY_mid							);
			$x_topLeft		= &$Interpolate( $lambda, 	$ImgX_topLeft,		$ImgX_topLeft		+ $ImgStretch	);
			$x_bottomLeft	= &$Interpolate( $lambda,	$ImgX_topLeft,		0									);
			$x_topRight		= &$Interpolate( $lambda, 	$ImgX_bottomRight,	$ImgX_bottomRight	- $ImgStretch	);
			$x_bottomRight	= &$Interpolate( $lambda,	$ImgX_bottomRight,	$ImgX_bottomRight	+ $ImgStretch 	);
			&drawFlipFrame( $imgIdx );
		}																				# end if-else
	}																					# until all steps processed
	#QUARTER RIGHT-HAND-RULE TURN OF VERSO IMAGE:
	for(my $lambda=$DeltaLambda; $lambda < 1.-$DeltaLambda/2.; $lambda+=$DeltaLambda) {	# repeat for each transit step
		if( $Simulation eq 'twirl' ) {													#  interpolate twirl apex coords & draw frame
			$x_left			= &$Interpolate( $lambda,	$ImgX_mid,							$ImgX_topLeft		);
			$x_right		= &$Interpolate( $lambda,	$ImgX_mid,							$ImgX_bottomRight	);
			$y_topLeft		= &$Interpolate( $lambda, 	$ImgY_topLeft		+ $ImgStretch,	$ImgY_topLeft		);
			$y_bottomLeft	= &$Interpolate( $lambda,	$ImgY_bottomRight	- $ImgStretch,	$ImgY_bottomRight	);
			$y_topRight		= &$Interpolate( $lambda, 	0, 									$ImgY_topLeft		);
			$y_bottomRight	= &$Interpolate( $lambda,	$ImgY_bottomRight	+ $ImgStretch,	$ImgY_bottomRight	);
			&drawTwirlFrame( ( $imgIdx + 1 ) % $NoImages );
		} else {																		#  interpolate flip apex coords & draw frame
			$y_top			= &$Interpolate( $lambda,	$ImgY_mid,							$ImgY_topLeft		);
			$y_bottom		= &$Interpolate( $lambda,	$ImgY_mid,							$ImgY_bottomRight	);
			$x_topLeft		= &$Interpolate( $lambda, 	0,									$ImgX_topLeft		);
			$x_bottomLeft	= &$Interpolate( $lambda,	$ImgX_topLeft		+ $ImgStretch,	$ImgX_topLeft		);
			$x_topRight		= &$Interpolate( $lambda, 	$ImgX_bottomRight	+ $ImgStretch,	$ImgX_bottomRight	);
			$x_bottomRight	= &$Interpolate( $lambda,	$ImgX_bottomRight	- $ImgStretch,	$ImgX_bottomRight 	);
			&drawFlipFrame( ( $imgIdx + 1 ) % $NoImages );
		}																				# end if-else
	}																					# until all steps processed
}																						#until all images processed
print	locate( $CursorY, $CursorX ), clline,											#report end of frame processing
		colored ['bold green'], $FrameNo, " frames\n\n";
undef $Canvas;																			#destroy the canvas object
undef $Images;																			#destroy the source image object
#-------------------------------------------------------------------------------------------------------------------------------
# 3) CREATE ANIMATED GIF IMAGE:
print 'Creating animated GIF image... ';												#report start of animation processing
$Frames->Write																			#output the animation
			(	delay		=> $$Anim{delay},
				loop		=> $$Anim{loops},
				dispose		=> 'background',
				filename	=> $$Anim{output}
			);
print colored ['bold green'], $$Anim{output}, "\n";										#report end of animation processing
exit;
#===== SUBROUTINES =============================================================================================================
#     Usage : &drawTwirlFrame( $IMGINDEX );
#   Purpose : Draw a "twirl" animation frame with the specified image index
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - September 24, 2009 - Original release.

sub drawTwirlFrame {																	#begin sub
	my $imgIdx = shift;																	# parameterize the argument

	@$Canvas = ();																		# clear the canvas
	$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );							# set canvas size
	$Canvas->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Canvas->Composite																	# init canvas with designated image
				(	image			=> $Images->[$imgIdx],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+0+$ImgStretch"
				);
	$Canvas->Distort																	# match image corners to perspective apexes:
				( 	points			=>	[	$ImgX_topLeft,		$ImgY_topLeft,		$x_left,	$y_topLeft,    # top left
											$ImgX_topLeft,		$ImgY_bottomRight,	$x_left,	$y_bottomLeft, # bottom left
											$ImgX_bottomRight,	$ImgY_topLeft,		$x_right,	$y_topRight,   # top right
											$ImgX_bottomRight,	$ImgY_bottomRight,	$x_right,	$y_bottomRight # bottom right
										],
					type			=> 'Bilinear',
					'virtual-pixel'	=> 'transparent',
					'best-fit'		=> 1,
				);
	push @$Frames, @$Canvas;															# add canvas to image sequence
	print	locate( $CursorY, $CursorX ), clline,										# report frame processing
			colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
}																						#end sub drawTwirlFrame
#-------------------------------------------------------------------------------------------------------------------------------
#     Usage : &drawFlipFrame( $IMGINDEX );
#   Purpose : Draw a "flip" animation frame with the specified image index
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - September 24, 2009 - Original release.

sub drawFlipFrame {																		#begin sub
	my $imgIdx = shift;																	# parameterize the argument

	@$Canvas = ();																		# clear the canvas
	$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );							# set canvas size
	$Canvas->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Canvas->Composite																	# init canvas with designated image
				(	image			=> $Images->[$imgIdx],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
				);
	$Canvas->Distort																	# match image corners to perspective apexes:
				( 	points			=>	[	$ImgX_topLeft,		$ImgY_topLeft,		$x_topLeft,		$y_top,    # top left
											$ImgX_topLeft,		$ImgY_bottomRight,	$x_bottomLeft,	$y_bottom, # bottom left
											$ImgX_bottomRight,	$ImgY_topLeft,		$x_topRight,	$y_top,    # top right
											$ImgX_bottomRight,	$ImgY_bottomRight,	$x_bottomRight,	$y_bottom  # bottom right
										],
					type			=> 'Bilinear',
					'virtual-pixel'	=> 'transparent',
					'best-fit'		=> 1,
				);
	push @$Frames, @$Canvas;															# add canvas to image sequence
	print	locate( $CursorY, $CursorX ), clline,										# report frame processing
			colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
}																						#end sub drawFlipFrame
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# end of anim_flipcard.pl
