LiveViewでグラフィックをグリグリ動かす

この記事は[「Elixir Advent Calendar 2019」](https://qiita.com/advent-calendar/2019/elixir)の22日目です

昨日は、@Shintaro_Hosoai さんの[「LiveViewでElixirを評価する(1)」](https://qiita.com/Shintaro_Hosoai/items/d1bc4c751168884321e6)でした

----

[fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com/)のpiacereです
ご覧いただいて、ありがとうございます :bow:

Phoenix LiveViewは、JavaScriptを一切使わずに、リアルタイムフロントを開発することができる革新的なElixirの技術ですが、国内では、なかなか情報が手に入りにくいため、入門段階から先に進めないケースも多いのでは無いかと思います

そこで、LiveViewで、リアルタイムフロントの花とも言える、グラフィックをグリグリ動かすサンプルを作ってみました

できるだけシンプルな例にすべく、必要最低限なコードのみに絞って、ポイントを押さえていきます

【2019/12/23追記】
本コラムを早速お試しいただいた @pojiro さんが、動画を撮ってくれました（下記画像をクリックすると見れます）
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/19a3fafb-c555-d2d5-0b6c-90e71ef30474.png)](https://video.twimg.com/tweet_video/EMbdUuiVUAA6eK4.mp4)

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

# 本コラムの検証環境、事前構築のコマンド

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.9.4　 　　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- Phoenix 1.4.9　　　 　※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- LiveView 0.3.1 　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)</b>

# LiveViewからのグラフィック操作の基本

LiveViewは、Elixirサーバサイドで生成したデータをフロント側へリアルタイム反映します

これを利用して、ブラウザでグラフィックをグリグリ動かすには、以下のような方法があります

- divタグのstyleアトリビュート値をElixirから連携
- canvasタグで指定したJavaScript内の値をElixirから連携（実はLiveViewでもJSは併用できます）
- divタグ内に予め配置したグラフィックの表示／非表示をElixirで切り替え（パタパタアニメの要領）

今回は、1番目の「divタグのstyleアトリビュート値をElixirから連携」を用いて、LiveViewによるグラフィック操作の実装例を紹介します

# 箱をElixirで指定した数だけ描画

まず、divタグを使って、四角いオブジェクトを描画してみます

[「LiveViewでSPA開発①」](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)で作成した環境下で、下記の青く四角い箱を10個、生成するコードを追加します

```elixir:lib/basic_web/live/boxes.ex
defmodule BasicWeb.Boxes do
  use Phoenix.LiveView
  use Phoenix.HTML

	def render(assigns) do
		~L"""
		<%= for box <- @boxes do %>
		<div style="left: <%= box.x %>%; top: <%= box.y %>%; position: absolute; background-color: blue; width: 100px; height: 100px;">
		</div>
		<% end %>
		"""
	end

	def animate(socket) do
		count = 10
		shift = 100 / (count + 1)

		boxes = 
			0..count
			|> Enum.map(&
				%{
					x:	shift * &1,
					y:	0,
				}
			)

		assign(socket, boxes: boxes)
	end

	def mount(_session, socket) do
		socket = animate(socket)
		{:ok, socket}
	end
end
```

このコード用のルーティングも追加します

```elixir:lib/basic_web/router.ex
defmodule BasicWeb.Router do
…
  scope "/", BasicWeb do
    pipe_through :browser
    …
    live "/realtime", QiitaSearchRealtime 
    live "/boxes",    Boxes
  end
```

Phoenixを起動します

```
iex -S mix phx.server
```

ブラウザで``「http://localhost:4000/boxes」``にアクセスすると、こんな画面が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f37d1cce-e5a9-fa16-e5a6-8cdb72ccaa44.png)

ここから、箱にアニメーションを加えていきます

# 箱をElixirで計算した角度で自転させる

transform: rotate()を使って、角度を変化させ続けることにより、箱を回転させてみます

```elixir:lib/basic_web/live/boxes.ex
defmodule BasicWeb.Boxes do
  use Phoenix.LiveView
  use Phoenix.HTML

	def render(assigns) do
		~L"""
		<%= for box <- @boxes do %>
		<div style="transform: rotate(<%= box.rotation %>deg); left: <%= box.x %>%; top: <%= box.y %>%; position: absolute; background-color: blue; width: 100px; height: 100px;">
		</div>
		<% end %>
		<br>
		<br>
		<br>
		<br>
		time: <%= @time %>
		"""
	end

	def animate(socket) do
		count = 10
		shift = 100 / (count + 1)
		time = socket.assigns.time

		boxes = 
			0..count
			|> Enum.map(&
				%{
					x:			shift * &1, 
					y:			0, 
					rotation:	rem(time, 360), 
				}
			)

		assign(socket, boxes: boxes)
	end

	def mount(_session, socket) do
		new_socket = 
			socket
			|> assign(time: 0)
			|> animate
		if connected?(new_socket), do: queue_animate(new_socket)
		{:ok, new_socket}
	end

	def queue_animate(socket), do: Process.send_after(self(), :animate, 0)

	def handle_info(:animate, socket) do
		queue_animate(socket)
		%{time: time} = socket.assigns
		new_socket = 
			socket
			|> assign(time: time + 1)
			|> animate
		{:noreply, new_socket}
	end
end
```

