Jeremy Ashkenas

Code as literature. Give it a read.

Context-free rule systems can be used to draw some truly beautiful pictures. By making heavy use of recursion and probability, the forms they create begin to approximate the organic. Context Free, a specialized program for making these sorts of drawings, has long been the best way to explore them, sporting a domain-specific language that makes it more fluent to describe the rules. I've put together a first stab at a context-free library for Ruby-Processing, which uses metaprogramming to allow you to use a similar syntax, and mix it in with the rest of your sketch.
Contextual Tree

contextual_tree.rb

The image above was drawn by the rule-system to the left. It consists of 5 rules (three seed, a leaf and a flower), each able to call the others. You can see how this style of rule definition is more appropriate than just making standard, object-oriented method calls, while still allowing you to make use of mixed-in Ruby code.
   1  # -- omygawshkenas
   2  
   3  require 'ruby-processing'
   4  
   5  class Contextual < Processing::App
   6    load_ruby_library "context-free"  
   7    attr_accessor :tree
   8    
   9    def setup_the_trees
  10      
  11      @tree = context_free do
  12        rule :seed do
  13          square
  14          leaf :y => 0 if size < 4.5 && rand < 0.018
  15          flower :y => 0 if size < 2.0 && rand < 0.02
  16          seed :y => -1, :size => 0.986, :rotation => 6, :brightness => 0.989
  17        end
  18        
  19        rule :seed, 0.1 do
  20          square
  21          seed :flip => true
  22        end
  23        
  24        rule :seed, 0.04 do
  25          square
  26          split do
  27            seed :flip => true
  28            rewind
  29            seed :size => 0.8, :rotation => rand(100), :flip => true
  30            rewind
  31            seed :size => 0.8, :rotation => rand(100)
  32          end
  33        end
  34        
  35        rule :leaf do
  36          the_size = rand(25)
  37          the_x = [1, 0, 0, 0][rand(4)]
  38          circle :size => the_size, :hue => 0.15, :saturation => 1.25, :brightness => 1.9, :x => the_x
  39        end
  40        
  41        rule :flower do
  42          split :brightness => rand(1.3)+4.7, :set_width => rand(15)+10, :set_height => rand(2)+2 do
  43            oval :rotation => 0
  44            oval :rotation => 45
  45            oval :rotation => 90
  46            oval :rotation => 135
  47          end
  48        end
  49      end
  50    end
  51    
  52    
  53    def setup
  54      setup_the_trees
  55      no_stroke
  56      smooth
  57      the_color = [0.5, 0.7, 0.8]
  58      @tree.setup :start_x => width/2, :start_y => height+20, :size => height/60, :color => the_color
  59      draw_it
  60    end
  61    
  62    def draw_the_background
  63      color_mode RGB, 1
  64      color = [0.0, 0.0, 0.00]
  65      background *color
  66      count = height/2
  67      push_matrix
  68      size = height / count
  69      (2*count).times do |i|
  70        color[2] = color[2] + (0.07/count)
  71        fill *color
  72        rect 0, i, width, 1
  73      end
  74    end
  75    
  76    def draw_it
  77      draw_the_background
  78      @tree.render :seed
  79    end
  80    
  81    def mouse_clicked
  82      draw_it
  83    end
  84    
  85  end
  86  
  87  Contextual.new(:width => 700, :height => 700, :title => "Contextual Tree")

context-free.rb

