NineBlocks


I've been having fun playing with GD.pm and quilting patterns. In "nine blocks" there are 9 squares, whose contents are limited to 16 basic patterns and radial symetry.

Here's a sample output. Comments welcome, especially on the code:

The idea comes from Jared Tarbell who has done some amazing artistry with Flash programming. While he has open-sourced his code, I don't have access to Macromedia products so I built it from the specs on his web pages.

Directions this project is going in:

  • A Christmas present. I printed a lot of them at 300 pixels to the inch on a sheet of 12" x 18" photographic paper.
  • An article for the Perl Review, Currently in press. :)
  • The code. There are a few different versions floating around;
    • version 1: used to make the christmas print
    • version 2: eric's revisions
    • version 3: daniel's revisions based on eric's revisions
    • version 4: daniel's revisions for the article

Not quite sure what to show on this page. Basically, I'll clean it up a little and figure that out later.

Problems Remaining

  • What to do with the global variables...
    • Maybe we should take the leap and turn this into an OO module?
    • That would make it much easier to do the fun stuff like treating each block as if it were a pixel of a bigger image.
  • Translate the data section into 52 GD::Polygon objects
  • ColourChoosing is tough
  • To do: find out where the name came from. The quilter this was given to, says these are actually a sub-class of a pattern called nine patches and she has never heard of nine blocks. Hm.

Mess of code.

#!/usr/bin/perl
# nineblocks.pl - create assorted quilt patterns using 16 graphical primitives
#   and radial symetry.  
#   Version 1
#   Copyright (C) 2004 Daniel Allen.  Distributed under the same
#   terms as Perl itself.  See the "Artistic License" in the Perl
#   source code distribution for licensing terms.

#   inspired by http://www.complexification.net/gallery/machines/nineblock/

use warnings;
use strict;
use GD;

my $XS = 20;    # dimensions of squares in pixels
my $XN = $XS*5; # dimensions of nineblocks in pixels

my $dim = 10;     # number of squares wide/tall
my $border = $XN/2;
my $truecolor = 0;

my @squares = (
               "",
               ".5,1 1,1 1,.5",
               ".5,.5 .5,1 1,.5",
               "0,0 .5,.5 1,0",
               "0,.5 1,.5, .5,0",
               ".5,.5 .5,1 1,1 1,.5",
               ".25,.25 .25,.75 .75,.75 .75,.25",
               "0,1 .5,1 .5,.5 1,.5 1,0",
               "0,.5 1,1 .5,0",
                ".25,.5 .5,0 0,0 .5,.99 1,0 .5,0 .75,.5 .25,.5", # edited                  
               "0,0 0,.5 1,1 .5,0",
               ".5,0 .5,1 1,1 1,0",
               "0,.5 .5,1 1,.5 .5,0",
               "0,0 .5,1 1,0",
               "0,1 1,1 1,0",
               "0,0 1,0 1,1 0,1"
               );

my @centers = (
              "",
              ".25,.25 .25,.75 .75,.75 .75,.25",
              "0,.5 .5,1 1,.5 .5,0",
              "0,0 1,0 1,1 0,1"
              );

              
my $image = new GD::Image($dim * $XN, $dim * $XN);
my $white = $image->colorAllocate(255,255,255); # background

my $xpos = 0;
my $ypos = 0;
my %seen;

my $maxPossible = (4 * 4 * 16 * 16 * 4);
if ($dim * $dim > ($maxPossible)) { 
    print "Dimensions $dim * $dim greater than $maxPossible different blocks.\n";
}

my $blockCount = 0;

OUT: while ($blockCount++ <= $maxPossible) {
    my @spec = (&r(3), &r(3), &r(15), &r(15), &r(3));
    next OUT if ( $seen{ join ' ', @spec }++ );

    my $block = &nineblock(@spec);
    $image->copy($block, $xpos, $ypos, 0, 0, $XN, $XN);
    $xpos += $XN;
    if ($xpos > (($dim ) * $XN)) {
        $xpos = 0;
        $ypos += $XN;
        if ($ypos > (($dim ) * $XN)) {
            last OUT;
        }
    }
}

