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

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

![Peek 2020-02-03 14-12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/1c451b27-4806-e77c-fa33-c84625469ac9.gif)

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

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

# 動機

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

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

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

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Phoneix LiveViewでcanvasをちゃんとリアルタイム更新するデモ😆 <a href="https://twitter.com/hashtag/kokuraex?src=hash&amp;ref_src=twsrc%5Etfw">#kokuraex</a> <a href="https://twitter.com/hashtag/fukuokaex?src=hash&amp;ref_src=twsrc%5Etfw">#fukuokaex</a><br><br>以前、kokura.exで見せた、グラフがガシガシ更新するデモは、canvasのidをLiveViewから変更してムリヤリ再描画させていたが、コチラはhookで更新😜<br><br>録画していなければ、60FPS以上で安定し、モタツキは無い😝 <a href="https://t.co/qiADhTJAqZ">pic.twitter.com/qiADhTJAqZ</a></p>&mdash; piacere @fukuoka.ex(love Elixir&amp;Gravity extremely) (@piacere_ex) <a href="https://twitter.com/piacere_ex/status/1222909878840123392?ref_src=twsrc%5Etfw">January 30, 2020</a></blockquote>

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

# 実現方法

## 状態更新による時間遷移

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

~~~ elixir
  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](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dataset)を使って、サーバーサイドからクライアントサイドへ渡します。
※データ渡しにdatasetを使う方法は https://github.com/pcorey/live_canvas で学びました。力技でびっくりしました。
　まだ、これがベストプラクティスなのかどうかはちょっと疑ってます。

データをサーバーから

~~~ elixir
      # 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で描画します。

~~~ javascript
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: