この記事は「Elixir Advent Calendar 2019」の22日目です
昨日は、@Shintaro_Hosoai さんの「LiveViewでElixirを評価する(1)」でした
fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
Phoenix LiveViewは、JavaScriptを一切使わずに、リアルタイムフロントを開発することができる革新的なElixirの技術ですが、国内では、なかなか情報が手に入りにくいため、入門段階から先に進めないケースも多いのでは無いかと思います
そこで、LiveViewで、リアルタイムフロントの花とも言える、グラフィックをグリグリ動かすサンプルを作ってみました
できるだけシンプルな例にすべく、必要最低限なコードのみに絞って、ポイントを押さえていきます
【2019/12/23追記】
本コラムを早速お試しいただいた @pojiro さんが、動画を撮ってくれました(下記画像をクリックすると見れます)
内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします
本コラムの検証環境、事前構築のコマンド
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.9.4 ※最新版のインストール手順はコチラ
- Phoenix 1.4.9 ※最新版のインストール手順はコチラ
- LiveView 0.3.1 ※最新版のインストール手順はコチラ
LiveViewからのグラフィック操作の基本
LiveViewは、Elixirサーバサイドで生成したデータをフロント側へリアルタイム反映します
これを利用して、ブラウザでグラフィックをグリグリ動かすには、以下のような方法があります
- divタグのstyleアトリビュート値をElixirから連携
- canvasタグで指定したJavaScript内の値をElixirから連携(実はLiveViewでもJSは併用できます)
- divタグ内に予め配置したグラフィックの表示/非表示をElixirで切り替え(パタパタアニメの要領)
今回は、1番目の「divタグのstyleアトリビュート値をElixirから連携」を用いて、LiveViewによるグラフィック操作の実装例を紹介します
箱をElixirで指定した数だけ描画
まず、divタグを使って、四角いオブジェクトを描画してみます
「LiveViewでSPA開発①」で作成した環境下で、下記の青く四角い箱を10個、生成するコードを追加します
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
このコード用のルーティングも追加します
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」
にアクセスすると、こんな画面が表示されます
ここから、箱にアニメーションを加えていきます
箱をElixirで計算した角度で自転させる
transform: rotate()を使って、角度を変化させ続けることにより、箱を回転させてみます
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コールでアニメーションループを作る
箱をElixirで上下左右に揺らすことで公転させる
transform: translateX()/translateY()を使って、上下左右の移動を行い、そこに三角関数を用いることで、公転軌道が作れます
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
フレームレート制御も加える
現在のアニメーション処理は、「待ち時間無し」にしていますが、実際、ゲームやPC等の性能によらないアニメーションを作る場合は、フレームレート(60FPSみたいな)によるアニメーション制御も行うことが多いため、フレームレート制御も加えてみます
Process.send_afterの第3引数に、秒間何フレームにするかの計算を入れるだけで、ここでは60FPSに設定しています
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.「いいね」よろしくお願いします
ページ左上のや のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!
明日は、@sanpo_shiho さんの「Elixirで作るニューラルネットワークを用いた手書き数字認識」です