anim_circuits.pl
Creates an animated GIF that loops image parts around circuits.

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:

Output screen shot

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:

Mona Lisa
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.

If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.


anim_circuits.pl -- [Download latest version: v1.0.0 - September 19, 2009]   [MD5 checksum]
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
			

Here are two simple variants where each animation frame has been initialized with a common underlaying image:
Mona Lisa   Mona Lisa

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.