ポイントは、以下4つです

- カウントアップされるtimeと360度との剰余を出すことで、transform: rotate()の回転角度を算出
- handle_infoで、アニメーション処理のキューイングとtimeのカウントアップ、回転角度算出を行う
- queue_animateで、Process.send_afterを使ってhandle_infoをコールする
- handle_info内でのqueue_animate呼出と、queue_animate内でのhandle_infoコールでアニメーションループを作る

これで、全ての箱が自転するようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9c2142e5-6199-b097-de6e-a4a73531b2f3.png)
# 箱をElixirで上下左右に揺らすことで公転させる

transform: translateX()／translateY()を使って、上下左右の移動を行い、そこに三角関数を用いることで、公転軌道が作れます

```elixir:lib/basic_web/live/boxes.ex
defmodule BasicWeb.Boxes do
  use Phoenix.LiveView
  use Phoenix.HTML

	def render(assigns) do
		~L"""
		<%= for box <- @boxes do %>
		<div style="transform: translateX(<%= box.translate_x %>%) translateY(<%= box.translate_y %>%) rotate(<%= box.rotation %>deg); left: <%= box.x %>%; top: <%= box.y %>%; position: absolute; background-color: blue; width: 100px; height: 100px;">
		</div>
		<% end %>
		<br>
		<br>
		<br>
		<br>
		time: <%= @time %>
		"""
	end

	def animate(socket) do
		count = 10
		shift = 100 / (count + 1)
		time = socket.assigns.time

		boxes = 
			0..count
			|> Enum.map(&
				%{
					x:				shift * &1, 
					y:				0, 
					rotation:		rem(time, 360), 
					translate_x:	:math.sin(time / 10) * 25,
					translate_y:	:math.cos(time / 10) * 25,
				}
			)

		assign(socket, boxes: boxes)
	end

	def mount(_session, socket) do
		new_socket = 
			socket
			|> assign(time: 0)
			|> animate
		if connected?(new_socket), do: queue_animate(new_socket)
		{:ok, new_socket}
	end

	def queue_animate(socket), do: Process.send_after(self(), :animate, 0)

	def handle_info(:animate, socket) do
		queue_animate(socket)
		%{time: time} = socket.assigns
		new_socket = 
			socket
			|> assign(time: time + 1)
			|> animate
		{:noreply, new_socket}
	end
end
```

箱が、自転しつつ、公転も行うようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6c1cc68a-8f27-fc8a-6387-bda0919e4fa2.png)

# フレームレート制御も加える

現在のアニメーション処理は、「待ち時間無し」にしていますが、実際、ゲームやPC等の性能によらないアニメーションを作る場合は、フレームレート（60FPSみたいな）によるアニメーション制御も行うことが多いため、フレームレート制御も加えてみます

Process.send_afterの第3引数に、秒間何フレームにするかの計算を入れるだけで、ここでは60FPSに設定しています

```elixir:lib/basic_web/live/boxes.ex
defmodule BasicWeb.Boxes do
  use Phoenix.LiveView
  use Phoenix.HTML

	def render(assigns) do
		~L"""
		<%= for box <- @boxes do %>
		<div style="transform: translateX(<%= box.translate_x %>%) translateY(<%= box.translate_y %>%) rotate(<%= box.rotation %>deg); left: <%= box.x %>%; top: <%= box.y %>%; position: absolute; background-color: blue; width: 100px; height: 100px;">
		</div>
		<% end %>
		<br>
		<br>
		<br>
		<br>
		time: <%= @time %>
		"""
	end

	def animate(socket) do
		count = 10
		shift = 100 / (count + 1)
		time = socket.assigns.time

		boxes = 
			0..count
			|> Enum.map(&
				%{
					x:				shift * &1, 
					y:				0, 
					rotation:		rem(time, 360), 
					translate_x:	:math.sin(time / 10) * 25,
					translate_y:	:math.cos(time / 10) * 25,
				}
			)

		assign(socket, boxes: boxes)
	end

	def mount(_session, socket) do
		new_socket = 
			socket
			|> assign(time: 0)
			|> animate
		if connected?(new_socket), do: queue_animate(new_socket)
		{:ok, new_socket}
	end

	def queue_animate(socket), do: Process.send_after(self(), :animate, trunc(1000 / 60))

	def handle_info(:animate, socket) do
		queue_animate(socket)
		%{time: time} = socket.assigns
		new_socket = 
			socket
			|> assign(time: time + 1)
			|> animate
		{:noreply, new_socket}
	end
end
```

アニメーションが、ややゆっくりになったかと思いますが、秒間60フレームにコントロールされた状態になっています

# 終わり

LiveViewでグラフィックをグリグリ動かすサンプルを作ってみました

これまでは、JavaScriptを用いなければ作れなかった、こうした処理が、LiveViewで簡単に作れるテイストが伝われば幸いです

このサンプルを元に、divタグとanimate関数をいじるだけで、より高度なグラフィック描画も可能（見た目はCSSで凝る感じで）なので、色々と遊んでみてください

# p.s.「いいね」よろしくお願いします

ページ左上の![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png)や![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada:

----

明日は、@sanpo_shiho さんの「Elixirで作るニューラルネットワークを用いた手書き数字認識」です