COOLNAMEHERE

Drawing Celtic Knotwork 6

Now the program does what I want it to. But if I hand this script off to somebody else and say "This program will make knotwork panels of any size," it'll be maybe 30 seconds before they ask how to set the size. "Go in and edit the code" won't cut it. Let's haul out our trusty OptParse library again.

require 'RMagick'
require 'optparse'
  
include Magick
  
# I am a single small section of a knotwork image. I know about my 
# dimensions, and can describe myself on a pixel-by-pixel basis.
class Tile
  
  def initialize(str = nil)
    @pixels = []
    (0..8).each { 
      row = []
      (0..8).each { row << nil }
      @pixels << row
    }
    
    if str then
      set_from_string(str)
    end
  end
  
  def at(x, y)
    return @pixels[x][y]
  end
  alias is_set? at
  
  def set(x, y, value=true)
    @pixels[x][y] = value
  end
  
  def unset(x, y)
    @pixels[x][y] = nil
    
    return true
  end
  
  def set_from_string(str)
    str.split("\n").each_with_index do |line, row|
      line.split(' ').each_with_index do |pixel, col|
        set(row, col, pixel)
      end
    end
  end
  
  def to_s
    str = ""
    @pixels.each { |row|
      str += "|"
      row.each { |pixel| 
        pixel ||= " "
        str += "#\{pixel}|" 
      }
      str += "\n"
    }
    return str
  end
end
  
# I am an arranged collection of Tiles. I know how to add and remove
# Tiles along a 2-d grid, and can also present myself as if I were a single
# large Tile.
class Grid
  def initialize(rows, columns)
    @tile_size = 9
    @rows      = rows
    @columns   = columns
    @pixels    = Array.new(rows*@tile_size) { |i|
      Array.new(columns*@tile_size)
    }
  end
  
  def set_tile(row, column, tile)
    if row >= @rows or column >= @columns then
      raise ArgumentError, \
      "set_tile at #\{row}, #\{column} outside of Grid area " \
      "(#\{@rows}, #\{@columns})"
    end
    
    pixel_origin_x = row * @tile_size
    pixel_origin_y = column * @tile_size
    (0...@tile_size).each { |tile_x|
      x = pixel_origin_x + tile_x
      (0...@tile_size).each { |tile_y|
        y = pixel_origin_y + tile_y
        @pixels[x][y] = tile.at(tile_x, tile_y)
      }
    }
  end
  
  def at(row, column)
    return @pixels[row][column]
  end
  
  def to_s
    str = ""
    @pixels.each { |row|
      str += row.join(' ')
      str += "\n"
    }
    
    return str
  end
  
end
  
