I used RMagick to create animated gifs. I've never programatically
generated images before, so I was very pleased to discover that it's
as easy as I hoped it would be.
On my (slowly dying) 1.3GHz Celeron M, a 200 x 200 movie with 40%
vapor fill takes 3 minutes to make, resulting in a movie of about 25
seconds and 700KB. The memory usage while running gets up to about
80MB. An example output is here: http://www.tie-rack.org/images/frost.gif
I decided to place the initial point of ice at a random location
instead of the center.
The vapor isn't in the images, as it would dramatically increase the
time it takes to render each frame (or at least it would the way I'm
doing it).
There's still some ugliness in there, but I became more interested in
tweaking than beautifying.
-Chris Shea
frost.rb
--------
require 'RMagick'
module Frost
ICE = 0
NEWICE = 1
VAPOR = 2
VACUUM = 3
ICECOLOR = 'blue'
class Window
def initialize(width, height, vapor_chance)
unless width % 2 == 0 and height % 2 == 0
raise ArgumentError, "divisible by 2"
end
@width = width
@height = height
row = Array.new(width, Frost::VACUUM)
@glass = Array.new(height) { row.dup }
@image = Magick::ImageList.new
#place random vapor
0.upto(height - 1) do |row|
0.upto(width - 1) do |col|
@glass[row][col] = Frost::VAPOR if rand < vapor_chance
end
end
#place first ice
#@glass[height / 2][width / 2] = Frost::NEWICE
@glass[rand(height)][rand(width)] = Frost::NEWICE
@step = 0
make_gif
end
def step
neighborhood_starts.each do |start|
n = find_neighbors(start)
n.step
@glass[start[0]][start[1]] = n.layout[0]
@glass[start[0]][(start[1]+1) % @width] = n.layout[1]
@glass[(start[0]+1) % @height][start[1]] = n.layout[2]
@glass[(start[0]+1) % @height][(start[1]+1) % @width] =
n.layout[3]
end
@step += 1
end
def neighborhood_starts
starts = []
offset = @step % 2
offset.step(@height - 1, 2) do |row|
offset.step(@width - 1, 2) do |col|
starts << [row,col]
end
end
starts
end
def find_neighbors(start)
one = @glass[start[0]][start[1]]
two = @glass[start[0]][(start[1] + 1) % @width]
three = @glass[(start[0] + 1) % @height][start[1]]
four = @glass[(start[0] + 1) % @height][(start[1] + 1) % @width]
Frost::Neighborhood.new(one,two,three,four)
end
def done?
@glass.each do |row|
return false if row.include? Frost::VAPOR
end
true
end
def make_gif
if @image.empty?
@image.new_image(@width, @height)
else
@image << @image.last.copy
end
@glass.each_with_index do |row, y|
row.each_with_index do |cell, x|
if cell == Frost::NEWICE
point = Magick::Draw.new
point.fill(Frost::ICECOLOR)
point.point(x,y)
point.draw(@image)
end
end
end
end
def create_animation
@image.write("frost_#{Time.now.strftime("%H%M")}.gif")
end
def go
until done?
step
make_gif
print '.'
end
print "\ncreating animation... "
create_animation
puts 'done'
end
end
class Neighborhood
def initialize(one,two,three,four)
@layout = [one,two,three,four]
transform(Frost::NEWICE, Frost::ICE)
end
attr_reader :layout
def step
if ice?
ice_over
else
rotate
end
end
def ice?
@layout.include? Frost::ICE
end
def rotate
if rand(2).zero?
@layout = [@layout[1],@layout[3],@layout[0],@layout[2]]
else
@layout = [@layout[2],@layout[0],@layout[3],@layout[1]]
end
end
def transform(from, to)
@layout.map! {|cell| cell == from ? to : cell}
end
def ice_over
transform(Frost::VAPOR, Frost::NEWICE)
end
end
end
if __FILE__ == $0
if ARGV.size != 3
puts "frost.rb <width> <height> <vapor chance (float)>"
puts "This shouldn't take too long: frost.rb 100 100 0.3"
exit
end
width = ARGV[0].to_i
height = ARGV[1].to_i
vapor_percent = ARGV[2].to_f
window = Frost::Window.new(width,height,vapor_percent).go
end