Ruby で Processing がブラウザ上からできる p5.rb を作りました

ruby.wasm + p5.js の組み合わせです。ほとんどの API は移植したので大体同じことができると思います。

https://p5rb.ongaeshi.me/

使い方

p5.rb を HTML に読みこめばすぐに使えます。

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@next/dist/browser.script.iife.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
    <script type="text/ruby" src="p5.rb"></script>
    <script type="text/ruby">
def setup
  createCanvas(720, 400)
end

def draw
  background(127)
  noStroke()
  0.step(height, 20) do |i|
    fill(129, 206, 15)
    rect(0, i, width, 10)
    fill(255)
    rect(i, 0, 10, height)
  end
end        

P5::init()
    </script>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>

オンラインエディタ

https://p5rb.ongaeshi.me/editor からオンラインエディタが使えます。

作ったものはURLで共有できます。モバイルからも使えるのでまずはこちらを触ってみるのがおすすめです。

できたものは #p5rb タグを付けて共有していただけるとよろこびます。

実装メモ

ruby.wasm の JS::Object 経由で p5.js のメソッドやプロパティをブリッジしています。Ruby の method_missing を使うことで 100 行程度で p5.js の全ての関数やプロパティを呼び出しています。method_missiong すごい。

p5.rb#L217-L332

# JS::Object can call property via function style
class JS::Object
  def method_missing(sym, *args, &block)
    ret = self[sym]

    case ret.typeof
    when "undefined"
      str = sym.to_s
      if str[-1] == "="
        self[str.chop.to_sym] = args.first
        return args.first
      end

      super
    when "function"
      self.call(sym, *args, &block).to_r
    else
      ret.to_r
    end
  end

  def respond_to_missing?(sym, include_private)
    return true if super
    self[sym].typeof != "undefined"
  end

  def to_r
    case self.typeof
    when "number"
      self.to_f
    when "string"
      self.to_s
    else
      self
    end
  end
end

# Call p5.js global functions
$p5 = nil

def method_missing(sym, *args, &block)
  return super unless $p5.respond_to?(:[])
  ret = $p5[sym]

  case ret.typeof
  when "undefined"
    # str = sym.to_s
    # if str[-1] == "="
    #   $p5[str.chop.to_sym] = args.first
    #   return args.first
    # end
    super
  when "function"
    $p5.call(sym, *args, &block).to_r
  else
    ret.to_r
  end
end

# Add new p5() to window.constructors.p5()
JS.eval("window.constructors = { p5: (...args) => new p5(...args) };")

module P5
  Vector = JS.global[:p5][:Vector]

  module_function

  def init(query = "main", obj = self)
    unless query.is_a?(String)
      query, obj = "main", query
    end

    $p5&.remove()
    $p5 = nil

    sketch = ->(p5) {
      $p5 = p5
      init_method(obj, :preload)
      init_method(obj, :setup)
      init_method(obj, :draw)
      init_event_method(obj, :mouseMoved)
      init_event_method(obj, :mouseDragged)
      init_event_method(obj, :mousePressed)
      init_event_method(obj, :mouseReleased)
      init_event_method(obj, :mouseClicked)
      init_event_method(obj, :doubleClicked)
      init_event_method(obj, :mouseWheel)
      init_event_method(obj, :keyPressed)
      init_event_method(obj, :keyReleased)
      init_event_method(obj, :keyTyped)
    }
    
    container = JS.global.document.querySelector(query)
    container.innerHTML = ""
    JS.global.window.constructors.p5(sketch, container)
  end

  def init_method(obj, sym)
    if obj.respond_to?(sym, true)
      m = obj.method(sym)
      $p5[sym] = ->() { m.call() }
    end
  end

  def init_event_method(obj, sym)
    if obj.respond_to?(sym, true)
      m = obj.method(sym)
      if m.parameters.count >= 1
        $p5[sym] = ->(e) { m.call(e) }
      else
        $p5[sym] = ->(e) { m.call() }
      end
    end
  end
end

おわりに

作ったものがあったら #p5rb タグを付けて放流してもらえたら嬉しいです。

全ての戻り値に to_r するのはやりすぎかもしれないけど、プロパティを Ruby の括弧無しメソッド呼び出しで呼べるのは本家でできてもいいのかもと思っています(後で PR 出してみよう)。