# I am a lovely Celtic knotwork panel. I know my dimensions, and can output 
# myself as ASCII art.
class KnotworkPanel
    @@top_left = Tile.new(%{. . . . x x x x x
                          . . . . x . . . .
                          . . . . x . . . .
                          . . . . x . . . x
                          . . . . x . . x .
                          . . . . x . . x .
                          . . . . . x . . x
                          . . . . . x . . x
                          . . . . . . x x .}.gsub(/^\s+/, "))
  
  @@top      = Tile.new(%{x x . . . . . x x
                          . . x x . x x . .
                          . . . . x . . . .
                          x x . . . x . x x
                          . . x . . . x . .
                          . x . x . . . x .
                          x . . . x . . . x
                          . . . x . x . . x
                          . . x . . . x x .}.gsub(/^\s+/, ")
                        )
  
  @@topright = Tile.new(%{x x x x x . . . .
                          . . . . x . . . .
                          . . . . x . . . .
                          x . . . x . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          x . . x . . . . .
                          . . . x . . . . .
                          . . x . . . . . .}.gsub(/^\s+/, ")
                        )
  
  @@left     = Tile.new(%{. . . . . . x . .
                          . . . . . x . . .
                          . . . . . x . . .
                          . . . . x . . . x
                          . . . . x . . x .
                          . . . . x . . x .
                          . . . . . x . . x
                          . . . . . x . . x
                          . . . . . . x x .}.gsub(/^\s+/, ")
                        )
  
  @@center   = Tile.new(%{. . x . . . x . .
                          . x . x . x . . .
                          x . . . x . . . x
                          . x . . . x . x .
                          . . x . . . x . .
                          . x . x . . . x .
                          x . . . x . . . x
                          . . . x . x . x .
                          . . x . . . x . .}.gsub(/^\s+/, ")
                        )
  
  @@right    = Tile.new(%{. x x . . . . . .
                          x . . x . . . . .
                          x . . x . . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          x . . x . . . . .
                          . . . x . . . . .
                          . . x . . . . . .}.gsub(/^\s+/, ")
                        )
  
  @@bot_left = Tile.new(%{. . . . . . x . .
                          . . . . . x . . .
                          . . . . . x . . x
                          . . . . x . . x .
                          . . . . x . . x .
                          . . . . x . . . x
                          . . . . x . . . .
                          . . . . x . . . .
                          . . . . x x x x x}.gsub(/^\s+/, ")
                        )
  
  @@bottom   = Tile.new(%{. x x . . . x . .
                          x . . x . x . . .
                          x . . . x . . . x
                          . x . . . x . x .
                          . . x . . . x . .
                          x x . x . . . x x
                          . . . . x . . . .
                          . . x x . x x . .
                          x x . . . . . x x}.gsub(/^\s+/, ")
                        )
  
  @@botright = Tile.new(%{. x x . . . . . .
                          x . . x . . . . .
                          x . . x . . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          x . . . x . . . .
                          . . . . x . . . .
                          . . . . x . . . .
                          x x x x x . . . .}.gsub(/^\s+/, ")
                        )
  
  def initialize(rows, columns=rows)
    @row_size = rows + 2
    @col_size = columns + 2
    
    @grid = Grid.new(@row_size, @col_size)
    
    # Set the top row
    @grid.set_tile(0, 0, @@top_left)
    (1...@col_size-1).each do |i|
      @grid.set_tile(0, i, @@top)
    end
    @grid.set_tile(0, @col_size-1, @@topright)
    
    # Set the center rows.
    (1...@row_size-1).each do |i|
      @grid.set_tile(i, 0, @@left)
      (1...@col_size-1).each do |j|
        @grid.set_tile(i, j, @@center)
      end
      @grid.set_tile(i, @col_size-1, @@right)
    end
    
    # Set the bottom row
    @grid.set_tile(@row_size-1, 0, @@bot_left)
    (1...@col_size-1).each do |i|
      @grid.set_tile(@row_size-1, i, @@bottom)
    end
    @grid.set_tile(@row_size-1, @col_size-1, @@botright)
  end
  
  def to_aa()
    return @grid.to_s
  end
  
  def to_image()
    
    filename = "panel-#\{@row_size}x#\{@col_size}.png"
    
    max_x = 9 * @row_size
    max_y = 9 * @col_size
    
    image = Image.new(max_x, max_y) { self.background_color = "white" }
    (0...max_y).each do |y|
      (0...max_x).each do |x|
        pixel = @grid.at(x, y)
        if pixel == "x" then
          image.pixel_color(x, y, "black")
        end
      end
    end
    image.write(filename)
    
  end
  
end
  
rows = 1
columns = 1
  
opts = OptionParser.new do |opts|
  opts.banner = "Usage #\{$0} |opts|"
  opts.separator ""
  opts.separator "Specific Options"
  
  opts.on("-r", "--rows [ROWS]",
          "Number of rows for this panel (default 1)") do |r|
    rows = r.to_i
    columns = rows
  end
  
  opts.on("-c", "--columns [COLUMNS]",
          "Number of columns for this panel (default ROWS)") do |c|
    columns = c.to_i
  end
  
  opts.on_tail("-h", "--help",
               "Show this message") do
    puts opts
    exit
  end
end
  
opts.parse!
  
panel = KnotworkPanel.new(rows, columns)
panel.to_image()
$ ruby knotworkpanel.rb 

3x3 panel

$ ruby knotworkpanel.rb --rows 2

4x4 panel

Me from the future says "See? This is what I was talking about. You say '2 rows', and you get a 4×4 square? How do you think people are going to react? Man, I need more coffee."

Ignoring me from the future for today, let's see what happens when we try to make a nice big KnotworkPanel.

$ ruby knotworkpanel.rb --rows 98 --columns 73

Hmm ... took a few seconds this time. If I cared about performance, I might go in and see where this could be
tightened up. I don't care about performance today, though, just results. And the results aren't too bad.

Copyright 1999 - 2008 Brian Wisti

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License.