#! \usr\bin\perl.exe ################################################################################ ################################################################################ # # Spectrum fill # # by Andrew Hardwick, http://duramecho.com # # Released under GNU Public Licence. # ################################################################################ ################################################################################ # Version 1, 2006/4/29 # Created to make wallless greyscale 16x16 Cretan maze favicon but looked naff. # Version 2, 2006/4/29 # Added (black) walls. # Version 3, 2006/4/29 # Added option of simple white path instead of greyscale. # Version 4, 2006/4/29 # Added option of spectrum path. # Version 5, 2006/4/30 # Added lots more documentation as program changed from disposable to to keep. # Added option of different path & wall widths. # Version 6, 2006/4/30 # Split large main routine up into 3 subroutines and generalised some parts. # Version 7, 2006/5/1 # Added option of output of a series of images with shifted colours. # Version 8, 2006/6/22 # Changed hardcoded labyrinth template to one read from an image file. # Version 9, 2006/6/23 # Removed 2 stages that cancel out now that a template image is used. # Version 10, 2006/6/23 # Replaced remaining sloppy global variable use with parameter passing. # Version 11, 2006/7/10 # Corrected bug that corrupted reading of non-square templates. # Version 12, 2006/7/10 # Corrected bug that corrupted output from even width or height templates. # Version 13, 2006/7/16 # Added option of cycling through spectrum multiple times & chosing start hue. # Version 14, 2006/7/16 # Made background & Monochrome (formerly 'White') path colours settable. # Version 15, 2006/7/20 # Added support for non-unicursal mazes (equivalent to flood fill). # Renamed from 'LabyrinthColourer.pl' to 'SpectrumFill.pl' # Improved path walking function not overwrite template array. # Version 16, 2006/7/20 # Added stop cells to block flood filling in unwanted directions in circuits. # Version 17, 2006/7/20 # Added support for multiple start cells. # Version 18, 2006/7/23 # Fixed PGM reading bug (it did not allow surround whitespace on text lines). # Version 19, 2006/7/23 # Added optional progress logging ("verbose mode"). # Version 20, 2006/8/9 # Documentation typo corrections. # Version 21, 2006/8/16 # Renamed setting 'StartHue' to 'StartOffset' as it is also used for greyscale. # Version 22, 2007/9/15 # Tidied by moving maze output from main routine to subroutines. # Tidied by moving colour calculation to a separate function. # Changed internal RGB colour format from PPM space separated to generic array. # Renamed 'ColourCycles' to 'NumberOfCycles' as works for monochrome too. # Removed unnecessary function passing of fixed global parameters. # Added 'OutputType' setting instead of guessing from 'NumberOfCycles'. # Version 23, 2007/9/16 # Changed internal coordinate system from y=0 at top to y=0 at bottom. # Added Javascript/HTML animation output ability. # Version 24, 2007/9/19 # Added Id caching to Javascript as lousy IE's getElementById is very slow. # Version 25, 2007/10/8 # Changed HTML animation page output to have background colour = maze wall. # Stopped path following routine wandering off template when unwalled. ################################################################################ # This program colours in a maze image with spectrum colour progression. # Additional features include alternative colouring schemes, image expanding # and expanding the path to a different width to the wall. ################################################################################ # How to use: # Set the maze shape & other parameters in the settings. Run it. # Template format: # An image in ASCII PGM format with 256 grey levels (0 to 255). # Each pixel is a cell. Grey level of the cell determines what it is. # 0 = wall cell. # 128 = start cell (fill starts in and propogates out of this type of cell). # 192 = stop cell (fill propogates into but not out of this type of cell). # 255 = path cell (fill propogates into & out of this type of cell). # The image must contain at least one start cell. # Perferably the path should be 1 pixel wide to get the cleanest colour fill. # If it is desired to resize the path to a different width from the wall # then all pixels with both x & y even must be walls or be surrounded on # all four sides by path cells. (This is because whole rows & columns are # resized together). # Set OutputType to the maze output type required. Options are: # Static = one static PPM image. # Series = an animation as a series of static PPM images that can be combined. # Javascript = an animation as a Javascript animated HTML page. # Set ColouringScheme to the colour scheme required. Options are: # Monochrome = path of single colour. # Greyscale = path graduates from black to white. # Spectrum = path graduates from red up through the spectrum back to red. # Set WallColour to the colour for the walls. Colours are represented # as ASCII strings of whitespace separated decimal RGB values eaching ranging # from 0 to 255. # Set ColourPath to the colour for the Monochrome path. Colours are represented # as ASCII strings of whitespace separated decimal RGB values eaching ranging # from 0 to 255. Irrevelant for Spectrum or Greyscale path colouring. # Set StartHue to where to start in a spectrum (or luminousity if the colouring # scheme is Greyscale). Range is 0 to 1. Irrevelant for Monochrome # path colouring. # Set NumberOfCycles to the number of times it to cycle through the spectrum # (or colouring greyscale) between the begining & end of the path. Irrevelant # for Monochrome path colouring. # Set WidthPath to the width of the path in pixels. Integer of 1 or more. # Set WidthWall to the width of the walls in pixels. Integer of 1 or more. # Set SeriesLength to the number larger of steps in an animation series # (which is the number of images output for 'Series' animation). Ignored # if the output type is 'Static'. # (One can combine the 'Series' animation frames from using Image Magick by # "convert -delay -loop 0 # *.ppm .gif". # If MNG is used instead of GIF then the file will be much # better compressed but popular current browsers don't natively support it.) # Set AnimationStepPeriod to the period between animation steps. Ignored # it the output type is not 'Javascript'. # The template & output filenames do not include the file type suffix. # The output is an image file in PPM format (Portable Pixel Map, a very # inefficiently compressed but very easy to implement format) because it # was the easiest for me to do. Most reasonable bitmap image editing # and converting programs (including Image Magick, Paint Shop Pro, Photoshop & # Gimp) can read PPM and convert it to something more compressed such # as PNG. ################################################################################ # Known deficiencies: # Settings are not checked for errors. # It is not optimised for speed. # The file formats are not optimised for compactness. ################################################################################ # This file is formated for 80 character (+linebreak) rows & 4 character tabs. ################################################################################ use strict; ################################################################################ # Settings ################################################################################ my %Settings=( # Output type: Static, Series or Javascript 'OutputType'=>'Javascript', # Colouring scheme: Spectrum, Monochrome or Greyscale 'ColouringScheme'=>'Spectrum', # Wall colour: 'R G B' in ASCII 'ColourWall'=>'0 0 0', # Path colour: 'R G B' in ASCII 'ColourPath'=>'255 255 255', # Start offset (hue or grey level): 0 to 1 'StartOffset'=>0, # Number of cycles through the colouration: >= 0 'NumberOfCycles'=>1, # Width of path in pixels: >= 0 'WidthPath'=>20, # Width of wall in pixels: >= 0 'WidthWall'=>5, # Number of frames of to cycle colouration through: >= 1 'SeriesLength'=>100, # Time between animation steps: >= 0 'AnimationStepPeriod'=>50, # Input file name minus extension 'TemplateFileName'=>'Input', # Output file name minus extension 'OutputFileName'=>'Output', # Enable progress logging: 0 = no, 1 = yes 'Verbose'=>1, ); ################################################################################ # Main routine ################################################################################ { # Load the template if($Settings{'Verbose'}) { print "Loading template '$Settings{'TemplateFileName'}.pgm'.\n";} my @Template=@{ReadPgmImageFile("$Settings{'TemplateFileName'}.pgm")}; # Walk the path numbering the cells if($Settings{'Verbose'}) { print "Walking the path numbering cells.\n";} my @Order=@{NumberPathCells(\@Template)}; # Create single image, animation as image series or animation as Javascript if($Settings{'OutputType'}eq'Static') { # Create single image CreateMazeStaticImage(\@Order,\@Template);} elsif($Settings{'OutputType'}eq'Series') { # Create image series CreateMazeAnimationImageSeries(\@Order,\@Template);} elsif($Settings{'OutputType'}eq'Javascript') { # Create Javascript animation CreateMazeAnimationJavascript(\@Order,\@Template);}} ################################################################################ # Create maze static image ################################################################################ # Creates & outputs a static image from from a path order and the settings. ################################################################################ # Parameter 0: # Pointer to an array containing the cells on the path in order starting # with the first one. Cells are represented as pointers to an array containing # the cell's x & y coordinates. # Parameter 1: # Pointer to the maze template (2d array of cell grey-values). ################################################################################ sub CreateMazeStaticImage { my @PathOrder=@{$_[0]}; my @Template=@{$_[1]}; if($Settings{'Verbose'}) { print "Creating static image.\n";} # Calculate cell colours if($Settings{'Verbose'}) { print "Calculating colours.\n";} my @Colours=@{CalculateCellColours(\@PathOrder, scalar(@Template),scalar(@{$Template[0]}), $Settings{'StartOffset'})}; # Output to file with path & wall width scaling if($Settings{'Verbose'}) { print "Expanding & outputting image.\n";} CreatePpmOutputFile("$Settings{'OutputFileName'}.ppm",\@Colours);} ################################################################################ # Create maze animation image series ################################################################################ # Creates & outputs a animation as a series of images # from from a path order and the settings. ################################################################################ # Parameter 0: # Pointer to an array containing the cells on the path in order starting # with the first one. Cells are represented as pointers to an array containing # the cell's x & y coordinates. # Parameter 1: # Pointer to the maze template (2d array of cell grey-values). ################################################################################ sub CreateMazeAnimationImageSeries { my @PathOrder=@{$_[0]}; my @Template=@{$_[1]}; if($Settings{'Verbose'}) { print "Creating animation image series.\n";} # Iterate over series frames for(my $c=0;$c<$Settings{'SeriesLength'};$c++) { if($Settings{'Verbose'}) { print "Image $c.\n";} # Calculate colour offset my $ColourOffset=($Settings{'StartOffset'}+ 1-$c/$Settings{'SeriesLength'}); $ColourOffset-=int($ColourOffset); # Calculate cell colours if($Settings{'Verbose'}) { print " Calculating colours.\n";} my @Colours=@{CalculateCellColours(\@PathOrder, scalar(@Template),scalar(@{$Template[0]}), $ColourOffset)}; # Create a sequence number suffix (of constant length) my $Suffix=('0'x(length($Settings{'SeriesLength'}-1)- length($c))).$c; if($Settings{'Verbose'}) { print " Expanding & outputting image.\n";} # Output to file with path & wall width scaling CreatePpmOutputFile("$Settings{'OutputFileName'}$Suffix.ppm", \@Colours);}} ################################################################################ # Create maze animation as Javascript ################################################################################ # Creates & outputs a animation as a Javascript & HTML # from from a path order and the settings. ################################################################################ # Parameter 0: # Pointer to an array containing the cells on the path in order starting # with the first one. Cells are represented as pointers to an array containing # the cell's x & y coordinates. # Parameter 1: # Pointer to the maze template (2d array of cell grey-values). ################################################################################ sub CreateMazeAnimationJavascript { my @PathOrder=@{$_[0]}; my @Template=@{$_[1]}; if($Settings{'Verbose'}) { print "Creating animation as Javascript.\n";} # Get the grid dimensions my $GridWidth=scalar(@Template); my $GridHeight=scalar(@{$Template[0]}); # Get the wall colour in a format suitable for Javascript my $WallColour=join('', map({$_=ToHex($_,2)} split('\s+',$Settings{'ColourWall'}))); # Get the path cell positions in a format suitable for Javascript my @PathCells; for(my $Step=0;$Step<@PathOrder;$Step++) { push(@PathCells,$PathOrder[$Step][1]*$GridWidth +$PathOrder[$Step][0]);} # Get the path colours in a format suitable for Javascript my @Colours; for(my $Step=0;$Step<@PathOrder;$Step++) { push(@Colours,"'#".join('',map({$_=ToHex($_,2)} CalculateColour($Settings{'StartOffset'}+ $Step/@PathOrder*$Settings{'NumberOfCycles'})))."'");} # Create output HTML file with header open(File,'>',"$Settings{'OutputFileName'}.html")|| die("Cannot write to '$Settings{'OutputFileName'}.html'.\n"); print File "", "\n\n\n\n", "\n", "\n", "\n", "\n\n"; close(File);} ################################################################################ # Read PGM Template File ################################################################################ # Reads a template file into an array of pixel colours. The file should be # be an image in ASCII PGM format. ################################################################################ # Parameter 0: # Template file name. # Returns: # Pointer to a 2d array of cell grey-values (0 = black, 255 = white). ################################################################################ sub ReadPgmImageFile { my $FileName=$_[0]; # Open a PGM image file & read in data open(File,"<$FileName")||die("Cannot read from '$FileName'.\n"); my @RawData; while(my $DataLine=) { # Discard comments $DataLine=~s/\#.*//; # Discard surrounding whitespace $DataLine=~s/^\s+//; $DataLine=~s/\s+$//; # Extract data from the line push(@RawData,split('\s+',$DataLine));} # Process header unless(@RawData>4) { die("Insufficent header data in '$FileName'\n");} unless(shift(@RawData)eq'P2') { die("'$FileName' is not in ASCII PGM format.\n");} my $SizeX=shift(@RawData); my $SizeY=shift(@RawData); my $Multiplier=255/shift(@RawData); # Extract grey values unless(@RawData>=$SizeX*$SizeY) { die("Insufficent greyscale data in '$FileName'\n");} my @GreyLevels; for(my $Y=$SizeY-1;$Y>=0;$Y--) { for(my $X=0;$X<$SizeX;$X++) { $GreyLevels[$X][$Y]=shift(@RawData)*$Multiplier;}} close(File); return \@GreyLevels;} ################################################################################ # Walk the path numbering the cells by order ################################################################################ # It traces the path and numbers the cells with the order they are visited in. ################################################################################ # Parameter: # Pointer to a the template as a 2d array of # grey levels (0 = wall, 128 = start point on path, 255 = rest of path). # Returns: # Pointer to an array containing the cells on the path in order starting # with the first one. Cells are represented as pointers to an array containing # the cell's x & y coordinates. ################################################################################ sub NumberPathCells { my @Template=@{$_[0]}; # Find the start points my($X,$Y); my @ToVisit; my @PathOrder; for($X=0;$X<@Template;$X++) { for($Y=0;$Y<@{$Template[$X]};$Y++) { if($Template[$X][$Y]==128) { push(@ToVisit,[$X,$Y]); push(@PathOrder,[$X,$Y]);}}} unless(@ToVisit) { die("No start point specified in the template.\n");} # Flood fill the path recording the order the cells were visited in my @Visited; my($Direction,$XTest,$YTest); while(@ToVisit) { # Get next (oldest unprocessed) cell to propogate the path ($X,$Y)=@{shift(@ToVisit)}; # Iterate over directions of propogation foreach $Direction ([1,0],[0,-1],[-1,0],[0,1]) { # Calculate neighbouring cell coordinates $XTest=$X+$$Direction[0]; $YTest=$Y+$$Direction[1]; # Skip if this neighbouring cell is off the edge if($XTest<0||$XTest>=@Template||$YTest<0||$YTest>=@{$Template[$X]}) { next;} # Test if it is an unvisited path cell if($Template[$XTest][$YTest]!=0&&!$Visited[$XTest][$YTest]) { # It is so add to order & mark as visited. $Visited[$XTest][$YTest]=1; push(@PathOrder,[$XTest,$YTest]); # Store it to visit later unless it is a stop cell unless($Template[$XTest][$YTest]==192) { push(@ToVisit,[$XTest,$YTest]);}}}} return \@PathOrder;} ################################################################################ # Calculate cell colours ################################################################################ # Calculates the colours of the cells based on their order on the path # and the specified colouring scheme. Note that, depending on the colouring # scheme chosen, the values of some of the parameters will not be needed # for the calculation and so can be given arbitrary values (even null). ################################################################################ # Parameter 0: # Pointer to an array containing the cells on the path in order starting # with the first one. Cells are represented as pointers to an array containing # the cell's x & y coordinates. # Parameter 1: # The width in cells. # Parameter 2: # The height in cells. # Parameter 3: # The colour offset. This is how much (as a fraction of a cycle from 0 to 1) # to shift the colours in the cycling colouring schemes. # Returns: # Pointer to a 2d array of cell colours. Colours are represented as ASCII # strings of whitespace separated decimal RGB values as used by PPM image # format. ################################################################################ sub CalculateCellColours { my @PathOrder=@{$_[0]}; my $SizeX=$_[1]; my $SizeY=$_[2]; my $Offset=$_[3]; # Initialise all cells to wall colour my @Colours; my($X,$Y); for($Y=0;$Y<$SizeY;$Y++) { for($X=0;$X<$SizeX;$X++) { $Colours[$X][$Y]=$Settings{'ColourWall'};}} # Colour in the path my($Lum,$Hue); my $StepsTotal=@PathOrder; for(my $Step=0;$Step<$StepsTotal;$Step++) { # Get cell position $X=$PathOrder[$Step][0]; $Y=$PathOrder[$Step][1]; # Colour the cell (as RGB PPM ASCII triplets of 0-255 range) $Colours[$X][$Y]=join(' ',CalculateColour($Offset+ $Step/$StepsTotal*$Settings{'NumberOfCycles'}));} return \@Colours;} ################################################################################ # Calculate colour ################################################################################ # Calculates the colour in the current colouring scheme for the a particular # phase through the cycle. ################################################################################ # Parameter: # The phase offset from the start of the cycle in cycles. # Irrelevant for the 'Monochrome' colouring scheme. # Returns: # Array of 3 members which are the red, green & blue colour components in # the 0-255 range. ################################################################################ sub CalculateColour { my $Phase=$_[0]; # Wrap phase to principal region $Phase=$Phase-int($Phase); # Calculate the colour appropriately my(@Colour,$Phase8Bit); if($Settings{'ColouringScheme'}eq'Spectrum') { # Spectrum path @Colour=HueToRgbBright($Phase);} elsif($Settings{'ColouringScheme'}eq'Greyscale') { # Greyscale path $Phase8Bit=int($Phase*256); @Colour=($Phase8Bit)x3;} else { # Monochrome path (a bit inefficient splitting as will be later joined!) @Colour=split('\s+',$Settings{'ColourPath'});} return @Colour;} ################################################################################ # Convert a hue to RGB ################################################################################ # Convert a hue to RGB with maximum brightness ################################################################################ # Converts hue to RGB with full saturation & maximum brightness # for gaudy rather than faithful conversion of hue to RGB. Instead of # maintaining an even luminance throughout the spectrum (which makes yellow # look like brown on a computer screen), it makes it as bright as possible. # This makes secondary colours twice as luminous as primary ones. ################################################################################ # Parameter: # A hue specified by a number in the range 0 to 1. # Returns: # An array of the red, green and blue components (each in the range 0-255). ################################################################################ sub HueToRgbBright { my $Hue=shift; my($Red,$Green,$Blue); if($Hue<1/3) { # Between red & green $Green=$Hue*3; $Red=1-$Green; $Blue=0;} elsif($Hue<2/3) { # Between green & blue $Blue=($Hue-1/3)*3; $Green=1-$Blue; $Red=0;} else { # Between blue & red $Red=($Hue-2/3)*3; $Blue=1-$Red; $Green=0;} # Maximise the luminance my $Top=$Red>$Green?$Red:$Green; $Top=$Top>$Blue?$Top:$Blue; $Red/=$Top; $Green/=$Top; $Blue/=$Top; # Scale to 255 return(int($Red*255),int($Green*255),int($Blue*255));} ################################################################################ # Convert an number to hexidecimal ################################################################################ # Converts an number to upper case hexidecimal form and returns it as a string. ################################################################################ # Parameter 0: # The number to convert. # Parameter 1: # The minimum number of digits to return. # Returns: # The number as hexidecimal. ################################################################################ sub ToHex { return sprintf("%.$_[1]X",$_[0]);} ################################################################################ # Create PPM Output File ################################################################################ # Creates a ASCII PPM image file from an array of cell colours. ################################################################################ # Parameter 0: # Output file name # Parameter 1: # Pointer to a 2d array of cell colours (in PPM RGB ASCII format with range # 0 to 255 per channel). ################################################################################ sub CreatePpmOutputFile { my $FileName=$_[0]; my @Colours=@{$_[1]}; my $WidthPath=$Settings{'WidthPath'}; my $WidthWall=$Settings{'WidthWall'}; # Calculate sizes my $SizeCellsX=@Colours; my $SizeCellsY=@{$Colours[0]}; my $SizePixelsX=int($SizeCellsX/2)*$WidthPath+ int(($SizeCellsX+1)/2)*$WidthWall; my $SizePixelsY=int($SizeCellsY/2)*$WidthPath+ int(($SizeCellsY+1)/2)*$WidthWall; # Create PGM image file & output header open(File,">$FileName")||die("Cannot write to '$FileName'.\n"); print File "P3\n"; # Format = RGB ASCII print File "$SizePixelsX $SizePixelsY\n"; # Width & height print File "255\n"; # 256 levels per channel # Write the pixel colours to the file my($X,$Y,$CellSizeX,$CellSizeY,$InCellX,$InCellY); for($Y=$SizeCellsY-1;$Y>=0;$Y--) { # Iterate over pixels in cell in y-direction $CellSizeY=$Y%2?$WidthPath:$WidthWall; for($InCellY=0;$InCellY<$CellSizeY;$InCellY++) { for($X=0;$X<$SizeCellsX;$X++) { # Iterate over pixels in cell in x-direction $CellSizeX=$X%2?$WidthPath:$WidthWall; for($InCellX=0;$InCellX<$CellSizeX,;$InCellX++) { # Write the pixel colour to the file print File "$Colours[$X][$Y]\n";}}}} # Finish the file close(File);} ################################################################################