はじめに
ElixirConf EU 2023 での発表を無事終え、精神のキャパシティーが空いたので久しぶりに投稿します
今回は Livebook のデプロイ機能を使って、 Web スライドを作ります
役に立つ場面はないと思いますが、こんなこともできるよ、というネタです
実装したノートブックはこちら
実行環境
- Elixir 1.14.2
- Livebook 0.9.2
セットアップ
セットアップセルに以下のコードを入力し、実行します
Mix.install([
{:kino, "~> 0.9.2"}
])
今回使うのは Kino だけです
スライドショーモジュールの定義
以下のようにスライドショーモジュールを定義します
defmodule SlideShow do
use GenServer
@slides [
"""
# 大分県
## Oita, Japan

""",
"""
## 日本一のおんせん県おおいた
Oita, the best hot-spring resort in Japan
- 源泉総数 Total number of springs: **5,093**
- 湧出量 Hot spring output: **298,264** l / minutes
""",
"""
<div style="transform: scale(1.3)">
## 絶品グルメ
Excellent Gourmet
</div>
<div style="transform: scale(1.2)">
### とり天
Chicken tempura
### 中津唐揚げ
Nakatsu flied chiken
### 関あじ・関さば
Seki horse mackerel and Seki mackerel
</div>
""",
"""
## 宇宙
## Space
> 大分空港は、宇宙港(スペースポート)として活用することとなりました。
>
> 大分空港からは、航空機が人工衛星を搭載したロケットを翼に吊り下げて、空港から飛び立ち、空中でロケットを打ち上げます。
> Oita Airport is to be utilized as a spaceport.
>
> From Oita Airport, an aircraft will take off from the airport with a rocket carrying a satellite suspended from its wings and launch the rocket in mid-air.
""",
"""
# ぜひ来てください
# Come and visit us!
"""
]
@page_count Enum.count(@slides)
def init(default_page) do
{:ok, default_page}
end
def show_slide(page) do
@slides
|> Enum.at(page)
|> then(fn content ->
"""
<div class='flex flex-col justify-center items-center p-8' style='height: 600px'>
"""
<> content <>
"""
</div>
"""
end)
|> Kino.Markdown.new()
end
def handle_call(:current, _from, current_page) do
{:reply, show_slide(current_page), current_page}
end
def handle_call(:next, _from, current_page) when current_page >= @page_count - 1 do
{:reply, show_slide(0), 0}
end
def handle_call(:next, _from, current_page) do
{:reply, show_slide(current_page + 1), current_page + 1}
end
def handle_call(:prev, _from, current_page) when current_page == 0 do
{:reply, show_slide(@page_count - 1), @page_count - 1}
end
def handle_call(:prev, _from, current_page) do
{:reply, show_slide(current_page - 1), current_page - 1}
end
end
解説は後で行います
スライドショーの開始
GenServer.start_link
でスライドショーのプロセスを開始します
第1引数にスライドショーモジュール、第2引数に初期ページとして 0 を指定します
{:ok, slide} = GenServer.start_link(SlideShow, 0)
画面の定義
スライドを表示するための枠を作ります
frame = Kino.Frame.new()
この時点では空の枠で、 Nothin here...
と表示されます
初期表示として現在のスライドを表示し、ページ移動のためのボタンを作ります
Kino.Frame.render(frame, GenServer.call(slide, :current))
prev_button = Kino.Control.button("<")
next_button = Kino.Control.button(">")
[prev_button, next_button]
|> Kino.Layout.grid(columns: 8)
Kino.Layout.grid(columns: 8)
によってボタンを横並びで左端に寄せています
動きの定義
prev_button をクリックしたら前ページ、next_button をクリックしたら次ページを表示するようにします
[prev: prev_button, next: next_button]
|> Kino.Control.tagged_stream()
|> Kino.listen(fn
{:prev, _} ->
Kino.Frame.render(frame, GenServer.call(slide, :prev))
{:next, _} ->
Kino.Frame.render(frame, GenServer.call(slide, :next))
end)
これを実行した時点でボタンによるページ移動ができるようになります
デプロイ
左メニューからロケットアイコンをクリックし、デプロイメニューを表示します
Slug に slide (任意の文字列)を入れて、 Deploy
をクリックします
下に DEPLOYMENTS
が表示され、しばらくすると Status が Running になります
/app/slide
をクリックすると、以下のような画面が開き、 Web スライドとして動作します
解説
以降、 SlideShow
モジュールの解説をします
状態管理
GenServer
を使うことでスライドの現在ページを保持します
スライドの内容定義
SlideShow
モジュールの @slides
に文字列のリストでスライドの内容を定義しています
ここを書き換えることで任意のスライドショーが作成できます
内容は Markdown で記述しています
従って、見出しは # 見出し
や ## 小見出し
のように記述します
# 大分県
また、 
で画像、 -
で列挙、 >
で引用が表現できます

...
- 源泉総数 Total number of springs: **5,093**
- 湧出量 Hot spring output: **298,264** l / minutes
...
> 大分空港は、宇宙港(スペースポート)として活用することとなりました。
>
> 大分空港からは、航空機が人工衛星を搭載したロケットを翼に吊り下げて、空港から飛び立ち、空中でロケットを打ち上げます。
更に HTML を埋め込むことも可能なので、 <div style="transform: scale(1.3)">
と </div>
で囲むことで、対象範囲内を拡大表示する、といったこともできます
<div style="transform: scale(1.3)">
## 絶品グルメ
Excellent Gourmet
</div>
show_slide
関数で指定ページのスライドを Markdown として解釈し、画面に表示します
def show_slide(page) do
@slides
|> Enum.at(page)
|> then(fn content ->
"""
<div class='flex flex-col justify-center items-center p-8' style='height: 600px'>
"""
<> content <>
"""
</div>
"""
end)
|> Kino.Markdown.new()
end
Livebook では Tailwind CSS を使っているため、ある程度の(Livebook で使っている) Tailwind CSS のクラスが使えます
class='flex flex-col justify-center items-center p-8'
により、中央寄せと余白を作っています
また、 style='height: 600px'
により、スライドの高さを固定しています
(h-[600px]
のように固定値を指定する Tailwind CSS のクラスは効かないので style 属性で指定しています)
ページ移動
以下のコードで現在ページを表示しています
handle_call
の戻り値タプルの2番目が実際に結果として返る値、3番目が保持する状態です
:current
では状態が変化しないので、 current_page をそのまま返します
def handle_call(:current, _from, current_page) do
{:reply, show_slide(current_page), current_page}
end
以下のコードで次ページを表示します
when current_page >= @page_count - 1
によって、最終ページの場合は先頭に戻るようにしています
def handle_call(:next, _from, current_page) when current_page >= @page_count - 1 do
{:reply, show_slide(0), 0}
end
def handle_call(:next, _from, current_page) do
{:reply, show_slide(current_page + 1), current_page + 1}
end
以下のコードで前ページを表示します
when current_page == 0
によって先頭ページの場合は最終ページに戻るようにします
def handle_call(:prev, _from, current_page) when current_page == 0 do
{:reply, show_slide(@page_count - 1), @page_count - 1}
end
def handle_call(:prev, _from, current_page) do
{:reply, show_slide(current_page - 1), current_page - 1}
end