Here's the library that allows these rule-systems to be used. It's probably the most complicated chunk of code on the site, and may get a little hard to follow.
   1  # A first stab at doing a Context-Free, 
   2  # domain-specific language for Ruby-Processing.
   3  # The next draft will probably break the rules 
   4  # and rulesets out into actual classes of their own.
   5  
   6  # -- omygawshkenas
   7  
   8  module Processing
   9    
  10    class ContextFree
  11      attr_accessor :rules, :app
  12      STOP_SIZE = 1.5
  13      AVAILABLE_OPTIONS = [:x, :y, :rotation, :size, :flip, :color, :hue, :saturation, :brightness]
  14      HSB_ORDER = {:hue => 0, :saturation => 1, :brightness => 2}
  15      
  16      def initialize()
  17        @rules = {}
  18        @finished = false
  19        @rewind_stack = []
  20      end
  21      
  22      # Create an accessor for the current value of every option.
  23      AVAILABLE_OPTIONS.each do |option_name|
  24        define_method option_name do
  25          @values[option_name]
  26        end
  27      end
  28          
  29      def setup(some_hash)
  30        @starting_values = some_hash
  31        @starting_values[:stop_size] ||= STOP_SIZE
  32      end
  33      
  34      # A shortcut for defining methods on yourself.
  35      def create_method(name, &block)
  36        self.class.send(:define_method, name, &block)
  37      end
  38      
  39      # Here's the first serious method: A Rule has an 
  40      # identifying name, a probability, and is associated with 
  41      # a block of code. These code blocks are saved, and indexed 
  42      # by name in a hash, to be run later, when needed.
  43      # The method then dynamically defines a method of the same 
  44      # name here, in order to determine which rule to run.
  45      def rule(rule_name, prob=1, &proc)
  46        @rules[rule_name] ||= {:procs => [], :total => 0}
  47        total = @rules[rule_name][:total]
  48        @rules[rule_name][:procs] << [(total...(prob+total)), proc]
  49        @rules[rule_name][:total] += prob
  50        unless ContextFree.method_defined? rule_name
  51          
  52          create_method rule_name do |options|
  53            merge_options(@values, options)
  54            pick = determine_rule rule_name
  55            @finished = true if @values[:size] < STOP_SIZE
  56            unless @finished
  57              get_ready_to_draw
  58              pick[1].call(options)
  59            end
  60          end
  61        end
  62      end
  63          
  64      # Rule choice is random, based on the assigned probabilities.
  65      def determine_rule(rule_name)
  66        rule = @rules[rule_name]
  67        chance = rand * rule[:total]
  68        pick = @rules[rule_name][:procs].select {|the_proc| the_proc[0].include?(chance) }
  69        return pick.flatten
  70      end
  71      
  72      # At each step of the way, any of the options may change, slightly.
  73      # Many of them have different strategies for being merged.
  74      def merge_options(old_ops, new_ops)
  75        return unless new_ops
  76        # Do size first
  77        old_ops[:size] *= new_ops[:size] if new_ops[:size]
  78        new_ops.each do |key, value|
  79          case key
  80          when :size
  81          when :x, :y
  82            old_ops[key] = value * old_ops[:size]
  83          when :rotation
  84            old_ops[key] = value * (Math::PI / 180.0) / 2
  85          when :hue, :saturation, :brightness
  86            adjusted = old_ops[:color].dup
  87            adjusted[HSB_ORDER[key]] *= value
  88            old_ops[:color] = adjusted
  89          when :flip
  90            old_ops[key] = !old_ops[key]
  91          when :width, :height
  92            old_ops[key] *= value
  93          when :color
  94            old_ops[key] = value
  95          else # Used a key that we don't know about or trying to set
  96            merge_unknown_key(key, value, old_ops)
  97          end
  98        end
  99      end
 100      
 101      # Using an unknown key let's you set arbitrary values, 
 102      # to keep track of for your own ends.
 103      def merge_unknown_key(key, value, old_ops)
 104        key_s = key.to_s
 105        if key_s.match(/^set/)
 106          key_sym = key_s.sub('set_', '').to_sym
 107          if key_s.match(/(brightness|hue|saturation)/)
 108            adjusted = old_ops[:color].dup
 109            adjusted[HSB_ORDER[key_sym]] = value
 110            old_ops[:color] = adjusted
 111          else
 112            old_ops[key_sym] = value
 113          end
 114        end
 115      end
 116      
 117      # Doing a 'split' saves the context, and proceeds from there, 
 118      # allowing you to rewind to where you split from at any moment.
 119      def split(options=nil, &block)
 120        save_context
 121        merge_options(@values, options) if options
 122        yield
 123        restore_context
 124      end
 125      
 126      def save_context
 127        @rewind_stack.push @values.dup
 128        @app.push_matrix
 129      end
 130      
 131      def restore_context
 132        @values = @rewind_stack.pop
 133        @app.pop_matrix
 134      end
 135      
 136      def rewind
 137        @finished = false
 138        restore_context
 139        save_context
 140      end
 141      
 142      # Render the is method that kicks it all off, initializing the options 
 143      # and calling the first rule.
 144      def render(rule_name)
 145        @values = {:x => 0, :y => 0, 
 146                   :rotation => 0, :flip => false, 
 147                   :size => 20, :width => 20, :height => 20,
 148                   :color => [0.5, 0.5, 0.5]}
 149        @values.merge!(@starting_values)
 150        @finished = false
 151        @app = Processing::App.current
 152        @app.reset_matrix
 153        @app.no_stroke
 154        @app.color_mode(App::HSB, 1.0)
 155        @app.translate @values[:start_x], @values[:start_y]
 156        begin
 157          self.send(rule_name, {})
 158        rescue SystemStackError
 159          @finished = true
 160          @app.reset_matrix
 161          @app.fill 0
 162          @app.rect 0, 0, @app.width, @app.height
 163        end
 164      end
 165      
 166      def get_ready_to_draw
 167        @app.translate(@values[:x], @values[:y])
 168        sign = (@values[:flip] ? -1 : 1)
 169        @app.rotate(sign * @values[:rotation])
 170      end
 171      
 172      def get_shape_values(some_options)
 173        old_ops = @values.dup
 174        merge_options(old_ops, some_options) if some_options
 175        @app.fill *old_ops[:color]
 176        return old_ops[:size], old_ops
 177      end
 178      
 179      # Square, circle, and oval are the primitive drawing
 180      # methods, but hopefully triangles will be added soon.
 181      def square(some_options=nil)
 182        size, options = *get_shape_values(some_options)
 183        @app.rect(-(size/2), -(size/2), size, size)
 184      end
 185      
 186      def circle(some_options=nil)
 187        size, options = *get_shape_values(some_options)
 188        @app.oval(-(size/2), -(size/2), size, size)
 189      end
 190      
 191      def oval(some_options=nil)
 192        rot = some_options[:rotation]
 193        @app.rotate(rot) if rot
 194        size, options = *get_shape_values(some_options)
 195        width = options[:width] || options[:size]
 196        height = options[:height] || options[:size]
 197        @app.oval(options[:x], options[:y], width, height)
 198        @app.rotate(-rot) if rot
 199      end
 200      alias_method :ellipse, :oval
 201      
 202    end
 203    
 204    # Processing::App's get a context_free method, as a hook for 
 205    # defining their rules.
 206    class App
 207      
 208      def context_free(&block)
 209        free = ContextFree.new
 210        free.instance_eval &block
 211        return free
 212      end
 213      
 214    end
 215    
 216  end
 217