use strict;
use warnings;
use POSIX qw(ceil);
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_morphs.pl: Creates an animated GIF that morphs image tiles asynchronously.
#===============================================================================================================================
#           Usage : perl anim_morphs.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/morphs/anim_morphs.shtml for details.
#         History : v1.0.0 - October 30, 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
srand( time() ^ ($$ + ($$ << 15)) );                                					#seed the random number generator

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 $TileWidth		= $$Anim{tile}{width};												#parameterize pixel width of a tile
my $TileHeight 		= $$Anim{tile}{height};												#parameterize pixel height of a tile
my $NoImages		= @{ $$Anim{image} };												#get the number of source images
my @Spinners		= ( '-', '\\', '|', '/' );											#define symbols for spinner

my $NoTileCols		= POSIX::ceil( $$Anim{imageDims}{width}  / $TileWidth  );			#compute integral number of tile columns
my $NoTileRows		= POSIX::ceil( $$Anim{imageDims}{height} / $TileHeight );			#compute integral number of tile rows
my $TileCount		= $NoTileCols * $NoTileRows;										#compute total number of tiles

my $CanvasWidth		= $NoTileCols * $TileWidth;											#set canvas width
my $CanvasHeight	= $NoTileRows * $TileHeight;                            			#set 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, "At least 2 images required\n" if $NoImages < 2;								#check for at least 2 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 => "${CanvasWidth}x${CanvasHeight}!" );						#scale images to canvas dimensions
print colored ['bold green'], "Done\n\n";												#report end of image processing
#-------------------------------------------------------------------------------------------------------------------------------
# 2) CREATE TILE IMAGES AND RECORD THEIR LOCATIONS:
print 'Creating tile images... ';														#report start of tile processing
my ( $CursorX, $CursorY ) = Cursor(); 													#record cursor position

