APL(Alexa Presentation Language)で1文字ずつテキストをアニメーションさせる

APL、特にコマンドについて書いた記事がほとんどないようなので、書きます。
アドベントカレンダーということで、それなりに高度なことをやろうと思います。

# やりたいこと
以下を実現したAlexaカスタムスキルを作成しようと思います。
　**・APLを使用し、画面付Echo端末で文字をアニメーション表示する**
　**・1文字ずつ微妙に時間をずらして表示させる**
　**・表示させる文字は動的に指定できる(今回はランダム)**

## APL標準コマンドはコンポーネント単位
APL1.1になってかなりいろいろなことができるようになりました。
APL標準コマンドを使うことで、コンポーネントを動かしたり、動的な変更が可能なプロパティの値を制御することができます。
ただ、APL標準コマンドの対象はコンポーネント1個が最小単位となるので、そこは注意が必要です。
例えば、textの値が「こんにちは」であるTextコンポーネントに対しては、この5文字を同時に扱うようなコマンドしか実行できません。

## 1文字ずつアニメーションさせるには
では、1文字ずつ動かすにはどうしたらよいかというと、単純に「1文字1コンポーネントにする」という方法で対応します。ここでポイントになってくるのは、「共通化」です。
内容がほとんど同じコンポーネントやコマンドが大量に出てきてしまうので、commandsやlayoutsをうまく使って対応するのがベターです。

# とりあえず固定文言をアニメーションで出してみる
例えば、以下のようなAPLテンプレートで文字を1文字ずつアニメーションで表示させることができます。
payload参照がないので、datasource側のjsonは空のものでOKです。

``` template.json
{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [],
    "resources": [],
    "styles": {},
    "onMount": [],
    "graphics": {},
    "commands": {
        "anime": {
            "description": "アニメーションで表示する。intervalに設定した時間だけ遅れて出現する。",
            "parameters": [
                {
                    "name": "interval",
                    "type": "number",
                    "default": 100
                }
            ],
            "commands": [
                {
                    "type": "SetValue",
                    "property": "opacity",
                    "value": 0
                },
                {
                    "type": "AnimateItem",
                    "easing": "ease-in-out",
                    "delay": "${interval}",
                    "duration": 1000,
                    "value": [
                        {
                            "property": "opacity",
                            "to": 1
                        },
                        {
                            "property": "transform",
                            "from": [
                                {
                                    "translateX": 30
                                },
                                {
                                    "translateY": 30
                                },
                                {
                                    "rotate": 30
                                }
                            ],
                            "to": [
                                {
                                    "translateX": 0
                                },
                                {
                                    "translateY": 0
                                },
                                {
                                    "rotate": 0
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    },
    "layouts": {
        "charLayouts": {
            "description": "animeコマンドを使い、intervalに設定した時間だけ待ってからtextの内容を出現させる。",
            "parameters": [
                {
                    "name": "interval",
                    "type": "number",
                    "default": "100"
                },
                {
                    "name": "text",
                    "type": "string",
                    "default": ""
                }
            ],
            "items": [
                {
                    "type": "Text",
                    "onMount": [
                        {
                            "type": "anime",
                            "interval": "${interval}"
                        }
                    ],
                    "text": "${text}"
                }
            ]
        }
    },
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "type": "Container",
                "width": "100vw",
                "height": "100vh",
                "alignItems": "center",
                "direction": "row",
                "justifyContent": "center",
                "items": [
                    {
                        "type": "charLayouts",
                        "interval": 100,
                        "text": "A"
                    },
                    {
                        "type": "charLayouts",
                        "interval": 200,
                        "text": "l"
                    },
                    {
                        "type": "charLayouts",
                        "interval": 300,
                        "text": "e"
                    },
                    {
                        "type": "charLayouts",
                        "interval": 400,
                        "text": "x"
                    },
                    {
                        "type": "charLayouts",
                        "interval": 500,
                        "text": "a"
                    }
                ]
            }
        ]
    }
}
```

