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




Friday, 12 February 2010

Developing the shapes

From the pentive? fractal it is clearly possible to develop some interesting "textured" shapes, here I produce an approximate circle from a set of rotations.


########################################################
# A Pentive fractal implemented using a
# Lindenmayer System in ruby-processing by Martin Prout
########################################################
require 'pentive'

class Pentive_Test < Processing::App
  attr_reader :pentive,:points, :production

  def setup
    size(500, 500)
    @pentive = Pentive.new(0, 0)
    @production = pentive.create_grammar(6)
    @points = pentive.translate_rules(production)
    no_loop()
  end
 
  def draw_element()
    stroke(255)
    points.each do |tmp|
      line(*tmp)
    end
  end

  def draw()
    background(0)
    translate(width/2, height/2)
    10.times do
      rotate(Math::PI/180 * 36)    
      draw_element()
    end
    save_frame("/home/tux/advance.png")
  end
end




Thursday, 11 February 2010

A Pentive? Fractal

The pentive fractal is another space filling fractal that I found over on a fractint site again for the grammar generator see my Cesàro fractal.

########################################################
# A Pentive fractal implemented using a
# Lindenmayer System in ruby-processing by Martin Prout
########################################################
require 'pentive'

class Pentive_Test < Processing::App
  attr_reader :pentive,:points, :production

  def setup
    size(600, 400)
    @pentive = Pentive.new(width/95, height*0.9)
    @production = pentive.create_grammar(8)
    @points = pentive.translate_rules(production)
    no_loop()
  end

  def draw()
    background(0)
    stroke(255)
    points.each do |tmp|
      line(*tmp)
    end
  end
end

####################################################
# The Pentive? fractal
####################################################
class Pentive

  attr_reader :draw_length, :xpos, :ypos, :theta, :axiom, :grammar, :delta
  DELTA = Math::PI/180 * 36 # 36 degrees

  def initialize xpos, ypos
    @axiom = "Q"  
    @theta  = -DELTA
    @grammar = Grammar.new(axiom)
    grammar.add_rule("F", "")
    grammar.add_rule("P","1-FR3+FS1-FU")   # abbreviated grammar 1 = two & 3 = four repeats
    grammar.add_rule("Q", "FT1+FR3-FS1+")
    grammar.add_rule("R", "1+FP3-FQ1+FT")
    grammar.add_rule("S", "FU1-FP3+FQ1-")  
    grammar.add_rule("T", "+FU1-FP+")
    grammar.add_rule("U", "-FQ1+FT-")
    @draw_length = 12
    @xpos = xpos
    @ypos = ypos
  end

  def create_grammar(gen)  
    grammar.generate(gen)
  end

  def translate_rules(prod)
    repeats = 1
    points = [] # An empty array to store lines as an array of points
    prod.scan(/./) do |ch|
      case(ch)
      when "F"
        temp = [xpos, ypos, (@xpos += draw_length * Math.cos(theta)), (@ypos += draw_length * Math.sin(theta))]
        points.push(temp)    
      when "+"
        @theta += DELTA * repeats
        repeats = 1    
      when "-"
        @theta -= DELTA * repeats
        repeats = 1
      when '1', '3'
        repeats += Integer(ch)
      when "P", "Q", "R", "S", "T", "U"        
      else
        puts("character '#{ch}' not in grammar")
      end
   end
    return points
  end
end




MPeano Fractal (Traveling Salesman Problem)

Uses my grammar library see the Cesàro fractal, here I included it in the mpeano.rb file, which is why it didn't need to be separately loaded (omitted for brevity).

########################################################
# A MPeano fractal implemented using a
# Lindenmayer System in ruby-processing by Martin Prout
########################################################
require 'mpeano'

class MPeano_Test < Processing::App
  attr_reader :mpeano, :points, :production

  def setup
    size(600, 600)
    @mpeano = MPeano.new(width/2, height*0.95)
    @production = mpeano.create_grammar(7)
    @points = mpeano.translate_rules(production)
    no_loop()
  end

  def draw()
    background(0)
    stroke(255)
    points.each do |tmp|
      line(*tmp)
    end
  end
end

####################################################
# The MPeano fractal has been used to study the
# Euclidean travelling salesman problem
####################################################
class MPeano

  attr_reader :draw_length, :xpos, :ypos, :theta, :axiom, :grammar, :delta

  def initialize xpos, ypos
    @axiom = "XFF--AFF--XFF--AFF"
    @delta = Math::PI/4 # 45 degrees
    @theta  = delta * 2
    @grammar = Grammar.new(axiom)
    grammar.add_rule("X", "+!X!FF-BQFI-!X!FF+")
    grammar.add_rule("F", "")
    grammar.add_rule("Y", "FFY")
    grammar.add_rule("A", "BQFI")
    grammar.add_rule("B", "AFF")
    @draw_length = 8
    @xpos = xpos
    @ypos = ypos
  end

  def create_grammar(gen)  
    grammar.generate(gen)
  end

  def translate_rules(prod)
    points = [] # An empty array to store lines as an array of points
    prod.scan(/./) do |ch|
      case(ch)
      when "F"
        temp = [xpos, ypos, (@xpos -= draw_length * Math.cos(theta)), (@ypos -= draw_length * Math.sin(theta))]
        points.push(temp)    
      when "+"
        @theta += delta    
      when "-"
        @theta -= delta  
      when "!"
        @delta = -delta
      when "I"
              @draw_length *= 1/Math.sqrt(2)    
      when "Q"
              @draw_length *= Math.sqrt(2)    
      when "X", "A", "B"    
      else
        puts("character '#{ch}' not in grammar")
      end
   end
   return points
  end