my $Tiles		= Image::Magick->new( magick => 'GIF' );								#instantiate an image object for the tiles
my @Tile_X;																				#array for tile top-left x-coordinates
my @Tile_Y;																				#array for tile top-left y-coordinates
{																						#start naked block as firewall
	my $tile	= Image::Magick->new( magick => 'GIF' );								# instantiate an image object for a tile

	for my $imgIdx ( 0..$NoImages-1 ) {													# repeat for each source image
		for (	my $y_topLeft	= 0;													#  for each tile row: start at top and work down
				$y_topLeft		<= $CanvasHeight - $TileHeight;
				$y_topLeft		+= $TileHeight
			) {
			for (	my $x_topLeft	= 0;												#   for each tile column: work left to right
					$x_topLeft		<= $CanvasWidth - $TileWidth;
					$x_topLeft		+= $TileWidth
				) {
				my $geometry	= "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft";	#    define tile geometry
				$tile 			= $Images->[$imgIdx]->Clone();							#    init tile with image
				$tile->Crop( geometry => $geometry );									#    crop tile area
				$tile->Set( page => '0x0+0+0' );										#    shrink canvas
				push @$Tiles, $tile;													#    store the tile image
				push @Tile_X, $x_topLeft;												#    store the tile coords
				push @Tile_Y, $y_topLeft;
				print	locate( $CursorY, $CursorX ), clline,							#    report tile processing
						colored ['bold yellow'], '(', $Spinners[$#$Tiles % 4], ')';
			}																			#   until all tile columns processed
		}																				#  until all tile rows processed
	}																					# until all images processed
	undef $tile;																		# destroy the tile image object
}																						#end naked block
print	locate( $CursorY, $CursorX ), clline,											#report end of tile processing
		colored ['bold green'], scalar @Tile_X, " tiles\n\n";
#-------------------------------------------------------------------------------------------------------------------------------
# 3) CREATE MORPHED IMAGES FOR EACH TILE:
print 'Morphing each tile... ';															#report start of morphing process
( $CursorX, $CursorY )	= Cursor();														#record cursor position

my @Countdown;																			#countdowns for start of morphing display
my @Morphs;																				#morphed images for each tile
{																						#start naked block as firewall
	my $tiles2morph	= Image::Magick->new( magick => 'GIF' );							# instantiate an image object for tile pairs

	for my $imgIdx ( 0..$NoImages-1 ) {													# repeat for each source image
		for my $tileIdx ( $imgIdx*$TileCount..($imgIdx+1)*$TileCount - 1 ){				#  repeat for each of the image's tiles
			@$tiles2morph			= ();												#   clear tile pairs
			push @$tiles2morph, $Tiles->[$tileIdx], 									#   set current tile as start image
								$Tiles->[($tileIdx+$TileCount)%($NoImages*$TileCount)];	#    and corresponding tile of next image as end image
			$Morphs[$tileIdx]		= $tiles2morph->Morph( frames => $$Anim{frames} );	#   generate sequence of morphed images
			$Countdown[$tileIdx]	= int rand( int $TileCount / 10 );					#   set random countdown
			print	locate( $CursorY, $CursorX ), clline,								#   report morphing process
					colored ['bold yellow'], '(', $Spinners[$#Morphs % 4], ')';
		}																				#  until all image's tiles processed
	}																					# until all images processed
	undef $tiles2morph;																	# destroy the image object for tile pairs
}																						#end naked block
undef $Tiles;																			#destroy the image object for the tiles
print	locate( $CursorY, $CursorX ), clline,											#report end of morphing process
		colored ['bold green'], scalar @Morphs, " morph sequences\n\n";
#-------------------------------------------------------------------------------------------------------------------------------
# 4) CREATE ANIMATION FRAMES:
print 'Creating animation frames... ';													#report start of frame processing
( $CursorX, $CursorY )	= Cursor();														#record cursor position

my $Canvas				= $Images->[0];													#init canvas with zeroth image
my $Frames				= Image::Magick->new( magick => 'GIF' );						#instantiate an image object for animation frames
my $FrameNo				= 0;															#init no of animation frames
my $NoSequencesDone;																	#number of morph-sequence displays ended per image
undef $Images;																			#destroy the source image object
{																						#start naked block as firewall
	my $morphedTile;																	# morphed tile to be displayed
	for my $imgIdx ( 0..$NoImages-1 ) {													# repeat for each source image
		$NoSequencesDone = 0;															#  reset ended-sequence count for new image
		until( $NoSequencesDone == $TileCount ) {										#  repeat
			$NoSequencesDone = 0;														#   reset ended-sequence count for current image
			for my $tileIdx ($imgIdx*$TileCount..($imgIdx+1)*$TileCount -1 ){			#   repeat for each of the image's tiles
				next if --$Countdown[$tileIdx] > 0;										#    decrement countdown & skip if greater than zero
				$Canvas->Composite														#    add any morphed tile image to canvas
							(	image		=> $morphedTile,
								compose		=> 'Over',
								geometry	=> "${TileWidth}x${TileHeight}+$Tile_X[$tileIdx]+$Tile_Y[$tileIdx]"
							)
				 if $morphedTile = shift @{$Morphs[$tileIdx]};
				++$NoSequencesDone unless $Morphs[$tileIdx]->[0];						#    increment sequence counter if no morphed images left
			}																			#   until all tiles processed
			push @$Frames, $Canvas->Clone();											#   add canvas to animation frames
			print	locate( $CursorY, $CursorX ), clline,								#   report frame processing
					colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
		}																				#  until all morph sequences displayed
	}																					# until all images processed
	undef $morphedTile;																	# destroy morphed tile object
}																						#end naked block
print	locate( $CursorY, $CursorX ), clline,											#report end of frame processing
		colored ['bold green'], scalar @$Frames, " frames\n\n";
undef $Canvas;																			#destroy the canvas object
undef @Countdown;																		#destroy the array for the countdowns
undef @Morphs;																			#destroy the image object for the morph sequences
undef @Tile_X;																			#destroy the arrays for the tile coordinates
undef @Tile_Y;
#-------------------------------------------------------------------------------------------------------------------------------
# 5) 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_morphs.pl