# nineblocks are at left-top corner of their background
# new background should be: + 2 * border - the total offset of each 9block

my $background = new GD::Image($dim*$XN + 2*$border - 2*$XN/5, 
                               $dim*$XN + 2*$border - 2*$XN/5,
                               $truecolor);

$white = $background->colorAllocate(255,255,255);
$background->copy($image, $border, $border, 0, 0, $dim * $XN, $dim * $XN);

&display($background);

sub r {
    my ($max) = @_;
    int rand $max;
}

sub nineblock {
    # edgeRot     = 0-3 ( times 90 degrees)
    # cornerRot   = 0-3
    # edgeShape   = 0-15
    # cornerShape = 0-15
    # centerShape = 0-3

    my ($edgeRot, $cornerRot, $edgeShape, $cornerShape, $centerShape) = @_;

    my $image = new GD::Image(3*$XS,3*$XS, $truecolor);
    my $white = $image->colorAllocate(255,255,255);
    my $xpos = 0;
    
    my @face = (&square($edgeRot,   $squares[$edgeShape]), 
                &square($cornerRot, $squares[$cornerShape]));
    
    my $center = &square(0, $centers[$centerShape]);

    $image->copy($center,
                 $XS, $XS, 0, 0, $XS, $XS);
    
    for (@face) {
        my $square = $_;
       
        my $max = (2*$XS - $xpos);
        $image->copy($square, $xpos, 0, 0, 0, $XS, $XS);
        $image->copy($square->copyRotate90, 2*$XS, $xpos, 0, 0, $XS, $XS);
        $image->copy($square->copyRotate180, $max, 2*$XS, 0, 0, $XS, $XS);
        $image->copy($square->copyRotate270, 0, $max, 0, 0, $XS, $XS);
        $xpos += $XS;
    }
    return $image;
}

sub square {
    my ($rot, $pointsets) = @_;

    my $image = new GD::Image($XS,$XS, $truecolor);
    my $white = $image->colorAllocate(255,255,255);


    my $derivedWhite =  255 * $xpos / ($dim * $XN);
    my $derivedBlack =  255 * $ypos / ($dim * $XN);

    $derivedBlack = rand (255) if (rand() < .5);

    #my $white = $image->colorAllocate(&r(255),0,&r(255));
    #my $black = $image->colorAllocate(0,0,0);
    my $black = $image->colorAllocate($derivedBlack,0,$derivedWhite);

    foreach my $points ( split /\+/, $pointsets ) {

        my $polygon1 = new GD::Polygon;

        &add_points($polygon1, $points);
        $image->filledPolygon($polygon1, $black);
    }
    
    
    $image->flipVertical;
    for (1 .. $rot) {
        $image = $image->copyRotate90;
    }
    
    return $image;
}

sub add_points {
    my ($poly, $points) = @_;

    foreach my $pair (split /\s/, $points) {
        $pair =~ /([\d.]+),([\d.]+)/;
        $poly->addPt($1 * $XS, $2 * $XS);
    }
}

sub display {
    my ($image) = @_;

    #   'display' program provided with ImageMagick
    open OUTFILE, "| display -" or die "couldn't open display";
    print OUTFILE $image->png;
    close OUTFILE;
}   

--DanielAllen


fishbot's thoughts:

When I initially looked at the code back in December or so, I thought that I would give you a hand with that remaining block. After looking at the formats fed into @squares and @centers for fifteen minutes, I gave up and worked on something else. The specification for the geometries is inscrutable.

  • It looks to be two arrays of strings containing a space delimited list of comma delimited pair. Except one, which contains a quadruple and a plus sign. I can see that you did it this way because you feed those co-ordinates into GD in a string, but a better data-structure would clean up the code in a number of ways.
  • You have a numbered series of squares at the top of your article, but I can't determine which string corresponds with which square. This made it harder still to mentally parse the strings.
  • You define the squares before the centers, then repeat all the centers in another array. Two better options: define the centers first, then simply copy that array on to the front of the squares or have a single array of hash with center flags. The latter option gives you a way to track rotational symetry in the squares, though it makes it tougher to randomly select a center. It is easy, though to generate a quick array of hashref to the centers at init.
  • This might seem messy, but because all the squares then have fixed numeric identifies, you can prevent repetitions with a "seen" multi-dimentional array:
if ( $seen[$corner][$crot][$edge][$erot][$center]++ ) 
{ 
   # redo randomization 
   # if $corner or $edge is rotationally symetrical, 
   #   always zero the $crot or $erot.
}

  • idea for co-ord datatype:
my %squares = (
               1 => { 
                       center => 1,
                       points => [ [   1, 0    ], 
                                   [ 0.5, 0.25 ], 
                                   etc. ],
                    },
               etc...

Or an array as the top level works too, with possibly an "id" field inside the square hash. Even if you never use the "id" field, you are mapping it to the blocks explicitly for the reader.

You can then create the %centers hash (or array) like this:

my %centers = grep { $squares{$_}->{'center'} ? 
                           \$squares{$_} : () } 
                   keys %squares; 

Obviously this isn't as compact, and might not fit in the article... but the bit that you can fit will be understandable. Then you can say # rest of the squares... and provide a url for the complete code.

  • You are right to say that the blocks, squares and quilt would perhaps be better represented by objects. That's a heavy refactor, though. You could minimize cost by manufacturing one of each block objects, and then pulling them from the list, calling rotate and draw methods on them. This approach falls apart when you have a nine-block where the same block is to be used twice.
  • Lastly, randint() is a better name than r(), I think.

Anyway, that is my constructive critique. Hope there are a few good ideas in there.

--fishbot

Daniel's response

Great ideas. Yes, the format's inscrutible, which I forgot after I wrote it. Exactly why I wanted another pair of eyes to look at it :)

In email, you suggested a format something like:

1: (center) 0,0; 1,0; 1,1; 0,1;
2:          0,1; 1,1; 1,0;
3:          0,0; .5,1; 1,0;
4:          0,.5; .5,1; 1,.5; .5,0;
5: (center) .5,0; .5,1; 1,1; 1,0;
6:          0,0; 0,.5; 1,1; .5,0;

with an appropriate comment at the top. The criticism regarding the most complicated one (with the plus sign) is well taken. In fact, I had a typo in it (an extra comma). When I reformat it as such:

7:          0,0; .25,.5; .5,0; .99,0; .75,.5; .5,0; + .5,1; .25,.5; .75,.5;

I think that helps a bit. It's not perfect, and I need to swap things around to use a normal coordinate scheme (partly my fault, partly GD's I think). But rather than expand this format further into a complex data structure, I think I'll just use this format in a __DATA__ section. And eliminate the explicitly enumerated @centers, so I can also deal with rotational symmetry, as you suggested.

I originally waffled about renaming r() to something descriptive like randint(), but I'm not actually paying for code by the character, and it is more descriptive, so randint() it is.


fishbot continues

Well, the randint() thing is a tough call... if you are going to do randint( $max ) then you aren't saving anything over the full int rand $max. But I think that it's still nice to factor that out. Using int rand $max a bunch of times within a function call is gross.

New thought: leave it in, but as:

sub randint($) { int rand $_[0]; }

You save a couple of lines, but no loss of clarity. And gain a bit of speed.

As we discussed the other night, each time you are drawing a square, you are splitting the string and iterating through the points, adding them to your polygon, then drawing it with a rotation and a colour.

Since those are discrete phases, and there is no positional information in either, nor any variant information in the first phase, I suggest that creating the polygon should happen only once at initialization. Then when you draw them for each square, you simply need to rotate and colour.

Factoring this into square_init() and assuming the above-mentioned __DATA__ format:

sub square_init
{
   my @squares;
   LINE: while ( <DATA> )
   {
      # allow for much needed comments and whitespace!
      next LINE if m/^\s*#/ || m/^\s+$/;
      
      my ( $id, $points ) =
           m/^(\d+): \s+ (?: \(cent(?:re|er)\) )? \s+ (.*)$/ix;
      my %data;
      $data{ 'points' } = $points;
      $data{ 'centre' } = ( m/\( cent(?:re|er) \)/ix )? 1 : 0;

      # create polygon object:
      my $poly = new GD::Polygon;
      
      for my $pair ( split /\s*;\s*/, $points )
      {
         my ( $x, $y ) = split /\s*,\s*/, $pair;
         $poly->addPt($x * $XS, $y * $XS);
      }

      $data{ 'poly' } = $poly;
      push @squares, \%data;    # revised a bit, 
                                # was off by one
   }
    
   my @centres = grep { $_->{ 'centre' } } @squares;

   return \@squares, \@centres;
}

Disclaimer: I didn't test this at all. Other than perl -c. But the idea is there.

Then square() can become:

sub square {
   my ( $rot, $id, $xpos, $ypos ) = @_;    
      # Notes: pass in the index of square instead
      #        also, $x|ypos are variant, and shouldn't
      #        be globals!  

   my $image = new GD::Image( $XS, $XS, $truecolor );
   my $white = $image->colorAllocate( 255, 255, 255 );  
      # ^ actually white, not used?

   my $derivedWhite =  255 * $xpos / ($dim * $XN);
   my $derivedBlack =  255 * $ypos / ($dim * $XN);

   $derivedBlack = rand (255) if ( rand() < 0.5 );  # not black at all
   my $black = $image->colorAllocate( $derivedBlack, 0, $derivedWhite );

   $image->filledPolygon( $squares[ $id ]->{ 'poly' }, 
                          $black );
   
   $image->flipVertical;   # why is this here?
   $image = $image->copyRotate90 for ( 1..$rot );  
   return $image;
}

And then add_points() can of course be ditched. The end result should be faster, I think. Benchmark! ;) Also, the code is more compact, overall, I think.

That just leaves the code for preventing repeat business:

my @seen;
OUT: while ( $blockCount++ <= $maxPossible ) 
{
   my ( $edge, $corner ) = ( randint 15, randint 15 );
   my ( $erot, $crot )   = ( randint 3,  randint 3  );
   my   $centre          =   randint 3;

   $erot = 0 if $squares[ $edge   ]->{ 'centre' };
   $crot = 0 if $squares[ $corner ]->{ 'centre' };
   
   # see note 1 below
   next OUT if ( $seen[$edge][$erot][$corner][$crot][$centre]++ );

   my $block = nineblock( ... );
   # ...rest as before...

And here is the __DATA__: (I had to break one of the lines, but the parser doesn't handle that.)

__DATA__

# id: centre  points (relative to 1x1 unit square)
#     centre  indicates squares that are rotationally 
#             symetrical
# -------------------------------------------------
1:  (centre) 0, 0; 1, 0; 1, 1; 0, 1
2:           0, 1; 1, 1; 1, 0
3:           0, 0; 0.5, 1; 1, 0
4:  (centre) 0, 0.5; 0.5, 1; 1, 0.5; 0.5, 0
5:           0.5, 0; 0.5, 1; 1, 1; 1, 0
6:           0, 0; 0, 0.5; 1, 1; 0.5, 0
7:           0.25, 0.5; 0.5, 0; 0, 0; 0.5, 0.99; 1, 0; \
             0.5, 0; 0.75, 0.5; 0.25, 0.5
8:           0, 0.5; 1, 1; 0.5, 0
9:  (centre) 0.25, 0.25; 0.25, 0.75; 0.75, 0.75; 0.75, 0.25
10:          0, 1; 0.5, 1; 0.5, 0.5; 1, 0.5; 1, 0
11:          0.5, 0.5; 0.5, 1; 1, 1; 1, 0.5
12:          0, 0.5; 1, 0.5; 0.5, 0
13:          0, 0; 0.5, 0.5; 1, 0
14:          0.5, 0.5; 0.5 ,1; 1, 0.5
15:          0.5, 1; 1, 1; 1, 0.5
16: (center)

Whew. I suppose I should cobble all this together and test it, no?

Note 1: I used a nested array here to do the 'seen' records. I wasn't thinking very clearly here. Because it is nested deeply, we actually end up with something like 4000 anonymous arrays, with all the boxing and overhead that entails. Avoiding that is well worth the cost of concatenating the numbers together. I did a benchmark:

Benchmark: timing 1000000 iterations of array, hash...
    array:  9 wallclock secs 
     hash:  7 wallclock secs 

          Rate array  hash
array  97857/s    --  -26%
hash  133067/s   36%    --

Additionally, the memory requirements are much lower. 10_000_000 iterations of my array implementation segfaults. Not that you would ever get up to ten million blocks, but the program is memory intensive to start with. So, I think that

next OUT if ( $seen[$edge][$erot][$corner][$crot][$centre]++ );
# should become 
next OUT if $seen{"$edge-$erot-$corner-$crot-$centre"}++;

Note 2: It took me a long time to realize that the $xpos in nineblock() is unrelated to the global $xpos, which is used as a global by the rest of the subroutines. This caused much oddness, as I continued to draw polys well outside my bounds for all rows after the first.

Ideally, $xpos and $ypos shouldn't be globals anywhere, or they should be everywhere. My version has them in a unpleasantly mixed state right now. There is something that looks like it should be a bug in your code, but it doesn't seem to manifest in any detectable way. You do my $xpos = 0 in nineblock(), then use both $xpos and $ypos in square(), where the former is inherited from the caller (nineblock()) and the latter is the global.

In theory, $xpos is always zero in square(), even though it shouldn't be. Or am I missing something?

Note 3: There is an off by one error in your code, I think: for rotations there are four valid positions 0,1,2,3 - but you do an int rand 3, which can only generate 0,1,2. rand EXPR returns numbers up-to-but-not-including, and int always rounds down. Similarly, the other random upper limits need to be bumped up. I couldn't figure out why I was never getting centre #4!

Note 4: Since we remove a number of configurations involving rotations of the rotationally symetrical units, the $maxPossible number is no longer correct. Additionally $blockCount++ <= $maxPossible isn't quite right to start with... as it increments each time we hit a duplicate pattern. We avoid an infinite loop, though, in that if we ask for more squares than are possible, we run out the clock picking used patterns. However, the probability of getting all the possible values out of the algorithm is statistically zero.

My theory was that there are now:

 16 x 4 x 16 x 4 x 4
- 4 x 3 x 16 x 4 x 4
-          4 x 3 x 4
  ------------------
               13262 Possible combinations

But it's been a number of years since I took combinatorics. In fact, this seems to contradict my solution above:

my %hash;
for my $a ( 0..15 )
{
   for my $b ( 0..3 )
   {
      $b = 0 if $a < 4;
      for my $c ( 0..15 )
      {
         for my $d ( 0..3 )
         {
            $d = 0 if $c < 4;
            for my $e ( 0..3)
            {
               $hash{"$a-$b-$c-$d-$e"}++;
            }
         }
      }
   }
}

print scalar( keys %hash ), "\n";
# prints: 10816

I trust the second number, though I can't account for the difference. Any real mathematicians out there?

fishbot implementation

Still a number of outstanding issues, but it produced the above quilt. It's about 25% faster than the previous implementation, though the gap narrows on larger quilts because it rejects more guesses. The number of lines is roughly the same... two or three less, I think that I determined.

I changed a number of things that are strictly style-related, just to help me navigate the code according to my preferences. I am not suggesting that any of those things constitute 'improvement'. They are just my coding quirks.

#!/usr/bin/perl
# Version 2

use warnings;
use strict;
use GD;

# global constants:
my $DIM    = shift || 30;  # num. of squares wide/tall
my $SQSIZE = shift || 10;  # width/height of squares in pixels
my $NBSIZE = $SQSIZE * 4;  # width/height of nineblocks in pixels
my $BDSIZE = $NBSIZE / 5;  # border (around NB) size

my ( $truecolor, $xpos, $ypos, $trial_limit  ) = ( 0, 0, 0, 0 );
my ( $squares, $centres ) = square_init();

my $image = new GD::Image( $DIM * $NBSIZE, $DIM * $NBSIZE );
$image->colorAllocate( 235, 225, 235 );  # background

my $max_allowed = 2.5 * 4 * 4 * 16 * 16 * 4;  # arbitrary

my %seen;
BLOCK: while ( $trial_limit++ <= $max_allowed ) 
{
   my ( $edge, $corner ) = ( int rand ( 16 ), int rand( 16 ) );
   my ( $erot, $crot )   = ( int rand ( 4 ),  int rand ( 4 ) );
   my   $centre          =   int rand ( 4 );

   $erot = 0 if $squares->[ $edge   ]->{ 'centre' };
   $crot = 0 if $squares->[ $corner ]->{ 'centre' };

   next BLOCK if $seen{"$edge-$erot-$corner-$crot-$centre"}++;

   my $block = nineblock( $edge, $erot, $corner, $crot, $centre );
   $image->copy( $block, $xpos, $ypos, 0, 0, $NBSIZE, $NBSIZE );

   $xpos += $NBSIZE;
   
   # wrap back to top:
   if ( $xpos > (( $DIM ) * $NBSIZE )) {
       $xpos = 0; $ypos += $NBSIZE;
       last BLOCK if ($ypos > (($DIM ) * $NBSIZE));
   }
}

my $background = new GD::Image($DIM*$NBSIZE + 2*$BDSIZE - 2*$NBSIZE/5, 
                               $DIM*$NBSIZE + 2*$BDSIZE - 2*$NBSIZE/5,
                               $truecolor);

$background->colorAllocate( 255, 255, 255 ); # background
$background->copy( $image, $BDSIZE, $BDSIZE, 0, 0, 
                   $DIM * $NBSIZE, $DIM * $NBSIZE );

display( $background );

sub nineblock {
    my ( $edge, $erot, $corner, $crot, $centre ) = @_;
    my $nxpos = 0;

    my $image = new GD::Image( 3*$SQSIZE, 3*$SQSIZE, $truecolor);
    $image->colorAllocate(255,255,255); # background
    
    my $edgepoly   = square( $edge,   $erot );
    my $cornerpoly = square( $corner, $crot );
    my $centerpoly = square( $centre, 0, 1  );

    $image->copy( $centerpoly, $SQSIZE, $SQSIZE, 
                  0, 0, $SQSIZE, $SQSIZE );
    
    for my $isq ( $edgepoly, $cornerpoly  ) {
        my $max = (2*$SQSIZE - $nxpos);
        $image->copy( $isq, $nxpos, 0, 0, 0, $SQSIZE, $SQSIZE);
        $image->copy( $isq->copyRotate90, 
                      2 * $SQSIZE, $nxpos, 0, 0, 
		      $SQSIZE, $SQSIZE );
        $image->copy( $isq->copyRotate180, 
	              $max, 2 * $SQSIZE, 0, 0, 
		      $SQSIZE, $SQSIZE );
        $image->copy( $isq->copyRotate270, 
	              0, $max, 0, 0, 
		      $SQSIZE, $SQSIZE );
        $nxpos += $SQSIZE;
    }
    return $image;
}

sub square {
   my ( $id, $rot, $centre ) = @_;    

   my $image = new GD::Image( $SQSIZE, $SQSIZE, $truecolor );
   $image->colorAllocate(255,255,255); # background

   my $derivedR =  255 * $xpos / ($DIM * $NBSIZE);
   my $derivedB =  255 * $ypos / ($DIM * $NBSIZE);

   $derivedB = int rand( 255 ) if ( rand( 1 ) < 0.5 );
   my $fore = $image->colorAllocate( $derivedR, 0, $derivedB );

   my $set = ( $centre )? $centres : $squares;
   $image->filledPolygon( $set->[ $id ]->{ 'poly' }, $fore );
   $image = $image->copyRotate90 for ( 1..$rot );  
   
   return $image;
}

sub display {
    my $image = shift;
    open OUTFILE, ">", "/home/fish/public_html/foo.png" 
    	or die "couldn't open display";
    print OUTFILE $image->png;
    close OUTFILE;
}

sub square_init {
   my @squares;

   LINE: while ( <DATA> )
   {
      # allow for much needed comments and whitespace!
      next LINE if m/^\s*#/ || m/^\s+$/;
      if ( s/\\\s*$// ) { $_ .= <DATA>; }  # join continued

      my ( $id, $points ) =
           m/^(\d+): \s+ (?: \(cent(?:re|er)\) )? \s+ (.*)$/ix;

      # create polygon object:
      my $poly = new GD::Polygon;
      for my $pair ( split /\s*;\s*/, $points )
      {
         my ( $x, $y ) = split /\s*,\s*/, $pair;
         $poly->addPt($x * $SQSIZE, $y * $SQSIZE);
      }

      push @squares, {
		      points => $points,
		      centre => ( m/\( cent(?:re|er) \)/ix )? 1 : 0,
		      poly => $poly,
	             };
   }
    
   my @centres = grep { $_->{ 'centre' } } @squares;

   return \@squares, \@centres;
}

__DATA__

# id: centre  points (relative to 1x1 unit square)
#     centre  indicates squares that are rotationally 
#             symetrical
# -------------------------------------------------
1:  (centre) 0, 0; 1, 0; 1, 1; 0, 1
2:           0, 1; 1, 1; 1, 0
3:           0, 0; 0.5, 1; 1, 0
4:  (centre) 0, 0.5; 0.5, 1; 1, 0.5; 0.5, 0
5:           0.5, 0; 0.5, 1; 1, 1; 1, 0
6:           0, 0; 0, 0.5; 1, 1; 0.5, 0
7:           0.25, 0.5; 0.5, 0; 0, 0; 0.5, 0.99; \
             1, 0; 0.5, 0; 0.75, 0.5; 0.25, 0.5
8:           0, 0.5; 1, 1; 0.5, 0
9:  (centre) 0.25, 0.25; 0.25, 0.75; 0.75, 0.75; 0.75, 0.25
10:          0, 1; 0.5, 1; 0.5, 0.5; 1, 0.5; 1, 0
11:          0.5, 0.5; 0.5, 1; 1, 1; 1, 0.5
12:          0, 0.5; 1, 0.5; 0.5, 0
13:          0, 0; 0.5, 0.5; 1, 0
14:          0.5, 0.5; 0.5 ,1; 1, 0.5
15:          0.5, 1; 1, 1; 1, 0.5
16: (center)



--fishbot

Daniel's response

Your code is definitely faster on my desktop machine- I think by more than 25%. Which makes sense; we're not re-running GD three times for every square. Efficiency is good, even if it wasn't an original design goal. :)

My code did create centre number 4, but I'm not presently sure why. Maybe I introduced a bug that canceled the other bug, which has been known to happen.

The number of duplicate patterns is indeed 5568, for a total possible number of 10816 drawable patterns. I'll think about that further. The original site doesn't exactly tell how he enumerates the duplicates.

Further from Daniel:

...so I asked my addition consultant. :)

The breakdown of non-duplicate patterns is: 4 centres * 52 edges * 52 corners = 10816.

52 = (4 squares with one orientation) + (12 squares with 4 orientations).

Quote from him: "That was easy. I thought I'd need the whole back of the envelope."

More from Daniel

I'm putting the draft article at NineBlocksPerlReview. PickyQuilters are welcome to comment.