end


 

Wednesday, 10 February 2010

DavidTour fractal

For the grammar.rb library see my Cesàro fractal (here it was included in the davidtour.rb file which is why it didn't need to be separately loaded, Grammar code omitted for brevity).

########################################################
# A David Tour fractal implemented using a
# Lindenmayer System in ruby-processing by Martin Prout
########################################################
require 'davidtour'

class David_Test < Processing::App
  attr_reader :david, :points, :production

  def setup
    size(800, 900)
    @david = DavidTour.new(width * 0.6, height/4)
    @production = david.create_grammar(5)
    @points = david.translate_rules(production)
    no_loop()
  end
  
  def draw()
    background(0)
    stroke(255)
    points.each do |tmp|
      line(*tmp)
    end
  end
end

####################################################
# The DavidTour fractal has been used to study the
# Euclidean travelling salesmam problem
####################################################
class DavidTour

  attr_reader :draw_length, :xpos, :ypos, :theta, :axiom, :grammar
  DELTA = Math::PI/3 # 60 degrees
  
  def initialize xpos, ypos
    @axiom = "FX-XFX-XFX-XFX-XFX-XF"
    @theta  = 0
    @grammar = Grammar.new(axiom)
    grammar.add_rule("F", "!F!-F-!F!")
    grammar.add_rule("X", "!X")
    @draw_length = 15
    @xpos = xpos
    @ypos = ypos
  end

  def create_grammar(gen)  
    @draw_length *= @draw_length * 0.5**gen
    grammar.generate(gen)
  end
  
  def translate_rules(prod)
    swap = false
    points = [] # An empty array to store lines as an array of points
    prod.scan(/./) do |ch|
      case(ch)
      when 'F'
        temp = [xpos, ypos, (@xpos += draw_length * Math.cos(theta)), (@ypos -= draw_length * Math.sin(theta))]
        points.push(temp)      
      when '+'
        @theta += (DELTA)      
      when '-'
        @theta += (swap ? DELTA : -DELTA)
      when '!'
        swap = !swap
      when 'X'    
      else
        puts("character '#{ch}' not in grammar")
      end
    end
    return points
  end
end



Tuesday, 2 February 2010

The Best of Both Worlds (combining PeasyCam and control_panel

This ruby processing sketch shows how to get the best of both worlds; the smooth easily setup PeasyCam, that you can use the scroll-wheel to zoom, and mouse to drag, and the possible fine grain control of a control panel. In this example I have initially disabled mouse control; use the button to toggle mouse control on or off. Use the control panel slider to set off the rotation or the freeze! button (to stop the rotation). If you only want fine grain rotation (cf. continuous rotation with the slider) then you will need get the camera to remember state (I believe Quark has done this in vanilla processing). If you are really keen you could also implement a slider to control the zoom of the peasy cam (in place of the mouse-wheel).

load_libraries 'PeasyCam', 'control_panel'
import 'peasy'

attr_reader :cam, :x_rotate, :y_rotate, :z_rotate, :controlled

def setup()
  size(200, 200, P3D)
  configure_panel()
  @controlled = true
  configure_camera()
end

def configure_camera()    
  @cam = PeasyCam.new(self, 100)
  cam.set_minimum_distance(50)
  cam.set_maximum_distance(500)
  mouse_control()
end

def configure_panel()
  control_panel do |c|
    c.slider(:x_rotate, -1.0..1.0, 0.0)
    c.slider(:y_rotate, -1.0..1.0, 0.0)
    c.slider(:z_rotate, -1.0..1.0, 0.0)
    c.button(:freeze!)
    c.button(:mouse_control)
  end
end

def freeze!()
  @x_rotate = 0.0
  @y_rotate = 0.0
  @z_rotate = 0.0
end

def mouse_control()     # toggle mouse controlled camera
  cam.set_mouse_controlled(!controlled)
  @controlled = !controlled
end

def draw()
  cam.rotate_x(x_rotate/100)
  cam.rotate_y(y_rotate/100)
  cam.rotate_z(z_rotate/100)
  rotate_x(-0.5)
  rotate_y(-0.5)
  background(0)
  fill(255, 0, 0)
  box(30)
  push_matrix()
  translate(0, 0, 20)
  fill(0, 0, 255)
  box(5)
  pop_matrix()
end




Sunday, 31 January 2010

Hello Peasy in ruby processing (a simple PeasyCam example)

load_libraries 'PeasyCam'
import 'peasy'

attr_reader :cam

def setup()
  size(200,200,P3D)
  configure_camera()
end

def configure_camera()
  @cam = PeasyCam.new(self, 100)
  cam.set_minimum_distance(50)
  cam.set_maximum_distance(500)
end

def draw()
  rotate_x(-0.5)
  rotate_y(-0.5)
  background(0)
  fill(255, 0, 0)
  box(30)
  push_matrix()
  translate(0, 0, 20)
  fill(0, 0, 255)
  box(5)
  pop_matrix()
end








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