MFM + Pages with AiScriptで三角形を描く

:::note warn
__Misskey ***v13***ではMFMにおいてKaTeXが使用できなくなりました。__
なお、以下記事で使用したKaTeXは新しいMFM関数`$[position ]`でおおよそ代替できるはずです。
:::

この記事は[Fediverse Advent Calendar 2021(第二会場)](https://adventar.org/calendars/6469)の8日目の記事です！！既に25日は終わってますが書く時間が取れたので書きます。

# 概要
Misskey(MFM + Misskey Web Pages + AiScript)で点・をつかって三角形を描いてみた。

[三角形(サンプル数30)](https://misskey.io/@qw/pages/1638653884525)

概要を説明すると**三角関数の和であらわされた目標とする図形をAiScriptを使って生成したMFMで表現しよう！** という感じです。

最初にMFMでのみやろうとしたときの物、次にPages+AiScriptでやった時の物を説明します。

# MFMとは?[^1]
> MFMとはMisskey Flavored Markdownの略で、Misskeyの様々な場所で使用できる専用のマークアップ言語です。

[MFM | Misskey Hub](https://misskey-hub.net/docs/features/mfm.html)から引用してきました。簡単に言うとテキストを装飾することができます。$\KaTeX$[^2]も使うことができ、これは今回の実装において大事です。装飾の方法についてはMisskey WebのMFMチートシートや、[📃MFMアートのちょっとした技など | Misskey](https://misskey.io/@hakohako_f2/pages/mfm-tech)などを参考にしてください。

ここでは今回使う構文について詳しく説明したいと思います。

[^1]: Misskeyについて知っている読者を想定しています。
[^2]: QiitaはKaTeXは使えないようですね。残念...

## 回転 rotate
```
$[rotate 回転させたいもの]
```
MFM関数と呼ばれるもの(たぶん)で、`$[(関数名).option 対象]`といった感じで書きます。入れ子にすることで多重に作用させることもできますが、改行が入ってはダメです。

rotateには

```
$[rotate.deg=90 🍮]
```

のように、角度を度数法で指定して反時計回りに回転させることができます。

## アニメーション(回転)

```
$[spin.left,speed=2s 🍮]
```

のようにかいて、対象を回転させるアニメーションをすることができます。`left`を`right`にすれば回転の向きが逆になり、`speed=`で回転の周期を指定できます[^3]。今回は使いませんが、`x`, `y`, `z`といったオプションで回転軸を指定することもできます(デフォルトは`z`)。

[^3]: $\mathbb{R}_{+}$ の必要あり。

## 大きく
```
$[x2 ]
```
で2倍にできます。3倍、4倍もできます。

## 中央寄せ
```
<center>MisskeyでFediverseの世界が広がります。</center>
```
で対象を中央寄せにできます。これはMFM関数ではないので一番外側で使います。

## KaTeX
MFMで数式ことKaTeXを使ったマークアップを使用することもできます。詳しくは[Supported Functions - KaTeX](https://katex.org/docs/supported.html)を参照してください。KaTeXの内側では他のMFMは使えないので一番内側で使うことになります。

今回、KaTeXはスペーシングのために使うので候補となるのは`\kern{distance}`のような水平方向のスペーシングと`\\[distance]`による垂直方向のスペーシング(正確には改行)になります。簡潔に説明するとdistanceの分、水平/垂直方向にスペースを設けることができます。

# MFMで三角形に動かしたい！
突然ですが、大体の図形は三角関数の和で表すことができます。
そんなことを考えながらdesmosをいじってたら[いい感じの三角形](https://www.desmos.com/calculator/yer8xe0uxz)を見つけることができました。

![desmos triangle](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/1af53c03-8cce-cdda-f329-d8f61ab35589.png)


$$
\left(x, y \right) = \left(5 \cos t + 1.4 \cos \left(-2 t \right), 5 \sin t + 1.4 \sin \left(-2 t\right) \right)
$$
といった感じで2つの回転の和であらわされているので、先ほど説明したMFMの`$[spin ]`を使えば三角形の軌跡を描けそうです！！[^4]
ここで課題となるのは、いかにして回転の半径を指定するかです。始めは`\kern{}`を使って指定していましたが、それだと[こんな感じ](https://misskey.io/notes/8tsxl08q1o)に

![spinのずれ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/f8f99927-4871-c6ad-105e-bdd47d05d822.gif)


回転の中心同士の距離ではなく、回転体同士の間の距離を指定することになるので、周りを回ってる物に合わせてマニュアルで(つまり計算結果とは違った)半径を指定する必要があります。

そこで今回使ったのはもう一つのスペーシング`\\[]`です。
これを使うことで回転体の中心に対する距離を指定することができました。
それでできたのが

![三角形の軌跡を描く点](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/90159bde-8f6d-1009-8b2e-be0e48c9235a.gif)


[これ](https://misskey.io/notes/8tsyay7mmd)です。

回転の影響が無視できるように回転対称な文字・を使いましたが、逆の回転をさせれば[絵文字を使ったバージョン](https://misskey.io/notes/8ttkatdqm1)もできます。

![三角形の軌跡を描く☃](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/fd83a8cc-86de-bd21-c71d-6cfc6f07bc14.gif)



[^4]: この時はrotateを知りませんでした。


# 三角形を描きたい
MFMで三角形の軌跡を描くことはできました。[フレームレートには上限が存在する](https://misskey.io/notes/8tl49qxtjo)ので複雑な図形ほど描くのは難しくなります[^5]。
そこで今度はrotateをたくさん使って点を描いてみる方法を考えてみます。
点をいっぱい描いて描写するわけなので、一つ一つ計算して手動でMFMを書いていては埒があかないので、プログラムを実行して出力するようにしてやります。
プログラミング言語はどんなものでもできそうですが、せっかくなのでMisskey上で実行したいです。なのでPagesを介してAiScriptを使ってMFMを出力しようと思います！[^6]

[^5]: 三角形を描きたい！はあくまで第1段階です。
[^6]: こんなことができちゃうのもMisskeyの好きなところの1つです。

# Pagesとは？
PagesとはMisskeyの機能の1つで、一般的な投稿(note)よりもフォーマルで長い文書を投稿するものです。
単純にMFMを使って長い文書を(セクション分けなどもできます)投稿することもできますが、組み込みできるブロックの1つに投稿フォームがあり、Pages上からnoteを直接投稿することもできます。
さらに、Pagesには変数や変数を使った場合分けを行うことができる上に、AiScriptの変数とのやり取り[^7]を介してAiScriptを使ってPagesを制御することさえも可能です[^8]。

[^7]: 非常にややこしいですが、AiScriptの変数とPagesの変数は別物なので、Pages上でAiScriptの変数と対応付けてやる必要があります。この仕様は今後修正されるとか...
[^8]: Pagesでのユーザーからの入力をAiScriptで受け取るということも可能らしいですが、やり方が分かりませんでした。有識者教えてください。

# AiScriptとは？
さっきからAiScriptとやらが登場してますが、AiScriptの説明を忘れてました。AiScript(あいすくりぷと)とはMisskeyで使用することができるスクリプト言語です[^9]。AiScriptの簡単な実行環境としては、[AiScript Playground](https://syuilo.github.io/aiscript/)があります。詳しい説明については[AiScriptのGitHubリポジトリ](https://github.com/syuilo/aiscript)や[2020年Misskeyの旅](https://qiita.com/syuilo/items/2cc2128a372597c5b609)や[AiScriptを使ってみた](https://qiita.com/YuzuRyo61/items/58d8c2f6a2c2bbfb7eda)を参照してください。

:::note warn
AiScriptは活発に更新されており、仕様が大きく変わることがよくあります。最終的には最新の情報を反映していると思われるGitHubのリポジトリを参照してください。
:::

先日変数代入の演算子が`<-`から`=`に、比較の演算子は`=`から`==`になりました。
この仕様変化によって躓いた点については後で述べます。

[^9]: [https://misskey-hub.net/docs/advanced/aiscript.html](https://misskey-hub.net/docs/advanced/aiscript.html)

# できたもの
[これ](https://misskey.io/notes/8ty4gubr1j)です。

![triangle](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/15ba763a-83b9-7179-d79a-d147f027aa26.png)


[Pagesのリンク](https://misskey.io/@qw/pages/1638653884525)


そしてこれがAiScriptのソースです。

```aiscript
@concatStr(str1, str2){
  `{str1}{str2}`
}

$content <- ""
$step <- 1
$steps <- 30

// 角度計算
@deg(stepn) {
  $out <- ((stepn / steps) * 360)
  out
}

@onestep(stepm, content) {
  // 1段階目
  content <- concatStr(content, "$[rotate.deg=")
  content <- concatStr(content, deg(stepm))
  content <- concatStr(content, " ")
  content <- concatStr(content, "\(\\[5em]\)")
  // 2段階目
  content <- concatStr(content, "$[rotate.deg=")
  content <- concatStr(content, ((3 * 360) - (deg(stepm) * 3)))
  content <- concatStr(content, " ")
  content <- concatStr(content, "\(\\[1.4em]\)・")
  // end
  content <- concatStr(content, "]]")
  content
}
// content <- onestep(1, content)
~ #i, steps {
  content <- onestep(i, content)
  content <- concatStr(content, "\(\\[-7.6em]\)")
}

content <- concatStr(content, "\(\\[7em]\)")
content <- concatStr("<center>", content)
content <- concatStr(content, "</center>")
```
[gist](https://gist.github.com/qwjyh/00e8a421056037597833648bfc12b9b2)

これが出力されるMFM

```
<center>$[rotate.deg=12 \(\\[5em]\)$[rotate.deg=1044 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=24 \(\\[5em]\)$[rotate.deg=1008 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=36 \(\\[5em]\)$[rotate.deg=972 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=48 \(\\[5em]\)$[rotate.deg=936 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=60 \(\\[5em]\)$[rotate.deg=900 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=72 \(\\[5em]\)$[rotate.deg=864 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=84 \(\\[5em]\)$[rotate.deg=828 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=96 \(\\[5em]\)$[rotate.deg=792 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=108 \(\\[5em]\)$[rotate.deg=756 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=120 \(\\[5em]\)$[rotate.deg=720 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=132 \(\\[5em]\)$[rotate.deg=684 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=144 \(\\[5em]\)$[rotate.deg=648 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=156 \(\\[5em]\)$[rotate.deg=612 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=168 \(\\[5em]\)$[rotate.deg=576 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=180 \(\\[5em]\)$[rotate.deg=540 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=192 \(\\[5em]\)$[rotate.deg=504 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=204 \(\\[5em]\)$[rotate.deg=468 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=216 \(\\[5em]\)$[rotate.deg=432 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=228 \(\\[5em]\)$[rotate.deg=396 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=240 \(\\[5em]\)$[rotate.deg=360 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=251.99999999999997 \(\\[5em]\)$[rotate.deg=324.0000000000001 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=264 \(\\[5em]\)$[rotate.deg=288 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=276 \(\\[5em]\)$[rotate.deg=252 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=288 \(\\[5em]\)$[rotate.deg=216 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=300 \(\\[5em]\)$[rotate.deg=180 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=312 \(\\[5em]\)$[rotate.deg=144 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=324 \(\\[5em]\)$[rotate.deg=108 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=336 \(\\[5em]\)$[rotate.deg=72 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=348 \(\\[5em]\)$[rotate.deg=36 \(\\[1.4em]\)・]]\(\\[-7.6em]\)$[rotate.deg=360 \(\\[5em]\)$[rotate.deg=0 \(\\[1.4em]\)・]]\(\\[-7.6em]\)\(\\[7em]\)</center>

https://misskey.io/@qw/pages/1638653884525
```
## 簡単な解説
`@concatStr(str1, str2)`は2つの文字列、数字を結合させるやつです。

forループ内の`\(\\[-7.6em]\)`は1段ごとのずれを吸収するためのスペーシングです。

MFMがやってるのはこんな感じのことです。

![mechanism](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/54d4584c-5d44-cfcb-2d20-4c4973e161f6.png)


# AiScript PlaygroundとMisskey WebのPagesでのAiScriptの挙動の違い

今回AiScriptでの実装作業は[AiScript Playground](https://syuilo.github.io/aiscript/)で行ったのですが、ここでのAiScriptのバージョンとMisskey PagesのAiScriptのバージョンが違っていたようで[^10]、謎のエラーがでて困ったのでここに仕様の違いをまとめておきたいと思います[^11]。

Playgroundで書いたコード

```
@concatStr(str1, str2){
  `{str1}{str2}`
}

$content <- ""
$step <- 1
$steps <- 30

// 角度計算
@deg(step) {
  (step / steps) * 360
}

@onestep(step, content) {
  // 1段階目
  content <- concatStr(content, "$[rotate.deg=")
  content <- concatStr(content, deg(step))
  content <- concatStr(content, " ")
  content <- concatStr(content, "\(\\[5em]\)")
  // 2段階目
  content <- concatStr(content, "$[rotate.deg=")
  content <- concatStr(content, 3 * 360 - deg(step) * 3)
  content <- concatStr(content, " ")
  content <- concatStr(content, "\(\\[1.4em]\)・")
  // end
  content <- concatStr(content, "]]")
  content
}

for (#i, steps) {
  content <- onestep(i, content)
  content <- concatStr(content, "\(\\[-7.6em]\)")
}

<: onestep(1, content)
```

⚠このコードは代入演算子・比較演算子に関するアップデートの結果動かなくなりました。

[^10]: Misskey PagesでのAiScriptのバージョンを知っている方がいたら教えてもらいたいです。また、先述の通りPagesに大型のアップデートが行われる可能性もあるので、この記事の情報が陳腐化するのは想像に難くないです。
[^11]: 特に、Pagesではエラーメッセージが表示されないので(Nullとしか現れない)、エラーの推測が大変でした。

## for
Get startedの

```
for(#i, 100)
```
は使えないです。Pagesでは

```
~ #i, 100
```
と書く必要があります。


## 関数の戻り値
```
@double(var) {
  var * 2
}
```
はエラーが起きます。

```
@double(var) {
  (var * 2)
}
```
のようにすると期待通りの挙動をします。

## 変数代入・比較
最近追加された仕様変更です。
Playgroundでは代入が`=`、比較が`==`ですが、Pagesでは`<-`, `=`です。


# さらなる一般化と課題
さて、私たちは三角形をMFMで描写することはできました(字数の制限上30個の点しかプロットすることはできませんでしたが)。
それ以外の図形は書くことはできないのでしょうか？

## 簡単な例
試しにDesmosのパラメータをいじってみてAdobeのAcrobat DCの[アイコンみたいなのを描いてみました](https://misskey.io/notes/8ty51rxij2)。

![pdf icon](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/069a47ce-c632-ca2c-c9e1-1e0220e724d6.png)


ちょっと変ですが[^12]、おおよそそれらしい形は出力できました。
しかし、これを描写するときに1つ問題が起きました。それは#簡単な解説で述べた1行の差異のパラメータが違った点です。このパラメータ自体あてずっぽうで推定した物なので、より考える必要がありそうです。
いくつかの問題が残っていることが判明してしまいましたが、さらなる青写真を描いてみたいと思います。

[^12]: 一番下の点の事ですが、原因は解明してません。

## フーリエ変換を使って一般化
フーリエ変換を使うと実変数関数を三角関数の和で書き表すことができます。
三角関数で書かれた図形はMFMのrotateを使って表現することができます。
では、三角関数に限らない一般的な関数もMFMで描画することができるのではないでしょうか。

詳しい説明は[3B1Bの動画](https://youtu.be/r6sGWTCMz2k)に譲る[^13]として表したい関数$f(t)$があった時に、
$$
c_n = \int_0^1 e^{-2 in\pi t} f(t) dt
$$
であらわされる係数を用いた和
$$
\sum_{n=-\infty}^{\infty} c_n e^{n 2 \pi i t}
$$
で表すことができます。$e^{i \theta} = \cos \theta + i \sin \theta$が成り立ち、MFMには$\sin, \cos$を計算する関数がある[^14]ので、積分を離散的に計算すれば、フーリエ係数を求め、最終的にAiScript+MFMで様々な図形を表現することができるかもしれないですね！

[^13]: 面倒だしそこまでの能力もありません
[^14]: https://github.com/syuilo/aiscript/blob/master/docs/std.md

## ロードマップ
現在できてないことをまとめておきます
- Pagesを介してAiScriptに値を与える方法
- 改行の差異を吸収するパラメータの計算方法を探す
- AiScriptでのフーリエ変換の実装

あとは最新のPages環境にも追従していきたいですね。

# おわり[^15]
ここまで長々と読んでいただきありがとうございました。

[^15]: これはQiita編集体験に関する愚痴ですが、AiScriptのコードブロックをうまく折り畳みに入れることができませんでした。VS Codeではうまくいくのに...

# 追記
今更気づきましたが、PagesのMFMなら文字数制限がないですね...
というわけで[作ってみました](https://misskey.io/@qw/pages/1640465852521)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356540/ea2717f3-8c97-3bfe-fbfe-8a38b923c656.png)
ちょっとずれてますね...