LoginSignup
16
6

More than 3 years have passed since last update.

作って学ぶPhoenix LiveView、物理シミュレーションの可視化

Last updated at Posted at 2020-02-03

Phoenix LiveViewを使って物理シミュレーションの可視化をしてみたので紹介します。
ソース: https://github.com/pojiro/ov_simulator

Peek 2020-02-03 14-12.gif

渋滞流のシミュレーションです。
渋滞はある一定以下の車間距離(言い換えると一定以上の密度)になると、
(相転移として)自然に発生することが研究により解明されています。

GIFでは渋滞のクラスタが後方に伝搬していく様子がみれます。

動機

Elixirはメッセージパッシングで(Elixir文脈の)プロセスの状態更新を行います。

Elixirを学び始めた頃、なんとなく、
プロセスの状態更新で物理シミュレーションの時間遷移を表現できそうだなとふわふわと思っていました。
このときはどのように実現するかの手段に対する知識がなくてアイデアで終わりました。

それからElixir、Phoenix、Phoenix LiveViewへと少しづつ学びを進めていた先日、
fukuokaexのもくもく会@piacerex さんが以下を紹介してくださったときに、手段が揃ったと思いました。
Phoenix LiveViewで物理シミュレーションの可視化ができます!

twitterで紹介されている動画のソースコードは https://github.com/pcorey/live_canvas

実現方法

状態更新による時間遷移

自身のプロセスへ状態更新のためのメッセージを投げて積分を回します。
これはサーバーサイトで行います。

  def handle_info(:update, %{assigns: %{particles: particles, step_size: step_size}} = socket) do
    particles = particles |> rk4(step_size)
    Process.send_after(self(), :update, 100)

    {:noreply, assign(socket, particles: particles)}
  end

粒子の状態(位置と速度)を持つparticlesをrk4(4次のルンゲクッタ)で積分し、粒子の状態を更新(時間遷移)させます。

描画

JavaScriptのcanvasを使ってクライアントサイドで行います。
particlesの状態はdatasetを使って、サーバーサイドからクライアントサイドへ渡します。
※データ渡しにdatasetを使う方法は https://github.com/pcorey/live_canvas で学びました。力技でびっくりしました。
 まだ、これがベストプラクティスなのかどうかはちょっと疑ってます。

データをサーバーから

      # particlesが更新されるとphx-hookが動作します
      # particlesはJSONエンコードされ文字列として、JavaScriptへ
      <div class="row" phx-hook="canvases"
        data-particles=<%= Jason.encode!(@particles)%> 
        data-space-size=<%= @space_size%>>
        <div class="column">
          <h2>circuit</h2>
          <canvas width="350px" height="350px" id="canvas1"></canvas>
        </div>
        <div class="column">
          <h2>limit cycle</h2>
          <canvas width="350px" height="350px" id="canvas2"></canvas>
        </div>
      </div>

クライアントへ、そしてcanvasで描画します。

hooks.canvases = {
  mounted() {
    let canvas1 = document.getElementById("canvas1")
    let canvas2 = document.getElementById("canvas2")
    let ctx1 = canvas1.getContext("2d")
    let ctx2 = canvas2.getContext("2d")

    Object.assign(this, {canvas1, ctx1, canvas2, ctx2})
  },
  updated(){
    let {canvas1, ctx1, canvas2, ctx2} = this
    let particles = JSON.parse(this.el.dataset.particles)
    let spaceSize = Number(this.el.dataset.spaceSize)

    let circuitRadius = 150
    let particleRadius = 10

    ctx1.clearRect(0, 0, canvas1.width, canvas1.height)
    ctx2.clearRect(0, 0, canvas2.width, canvas2.height)
    particles.forEach(particle => {
      let color_value = Math.round(particle.velocity / 2.0 * 200)
      // Draw Circuit
      ctx1.fillStyle = `rgba(${color_value}, 0, ${255 - color_value}, 1)`
      ctx1.beginPath()
      ctx1.arc(
        particleRadius + circuitRadius + circuitRadius * Math.cos(2 * Math.PI/spaceSize * particle.position),
        particleRadius + circuitRadius + circuitRadius * Math.sin(2 * Math.PI/spaceSize * particle.position),
        particleRadius, 0, 2 * Math.PI);
      ctx1.fill();

      // Draw limit cycle
      ctx2.fillStyle = `rgba(${color_value}, 0, ${255 - color_value}, 1)`
      ctx2.beginPath()
      ctx2.arc(
        particle.headway / 4.0 * 320,
        320 - particle.velocity / 2.0 * 320,
        particleRadius, 0, 2 * Math.PI);
      ctx2.fill();
    })
  }
};

おわり

Elixirを始めたころのアイデアを一つ実装することができました。

LiveViewを使うことで、サーバー-クライアント間のデータ授受がシンプルに実現できました。
また、数値計算のロジックをElixir、描画のみをcanvasで行うというように明確に分離されるので書きやすかったです。

この記事を読んで、これなら作れると思っていただけたら幸いです。

「いいね」よろしくお願いします。:wink:

16
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6