use strict;
use warnings;
use POSIX qw(ceil);
use Switch;
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_circuits.pl: Creates an animated GIF that loops image parts around circuits.
#===============================================================================================================================
#           Usage : perl anim_circuits.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_circuits.shtml for details.
#         History : v1.0.0 - September 19, 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 );													#read the XML data file

my $ImageFile	= $$Anim{image}{file};													#parameterize path of source image file
my $AnimFile	= $$Anim{output};														#parameterize path of target image file
my $TileWidth	= $$Anim{tile}{width};													#parameterize pixel width of a tile
my $TileHeight 	= $$Anim{tile}{height};													#parameterize pixel height of a tile
my @Spinners	= ( '-', '\\', '|', '/' );												#define symbols for spinner
die FATAL, "Cannot locate image file\n" unless -e $ImageFile;
print colored ['bold green'], "XML data read\n\n";										#report end of initialization
#-------------------------------------------------------------------------------------------------------------------------------
# 1) SCALE THE SOURCE IMAGE WITH AN EVEN NUMBER OF TILE COLUMNS & SET THE CANVAS DIMENSIONS:
my $Image			= Image::Magick->new( magick => 'GIF' );							#instantiate an image object for the image
	$Image->Read( "$ImageFile" );														#init with source image file
my ( $ImageWidth,																		#get width & height of source image
	 $ImageHeight )	= $Image->Get( 'width', 'height' );

my $NoTileCols		= POSIX::ceil( $$Anim{image}{scale} * $ImageWidth  / $TileWidth  );	#compute integral number of tile columns
++$NoTileCols if $NoTileCols % 2;					 									#ensure even no of tile columns
my $NoTileRows		= POSIX::ceil( $$Anim{image}{scale} * $ImageHeight / $TileHeight );	#compute integral number of tile rows
my $TileCount		= $NoTileCols * $NoTileRows;										#compute total number of tiles

my $CanvasWidth		= ( $NoTileCols + $NoTileCols ) * $TileWidth;						#set canvas width
my $CanvasHeight	= ( $NoTileRows + $NoTileCols ) * $TileHeight;						#set canvas height

print	"\r", clline,							  										#echo initialization results
		colored ['reset'], 'Source Image:    ', colored ['bold white'], $ImageFile, "\n",
		colored ['reset'], '  Width:         ', colored ['bold white'], $ImageWidth, " px\n",
		colored ['reset'], '  Height:        ', colored ['bold white'], $ImageHeight, " px\n",
		colored ['reset'], 'Target Image:    ', colored ['bold white'], $AnimFile, "\n",
		colored ['reset'], '  Width:         ', colored ['bold white'], $CanvasWidth, " px\n",
		colored ['reset'], '  Height:        ', colored ['bold white'], $CanvasHeight, " px\n",
		colored ['reset'], 'Tile Details:    ', colored ['bold white'], $TileCount, " tiles\n",
		colored ['reset'], '  No. of rows:   ', colored ['bold white'], $NoTileRows, "\n",
		colored ['reset'], '  No. of cols:   ', colored ['bold white'], $NoTileCols, "\n",
		colored ['reset'], '  Tile width:    ', colored ['bold white'], $TileWidth, " px\n",
		colored ['reset'], '  Tile height:   ', colored ['bold white'], $TileHeight, " px\n\n",
		colored ['reset'], '-' x 80, "\n\n";

