Rubyでslコマンドを真似してみた

slコマンドをご存知ですか？
コンソール画面に蒸気機関車を走らせるコマンドです。

![sl.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/09412b41-0c4c-abe7-233a-041d6ab6ad67.gif)

slコマンドの使い方などに関しては以下の記事などを参考にしてみてください。
[ジョークプログラムslを動かしてみた - Qiita](https://qiita.com/gm_kou/items/2840a6670dc2e733d00a)

僕は職場の先輩に教えてもらってこのコマンドと出会ったのですが、
**めっちゃ面白いやん、俺も真似してみたい！**
と思いました。

機関車ではなく、何か別のものを自分で動かしてみたいなー、と。

## slコマンドを覗いてみる

slコマンドのソースコードは以下から見ることができます。
[https://github.com/mtoyoda/sl](https://github.com/mtoyoda/sl)

たぶんC言語で書かれています。
これを真似してみようと思ったのですが、僕はC言語はあんまりわからないので、自分で模索しながらRubyで作っていくことを決意しました。

slコマンドを再現するためには様々な要素を組み合わせる必要があります。
ここからはその必要な要素を順に説明していきます。
最後にはクリスマス感ある完成物も載せてありますので、「知ってるよ」ってところは飛ばし読みで大丈夫です。

## 簡単なアニメーションを実装する
まずは、0から19までの数字をカウントアップして出力するプログラムを作ってみます。

![qiita1.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/119e675e-7f10-f653-15aa-6e05ac5fedd4.gif)

普通に書くと、出力した文字の右に次の数字が出力されていきます。

```rb
20.times do |i|
  print i
  sleep 0.2 # 0.2秒待つ
end
```

出力結果

```
012345678910111213141516171819
```

ここで、上の画像のようなアニメーションでは **前の数字と同じ場所に数字を上書きする** と考えます。

結論から言うと、以下のようなコードを書きます。

```rb
20.times do |i|
  print i
  sleep 0.2
  print "\r" # この1行を追加
end
```

`\r` は **キャリッジリターン** といい、カーソルの位置をその行の先頭に戻すことができます。
つまり、上のコードでは、
数字を出力 → 0.2秒待つ → カーソルを行の先頭まで戻す
ということを繰り返しています。

つまり、まず「0」と出力し、その後カーソルの位置を先頭に戻してから「0」と同じ位置に「1」を出力（上書き）することで、まるで「0」が「1」に変化したようなアニメーションを作り出しています。

## 文字を動かす（L → R）
先ほどの知識を用いて、短い文字列を左から右に動かしてみましょう。

![qiita2.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/041dd742-49e6-9eb2-50fc-a21c0cc6d9f5.gif)


考え方としては、ループを繰り返す度に「Hello World」の先頭に半角スペースを付け足すことで、右に動いていくようなアニメーションを再現することができます。

```rb
# 動かしたい文字列を用意
str = "Hello World"

20.times do |i|
  print str
  sleep 0.1
  print "\r"

  # 文字列の先頭に半角スペースを追加
  str = " " + str
end
```
こんな感じのコードになります。

## 文字を動かす（L ← R）

実際のslコマンドでは、機関車は右から左へと通過していきます。
ですので、ここでは「Hello World」を先ほどとは逆向きに動かしてみます。

考え方は先ほどとほぼ同じで、今度は逆に最初から先頭にいくつか空白を付けておき、ループの度に1つずつ減らしていきます。

```rb
str = "Hello World"

# 先頭に20個の半角スペースを追加
str = (' ' * 20) + str

# 文字列strの文字数だけ繰り返す
str.size.times do |i|
  print str
  sleep 0.1
  print "\r"

  # 文字列の先頭の1文字を削除
  str.slice!(0)
end
```

**しかし！**

これではうまくはいきません。
以下の画像のように、「Hello Worldddddddddddd」と、「d」だけが残り続けてしまいます。

![qiita3.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/ec129774-1321-c633-cc0b-5ce87615a879.gif)


残像が、、、見える、、、

このアニメーションの仕組みは、すでに説明した通り、直前に出力した文字を上から書き換えています。
今回は上書きする文字列が直前の文字列より「1文字少ない」ため、最後の1文字は上書きできていないのです（なので末尾の「d」だけが残り続けます）。

これを回避するためには、上書きする文字列の長さが直前の文字列と同じ（or 長い）必要があります。
今回は、先頭の1文字を削除した後に、その文字列の末尾に半角スペースを1つ追加します。

```rb
str = "Hello World"
str = (' ' * 20) + str

str.size.times do |i|
  print str
  sleep 0.1
  print "\r"
  str.slice!(0)

  # 末尾に半角スペースを追加
  str += ' '
end
```

これで右から左へ「Hello World」を流すことができました。

![qiita3-2.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/da875734-07c9-f2be-e89d-8f512725fc51.gif)

## コンソールの右端から出現させる

だいぶslコマンドに近づいてきました。
本家slコマンドでは、コンソールの画面幅にかかわらず、きれいに画面右端から機関車が登場します。

これを実現するためには何が必要か・・・そう、「**コンソールの画面幅**」がわかればできるはずです。
より正確には、「**1行に収まる文字数**」が分かれば、それに合わせて出力する文字列の長さを調整すればよいはずです。

Rubyでは以下の値でコンソールのサイズを取得することができます。

```rb
`tput lines` # 行数
`tput cols` # 列数
```

参考：[Rubyでターミナルのサイズを取得する - Qiita](https://qiita.com/kwyt/items/d54525639ff7ec55b293)

今回はこの「列数」の方を用いて、「Hello World」を画面右端から流してみましょう。

```rb
str = "Hello World"

# コンソールの列数を取得し、数値に変換
cols = `tput cols`.to_i

# 列数分の空白をstrの先頭に追加
str = (' ' * cols) + str

str.size.times do |i|

  # strの先頭からcols番目までの文字を出力
  print str[0..(cols - 1)]

  print "\r"
  sleep 0.05
  str.slice!(0)
  str += ' '
end
```

上のコードのポイントは、「文字列`str`の先頭から`cols`番目までの文字を出力する」という箇所です。
Rubyでは配列のような要領で、文字列に対しても範囲を指定することができます。

```文字列の一部を出力する例.rb
str = "Progate"
print str[3] # 結果: g
print str[0..2] # 結果: Pro
```

これで「画面右端から文字列を流す」というところまで進みました！

![qiita4.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/83a7ef14-b904-3cef-f2e5-22017e18af27.gif)

## 複数行を出力する

ここまで来たらslコマンドまではあと1歩です！
今までは「Hello World」という1行のみを動かしていましたが、複数行でも動かせるようにしてみましょう。

まずは動かしたいものを用意します。
方法はいくつかありますが、僕は以下のように配列に１行ずつ文字列を用意しました。

```rb
list = [
'                 ',
'   PPPPPPPPPP    ',
'   PP       PP   ',
'   PP       PP   ',
'   PPPPPPPPPP    ',
'   PP            ',
'   PP            ',
'   PP            ',
'                 ',
]
```
Progateの「P」です。
配列に9個の文字列が入っています。

この配列を繰り返し処理を用いて1行ずつ出力していくのですが、
9行を出力し終えた後、**カーソルは9行目の末尾**にいます。
0.0何秒か待った後には、直前で出力したものを上書きするためにカーソルは1行目の先頭にいる必要があります。

実はRubyでは以下のようにすることで、カーソルの位置を自由に動かすことができます。

```rb
# カーソルを1つ上に動かす
print "\e[1A"

# カーソルを1つ下に動かす
print "\e[1B"

# カーソルを1つ右に動かす
print "\e[1C"

# カーソルを1つ左に動かす
print "\e[1D"
```
`1` の部分を任意の数字に書き換えれば、その数だけ移動します。

今回は9行目の末尾から1行目の先頭まで移動させたいので、
 1. カーソルを先頭に移動させる（`\r`を使用）
 2. カーソルを8行上に移動させる
としてみます。

```rb
list = [
  # 省略
]

cols = `tput cols`.to_i

# 配列の各要素の先頭にスペースを追加
list.map! do |str|
  (' ' * cols) + str
end

list[0].size.times do

  # 配列の各要素を出力
  list.each do |str|
    print str[0..(cols - 1)]
  end

  # カーソルを1行目の先頭に戻す
  print "\r"
  print "\e[8A"

  # 配列の各要素を整形
  list.map! do |str|
    str.slice!(0)
    str += ' '
  end
  sleep 0.03
end
```

1行の時との違いは、配列の各要素に対して処理をする必要があるので、 `each` や `map` を使用しています。

これで、以下のように大きな「P」をコンソールの右端から動かすことができます。

![qiita5.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/4bfa0f07-3ac0-aede-00d9-cf3946d0de91.gif)


ここまで来れば、もはやslを再現できたようなものです。

## アスキーアートを書く
もう後は頑張ってお絵かきするだけです。
僕はこれまで呼び方を知らなかったのですが、こういう文字だけで絵を書くことを「アスキーアート」と呼ぶそうです。

この記事は「Progate Advent Calendar」の25日目の記事なんですが、
今回は時間が無かったので（言い訳）、以下のサイトで画像からアスキーアートを作ってもらいました。

[AA変換(アスキーアート生成)｜Web便利ツール@ツールタロウ](https://tool-taro.com/image_to_ascii/)

気を付ける必要があるのは、アスキーアートの中にクォーテーションが含まれていると、当然ですが文字列を囲んでいるクォーテーションと認識されてしまい崩れてしまう場合があります。
また、上記のサイトの場合は問題なさそうですが、アスキーアート内で全角文字を使用する際は2文字分の幅で出力される場合があるので注意が必要です。

　

　

で、

　

　

今回の完成物が、

　

　

こちら！!

![xmas.gif](https://qiita-image-store.s3.amazonaws.com/0/81183/3818559f-ba65-8cd1-03fc-9c41c33d3a06.gif)

「**Advent Calendar**」らしく、クリスマスにちなんだものにしてみました！
見難いですがサンタさんの帽子の先端も上下に揺れてます！！（地味なこだわり）
トナカイの歩き方がややぎこちないですが、そこは気にせずw

以下に~~クソコード~~ソースコードも公開してありますので、参考にしてみてください！
（※ この記事での説明とは少し実装方法が異なる箇所もあります）
https://github.com/Kite0301/sl-xmas
大好きな黒霧島を飲みながら書いたコードなので雑なのは許してくだせえ

## 最後に

今後は更に、
・上から雪を降らせる
・サンタに色を付ける
など、改良してみたいな、と思ってます。

プログラミングは楽しんでなんぼだと思ってます。
「**豊かな発想**」と「**何でもできると信じる気持ち**」で、プログラミングの力を存分に活用してみてください！

（コンソールにサンタ走らせて何の役に立つのかは知らないけど）