In this article I describe yet another one of my tiles-in-motion ideas. Here an image is again first sliced and diced into tiles. Then columns of tiles transit around in separate lanes. The tiles drop out the bottom of the starting area and move up along the outside perimeter, half on one side and half on the other, and finally drop back to their starting location, reconstituting the source image. The concept is illustrated in the following route diagram for a 4 x 4 set of tiles:
Slicing and dicing is not a problem as we have amptly demonstrated with the Perl script img2puzzle.pl and, more recently, with anim_reassemble.pl. The main obstacle is decidedly how to describe the route that each tile must follow. Clearly, each tile must hit specific waypoints on its way back to its starting position. The question is whether it is easier to have these waypoints declared explicitely as coordinates or described implicitely by some sort of metadata.
We did try at first using an explicit approach but the coding got rather messy and therefore difficult to debug. After much teeth nashing and further cogitating, we turned instead to the the control paradigm used by the Logo programming language and its turtle graphics as we once did for the script img2photomosaic_slides.pl.
A Logo "turtle" has parameters for orientation and position. These are normally manipulated by specifying the numbers of degrees to turn by and the length of the step to take. We can now modify this paradigm for the task at hand. Let a tile be a "turtle" whose inherent behaviour is to always be in motion, changing its current position at each step by a either a tile width or height. Controlling its path is then just a matter of associating an orientation to each step such that the resulting sequence will take it along the desired circuit. In addition, it must come to a stop once all its orientation directives have been executed. As we'll see, the implementation is trivial thought not necessarily straightforward.
As we have done for all our animation scripts, an XML file conveys all the required specifications.
To explain the XML tags, let's examine the data file monalisa_circuit.xml
( [Download monalisa_circuit.xml]
[MD5 checksum] ).
It governs the creation of the following animated GIF image:
XML file "monalisa_circuit.xml" | Remarks |
---|---|
<animation> | start of XML declaration |
<output>monalisa_circuit.gif</output> | name of the output file |
<delay>15</delay> | the number of milliseconds in delaying the image frame views |
<loops>0</loops> | the number of times to cycle the animated GIF: 0 results in infinite looping |
<image> | start of source image specifications |
<file>Mona_Lisa.jpg</file> | name of the source file |
<scale>0.20</scale> | scaling factor to be applied to the source. A value of "1" implies no scaling. It is applied to the source image prior to the final dimension adjustments for an integral number of tile rows and columns. |
</image> | end of source image specifications |
<tile> | start of tile specifications |
<width>15</width> | in pixels |
<height>15</height> | in pixels |
</tile> | end of tile specifications |
</animation> | end of XML declaration |
For the processing phase, Perl is used along with ImageMagick's drawing primitives accessed through its PerlMagick interface. The Perl script anim_circuits.pl, displayed below, is the resulting code. It is released for personal, non-commercial and non-profit use only.
The listing includes the line numbers in order to reference them in the following general remarks.
perl anim_circuits.pl MonaLisa_circuit.xml
Here we are requesting that the XML file "MonaLisa_circuit.xml" be processed. A screen shot at the end of processing is:If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.
001 use strict; 002 use warnings; 003 use POSIX qw(ceil); 004 use Switch; 005 use Image::Magick; 006 use Term::ANSIScreen qw/:color :cursor :screen/; 007 use Win32::Console::ANSI qw/ Cursor /; 008 use XML::Simple; 009 $XML::Simple::PREFERRED_PARSER = 'XML::Parser'; 010 use constant FATAL => colored ['white on red'], "\aFATAL ERROR: "; 011 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================ 012 # anim_circuits.pl: Creates an animated GIF that loops image parts around circuits. 013 #=============================================================================================================================== 014 # Usage : perl anim_circuits.pl XmlFile 015 # Arguments : XmlFile = path of XML file describing the animation sequence. 016 # Input Files : See arguments. 017 # Output Files : The animated GIF specified in the XML data file. 018 # Temporary Files : None. 019 # Remarks : See http://www.webpraxis.ab.ca/transits/anim_circuits.shtml for details. 020 # History : v1.0.0 - September 19, 2009, - Original release. 021 #=============================================================================================================================== 022 # 0) INITIALIZE: 023 $| = 1; #set STDOUT buffer to auto-flush 024 cls(); #clear screen 025 print colored ['black on white'], "$0\n\n\n", #display program name 026 colored ['reset'], 'Initializing... '; #report start of initialization 027 028 my $XmlFile = shift || die FATAL, 'No XML file specified'; #get path of XML data file 029 my $Anim = XMLin( $XmlFile ); #read the XML data file 030 031 my $ImageFile = $$Anim{image}{file}; #parameterize path of source image file 032 my $AnimFile = $$Anim{output}; #parameterize path of target image file 033 my $TileWidth = $$Anim{tile}{width}; #parameterize pixel width of a tile 034 my $TileHeight = $$Anim{tile}{height}; #parameterize pixel height of a tile 035 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner 036 die FATAL, "Cannot locate image file\n" unless -e $ImageFile; 037 print colored ['bold green'], "XML data read\n\n"; #report end of initialization 038 #------------------------------------------------------------------------------------------------------------------------------- 039 # 1) SCALE THE SOURCE IMAGE WITH AN EVEN NUMBER OF TILE COLUMNS & SET THE CANVAS DIMENSIONS: 040 my $Image = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the image 041 $Image->Read( "$ImageFile" ); #init with source image file 042 my ( $ImageWidth, #get width & height of source image 043 $ImageHeight ) = $Image->Get( 'width', 'height' ); 044 045 my $NoTileCols = POSIX::ceil( $$Anim{image}{scale} * $ImageWidth / $TileWidth ); #compute integral number of tile columns 046 ++$NoTileCols if $NoTileCols % 2; #ensure even no of tile columns 047 my $NoTileRows = POSIX::ceil( $$Anim{image}{scale} * $ImageHeight / $TileHeight ); #compute integral number of tile rows 048 my $TileCount = $NoTileCols * $NoTileRows; #compute total number of tiles 049 050 my $CanvasWidth = ( $NoTileCols + $NoTileCols ) * $TileWidth; #set canvas width 051 my $CanvasHeight = ( $NoTileRows + $NoTileCols ) * $TileHeight; #set canvas height 052 053 print "\r", clline, #echo initialization results 054 colored ['reset'], 'Source Image: ', colored ['bold white'], $ImageFile, "\n", 055 colored ['reset'], ' Width: ', colored ['bold white'], $ImageWidth, " px\n", 056 colored ['reset'], ' Height: ', colored ['bold white'], $ImageHeight, " px\n", 057 colored ['reset'], 'Target Image: ', colored ['bold white'], $AnimFile, "\n", 058 colored ['reset'], ' Width: ', colored ['bold white'], $CanvasWidth, " px\n", 059 colored ['reset'], ' Height: ', colored ['bold white'], $CanvasHeight, " px\n", 060 colored ['reset'], 'Tile Details: ', colored ['bold white'], $TileCount, " tiles\n", 061 colored ['reset'], ' No. of rows: ', colored ['bold white'], $NoTileRows, "\n", 062 colored ['reset'], ' No. of cols: ', colored ['bold white'], $NoTileCols, "\n", 063 colored ['reset'], ' Tile width: ', colored ['bold white'], $TileWidth, " px\n", 064 colored ['reset'], ' Tile height: ', colored ['bold white'], $TileHeight, " px\n\n", 065 colored ['reset'], '-' x 80, "\n\n"; 066 067 $ImageWidth = $NoTileCols * $TileWidth; #set image width 068 $ImageHeight = $NoTileRows * $TileHeight; #set image height 069 $Image->Scale( geometry=>"${ImageWidth}x${ImageHeight}!" ); #scale image 070 #------------------------------------------------------------------------------------------------------------------------------- 071 # 2) CREATE THE TILE IMAGES & DEFINE THEIR LOCATIONS ON THE CANVAS: 072 print 'Creating tile images... '; #report start of tile processing 073 my ( $CursorX, $CursorY ) = Cursor(); #record cursor position 074 my $Offset_X = ( $NoTileCols / 2 ) * $TileWidth; #define x-offset as to center image on canvas 075 my $Offset_Y = ( $NoTileCols / 2 ) * $TileHeight; #define y-offset as to center image on canvas 076 my $Tiles = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the tiles 077 my @Tile_X; #array for tile top-left x-coordinates 078 my @Tile_Y; #array for tile top-left y-coordinates 079 080 { #start naked block as firewall 081 my $tile = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for a tile 082 083 for ( my $y_topLeft = 0; # for each tile row: start at top and work down 084 $y_topLeft <= $ImageHeight - $TileHeight; 085 $y_topLeft += $TileHeight 086 ) { 087 for ( my $x_topLeft = 0; # for each tile column: work left to right 088 $x_topLeft <= $ImageWidth - $TileWidth; 089 $x_topLeft += $TileWidth 090 ) { 091 my $geometry = "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft"; # define tile geometry 092 $tile = $Image->Clone(); # init tile with full image 093 $tile->Crop( geometry => $geometry ); # crop tile area 094 $tile->Set( page => '0x0+0+0' ); # shrink canvas 095 push @$Tiles, @$tile; # store the tile image 096 push @Tile_X, $Offset_X + $x_topLeft; # store the tile x-coords 097 push @Tile_Y, $Offset_Y + $y_topLeft; # store the tile y-coords 098 print locate( $CursorY, $CursorX ), clline, # report tile processing 099 colored ['bold yellow'], '(', $Spinners[$#{$Tiles} % 4], ')'; 100 } # until all tile columns processed 101 } # until all tile rows processed 102 undef $tile; # destroy the tile image object 103 } #end naked block 104 print locate( $CursorY, $CursorX ), clline, #report end of tile processing 105 colored ['bold green'], scalar @Tile_X, " tiles\n\n"; 106 #------------------------------------------------------------------------------------------------------------------------------- 107 # 3) DEFINE A SET OF DIRECTIONS FOR EACH TILE TREATED AS A "TURTLE": 108 print 'Defining tile circuits... '; #report start of circuit definitions 109 my @Circuit; #array for tile directions 110 111 for my $row ( 1..$NoTileRows ) { #repeat for each tile row 112 for my $col ( 1..$NoTileCols ) { # repeat for each tile column 113 if( $col <= $NoTileCols / 2 ) { # if tile lies left of vertical center line 114 push @Circuit, scalar reverse # define directions in reverse order: 115 'S' x ( $NoTileRows - $row + $col ) . # 1st transit: move down to appropriate position 116 'W' x ( 2 * $col - 1 ) . # 2nd transit: move left along bottom track 117 'N' x ( $NoTileRows + 2 * $col - 1 ) . # 3rd transit: move up along side track 118 'E' x ( 2 * $col - 1 ) . # 4th transit: move right along top track 119 'S' x ( $col + $row - 1 ); # 5th transit: move down back to starting location 120 } else { # else tile lies right of vertical center line 121 push @Circuit, $Circuit[$NoTileCols * $row - $col]; # set directions as mirror image about line 122 $Circuit[$#Circuit] =~ tr/WE/EW/; 123 } # end if-else 124 } # until all tile columns processed 125 } #until all tile rows processed 126 print colored ['bold green'], "Done\n\n"; #report end of circuit definitions 127 #------------------------------------------------------------------------------------------------------------------------------- 128 # 4) CREATE ANIMATION FRAMES: 129 print 'Creating animation frames... '; #report start of frame processing 130 ( $CursorX, $CursorY ) = Cursor(); #record cursor position 131 my $Canvas = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the canvas 132 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames 133 $Frames->Set( size => "${CanvasWidth}x${CanvasHeight}" ); #set frame size to canvas dimensions 134 $Frames->ReadImage( 'xc:transparent' ); #set frame background to transparent 135 $Frames->Composite #set centered source image as 1st frame 136 ( image => $Image, 137 compose => 'Over', 138 geometry => "${ImageWidth}x${ImageHeight}+$Offset_X+$Offset_Y" 139 ); 140 my $FrameNo = 1; #init no of animation frames 141 my $TilesBackInPlace = 0; #init no of tiles back at their starting locations 142 undef $Image; #destroy the source image object 143 144 until( $TilesBackInPlace == $TileCount ) { #repeat 145 $TilesBackInPlace = 0; # reset return counter 146 @$Canvas = (); # clear canvas 147 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size 148 $Canvas->ReadImage( 'xc:transparent' ); # set canvas background to transparent 149 for( 0..$TileCount-1 ) { # repeat for each tile 150 switch ( my $direction = chop $Circuit[$_] ) { # process next motion code if any: 151 case 'N' { $Tile_Y[$_] -= $TileHeight; } # case move up one tile position, or 152 case 'S' { $Tile_Y[$_] += $TileHeight; } # case move down one tile position, or 153 case 'E' { $Tile_X[$_] += $TileWidth; } # case move right one tile position, or 154 case 'W' { $Tile_X[$_] -= $TileWidth; } # case move left one tile position 155 } # end of case 156 ++$TilesBackInPlace unless length $Circuit[$_]; # increment return counter if no more orientations 157 $Canvas->Composite # add tile to canvas 158 ( image => $Tiles->[$_], 159 compose => 'Over', 160 geometry => "${TileWidth}x${TileHeight}+$Tile_X[$_]+$Tile_Y[$_]" 161 ); 162 } # until all tiles processed 163 push @$Frames, @$Canvas; # add canvas to image sequence 164 print locate( $CursorY, $CursorX ), clline, # report frame processing 165 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')'; 166 } #until all tiles have returned 167 print locate( $CursorY, $CursorX ), clline, #report end of frame processing 168 colored ['bold green'], $FrameNo, " frames\n\n"; 169 undef $Tiles; #destroy the image object for the tiles 170 undef $Canvas; #destroy the canvas object 171 #------------------------------------------------------------------------------------------------------------------------------- 172 # 5) CREATE ANIMATED GIF IMAGE: 173 print 'Creating animated GIF image... '; #report start of animation processing 174 $Frames->Write #output the animation frame 175 ( delay => $$Anim{delay}, 176 loop => $$Anim{loops}, 177 dispose => 'background', 178 filename => $AnimFile 179 ); 180 print colored ['bold green'], $AnimFile, "\n"; #report end of animation processing 181 exit; 182 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================ 183 # end of anim_circuits.pl
© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.