use strict;
use warnings;
use Image::Magick;
use List::AllUtils qw(max);
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_grid_transits.pl: Creates an animated GIF of images translating about a grid.
#===============================================================================================================================
#           Usage : perl anim_grid_transits.pl XmlFile
#       Arguments : XmlFile = path of XML file describing the animation sequence.
#     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/transits/anim_grid_transits.shtml for details.
#         History : v1.0.0 - September 15, 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(transit 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 $ImgDims		= "${ImgWidth}x${ImgHeight}";											#set dimension declaration of the images
my %ImgFile2Idx;																		#hash for matching image files to ImageMagick indexes
print colored ['bold green'], "XML data read\n\n";										#report end of initialization
#-------------------------------------------------------------------------------------------------------------------------------
# 1) LOAD AND SCALE THE IMAGES:
print 'Reading & scaling images... ';													#report image processing
my $Images = Image::Magick->new( magick => "JPG" );										#instantiate an object for the images
for my $transitIdx ( 0..$#{$$Anim{transit}} ) {											#repeat for each transit declaration
	for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {						# repeat for each corresponding image
		my $file = $$Anim{transit}[$transitIdx]{image}[$imgIdx]{file}					#  parameterize the file name
					|| die FATAL, "Missing filename declaration\n";
		next if defined $ImgFile2Idx{$file};											#  skip image if already processed
		die FATAL, "Cannot locate image file '$file'\n" unless -e $file;
		$Images->Read( $file );															#  read image file
		$ImgFile2Idx{$file} = $#{$Images};												#  correlate file name to image array index
	}																					# until all images processed
}																						#until all transits processed
$Images->Quantize( colors => 256, colorspace => 'RGB' );								#ensure uniform color space
$Images->Scale( geometry => "$ImgDims!" );												#scale images to specified dimensions
print colored ['bold green'], scalar keys %ImgFile2Idx, " unique images\n\n";			#report image processing
#-------------------------------------------------------------------------------------------------------------------------------
# 2) COMPUTE THE DIMENSIONS OF THE ANIMATION CANVAS:
print 'Computing canvas size... ';														#report canvas processing
my $CanvasWidth;																		#canvas width in pixels
my $CanvasHeight;																		#canvas height in pixels
{																						#start naked block
	my $maxCol	= undef;																# maximum canvas column number
	my $maxRow	= undef;																# maximum canvas row number

	for my $transitIdx ( 0..$#{$$Anim{transit}} ) {										# repeat for each transit declaration
		for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {					#  repeat for each corresponding image
			my $refImage = $$Anim{transit}[$transitIdx]{image}[$imgIdx];				#   define a reference to the current image
			unless( defined $$refImage{start} ) {										#   if missing start location
				my $match = ( grep	{	$$refImage{file}								#    search for image index in previous transit
										eq
										$$Anim{transit}[$transitIdx-1]{image}[$_]{file}
									}
									0..$#{$$Anim{transit}[$transitIdx-1]{image}}
							)[0];
				$$refImage{start} = $$Anim{transit}[$transitIdx-1]{image}[$match]{end}	#    set start location to prev end location
				 if defined $match;														#     if match found
			}																			#   end if
			for my $where ( 'start', 'end' ) {											#   repeat for each extreme image position
				die FATAL, "Missing $where location for image '$$refImage{file}'\n"		#    check that position is defined
				 unless defined $$refImage{$where};
				my ( $row, $col )	= split /,/, $$refImage{$where};					#    parse location data
				$maxRow				= max grep { defined $_ } $maxRow, $row;			#    update maximum canvas row number
				$maxCol				= max grep { defined $_ } $maxCol, $col;			#    update maximum canvas column number
			}																			#   until both positions processed
		}																				#  until all images processed
	}																					# until all transits processed
	$CanvasWidth	= $maxCol * $ImgWidth;												# compute canvas width
	$CanvasHeight	= $maxRow * $ImgHeight;												# compute canvas height
}																						#end naked block
print colored ['bold green'], "${CanvasWidth}x${CanvasHeight} px\n\n";					#report canvas processing
#-------------------------------------------------------------------------------------------------------------------------------
# 3) 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 @Spinners				= ( '-', '\\', '|', '/' );									#define symbols for spinner

for my $transitIdx ( 0..$#{$$Anim{transit}} ) {											#repeat for each transit declaration
	for( my $lambda = 0.; $lambda < 1. + $DeltaLambda/2.; $lambda += $DeltaLambda ) {	# repeat for each transit step
		@$Canvas = ();																	#  clear the canvas
		$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );						#  set canvas size
		$Canvas->ReadImage( 'xc:transparent' );											#  set canvas background to transparent
		for my $imgIdx ( 0..$#{$$Anim{transit}[$transitIdx]{image}} ) {					#  repeat for each transit image
			my $refImage			= $$Anim{transit}[$transitIdx]{image}[$imgIdx];		#   define a reference to the current image
			my ($r_start,$c_start)	= split /,/, $$refImage{start};						#   parse the start location
			my ($r_end,  $c_end  )	= split /,/, $$refImage{end};						#   parse the end location
			my $x_topLeft			= &$Interpolate										#   compute top-left image x-coordinate:
										(	$lambda,
											( $c_start - 1 ) * $ImgWidth,				#    translate start column no to coordinate
											( $c_end   - 1 ) * $ImgWidth				#    translate end column no to coordinate
										);
			my $y_topLeft			= &$Interpolate										#   compute top-left image y-coordinate:
										(	$lambda,
											( $r_start - 1 ) * $ImgHeight,				#    translate start row no to coordinate
											( $r_end   - 1 ) * $ImgHeight				#    translate end row no to coordinate
										);
			$Canvas->Composite															#   blend image onto canvas
						(	image		=> $Images->[$ImgFile2Idx{$$refImage{file}}],
							compose		=> 'Blend',
							geometry	=> "$ImgDims+$x_topLeft+$y_topLeft"
						);
		}																				#  until all transit images processed
		push @$Frames, @$Canvas;														#  add canvas to image sequence
		print	locate( $CursorY, $CursorX ), clline,									#  report frame processing
				colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
	}																					# until all steps processed for transit
}																						#until all transits processed
print	locate( $CursorY, $CursorX ), clline,											#report end of frame processing
		colored ['bold green'], $FrameNo, " frames\n\n";
undef $Canvas;																			#destroy the canvas object
#-------------------------------------------------------------------------------------------------------------------------------
# 4) 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;
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# end of anim_grid_transits.pl