簡単に説明すると、上記APLは以下のような構造になっています。
　・自作レイアウトとして、layoutsに"charLayouts"を、自作コマンドとしてcommandsに"anime"( = 軽く回転しながらもやっと出現する)を作成して定義している。
　・"charLayouts"はパラメータ"text"と"interval"を持つ。
　・"charLayouts"のパラメータ"text"は表示文言になる。
　・"charLayouts"内では"anime"が使用されている。
　・"anime"はパラメータ"interval"を持つ。
　・"charLayouts"のパラメータ"interval"は"charLayouts"内のコマンド"anime"にそのまま渡す。
　・"anime"にはアニメーションの内容が定義されている。
　・"anime"のパラメータ"interval"はアニメーションを実行するタイミングになる。
　・つまり、"charLayouts"を量産し、"interval"の値をちょっとずつずらすことで1文字ずつアニメーションさせることができる。

共通で使用する部分を"charLayouts"として定義し、コマンド部分も"anime"と定義することで、何度も使うレイアウトやコマンドを一回定義するだけで済んでいます。
イメージ的には、何度も出てくる処理を関数化した、みたいな感じです。

上記テンプレートをAlexaスキル側でAPL表示の際に設定すると、"Alexa"という文字が時間差で1文字ずつ同じアニメーションで出現する様子が画面に表示されます。
(Lambda側のソースは割愛)
![anime1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354808/361224d9-5cb9-06b3-8ebd-14d4be21dad6.gif)

今回はもやっと文字を表示させていますが、各文字を画面外のランダムな位置からスライドして登場させたり、回転させながら出現させたり等、工夫次第でいろいろな演出ができます。

# 文字列部分のコンポーネントを動的に作成
上記の例ではすでに完成済みのテンプレートを読み込ませることで固定の文字列を出力していますが、工夫すれば文字列を動的に変更することができます。
具体的には、lambda側で以下を実施します。
　・対象の文字列を1文字ずつに分断する。
　・分断した文字数分だけcharLayoutsコンポーネントを作成し、配列化。
　・作成したテンプレのitems部分に対し、作成したcharLayoutsコンポーネント配列をにセット。

結構力技なうえにテンプレを直接書き換える、かなり雑な方法ですが参考例として挙げさせていただきます。

``` aplTemplate.json
{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [],
    "resources": [],
    "styles": {},
    "onMount": [],
    "graphics": {},
    "commands": {
        "anime": {
            "description": "アニメーションで表示する。intervalに設定した時間だけ遅れて出現する。",
            "parameters": [
                {
                    "name": "interval",
                    "type": "number",
                    "default": 100
                }
            ],
            "commands": [
                {
                    "type": "SetValue",
                    "property": "opacity",
                    "value": 0
                },
                {
                    "type": "AnimateItem",
                    "easing": "ease-in-out",
                    "delay": "${interval}",
                    "duration": 1000,
                    "value": [
                        {
                            "property": "opacity",
                            "to": 1
                        },
                        {
                            "property": "transform",
                            "from": [
                                {
                                    "translateX": 30
                                },
                                {
                                    "translateY": 30
                                },
                                {
                                    "rotate": 30
                                }
                            ],
                            "to": [
                                {
                                    "translateX": 0
                                },
                                {
                                    "translateY": 0
                                },
                                {
                                    "rotate": 0
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    },
    "layouts": {
        "charLayouts":{
            "description": "animeコマンドを使い、intervalに設定した時間だけ待ってからtextの内容を出現させる。",
            "parameters": [
                {
                    "name": "interval",
                    "type": "number",
                    "default": "100"
                },
                {
                    "name": "text",
                    "type": "string",
                    "default": ""
                }
            ],
            "items": [
                {
                    "type": "Text",
                    "onMount": [
                        {
                            "type": "anime",
                            "interval": "${interval}"
                        }
                    ],
                    "text": "${text}"
                }
            ]
        }
    },
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "type": "Container",
                "width": "100vw",
                "height": "100vh",
                "alignItems": "center",
                "direction": "row",
                "justifyContent": "center",
                "items": []
            }
        ]
    }
}
```

