Drawing Celtic Knotwork 7
Okay, it's done! That is to say, it does all the things I want it to for now. There's a lot more stuff that I would like to do with this program, but it's important to know when to stop and take a breath. Let's just go back and clean up the code a little bit. Not actually change any functionality or user interface, so Me From The Future is just going to have to wait. I only want to make it easier to read the code that I have already written.
require 'RMagick'
require 'optparse'
include Magick
TILE_SIZE = 9
# 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 = []
1.upto(TILE_SIZE) {
row = []
1.upto(TILE_SIZE) { 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
attr_reader :rows, :columns
def initialize(rows, columns)
@tile_size = TILE_SIZE
@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)
# Make room for the border tiles
row_size = rows + 2
col_size = columns + 2
last_row = row_size - 1
last_col = col_size - 1
@grid = Grid.new(row_size, col_size)
# Set the top row
@grid.set_tile(0, 0, @@top_left)
(1...last_col).each do |i|
@grid.set_tile(0, i, @@top)
end
@grid.set_tile(0, last_col, @@topright)
# Set the center rows.
(1...last_row).each do |i|
@grid.set_tile(i, 0, @@left)
(1...last_col).each do |j|
@grid.set_tile(i, j, @@center)
end
@grid.set_tile(i, last_col, @@right)
end
# Set the bottom row
@grid.set_tile(last_row, 0, @@bot_left)
(1...last_col).each do |i|
@grid.set_tile(last_row, i, @@bottom)
end
@grid.set_tile(last_row, last_col, @@botright)
end
def to_aa()
return @grid.to_s
end
def to_image()
filename = "panel-#\{@grid.rows}x#\{@grid.columns}.png"
x_size = TILE_SIZE * @grid.rows
y_size = TILE_SIZE * @grid.columns
image = Image.new(x_size, y_size) { self.background_color = "white" }
(0...y_size).each do |y|
(0...x_size).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
def main
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()
end
main()
