Phoenix LiveViewを使って物理シミュレーションの可視化をしてみたので紹介します。
ソース: https://github.com/pojiro/ov_simulator
渋滞流のシミュレーションです。
渋滞はある一定以下の車間距離(言い換えると一定以上の密度)になると、
(相転移として)自然に発生することが研究により解明されています。
GIFでは渋滞のクラスタが後方に伝搬していく様子がみれます。
動機
Elixirはメッセージパッシングで(Elixir文脈の)プロセスの状態更新を行います。
Elixirを学び始めた頃、なんとなく、
プロセスの状態更新で物理シミュレーションの時間遷移を表現できそうだなとふわふわと思っていました。
このときはどのように実現するかの手段に対する知識がなくてアイデアで終わりました。
それからElixir、Phoenix、Phoenix LiveViewへと少しづつ学びを進めていた先日、
fukuokaexのもくもく会で @piacerex さんが以下を紹介してくださったときに、手段が揃ったと思いました。
Phoenix LiveViewで物理シミュレーションの可視化ができます!
Phoneix LiveViewでcanvasをちゃんとリアルタイム更新するデモ😆 #kokuraex #fukuokaex
— piacere @fukuoka.ex(love Elixir&Gravity extremely) (@piacere_ex) January 30, 2020
以前、kokura.exで見せた、グラフがガシガシ更新するデモは、canvasのidをLiveViewから変更してムリヤリ再描画させていたが、コチラはhookで更新😜
録画していなければ、60FPS以上で安定し、モタツキは無い😝 pic.twitter.com/qiADhTJAqZ
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で行うというように明確に分離されるので書きやすかったです。
この記事を読んで、これなら作れると思っていただけたら幸いです。
**「いいね」**よろしくお願いします。