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

Thursday 9 September 2010

Translating the default.es to ruby processing DSL

It was very constructive to see if I could translate the EisenScript to my ruby processing DSL, I immediately learnt something about the EisenScript (recursion seems to be implicit, something I had not understood before). What I learnt about my library, was I was losing some context by directly altering the attitude (the image expected from the EisenScript appeared briefly but quickly degraded). I have now re-factored the sketch to only adjust the initial angle, the x rotation seems to affect the amplitude of deformation from the plane of the spirals. The y and z rotation give control of the attitude, and need to be used in combination to fully explore the 3 dimensional shape, which is now somewhat like that produced by StrucureSynth. 

Here is my revised 3d context free DSL (test_free.rb):-

# A Context-Free library for Ruby-Processing, inspired by
# contextfreeart.org and StructureSynth
# Built on Jeremy Ashkenas context free DSL script

module Processing

  class ContextFree


    include Processing::Proxy

    attr_accessor :rules, :app, :xr, :yr, :zr

    AVAILABLE_OPTIONS = [:x, :y, :z, :rx, :ry, :rz, :size, :color, :hue, :saturation, :brightness, :alpha]
    HSB_ORDER         = {:hue => 0, :saturation => 1, :brightness => 2, :alpha => 3}

    # Define a context-free system. Use this method to create a ContextFree
    # object. Call render() on it to make it draw.
    def self.define(&block)
      cf = ContextFree.new
      cf.instance_eval &block
      cf
    end


    # Initialize a bare ContextFree object with empty recursion stacks.
    def initialize
      @app          = $app
      @graphics     = $app.g
      @finished     = false
      @rules        = {}
      @rewind_stack = []
      @matrix_stack = []
      @xr = 0
      @yr = 0
      @zr = 0
    end


    # Create an accessor for the current value of every option. We use a values
    # object so that all the state can be saved and restored as a unit.
    AVAILABLE_OPTIONS.each do |option_name|
      define_method option_name do
        @values[option_name]
      end
    end


    # Here's the first serious method: A Rule has an
    # identifying name, a probability, and is associated with
    # a block of code. These code blocks are saved, and indexed
    # by name in a hash, to be run later, when needed.
    # The method then dynamically defines a method of the same
    # name here, in order to determine which rule to run.
    def rule(rule_name, prob=1, &proc)
      @rules[rule_name] ||= {:procs => [], :total => 0}
      total = @rules[rule_name][:total]
      @rules[rule_name][:procs] << [(total...(prob+total)), proc]
      @rules[rule_name][:total] += prob
      unless ContextFree.method_defined? rule_name
        self.class.class_eval do
          eval <<-METH
            def #{rule_name}(options)
              merge_options(@values, options)
              pick = determine_rule(#{rule_name.inspect})
              @finished = true if @values[:size] < @values[:stop_size]
              unless @finished
                get_ready_to_draw
                pick[1].call(options)
              end
            end
          METH
        end
      end
    end


    # Rule choice is random, based on the assigned probabilities.
    def determine_rule(rule_name)
      rule = @rules[rule_name]
      chance = rand * rule[:total]
      pick = @rules[rule_name][:procs].select {|the_proc| the_proc[0].include?(chance) }
      return pick.flatten
    end


    # At each step of the way, any of the options may change, slightly.
    # Many of them have different strategies for being merged.
    def merge_options(old_ops, new_ops)
      return unless new_ops
      # Do size first
      old_ops[:size] *= new_ops[:size] if new_ops[:size]
      new_ops.each do |key, value|
        case key
        when :size
        when :x, :y, :z
          old_ops[key] = value * old_ops[:size]
        when :rz, :ry, :rx
          old_ops[key] = value * (Math::PI / 180.0)
        when :hue, :saturation, :brightness, :alpha
          adjusted = old_ops[:color].dup
          adjusted[HSB_ORDER[key]] *= value
          old_ops[:color] = adjusted
        when :width, :height
          old_ops[key] *= value
        when :color
          old_ops[key] = value
        else # Used a key that we don't know about or trying to set
          merge_unknown_key(key, value, old_ops)
        end
      end
    end


    # Using an unknown key let's you set arbitrary values,
    # to keep track of for your own ends.
    def merge_unknown_key(key, value, old_ops)
      key_s = key.to_s
      if key_s.match(/^set/)
        key_sym = key_s.sub('set_', '').to_sym
        if key_s.match(/(brightness|hue|saturation|alpha)/)
          adjusted = old_ops[:color].dup
          adjusted[HSB_ORDER[key_sym]] = value
          old_ops[:color] = adjusted
        else
          old_ops[key_sym] = value
        end
      end
    end

    # Doing a 'split' saves the context, and proceeds from there,
    # allowing you to rewind to where you split from at any moment.
    def split(options=nil, &block)
      save_context
      merge_options(@values, options) if options
      yield
      restore_context
    end

    # Saving the context means the values plus the coordinate matrix.
    def save_context
      @rewind_stack.push @values.dup
      @matrix_stack << @graphics.get_matrix
    end

    # Restore the values and the coordinate matrix as the recursion unwinds.
    def restore_context
      @values = @rewind_stack.pop
      @graphics.set_matrix @matrix_stack.pop
    end

    # Rewinding goes back one step.
    def rewind
      @finished = false
      restore_context
      save_context
    end

    # Render the is method that kicks it all off, initializing the options
    # and calling the first rule.
    def render(rule_name, starting_values={})
      @values = {:x => 0, :y => 0, :z => 0,
                 :rz => 0, :ry => 0, :rx => 0,
                 :size => 1, :width => 1, :height => 1,
                 :start_x => width/2, :start_y => height/2, :start_z => 0,
                 :color => [0.5, 0.5, 0.5, 1],
                 :stop_size => 1.5}
      @values.merge!(starting_values)
      @finished = false
      @app.reset_matrix
      @app.no_stroke
      @app.color_mode HSB, 1.0
      @app.translate @values[:start_x], @values[:start_y], @values[:start_z]
      self.send(rule_name, {})
    end

    def rotate_x rt
      @xr = rt
    end
    def rotate_y rt
      @yr = rt
    end
    def rotate_z rt
      @zr = rt
    end

    # Before actually drawing the next step, we need to move to the appropriate
    # location.
    def get_ready_to_draw
      @app.translate(@values[:x], @values[:y], @values[:z])
      @app.rotate_x(@values[:rx] + xr)
      @app.rotate_y(@values[:ry] + yr)
      @app.rotate_z(@values[:rz] + zr)
    end


    # Compute the rendering parameters for drawing a shape.
    def get_shape_values(some_options)
      old_ops = @values.dup
      merge_options(old_ops, some_options) if some_options
      @app.fill *old_ops[:color]
      return old_ops[:size], old_ops
    end


    # Sphere, cube are the primitive drawing
    def cube(some_options=nil)
      size, options = *get_shape_values(some_options)
      rotz = options[:rz]
      roty = options[:ry]
      rotx = options[:rx]
      @app.rotate_x rotx unless rotx.nil?
      @app.rotate_y roty unless roty.nil?
      @app.rotate_z rotz unless rotz.nil?
      @app.translate(options[:x]  * size, options[:y] * size , options[:z]  * size)
      @app.box(size)
      @app.rotate_z(-1 * rotz) unless rotz.nil?  # unwind rotations in an oredered way
      @app.rotate_y(-1 * roty) unless roty.nil?
      @app.rotate_x(-1 * rotx) unless rotx.nil?    
  
    end


    def sphere(some_options=nil)
      size, options = *get_shape_values(some_options)
      rotz = options[:rz]
      roty = options[:ry]
      rotx = options[:rx]
      @app.rotate_x rotx unless rotx.nil?
      @app.rotate_y roty unless roty.nil?
      @app.rotate_z rotz unless rotz.nil?
      @app.sphere_detail 10
      @app.sphere(size)
      @app.rotate_z(-1 * rotz) unless rotz.nil?  # unwind rotations in an oredered way
      @app.rotate_y(-1 * roty) unless roty.nil?
      @app.rotate_x(-1 * rotx) unless rotx.nil?    
    end
  end
end





My Translation of default.es to ruby processing



# default.rb
# virtually a direct translation of default.es
# the StructureSynth default script

load_libraries :test_free, :control_panel

attr_reader :amplitude, :yrot, :zrot

def setup_the_spiral
  @spiral = ContextFree.define do

    rule :default do
      split do
      R1 :brightness => 1
      rewind
      R2 :brightness => 1
    end
    end

    rule :R1 do                      
      sphere :brightness => 1
      R1 :size => 0.99, :x => 0.25, :rz => 6, :ry => 6
    end

    rule :R2 do                      
      sphere :brightness => 1
      R2 :size => 0.99, :x => -0.25, :rz => 6, :ry => 6
    end

  end
end

def configure_control # setup control panel gui
  control_panel do |c|
    c.title = "Amplitude+Attitude"
    c.slider :amplitude, -0.025..0.015, 0.0
    c.slider :yrot, -6.3..6.3, 0.005
    c.slider :zrot, -6.3..6.3, 0.005
  end
end

def setup
  size 800, 800, P3D
  configure_control
  smooth
  setup_the_spiral
end

def draw
  background 0
  lights
  @spiral.render :default, :start_x => 0, :start_y => 0, :start_z => -40, :size => height/400,
  :stop_size => 0.2, :color => [0, 0.8, 0.8], :rx => amplitude, :ry => yrot, :rz => zrot

end



No comments:

Post a Comment

Followers

Blog Archive

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