$ImageWidth			= $NoTileCols * $TileWidth;											#set image width
$ImageHeight 		= $NoTileRows * $TileHeight;										#set image height
$Image->Scale( geometry=>"${ImageWidth}x${ImageHeight}!" );								#scale image
#-------------------------------------------------------------------------------------------------------------------------------
# 2) CREATE THE TILE IMAGES & DEFINE THEIR LOCATIONS ON THE CANVAS:
print	'Creating tile images... ';														#report start of tile processing
my ( $CursorX, $CursorY )	= Cursor(); 												#record cursor position
my $Offset_X				= ( $NoTileCols / 2 ) * $TileWidth;							#define x-offset as to center image on canvas
my $Offset_Y				= ( $NoTileCols / 2 ) * $TileHeight;						#define y-offset as to center image on canvas
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 $y_topLeft	= 0;														# for each tile row: start at top and work down
			$y_topLeft		<= $ImageHeight - $TileHeight;
			$y_topLeft		+= $TileHeight
		) {
		for (	my $x_topLeft	= 0;													#  for each tile column: work left to right
				$x_topLeft		<= $ImageWidth - $TileWidth;
				$x_topLeft		+= $TileWidth
			) {
			my $geometry	= "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft";		#   define tile geometry
			$tile 			= $Image->Clone();											#   init tile with full 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, $Offset_X + $x_topLeft;										#   store the tile x-coords
			push @Tile_Y, $Offset_Y + $y_topLeft;										#   store the tile y-coords
			print	locate( $CursorY, $CursorX ), clline,								#   report tile processing
					colored ['bold yellow'], '(', $Spinners[$#{$Tiles} % 4], ')';
		}																				#  until all tile columns processed
	}																					# until all tile rows 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) DEFINE A SET OF DIRECTIONS FOR EACH TILE TREATED AS A "TURTLE":
print	'Defining tile circuits... ';													#report start of circuit definitions
my @Circuit;																			#array for tile directions

for my $row ( 1..$NoTileRows ) {														#repeat for each tile row
	for my $col ( 1..$NoTileCols ) {													# repeat for each tile column
		if( $col <= $NoTileCols / 2 ) {													#  if tile lies left of vertical center line
			push @Circuit,	scalar reverse												#   define its directions in reverse order:
								'S' x ( $NoTileRows - $row + $col	) .					#    1st transit: move down to appropriate position
								'W' x ( 2 * $col - 1				) .					#    2nd transit: move left along bottom track
								'N' x ( $NoTileRows + 2 * $col - 1	) .					#    3rd transit: move up along side track
								'E' x ( 2 * $col - 1				) .					#    4th transit: move right along top track
								'S' x ( $col + $row	- 1 			);					#    5th transit: move down back to starting location
		} else {																		#  else tile lies right of vertical center line
			push @Circuit, $Circuit[$NoTileCols * $row - $col];							#   set directions as mirror image about line
			$Circuit[$#Circuit] =~ tr/WE/EW/;
		}																				#  end if-else
	}																					# until all tile columns processed
}																						#until all tile rows processed
print colored ['bold green'], "Done\n\n";												#report end of circuit definitions
#-------------------------------------------------------------------------------------------------------------------------------
# 4) CREATE ANIMATION FRAMES:
print	'Creating animation frames... ';												#report start of frame processing
( $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
$Frames->Set( size => "${CanvasWidth}x${CanvasHeight}" );								#set frame size to canvas dimensions
$Frames->ReadImage( 'xc:transparent' );													#set frame background to transparent
$Frames->Composite																		#set centered source image as 1st frame
			(	image		=> $Image,
				compose		=> 'Over',
				geometry	=> "${ImageWidth}x${ImageHeight}+$Offset_X+$Offset_Y"
			);
my $FrameNo				= 1;															#init no of animation frames
my $TilesBackInPlace	= 0;															#init no of tiles back at their starting locations
undef $Image;																			#destroy the source image object

until( $TilesBackInPlace == $TileCount ) {												#repeat
	$TilesBackInPlace	= 0;															# reset return counter
	@$Canvas 			= ();															# clear canvas
	$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );							# set canvas size
	$Canvas->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	for( 0..$TileCount-1 ) {															# repeat for each tile
		switch ( my $direction = chop $Circuit[$_] ) {									#  process next motion code if any:
			case 'N'	{	$Tile_Y[$_] -= $TileHeight;	}								#   case move up one tile position, or
			case 'S'	{	$Tile_Y[$_] += $TileHeight;	}								#   case move down one tile position, or
			case 'E'	{	$Tile_X[$_] += $TileWidth;	}								#   case move right one tile position, or
			case 'W'	{	$Tile_X[$_] -= $TileWidth;	}								#   case move left one tile position
		}																				#  end of case
		++$TilesBackInPlace unless length $Circuit[$_];									#  increment return counter if no more orientations
		$Canvas->Composite																#  add tile to canvas
					(	image		=> $Tiles->[$_],
						compose		=> 'Over',
						geometry	=> "${TileWidth}x${TileHeight}+$Tile_X[$_]+$Tile_Y[$_]"
					);
	}																					# until all tiles 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 tiles have returned
print	locate( $CursorY, $CursorX ), clline,											#report end of frame processing
		colored ['bold green'], $FrameNo, " frames\n\n";
undef $Tiles;																			#destroy the image object for the tiles
undef $Canvas;																			#destroy the canvas object
#-------------------------------------------------------------------------------------------------------------------------------
# 5) CREATE ANIMATED GIF IMAGE:
print	'Creating animated GIF image... ';												#report start of animation processing
$Frames->Write																			#output the animation frame
			(	delay		=> $$Anim{delay},
				loop		=> $$Anim{loops},
				dispose		=> 'background',
				filename	=> $AnimFile
			);
print colored ['bold green'], $AnimFile, "\n";											#report end of animation processing
exit;
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# end of anim_circuits.pl
