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_pad.pl: Creates an animated GIF that peels "3D" sheets off a pad.
#===============================================================================================================================
#           Usage : perl anim_pad.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_pad.shtml for details.
#         History : v1.0.0 - October 1, 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 $NoImages		= @{ $$Anim{image} };												#get the number of source images
my @Spinners		= ( '-', '\\', '|', '/' );											#define symbols for spinner

my $CanvasWidth		= $ImgWidth + 2 * $ImgStretch;										# set canvas width
my $CanvasHeight	= $ImgHeight;														# set frame canvas height
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 stated 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 frame canvas
my $Parallelogram		= Image::Magick->new( magick => 'GIF' );						#instantiate an image object for the distorted image
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 "rotation" step-size

my $ImgX_topLeft		= $ImgStretch;													#top-left corner coords for 2D image
my $ImgY_topLeft		= 0;
my $ImgX_bottomRight	= $CanvasWidth  - $ImgStretch - 1;								#bottom-right corner coords for 2D image
my $ImgY_bottomRight	= $CanvasHeight - 1;

my $x_left;																				#apex coords for distorted image
my $x_right;
my $y_bottom;

for my $imgIdx ( 0..$NoImages - 1 ) {													#repeat for each pairing of images
	#QUARTER RIGHT-HAND-RULE ROTATION OF IMAGE HINGED AT THE TOP:
	for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) {	# repeat for each rotation step
		$x_left		= &$Interpolate( $lambda,	$ImgX_topLeft,		0					);
		$x_right	= &$Interpolate( $lambda,	$ImgX_bottomRight,	$CanvasWidth - 1	);
		$y_bottom	= &$Interpolate( $lambda,	$ImgY_bottomRight,	0					);
		&drawFrame( $imgIdx );							 								#  draw the animation frame
	}																					# until all rotation 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 $Parallelogram;																	#destroy the distorted-image object
undef $Images;																			#destroy the object for source images
#-------------------------------------------------------------------------------------------------------------------------------
# 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 : &drawFrame( $IMGINDEX );
#   Purpose : Draw an animation frame with the specified image index
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - October 1, 2009 - Original release.

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

	@$Parallelogram = ();																# clear the distorted-iamge canvas
	$Parallelogram->Set( size => "${CanvasWidth}x${CanvasHeight}" );					# set canvas size
	$Parallelogram->ReadImage( 'xc:transparent' );										# set canvas background to transparent
	$Parallelogram->Composite															# init canvas with designated image
				(	image			=> $Images->[$imgIdx],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
				);
	$Parallelogram->Distort																# match image corners to perspective apexes:
				( 	points			=>	[	$ImgX_topLeft,		$ImgY_topLeft,		$ImgX_topLeft,		$ImgY_topLeft, # top left
											$ImgX_topLeft,		$ImgY_bottomRight,	$x_left,			$y_bottom,     # bottom left
											$ImgX_bottomRight,	$ImgY_topLeft,		$ImgX_bottomRight,	$ImgY_topLeft, # top right
											$ImgX_bottomRight,	$ImgY_bottomRight,	$x_right,			$y_bottom      # bottom right
										],
					type			=> 'Bilinear',
					'virtual-pixel'	=> 'transparent',
					'best-fit'		=> 1,
				);

	@$Canvas = ();																		# clear the frame canvas
	$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );							# set canvas size
	$Canvas->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Canvas->Composite																	# init canvas with next image as background
				(	image			=> $Images->[( $imgIdx +  1 ) % $NoImages],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgStretch+0"
				);
	$Canvas->Composite																	# add parallelogram as foreground
				(	image			=> $Parallelogram,
					compose			=> 'Over',
					geometry		=> "${CanvasWidth}x${CanvasHeight}+0+0"
				);
	push @$Frames, @$Canvas;															# add frame 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_pad.pl
