Drawing Celtic Knotwork 5I bet you feel really cheated by now. I've been going on for all this time about celtic knotwork and drawing pictures with the computer, and all you've seen is a bunch of dots and crosses that kind of look like a picture if you go cross-eyed for a second. You can cheer up, folks, because you've finished the boring part. Now we want to make a real live picture! But how are we going to do it? You know that I'm not about to go making my own image creation library for a little project like this - or any other project, if I can help it. There's no need, either, because there are at least two great choices available:
Each of these options are Ruby-based interfaces to very powerful image generation and manipulation libraries which have been around for ... ages, really. Either one would provide more than enough muscle to squish bitmaps together. Don't you love it when I get technical? Since it's a coin-toss for either library based on utility, we have to move on to the next measure: documentation. Ruby/GD might have some great documents hidden around somewhere, but I sure can't find them. RMagick, on the other hand, has a useful manual accessible online. See, I've started to learn that most great programmers aren't really all-knowing - they just know how to look stuff up quickly. If I have a good manual handy, then that makes it easier to become a great programmer. Or at least I can make myself look useful...
require 'RMagick'
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
#####
# Test code
#####
$source_string =<<HERE
x . . . . . . . x
. x . . . . . x .
. . x . . . x . .
. . . x . x . . .
. . . . x . . . .
. . . x . x . . .
. . x . . . x . .
. x . . . . . x .
x . . . . . . . x
HERE
$source_string.gsub!(/^\s+/m, ")
require 'test/unit'
class TC_Tile < Test::Unit::TestCase
def setup
@@tile = Tile.new()
end
def test_pixels
assert_equal(nil, @@tile.is_set?(0, 0),
"By default, any pixel in a Tile is blank")
assert(@@tile.set(0, 0),
"Use Tile#set(row, col) to set a pixel at coordinates (row, col)")
assert(@@tile.is_set?(0, 0),
"A pixel (row, col) is set after Tile#set(row, col) has been called")
assert(@@tile.unset(0, 0),
"Use Tile#unset(row, col) to clear a pixel at coordinates (row, col)")
assert_equal(nil, @@tile.is_set?(0, 0),
"An unset pixel has no set value")
@@tile.set(1, 1)
assert_equal(nil, @@tile.is_set?(0, 0),
"Setting one pixel has no effect on other pixels in a Tile")
assert(@@tile.is_set?(1, 1),
"Tile remembers the set status of each pixel in its confines.")
assert(@@tile.set_from_string($source_string),
"You can use ASCII art strings to set the pixels in a Tile")
assert(@@tile.is_set?(0, 0))
assert(@@tile.is_set?(1, 0))
assert_equal('x', @@tile.at(0, 0),
"A Tile remembers the value assigned, if given, " \
"during Tile#set(row, col, val)")
end
end
class TC_Grid < Test::Unit::TestCase
def test_simple_grid
grid = Grid.new(1, 1)
tile = Tile.new($source_string)
grid.set_tile(0, 0, tile)
assert_equal("x", grid.at(0, 0),
"Use Grid#pixel_at(row, col) to access pixel at (row, col) " \
"distance from upper left corner")
assert_equal($source_string, grid.to_s)
assert_raise(ArgumentError, "You cannot set a Tile outside of the Grid." {
grid.set_tile(1, 1, tile)
}
end
def test_large_grid
grid = Grid.new(1, 2)
tile1 = Tile.new($source_string)
tile2 = Tile.new($source_string)
grid.set_tile(0, 0, tile1)
grid.set_tile(0, 1, tile2)
assert_equal("x", grid.at(0, 0))
assert_equal("x", grid.at(0, 9),
"Grid#pixel_at uses whole grid as coordinate system")
expected_output =<<HERE
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 . . x . . . . . x .
x . . . . . . . x x . . . . . . . x
HERE
expected_output.gsub!(/^\s+/, ")
assert_equal(expected_output, grid.to_s)
end
end
class TestKnotworkPanel < Test::Unit::TestCase
def test_ascii
panel = KnotworkPanel.new(1)
ascii_output_1 =<<HERE
. . . . 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 . 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 . . 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 . . . 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 . 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 . . 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 . . . . . . x . . . .
. . . . x x x x x x x . . . . . x x x x x x x . . . .
HERE
ascii_output_1.gsub!(/^\s+/, ")
real_output = panel.to_aa.split("\n")
ascii_output_1.split("\n").each_with_index do |line, i|
assert_equal(line, real_output[i],
"line #{i} doesn't match")
end
end
def test_large_panels
ascii_output_1 =<<HERE
. . . . 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 . 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 . . 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 . . . 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 . 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 . 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 . . . . 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 . . . 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 . . . .
. . . . x x x x x x x . . . . . x x x x x x x . . . .
HERE
ascii_output_1.gsub!(/^\s+/, ")
panel = KnotworkPanel.new(2, 1)
real_output = panel.to_aa.split("\n")
ascii_output_1.split("\n").each_with_index do |line, i|
assert_equal(line, real_output[i],
"line #{i} doesn't match")
end
end
end
panel = KnotworkPanel.new(8, 18)
panel.to_image()
Thanks to all the work we did building Tiles and Grids and ASCII art KnotworkPanels, we only need to add a few lines to allow KnotworkPanels to create a nice black and white PNG image file. Here's what we get: ![]() Cool, eh? |
![]() |
|
|
Copyright 1999 - 2009 Brian Wisti
|
||