DenoでTerminalをアニメーションさせて遊ぶ

# はじめに

この本を参考に、ターミナルでAtomが動くバチクソかっこいいサンプルをDenoに移植しました。

http://mame.github.io/trance-book/

こんな感じで動きます。

![atom.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18902/95fa457d-1c40-da88-69a7-9ad73bc02ca0.gif)

ページをめくるたびに失神するほどかっこいいので、みんなで読んで失神しよう。

# 実行してみる

denoはURLを渡すだけで直接実行できます。
何はともあれ実行してみてください。

```sh
deno run --unstable https://gist.githubusercontent.com/zakuroishikuro/dc92d9d6a873e24f40ace683df177245/raw/6b48afbcd41d228ab52e6b4b8f802292a9abce0d/atom.ts
```
画面の大きさを取得する`Deno.consoleSize`を使うために`--unstable`フラグが必要。
画面をデカくすればするほどカッコいいよ。
Ctrl+cで終了してね。

元のソースはこれ。

https://github.com/mame/trance-book/blob/master/8-1/subcharacter-rendering-demo.rb

# どうやって動かしてるの？

画面全体に文字列を出力して、消して、また出力してるだけ。
消すってなんだよ！？

普通の文字とは違う特殊な文字(エスケープシーケンス)を出力すると、何も表示されず不思議なことが起きます。
ここで使ってるのは二つ。

- `"\x1b[2J"`: 画面全体を消去
- `"\x1b[1:1H"`: カーソルを左上に移動

他にもいろいろあります。一覧はこちら。

https://ttssh2.osdn.jp/manual/4/ja/about/ctrlseq.html

もっと詳しく知りたい人はこちら。歴史的経緯も分かってめちゃくちゃ面白い。

https://zariganitosh.hatenablog.jp/entry/20150209/ascii_code_secrets

https://zariganitosh.hatenablog.jp/entry/20150224/escape_sequence

で、JavaScriptで文字列を出力するといったら`console.log`なんだけど
これ使うと最後に常に改行されるので、改行せずに文字列を出力する関数を作っておく。
文字列をUint8Arrayに変換して標準出力(Deno.stdout)にブチ込む。

```ts:print(str)
const encoder = new TextEncoder();

/** console.logだと毎回改行されて困るので、改行せず文字列を出力する関数 */
async function print(str: string) {
  const bytes = encoder.encode(str);
  await Deno.stdout.write(bytes);
}
```

これで`print("\x1b[2J")`すれば改行せずに画面を消せる。
ちなみにRubyでは改行するのが`puts`関数で、改行しないのが`print`関数なのでそれに倣う。

# sleep関数でn秒ディレイする

ループ内で待つ処理を挟むために、awaitでn秒待つsleep関数を作りました。
setIntervalでやってもいいんだけど、元のイカすソースに似せたかった。

```ts:sleep(n)
/** n秒待つ関数 (await忘れずに) */
function sleep(sec: number) {
  return new Promise((res) => {
    setTimeout(() => res(null), sec * 1000);
  });
}
```

これで`await sleep(1)`すれば処理が1秒中断されます。
Denoではトップレベルawaitが有効なのでasyncするために関数の中で実行しなくて済む。


# サブキャラクターレンダリング

半角文字は縦に長い。
半角1文字をタテに2ドットあると見立ててコンマやコロンなどを駆使し1文字で2ドット分の情報を描画する手法。
こういうとき以外で使うかは知らない。

|ドット|サブ| 
|:-:|:-:|
|□ <br> □| [ ] |
|■ <br> □| ['] |
|□ <br> ■| [,] |
|■ <br> ■| [;] |

[点字](https://qiita.com/zakuroishikuro/items/15d1a69178895edf9a21) 使ったらもっとすごそう

# ソース全体

肝心の処理部分。
複素数が必要だったのでSkypackからmathjsをimportしてる。

```ts
// deno run --unstable https://raw.githubusercontent.com/zakuroishikuro/sharin/main/asobu/atom.ts
//
// original source code:
// https://github.com/mame/trance-book/blob/master/8-1/subcharacter-rendering-demo.rb
//
// from:
//   あなたの知らない超絶技巧プログラミングの世界 by 遠藤侑介
//   8-1-2 アスキーアートの生成（1）：サブキャラクターレンダリング

import { complex, multiply } from "https://cdn.skypack.dev/mathjs";

/** n秒待つ関数 (await忘れずに) */
function sleep(sec: number) {
  return new Promise((res) => {
    setTimeout(() => res(null), sec * 1000);
  });
}

const encoder = new TextEncoder();

/** console.logだと毎回改行されて困るので、改行せず文字列を出力する関数 */
async function print(str: string) {
  const bytes = encoder.encode(str);
  await Deno.stdout.write(bytes);
}

// コンソールの大きさを取得。--unstalbeフラグが必要。
const {rows} = Deno.consoleSize(Deno.stdout.rid)

const S = rows; // 描画領域のの
const A = S / 2.1; // 長径
const B = S / 8.0; // 短径

print("\x1b[2J"); // 画面クリア

let n = 0;
while (true) {
  // バッファ
  const s = [...Array(S)].map(() => [..." ".repeat(S * 2)]);

  [...Array(100)].forEach((_, i) => {
    // 楕円の媒介変数表示
    const t = Math.PI * 2 * (i + n) / 100;
    const e = complex(A * Math.cos(t), B * Math.sin(t));

    // 楕円を 3 つ描く
    [-1, 0, 1].forEach((j) => {
      // 楕円を傾ける
      const e2 = multiply(
        e,
        complex({ r: 1.0, phi: Math.PI * 2 / 3 * j + n / 500 }),
      );

      // ドットを置く
      const x = Math.floor(e2.re * 2 + S);
      const y = Math.floor(e2.im * 2 + S);

      const row = s[Math.floor(y / 2)];
      if (i === 99) {
        [row[x - 1], row[x]] = "()";
      } else if (y % 2 == 0) {
        row[x] = row[x] == " " || row[x] == "'" ? "'" : ";";
      } else {
        row[x] = row[x] == " " || row[x] == "," ? "," : ";";
      }
    });
  });

  print("\x1b[1;1H" + s.map((row) => row.join("")).join("\n"));
  await sleep(0.03);

  n++;
}
```

移植しただけの自分にはたいして解説できねぇ。すまねぇ。
ソースコードとにらめっこして解読してほしい。ごめんなさい。

おしり。