Blockless SLyDIFi でオーバーレイ

この記事は [SATySFi Advent Calendar 2019](https://adventar.org/calendars/3929) 21 日目の記事です．前日は wasabiz さんの記事でした．明日は puripuri2100 さんの記事が入る予定です．

monaqa です．本日は，試験的に実装してみた Blockless SLyDIFi についてお話します．

# はじめに

Blockless SLyDIFi とは，Blockless SATySFi の考え方を応用して作られた SLyDIFi です．

## SLyDIFi とは

SATySFi でスライドを作成するためのクラスファイルです． slidify (slide + -ify) という単語の i と y をひっくり返したから SLyDIFi です．安直ですね．詳しくは，[SATySFi Advent Calendar 2019 の 18 日目の記事](https://qiita.com/monaqa/items/0c6e5dc1a1c3b475c905) をご覧ください．

## Blockless SATySFi とは

wasabiz さんが 3 ヶ月前の記事で提案された，新しい SATySFi のプログラミングスタイルです．なんと， **ブロックコマンドを一切使わない** にもかかわらず，本家 SATySFi とほぼ同じようなことができてしまいます． SATySFi 本体のコードをいじることなく，ブロックコマンドを取り除けてしまうというのは驚きですね．
どういう原理で動くのか，どういうメリットがあるのかなど，詳しくは本家の記事 [Blockless SATySFi](https://qiita.com/wasabiz/items/9271e5521d04f419dbd2) を御覧ください．

# どんなことができるようになったか

一言で言えば，SLyDIFi でオーバーレイ（PowerPoint でいうところのアニメーションに似た機能）ができるようになりました．そう，Blockless SLyDIFi ではたとえばこんなスライドを生成できます．

![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/7af3bd9a-dd10-0b1d-8907-14c871eb3eef.gif)

このスライドは，以下のようなコマンドをコンパイルすることで得られます． 5枚のフレームを別々に作るのではなく， `frame-anime` という1つの関数の中で表示を制御していることがわかります．

```
@import: slydifi-blockless

let-inline ctx \gray it =
  let ctx2 = ctx |> set-text-color (SlydifiColor.html-color 0xAAAAAA) in
  read-inline ctx2 it

in

Slydifi.document(|
  draft-mode = false;
  fonts = (|
    frame-title-cjk = `mplus-sans-b`;    % 自分が持っているフォントに変えること
    normal-text-cjk  = `mplus-sans-r`;   % 自分が持っているフォントに変えること
    frame-title-latin = `mplus-sans-b`;  % 自分が持っているフォントに変えること
    normal-text-latin  = `mplus-sans-r`; % 自分が持っているフォントに変えること
    mono-text-latin  = `mplus-mono-r`;   % 自分が持っているフォントに変えること
    font-ratio-cjk = 1.0;
  |);
|)[
   frame-anime(5){Overlay 機能のテスト}[
     p{こんなふうに，任意の段落の表示切り替えを好きに制御できる．};

     p-switch(Range(2, 4)){☆★★★☆：2–4のとき有効．}{\gray{☆★★★☆：2–4のとき有効．}};
     p-switch(Only(3)    ){☆☆★☆☆：3 枚目だけ有効．}{\gray{☆☆★☆☆：3 枚目だけ有効．}};
     p-switch(Before(2)  ){★★☆☆☆：2 枚目まで有効．}{\gray{★★☆☆☆：2 枚目まで有効．}};
     p-switch(After(3)   ){☆☆★★★：3 枚目以降有効．}{\gray{☆☆★★★：3 枚目以降有効．}};
     p-switch(GeneralRange(fun i -> (i mod 2) == 1))
       {★☆★☆★：奇数枚目のみ有効．}{\gray{★☆★☆★：奇数枚目のみ有効．}};

    p-ghost(Only(5)){終わり．};
   ];
]
```

# 仕組み

Blockless SLyDIFi では，以下のように考えてオーバーレイを実現します．

* 1つのスライドはいくつかのフレームを寄せ集めて作られている
* 1つのフレームはいくつかの **セル (cel)** を重ねることで作られている
* セルは自然数を入力引数として与えることで姿形が変わる
* 同じセルで構成されるフレームでも，中のセルに1から N までの自然数を渡すことによって，その外観を変化させることができる

セルとはアニメーションのセル画から借りてきた言葉です．私はアニメの作成方法に全く詳しくありませんが，セルアニメという種類のアニメではキャラクターごとに作成されたセル画を重ねることで多様なシーンを撮影するそうです（出典：[Wikipedia](https://ja.wikipedia.org/wiki/セルアニメ)）．セルを重ねるという行為は，オーバーレイという言葉にも合致しますね．

本記事ではもう少しだけ具体的に，型に着目してどのように実装されているのか追ってみます．

## block, cel

まず，以下のような型を新たに定義します．

```
type block = context -> block-boxes
type cel = int -> block
```

`block` は [Blockless SATySFi](https://qiita.com/wasabiz/items/9271e5521d04f419dbd2) で導入された型です． Blockless SATySFi では，ブロックコマンドの代わりに「返り値として `block` 型をとる関数」を用いることで，本家 SATySFi と同じような機能を実現します．それに対して `cel` は Blockless SLyDIFi 特有の型であり，これがまさに先程のコンセプトに登場したセルを表しています．

なお，SATySFi において block は一つのブロックボックスとほぼ同義でしたが，SLyDIFi において block を返り値に持つ関数は `frame` や `frame-anime` などフレームを生成するものしか存在しません．したがって，SLyDIFi での block は「1枚のフレーム または 何枚かのフレームの集まり」とほぼ同義です．一方で `cel` は `p` や `p-ghost` の戻り値のような「フレームの1要素」を表しており， 1つのフレームの本体は `cel` を集めることにより表現されます．

## document

SLyDIFi においてスライド全体を生成する役割を担っているのが document 関数です． document 関数は以下のような型を持ちます．少しややこしいですが，オプションを指定するためのレコードと block のリストを引数にとり， document 型を返す関数ということです．この構造は Blockless SATySFi でも変わりません．

```
val document: 'a -> block list -> document
  constraint 'a :: (|
    fonts: (|
      frame-title-cjk : string;
      normal-text-cjk : string;
      frame-title-latin : string;
      normal-text-latin : string;
      mono-text-latin : string;
      font-ratio-cjk : float;
    |);
    draft-mode : bool;
  |)
```

SLyDIFi における document は，上のコードの通り block list を本文として受け取ることで具現化されますが，これは「1つのスライドはいくつかのフレームを寄せ集めて作られる」ということを表しています．

## frame

SLyDIFi では，`+frame` や `+section` といったフレーム作成のためのコマンドが用意されていました． Blockless SLyDIFi では， `frame` に加えて `frame-anime` という関数が定義されています [^1]．

[^1]: ここでは述べませんが， section や make-title などの関数も同様に定義できます．

`frame` は以前の SLyDIFi と同様に1枚のフレームを作成するための関数です． `frame-anime` はアニメーションの付いた何枚かのフレーム群を作成するための関数であり，生成したいフレームの枚数 (`int`)，中身の cel たち (`cel list`) を引数に取り，最終的なフレーム群 (`block`) を返します．

```
val frame-anime : int -> inline-text -> cel list -> block
val frame : inline-text -> cel list -> block
```

`frame` は，「生成したいフレームの枚数」を1と定めた `frame-anime` と全く同じ効果を持ちます．

`frame-anime` の動きについて，もう少し具体的に説明します． `frame-anime` の本体は `cel` のリストであること， `cel` とは「自然数を取って `block` を返す関数」であるということは既に述べました． `frame-anime(N)[cels...]` は，`cels...` に渡すインデックスをインクリメントさせながら N 枚のフレームを生成します．つまり，

```
frame-anime(3)[cel-hoge; cel-fuga; cel-piyo];
```
というコードは

```
frame[cel-hoge 1; cel-fuga 1; cel-piyo 1]; % 1枚目のフレーム
frame[cel-hoge 2; cel-fuga 2; cel-piyo 2]; % 2枚目のフレーム
frame[cel-hoge 3; cel-fuga 3; cel-piyo 3]; % 3枚目のフレーム
```
みたいな感じの働きをします[^2]，各々のセルは渡された引数によりふるまいを変えるため，ただインデックスをインクリメントさせて繰り返すだけで，フレームごとに変化をつけることができます．

[^2]: 本当は `frame` コマンドも `block list` ではなく `cel list` を引数に取るため，このコードは正しくありません．実際に書くとエラーが発生します．

なお，ここで用いられるインデックスは `frame-anime` が持っているものであり，`frame-anime` が終わるたびにリセットされます．つまり， `frame-anime` を2つつなげたとき，2番目の `frame-anime` のインデックスはまた1から始まるということです．

## cel (paragraph)

```
val p-ghost : celRange -> inline-text -> cel
val p-switch : celRange -> inline-text -> inline-text -> cel
val p : inline-text -> cel
```

通常の SLyDIFi において，段落はブロックコマンド `+p` を使って生成されていました． `+p` は中身の `inline-text` を引数に取るコマンドであり，最終的に段落の形をしたブロックボックス列となります． Blockless SLyDIFi では， `p` に加えてオーバーレイ特有の機能を持った `p-ghost` と `p-switch` が提供されています．これらは以下のような機能を持った関数です．

* `p` : 表示させたい中身 (`inline-text`) を引数に取り，段落の形をしたセルを返す．すなわち，通常の SLyDIFi の `+p` と同じ．
* `p-ghost` : インデックスの範囲指定 (`celRange`) と表示させたい中身 (`inline-text`) を引数にとり，「フレームのインデックスが指定した範囲にあったら中身を表示し，そうでなければ表示しない」という動きをする．フレーム番号によって，一枚のセルを抜き差しするイメージ．
* `p-switch` : インデックスの範囲指定 (`celRange`) と表示させたい中身その1, 中身その2 (ともに `inline-text`) を引数にとり，「フレームのインデックスが指定した範囲にあったら中身その1を，そうでなければ中身その2を表示する」という動きをする．フレーム番号によって，2枚のセルをとっかえひっかえするイメージ．

なお，`celRange` とはインデックスの範囲指定を表す代数的データ型であり，以下のように定義されています．

```
type celRange =
  | Always of unit
  | Only of int
  | Before of int
  | After of int
  | Range of int * int
  | GeneralRange of int -> bool

let-rec is-in-range : celRange -> int -> bool | cel-range i =
  match cel-range with
  | Always() -> true
  | Only(n) -> (i == n)
  | Before(n) -> (i <= n)
  | After(n) -> (i >= n)
  | Range(m, n) -> (i >= m) && (i <= n)
  | GeneralRange(f) -> (f i)
```

なお，`p` は「常に中身を表示する」という範囲指定のついた `p-ghost` と等価です．したがって，フレームのインデックスにかかわらず常に表示されることとなります．

# 結局何が起きているのか

もう一度，最初にお見せしたコードを，本質部分に絞って見てみましょう．

```
Slydifi.document(| オプション |)[

   frame-anime(5){Overlay 機能のテスト}[
     p{こんなふうに，任意の段落の表示切り替えを好きに制御できる．};

     p-switch(Range(2, 4)){☆★★★☆：2–4のとき有効．}{\gray{☆★★★☆：2–4のとき有効．}};
     p-switch(Only(3)    ){☆☆★☆☆：3 枚目だけ有効．}{\gray{☆☆★☆☆：3 枚目だけ有効．}};
     p-switch(Before(2)  ){★★☆☆☆：2 枚目まで有効．}{\gray{★★☆☆☆：2 枚目まで有効．}};
     p-switch(After(3)   ){☆☆★★★：3 枚目以降有効．}{\gray{☆☆★★★：3 枚目以降有効．}};
     p-switch(GeneralRange(fun i -> (i mod 2) == 1))
       {★☆★☆★：奇数枚目のみ有効．}{\gray{★☆★☆★：奇数枚目のみ有効．}};

    p-ghost(Only(5)){終わり．};

   ];

]
```

document の本体には `frame-anime(5){...}[...]` からなるブロック1つしかありませんから，このコードでは5枚のフレームが作成されることになります． `frame-anime` の本体は，1つの `p`， 5つの `p-switch`，1つの `p-ghost`，計7つのセルで構成されています． `frame-anime` は，それぞれのセルに1から5のインデックスを渡すことでその役割を果たし，各々のセルは渡されたインデックスに応じて振る舞いを変えることでその役割を果たします．
たとえば

```
p-switch(Range(2, 4)){☆★★★☆：2–4のとき有効．}{\gray{☆★★★☆：2–4のとき有効．}};
```
の動きを考えてみます．
最初のスライドでは，`p-switch` には1が渡されます． 1 は `Range(2, 4)` の範囲外ですから，1枚目のスライドでは `{\gray{☆★★★☆：2–4のとき有効．}}` のほうが出力されます．
2枚目のスライドでは `p-switch` には1が渡されます．その後インデックスがインクリメントされて 2 が渡されたとき， 2 は `Range(2, 4)` の範囲内となるため，先ほどとは変わって `{☆★★★☆：2–4のとき有効．}` のほうが出力されます．

# 制約

現状のオーバーレイ機能付き Blockless SLyDIFi にはいくつかの制限があります．

* 現在の SLyDIFi との互換性がない．
* セル単位でしか表示の切り替えができない．インラインテキスト中の特定の文字だけ色を変える，といったことをするためには，色設定の異なる同じ内容のテキストを2つ用意する必要がある．

# おわりに

長くなりましたが，読んでくださりありがとうございました．コードは [こちらの gist](https://gist.github.com/monaqa/92675b167195fe2b4e2a3cfc13efac83) で公開していますので，実装が気になる方はこちらをのぞいてみてください．では，良いお年を！