``` index.js
// 主要部分のみ記載
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) {
        let st = '';
        let rt = '';
        
        st += 'こんにちは！';
        rt += 'エーピーエルのテストです。';
        st += rt;
        
        // ※実際はスキル実行デバイスがAPL対応しているかどうかのチェックを入れる必要あり

        const aplTemplate = require('./apl/aplTemplate.json');
        var aplData = {};   // payloadを使用していないので、空でも良い
        
        // 連続で呼んだ際、二重に文字が挿入されてしまうので、初期化
        aplTemplate.mainTemplate.items[0].items = [];

        const targetStringArray = [
            'APLはAlexa Presentation Languageの略だぞ!!!',
            'カッコいいアニメーション',
            '令和もよろしくね！'
        ];

        const targetString = targetStringArray[Math.floor(Math.random() * targetStringArray.length)];
        
        for(let i = 0; i < targetString.length; i++){
            // 1文字ずつcharLayoutsのインスタンスを生成
            let charInstance = {
                type:"charLayouts",
                interval:i * 100,                // インデックス番号を利用することで表示間隔を微妙にずらしている
                text:targetString.substring(i,i+1)
            };
            
            // テンプレートに直接突っ込む
            aplTemplate.mainTemplate.items[0].items.push(charInstance);
        }

        // 画面作成
        handlerInput.responseBuilder.addDirective({
            type: 'Alexa.Presentation.APL.RenderDocument',
            version: '1.0',
            document: aplTemplate,
            datasources: aplData
        });
        
        return handlerInput.responseBuilder
            .speak(st)
            .reprompt(rt)
            .getResponse();
    }
};
```

実際はContainerあたりをさらに別のLayoutsにしてしまってそちらに突っ込む(=テンプレ部はいじらないようにする)とか、Sequenceとpayloadをうまく使ってdatasource部にデータを持たせるとかすべきと思います。
ともあれ、こんな感じで1文字ずつdelayを微妙にずらすことができました。
実際にアニメーションを見てみると、1文字ずつ表示されてかなりリッチな感じになっています。

![anime2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354808/c1b893c0-c874-5cfc-b252-f62c29dec5bf.gif)

今回は文字列を出現させるアニメーションでやってみましたが、ほかにもいろいろ応用できると思います。
例えば、文字列中の特定の文字だけ消えるとか、何個かの画像を画面外から降らせるとか、意外と何でもできてしまいます。

APLについてはとにかく記事等が少ない印象がありますが、基本的には公式のリファレンスを見て書いていけば良いので、しっかり目を通し、わからないところはフォーラムや問い合わせフォームで聞くと良いと思います。

なお、APLに凝りすぎるとレスポンスの最大サイズ(24KB)を超えてしまう可能性が出てくるので、注意が必要です。最大サイズを超えた場合はカードが送信されてくるみたいです。
そのため、ある程度慣れてくると共通化はかなり重要になってきます。

# 宣伝
APLはコマンドもそうですが、コンポーネントの使い方を工夫すれば意外といろいろできます。
私が最近作ったAlexaスキル「方程式ロボ」では、APLの表示可否やAPLコマンドを駆使して画面描写を実施しています。
(ロボ、x^2、ｘ、√,i の部分は画像ですが、それ以外はすべてAPLのコンポーネントやコマンドで実現しています)
もしよろしければ、ためしに呼び出してみてください。スキル内の画面表示を見て、どうやってAPLで表現しているのか考えてみるのも良い勉強になるかと思います。

![方程式ロボキャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354808/88036fe4-9743-834f-c153-abb5a15d37ba.png)


# 参考URL
[APL標準コマンド](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-standard-commands.html)
[APLのユーザー定義コマンド](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-user-commands.html)
[APLレイアウト](https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/apl-layout.html)