Jeremy Ashkenas

Code as literature. Give it a read.

Ruby-Processing lets you ditch Processing's crusty ol' Java syntax for a new pair of Ruby slippers. Processing is a framework for doing drawings, animations, and beautiful interactions with code — and now you can code them in Ruby. The project can be used to make web applets and cross-platform applications, and is fully open source. If you'd like to download it and get started, or even contribute, then I'll kindly direct you to the GitHub repository. For comprehensive instructions and aid, check out the wiki.
Tarbelliana

sand_traveler.rb

As an example of a Ruby-Processing app, the above image was generated by a Ruby port of Jared Tarbell's Sand Traveler. Here's the complete code.
   1  # After Jared Tarbell
   2  
   3  # Description:
   4  # Sand Traveler was a special commision, produced in Processing 
   5  # by Jared Tarbell for Sonar 2004, Barcelona.
   6  # This Ruby port has been tweaked, half of the (non-running) 
   7  # code exorcised, color palette broadened, and done in negative.
   8  
   9  # -- omygawshkenas
  10  
  11  require 'ruby-processing'
  12  
  13  class SandTraveler < Processing::App
  14    include Math
  15    attr_reader :cities, :num
  16    
  17    def setup
  18      @dx, @dy = 0, 0
  19      @num = 150
  20      @cities = []
  21      smooth
  22      reset_all
  23    end
  24    
  25    def draw
  26      @cities.each {|city| city.move }
  27    end
  28    
  29    def mouse_pressed
  30      reset_all
  31    end
  32    
  33    def reset_all
  34      @source_colors = [[rand(255), rand(255), rand(255), rand(30) + 45],
  35                       [rand(255), rand(255), rand(255), rand(30) + 45]]
  36      background 0
  37      vt = 4.2
  38      vvt = 0.2
  39      ot = rand * PI * 2
  40      @num.times do |i|
  41        tinc = ot + (1.1 - i / @num) * 2 * i * (PI * 2) / @num
  42        vx, vy = vt * sin(tinc), vt * cos(tinc)
  43        @cities[i] = City.new(width/2+vx*2, height/2+vy*2, vx, vy, i)
  44        vvt -= 0.00033
  45        vt += vvt
  46      end
  47      @cities.each { |city| city.find_friend }
  48    end
  49    
  50    def some_color
  51      choice = @source_colors[rand(2)].dup
  52      choice.map! { |c| c + (rand * 120 - 60) }
  53      choice[3] = choice[3] * 0.7
  54      return choice
  55    end
  56  end
  57  
  58  class City
  59    include Math
  60    attr_reader :x, :y, :color
  61    
  62    def initialize(dx, dy, vx, vy, index)
  63      @app = Processing::App.current
  64      @x, @y = dx, dy
  65      @vx, @vy = vx, vy
  66      @index = index
  67      @color = @app.some_color
  68      @num_travelers = 13
  69    end
  70    
  71    def move
  72      if (@vx.abs + @vy.abs) > 0.01
  73        @vx += (@friend.x - @x) / 1000
  74        @vy += (@friend.y - @y) / 1000
  75        @vx *= 0.946; @vy *= 0.946
  76        @x += @vx; @y += @vy
  77        draw_travelers
  78      end
  79    end
  80    
  81    def find_friend
  82      @friend = @app.cities[(@index + rand(@app.num/5) + 1) % @app.num]
  83    end
  84    
  85    def draw_travelers
  86      @app.stroke *@friend.color
  87      @num_travelers.times do |i|
  88        t = rand * PI * 2
  89        dx = sin(t) * (@x - @friend.x) / 2 + (@x + @friend.x) / 2
  90        dy = sin(t) * (@y - @friend.y) / 2 + (@y + @friend.y) / 2
  91        if rand < 0.01
  92          dx += rand * 6 - 3
  93          dy += rand * 6 - 3
  94        end
  95        @app.point dx, dy
  96      end
  97    end
  98  end
  99  
 100  SandTraveler.new :width => 700, :height => 700, 
 101                   :title => "Sand Traveler", :full_screen => true

ruby-processing.rb

