LoginSignup
4
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-07-15

はじめに

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

こんな感じで動きます。

atom.gif

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

実行してみる

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

deno run --unstable https://gist.githubusercontent.com/zakuroishikuro/dc92d9d6a873e24f40ace683df177245/raw/6b48afbcd41d228ab52e6b4b8f802292a9abce0d/atom.ts

画面の大きさを取得するDeno.consoleSizeを使うために--unstableフラグが必要。
画面をデカくすればするほどカッコいいよ。
Ctrl+cで終了してね。

元のソースはこれ。

どうやって動かしてるの?

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

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

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

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

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

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

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でやってもいいんだけど、元のイカすソースに似せたかった。

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ドット分の情報を描画する手法。
こういうとき以外で使うかは知らない。

ドット サブ

[ ]

[']

[,]

[;]

点字 使ったらもっとすごそう

ソース全体

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

// 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++;
}

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

おしり。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3