29
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ElixirAdvent Calendar 2019

Day 22

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

Last updated at Posted at 2019-12-22

この記事は「Elixir Advent Calendar 2019」の22日目です

昨日は、@Shintaro_Hosoai さんの「LiveViewでElixirを評価する(1)」でした


fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます :bow:

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

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

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

【2019/12/23追記】
本コラムを早速お試しいただいた @pojiro さんが、動画を撮ってくれました(下記画像をクリックすると見れます)
image.png

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

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

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

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

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

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

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

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

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

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

「LiveViewでSPA開発①」で作成した環境下で、下記の青く四角い箱を10個、生成するコードを追加します

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

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

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

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

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

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

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

箱をElixirで上下左右に揺らすことで公転させる

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

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

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

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

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

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.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:


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

29
14
0

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
29
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?