When you download Ruby-Processing, you get a folder of scripts for generating blank sketches, applets, and applications, a handful of libraries, and this: the entire Ruby-Processing source.
   1  # Ruby-Processing is for Code Art.
   2  # Send suggestions, ideas, and hate-mail to jashkenas [at] gmail.com
   3  # Also, send samples and libraries.
   4  #
   5  # This class is a thin wrapper around Processing's PApplet.
   6  # Most of the code here is for interfacing with Swing, 
   7  # web applets, going fullscreen, and drawing sliders.
   8  #
   9  # Revision 0.8
  10  # - omygawshkenas
  11  
  12  require 'java'
  13  
  14  module Processing 
  15  
  16    # Conditionally load core.jar
  17    require File.expand_path(File.dirname(__FILE__)) + "/core.jar" unless Object.const_defined?(:JRUBY_APPLET)  
  18    include_package "processing.core"
  19  
  20    class App < PApplet
  21  
  22      include_class "javax.swing.JFrame"
  23      include_class "javax.swing.JPanel"
  24      include_class "javax.swing.JLabel"
  25  
  26      # Alias some methods for familiarity for Shoes coders.
  27      attr_accessor :frame, :title
  28      alias_method :oval, :ellipse
  29      alias_method :stroke_width, :stroke_weight
  30      alias_method :rgb, :color
  31      alias_method :gray, :color
  32  
  33      # Watch the definition of these methods, to make sure 
  34      # that Processing is able to call them during events.
  35      METHODS_TO_WATCH_FOR = { :mouse_pressed => :mousePressed,
  36                               :mouse_dragged => :mouseDragged,
  37                               :mouse_clicked => :mouseClicked,
  38                               :mouse_moved   =>   :mouseMoved, 
  39                               :mouse_released => :mouseReleased,
  40                               :key_pressed => :keyPressed,
  41                               :key_released => :keyReleased }
  42  
  43      def self.method_added(method_name)
  44        if METHODS_TO_WATCH_FOR.keys.include?(method_name)
  45          alias_method METHODS_TO_WATCH_FOR[method_name], method_name
  46        end
  47      end
  48  
  49      def self.current=(app); @current_app = app; end
  50      def self.current; @current_app; end
  51      def self.slider_frame; @slider_frame; end
  52  
  53      # Detect if a library has been loaded (for conditional loading)
  54      @@loaded_libraries = Hash.new(false)
  55      def self.library_loaded?(folder)
  56        @@loaded_libraries[folder.to_sym]
  57      end
  58      def library_loaded?(folder); self.class.library_loaded?(folder); end
  59  
  60      # For pure ruby libs.
  61      # The library should have an initialization ruby file 
  62      # of the same name as the library folder.
  63      def self.load_ruby_library(folder)
  64        unless @@loaded_libraries[folder.to_sym]
  65          Object.const_defined?(:JRUBY_APPLET) ? prefix = "" : prefix = "library/"
  66          @@loaded_libraries[folder.to_sym] = require "#{prefix}#{folder}/#{folder}"
  67        end
  68        return @@loaded_libraries[folder.to_sym]
  69      end
  70  
  71      # Loading libraries which include native code needs to 
  72      # hack the Java ClassLoader, so that you don't have to 
  73      # futz with your PATH. But its probably bad juju.
  74      def self.load_java_library(folder)
  75        unless @@loaded_libraries[folder.to_sym]
  76          if Object.const_defined?(:JRUBY_APPLET) # Applets preload all the java libraries.
  77            @@loaded_libraries[folder.to_sym] = true if JRUBY_APPLET.get_parameter("archive").match(%r(#{folder}))
  78          else
  79            base = "library#{File::SEPARATOR}#{folder}#{File::SEPARATOR}"
  80            jars = Dir.glob("#{base}*.jar")
  81            base2 = "#{base}library#{File::SEPARATOR}"
  82            jars = jars + Dir.glob("#{base2}*.jar")
  83            jars.each {|jar| require jar }
  84            return false if jars.length == 0
  85            # Here goes...
  86            sep = java.io.File.pathSeparator
  87            path = java.lang.System.getProperty("java.library.path")
  88            new_path = base + sep + base + "library" + sep + path
  89            java.lang.System.setProperty("java.library.path", new_path)
  90            field = java.lang.Class.for_name("java.lang.ClassLoader").get_declared_field("sys_paths")
  91            if field
  92              field.accessible = true
  93              field.set(java.lang.Class.for_name("java.lang.System").get_class_loader, nil)
  94            end
  95            @@loaded_libraries[folder.to_sym] = true
  96          end
  97        end
  98        return @@loaded_libraries[folder.to_sym]
  99      end
 100  
 101      # Creates a slider, in a new window, to control an instance variable.
 102      # Sliders take a name and a range (optionally), returning an integer.
 103      def self.has_slider(name, range=0..100)
 104        return if Object.const_defined?(:JRUBY_APPLET)
 105        min, max = range.begin * 100, range.end * 100
 106        attr_accessor name
 107        initialize_slider_frame unless @slider_frame
 108        slider = Slider.new(min, max)
 109        slider.add_change_listener do
 110          val = slider.get_value / 100.0
 111          slider.update_label(val)
 112          Processing::App.current.send(name.to_s + "=", val)
 113        end
 114        slider.set_minor_tick_spacing((max - min).abs / 10) 
 115        slider.set_paint_ticks true
 116        slider.paint_labels = true
 117        label = JLabel.new("<html><br>" + name.to_s + "</html>")
 118        slider.label = label
 119        slider.name = name
 120        @slider_frame.sliders << slider
 121        @slider_frame.panel.add label
 122        @slider_frame.panel.add slider
 123      end
 124  
 125      def initialize(options = {})
 126        super()
 127        App.current = self
 128        options = {:width => 400, 
 129                  :height => 400, 
 130                  :title => "",
 131                  :full_screen => false}.merge(options)
 132        @width, @height, @title = options[:width], options[:height], options[:title]
 133        display options
 134        display_slider_frame if self.class.slider_frame
 135      end
 136  
 137      def setup() end
 138      def draw() end
 139  
 140      def display_slider_frame
 141        f = self.class.slider_frame
 142        f.add f.panel
 143        f.set_size 200, 32 + (71 * f.sliders.size)
 144        f.setDefaultCloseOperation(JFrame::DISPOSE_ON_CLOSE)
 145        f.set_resizable false
 146        f.set_location(@width + 10, 0)
 147        f.show
 148      end
 149  
 150      def display_full_screen(graphics_env)
 151        @frame = java.awt.Frame.new
 152        mode = graphics_env.display_mode
 153        @width, @height = mode.get_width, mode.get_height
 154        gc = graphics_env.get_default_configuration
 155        @frame.set_undecorated true
 156        @frame.set_background java.awt.Color.black
 157        @frame.set_layout java.awt.BorderLayout.new
 158        @frame.add(self, java.awt.BorderLayout::CENTER)
 159        @frame.pack
 160        graphics_env.set_full_screen_window @frame
 161        @frame.set_location(0, 0)
 162        @frame.show
 163        self.init
 164        self.request_focus
 165      end
 166  
 167      def display_in_a_window
 168        @frame = JFrame.new(@title)
 169        @frame.add(self)
 170        @frame.setSize(@width, @height + 22)
 171        @frame.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE)
 172        @frame.setResizable(false)
 173        @frame.show
 174        self.init
 175      end
 176  
 177      def display_in_an_applet
 178        JRUBY_APPLET.setSize(@width, @height)
 179        JRUBY_APPLET.background_color = nil
 180        JRUBY_APPLET.double_buffered = false
 181        JRUBY_APPLET.add self
 182        JRUBY_APPLET.validate
 183        # Add the callbacks to peacefully expire.
 184        JRUBY_APPLET.on_stop { self.stop }
 185        JRUBY_APPLET.on_destroy { self.destroy }
 186        self.init
 187      end
 188  
 189      # Tests to see which display method should run.
 190      def display(options)
 191        if Object.const_defined?(:JRUBY_APPLET) # Then display it in an applet.
 192          display_in_an_applet
 193        elsif options[:full_screen] # Then display it fullscreen.
 194          graphics_env = java.awt.GraphicsEnvironment.get_local_graphics_environment.get_default_screen_device
 195          graphics_env.is_full_screen_supported ? display_full_screen(graphics_env) : display_in_a_window
 196        else # Then display it in a window.
 197          display_in_a_window
 198        end
 199      end
 200  
 201      # There's just so many functions in Processing,
 202      # Here's a convenient way to look for them.
 203      def find_method(method_name)
 204        reg = Regexp.new("#{method_name}", true)
 205        self.methods.sort.select {|meth| reg.match(meth)}
 206      end
 207  
 208      # Specify what rendering Processing should use.
 209      def render_mode(mode_const)
 210        size(@width, @height, mode_const)
 211      end
 212  
 213      def mouse_x; mouseX; end
 214      def mouse_y; mouseY; end
 215      def pmouse_x; pmouseX; end
 216      def pmouse_y; pmouseY; end
 217  
 218      def close
 219        @frame.dispose
 220      end
 221  
 222      def quit
 223        java.lang.System.exit(0)
 224      end
 225  
 226  
 227      private
 228  
 229      def self.initialize_slider_frame
 230        @slider_frame ||= JFrame.new
 231        class << @slider_frame
 232          attr_accessor :sliders, :panel
 233        end
 234        @slider_frame.sliders ||= []
 235        slider_panel ||= JPanel.new(java.awt.FlowLayout.new(1, 0, 0))
 236        @slider_frame.panel = slider_panel
 237      end
 238  
 239    end
 240  
 241    class Slider < javax.swing.JSlider
 242      attr_accessor :name, :label
 243  
 244      def initialize(*args)
 245        super(*args)
 246      end
 247  
 248      def update_label(value)
 249        value = value.to_s
 250        value << "0" if value.length < 4
 251        label.set_text "<html><br>#{@name.to_s}: #{value}</html>"
 252      end
 253    end
 254  
 255  end
 256