Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Monday, 8 March 2010

Towards Post Production Transformation of LSystem Fractals

I've finally got round to reading "Fractals Everywhere" by Michael F. Barnsley. The math is not overly complicated just a bit unfamiliar. Anyway I'm sort of inspired by some the transforms in the book, to see what I could come up with. I'm particularly attracted by the idea of projecting a 2D fractal into a 3D dimensional space. There are possibly too many different ways of do this (including paper modelling, something I'd not considered before I saw it today over on the processing discourse). My first approach will be to produce a data set for a regular 2D fractal, and thereafter apply some rules to create a deformation of the pattern. To this end I've re-worked my snake-kolam to create a data set (an array of points). The first transformation rules I have created (see ScalingTool class) merely scale and center the fractal. Here is the code:-


##
# Lindenmayer System in ruby-processing by Martin Prout
###

class Kolam_Test < Processing::App
  load_libraries 'kolam'
  attr_reader :kolam

  def setup
    size 500, 500
    @kolam = Kolam.new
    kolam.create_grammar 3      # create grammar from rules
    kolam.translate             # translate grammar to points
    no_loop
  end

  def draw
    background 0
    kolam.render width, height  # adjust points to fit frame & render
  end
end

############################
# library/kolam/kolam.rb
# Non-stochastic grammar
# with unique premise/rules
############################
class Grammar
  attr_accessor :axiom, :rules

  def initialize axiom
    @axiom = axiom
    @rules = Hash.new
  end

  def add_rule premise, rule
    rules.store(premise, rule)
  end

  ##########################################
  # replace each pre char with a unique rule
  ##########################################
  def new_production production
    production.gsub!(/./) { |c| (r = @rules[c]) ? r : c }
  end

  ##########################################
  # control the number of iterations
  # default 0, returns the axiom
  ##########################################
  def generate repeat = 0
    prod = axiom
    repeat.times do
      prod = new_production prod
    end
    return prod
  end
end

############################
# snake kolam using l-systems
############################
BORDER = 10 # global border constant
XPOS = 0    # global point array constants
YPOS = 1

class Kolam
  include Processing::Proxy
  attr_accessor :axiom, :xpos, :ypos, :grammar, :production, :draw_length, :points
  ANGLE = 2
  DELTA = (Math::PI/180) * 90.0 # convert degrees to radians using ruby

  def initialize
    @axiom = "FX+F+FX+F"
    @grammar = Grammar.new(axiom)
    grammar.add_rule("X", "X-F-F+FX+F+FX-F-F+FX")
    @theta = DELTA
    @points = [[0, 0]]              # initialize points array with first point
    @draw_length = 1.0
    @production = axiom
    @xpos = 0
    @ypos = 0
  end

  def translate                       # NB not using processing affine transforms here
    turtle = [xpos, ypos, 0.0]
    production.scan(/./).each do |element|
      case element
      when 'F'
        turtle = store_line(turtle, draw_length)
      when '+'
        turtle[ANGLE] += DELTA  
      when '-'
        turtle[ANGLE] -= DELTA  
      when 'X'                     # do nothing except recognize 'X' as a word in the L-system grammar
      else
        puts "Character '#{element}' is not in grammar"
      end
    end
  end

  def render(width, height)
    st = ScalingTool.new width, height, points
    data = st.scale_to_fit
    no_fill
    stroke(0, 255, 0)
    stroke_width(2)
    begin_shape
    data.each do |point|
      vertex(point[XPOS], point[YPOS])
    end
    end_shape
  end

  ##############################
  # create grammar from axiom and rules
  # leave scaling & postioning to render
  ##############################

  def create_grammar(gen)
    @production = @grammar.generate gen
  end

  private
  ######################################################
  # calculate and store line using current turtle and length parameters
  # returns a turtle corresponding to the new position
  ######################################################

  def store_line(turtle, length)
    new_xpos = turtle[XPOS] + length * Math.cos(turtle[ANGLE])
    new_ypos = turtle[YPOS] + length * Math.sin(turtle[ANGLE])
    *point = new_xpos, new_ypos                 # collect coordinates
    @points.push(point)
    *turtle = new_xpos, new_ypos, turtle[ANGLE] % (Math::PI * 2) # collect coordinates & angle
  end
end

###################################
# scaling tool, scale and center fractal
###################################

class ScalingTool
  attr_reader :max_height, :max_width, :lowest_y, :lowest_x, :highest_y, :highest_x, :raw_data, :scale
  def initialize max_width, max_height, data = []
    @max_width = max_width - BORDER
    @max_height = max_height - BORDER
    @raw_data = data
    @lowest_x = 0
    @lowest_y = 0
    @highest_x = 0
    @highest_y = 0
    @scale = 1
  end

  def scale_to_fit
    processed = raw_data
    scale = calculate_scale_factor
    processed.each do |item|
      item[XPOS] = scale * (item[XPOS] - lowest_x) + BORDER/2
      item[YPOS] = scale * (item[YPOS] - lowest_y) + BORDER/2
    end
    return processed
  end

  private

  def calculate_x_range
    @raw_data.each do |item|
      @lowest_x = item[XPOS] unless lowest_x < item[XPOS]
      @highest_x = item[XPOS] unless highest_x > item[XPOS]   
    end
    highest_x - lowest_x
  end

  def calculate_y_range
    @raw_data.each do |item|
      @lowest_y = item[YPOS] unless lowest_y < item[YPOS]
      @highest_y = item[YPOS] unless highest_y > item[YPOS]
    end
    highest_y - lowest_y
  end

  #################################################
  # Returns the smallest of width or height factors
  # as side effect stores lowest x and y values
  #################################################

  def calculate_scale_factor
    scale_x = (max_height * 1.0)/calculate_x_range
    scale_y = (max_width * 1.0)/calculate_y_range
    (scale_x < scale_y) ? scale_x : scale_y
  end
end




2 comments:

  1. For those wanting to do 3D stuff with Ruby Processing, I also have a few screencasts up on YouTube, as well as some F/OSS code available on GitHub. http://www.prestonlee.com/2010/05/17/3d-osx-applications-with-ruby-processing-screencast/

    ReplyDelete
  2. You made two identical comments, so I deleted one.
    Probably the wrong on as this looks to be displaced from where you wanted to comment. Comment again where you wanted to and I'll put it right. I had to control post because of a rash of exclusively Chinese spam.

    ReplyDelete

Followers

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2