25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

256文字で書かれたとんでもコード「A City in a Bottle 🌆」をざっくり読んでみる

Posted at

はじめに

以下のツイートをご存知でしょうか。

以下のたった256文字のコードで街並みの映像が流れるというハイレベルなHTML1です。

A City in a Bottle
<canvas style=width:99% id=c onclick=setInterval('for(c.width=w=99,++t,i=6e3;i--;c.getContext`2d`.fillRect(i%w,i/w|0,1-d*Z/w+s,1))for(a=i%w/50-1,s=b=1-i/4e3,X=t,Y=Z=d=1;++Z<w&(Y<6-(32<Z&27<X%w&&X/9^Z/8)*8%46||d|(s=(X&Y&Z)%3/Z,a=b=1,d=Z/w));Y-=b)X+=a',t=9)>

コードゴルフを普段行っているわけではない私からすると、このコードは訳分かんなすぎて面白かったので、一般プログラマなりにできる範囲で読んでみることにしました。

一旦バラしてみる

読むと意気込んだは良いものの、そのままではやはり意味不明です。
ということで、少しずつバラしていきたいと思います。

方針としては以下の形で少しづつ崩していきます。

  • onclick内にあるsetInterval()<script>タグに抜き出す。
  • setInterval()に与えられている文字列2をファンクションに書き直す。
  • forwhileに書き直し、変数初期化などを外に抜き出す。
  • 長すぎる条件文はファンクションに抜き出す。
  • マジックナンバーを定数化しそれっぽい名前で定義する。
  • 変数をそれっぽい名前に変更する。
  • その他、細かい部分を見やすく変更する。
  • (動作を細かく見れるようにするためにクリックでsetInterval()を停止できるようにする。)

とりあえず、以上を行った結果が以下になります。
気になる方は、.htmlで保存して動かしてみてください。

A City in a Bottle(とりあえずバラしたもの)
<script>
    // ▽テスト用に止められるようにしておく------------
    let intervalId;
    let isMovieRun = false;

    function movieCtrl() {
        if(isMovieRun){
            movieStop();
        }else{
            movieStart();
        }
        isMovieRun = !isMovieRun;
    }

    function movieStop() {
        clearInterval(intervalId);
    }
    // △テスト用に止められるようにしておく------------

    let timer;
    let width;
    let drawCellIndex;

    function movieStart() {
        timer = 9;
        intervalId = setInterval(movie, timer);
    }

    function movie() {
        width = 99;
        canvasId.width = width;
        ++timer;
        drawCellIndex = 6e3;

        while(drawCellIndex--) {
            const columnIndex = drawCellIndex % width;

            let addX = columnIndex / 50 - 1;
            let subY = 1 - drawCellIndex / 4e3;
            let texture = subY;
            let depth = 1;

            let X = timer;
            let Z = 1;
            let Y = 1;

            function condition() {
                const DISTANCE = 32;
                const ROAD_WIDTH = 27;
                const MAX_BUILDING_HEIGHT = 46;
                const CAMERA_HEIGHT = 6;
                const BUILDING_WIDTH = 9;
                const BUILDING_DEPTH = 8;
                const BUILDING_HEIGHT_OFFSET = 8;
                const WINDOW_OFFSET = 3;
                const SHADOW_OFFSET_X = 1;
                const SHADOW_OFFSET_Y = 1;

                return  ( ++Z < width ) & (
                            Y < (
                                CAMERA_HEIGHT - (
                                    (
                                        ( DISTANCE < Z ) &
                                        ( ROAD_WIDTH < ( X % width ))
                                    ) &&
                                    (
                                        ( X / BUILDING_WIDTH ) ^
                                        ( Z / BUILDING_DEPTH )
                                    )
                                ) * BUILDING_HEIGHT_OFFSET % MAX_BUILDING_HEIGHT
                            ) || 

                            depth | (
                                texture = ( X & Y & Z ) % WINDOW_OFFSET / Z,
                                addX = SHADOW_OFFSET_X,
                                subY = SHADOW_OFFSET_Y,
                                depth = Z / width
                            )
                        );
            }

            while(condition()) {
                X += addX;
                Y -= subY;
            }

            const fillRectX = drawCellIndex % width;
            const fillRectY = drawCellIndex / width | 0;
            const fillRectW = 1 - (depth * Z / width) + texture;
            const fillRectH = 1;

            canvasId.getContext`2d`.fillRect(fillRectX, fillRectY, fillRectW, fillRectH);
        }
    }
</script>

<canvas style=width:99% id=canvasId onclick=movieCtrl()>

実際に読んでみる

さて、ここからコメントを入れながら少しずつ読んでみたのですが、かなりギークな方法によって丁寧に文字数が削減されていることがわかりました。
特にcondition()に切り分けた条件部分はざっくり読みはしましたが、正直理解しきっているかといえば自信がないです3

とりあえず、可能な限りコメントを入れてなんとか読んだものを以下にのせます。

A City in a Bottle(とりあえずバラしてコメント入れたもの)
<script>
    // ▽テスト用に止められるようにしておく------------
    let intervalId;
    let isMovieRun = false;

    function movieCtrl() {
        if(isMovieRun){
            movieStop();
        }else{
            movieStart();
        }
        isMovieRun = !isMovieRun;
    }

    function movieStop() {
        clearInterval(intervalId);
    }
    // △テスト用に止められるようにしておく------------

    let timer; // 経過時間
    let width; // 横幅
    let drawCellIndex; // 描画セル数

    function movieStart() {
        timer = 9;
        // setIntervalのミリ秒指定は最短10msであるため、実質的には10ms毎に実行される
        intervalId = setInterval(movie, timer);
    }

    function movie() {
        width = 99; // 横幅を99に設定
        canvasId.width = width; // Canvasの描画内容をリセット(canvasタグのwidthを99pxに設定し、画像を拡大された状態にする)
        ++timer; // 経過時間インクリメント
        drawCellIndex = 6e3; // 6000セル描画(画面としては切りが良くない為、一番下に段差がでる。文字数は増えてしまうが6039辺りならちょうどいい)

        while(drawCellIndex--) { // 6000セル分描画(1フレーム分ループ)(下から描画する)
            const columnIndex = drawCellIndex % width; // 描画する列

            let addX = columnIndex / 50 - 1; // Xの計算に使用する(-1で表示されるアングルをずらしている)
            let subY = 1 - drawCellIndex / 4e3; // Yの計算に使用する(描画セルが4001~6000の間は負数で地面)
            let texture = subY; // 窓や背景を表すテクスチャの値(描画セルが大きいほど色は薄くなる為グラデーションがかかる)(窓や背景の描画に使用)
            let depth = 1; // 現在描画部分の奥行用補正値(0~1)(建物部分の描画に使用)

            let X = timer; // 横(値が大きい程、右を示す)
            let Z = 1; // 奥行(建物部分の描画に使用)(建物の描画する面がどの距離にあるかで濃さを変える)(値が大きい程、奥を示す)
            let Y = 1; // 縦(値が大きい程、下を示す)

            function condition() {
                const DISTANCE = 32; // 建物の遠さ
                const ROAD_WIDTH = 27; // 密集している建物同士の距離
                const MAX_BUILDING_HEIGHT = 46; // 建物の最大の高さ
                const CAMERA_HEIGHT = 6; // 地面からの距離(2以上ないと最初のループで建物の描画内容の計算がfalseになってしまうためバグる)
                const BUILDING_WIDTH = 9; // 建物の横幅
                const BUILDING_DEPTH = 8; // 建物の奥行の長さ
                const BUILDING_HEIGHT_OFFSET = 8; // 建物の高さの補正値(8px単位で建物の高さを変える)
                const WINDOW_OFFSET = 3; // 窓の計算用の補正値
                const SHADOW_OFFSET_X = 1; // 影計算用の補正値X
                const SHADOW_OFFSET_Y = 1; // 影計算用の補正値Y

                return  ( ++Z < width ) & ( // 「(奥行(インクリメント) < width) & 以下の計算結果 (奥行がwidth()まで行くとfalse)

                            // 建物の描画内容を計算する(初回は必ずtrue)
                            Y < ( // 「縦 < 建物の高さの計算結果が成立する場合はその奥行において建物が存在する
                                CAMERA_HEIGHT - ( // 地面からの距離から、建物の高さの結果を引く
                                    ( // ここで描画する建物があるか確認。ない場合は「建物の高さ:0」を返す("建物の遠さの判定"と"密集している建物同士の距離の判定"がともにtrueで1)
                                        ( DISTANCE < Z ) & // 「建物の遠さ < 奥行 true
                                        ( ROAD_WIDTH < ( X % width )) // 「密集している建物同士の距離 < ( % 横幅) true"横 % 横幅"は現在の列を表す
                                    ) &&
                                    ( // 描画する建物がある場合、その建物の高さを返す(建物の横幅と建物の奥行の長さごとにXORでランダムっぽく建物の高さを変える)
                                        ( X / BUILDING_WIDTH ) ^ // 「横 / 建物の横幅」
                                        ( Z / BUILDING_DEPTH ) // 「奥行 / 建物の奥行の長さ」
                                    )
                                ) * BUILDING_HEIGHT_OFFSET % MAX_BUILDING_HEIGHT // 「(建物の高さ * 建物の高さの補正値) % 建物の最大の高さ」を計算する(判定結果は"X = timer"により徐々に増えていくため「建物の最大の高さ」の余りでうまいこと均す)
                            ) || 

                            // 建物以外の描画内容を計算する(上の建物の判定がfalseの場合に実行)(おそらく全てのセルで1度はここを通る)
                            depth | ( // "depth"の値と"Z / width"の値の論理和を返す(初回の"depth=1"か"Zが99以上"で演算結果が1以上の値になる為、おそらく"Z / width"は条件に影響なし)
                                texture = ( X & Y & Z ) % WINDOW_OFFSET / Z, // 窓などのテクスチャの計算(Zで割る関係上、手前の方が暗くなりやすい)
                                addX = SHADOW_OFFSET_X, // 次は右側にずらして判定させる(斜めに影を作る)
                                subY = SHADOW_OFFSET_Y, // 次は上側にずらして判定させる(斜めに影を作る)
                                depth = Z / width // 全体的に手前の方が暗くなるようにする(Zが大きいほうが薄くなる)
                            )
                        );
            }

            while(condition()) {
                X += addX; // 判定場所をずらす(ループ初回は必ず"columnIndex / 50 - 1"を加算)
                Y -= subY; // 判定場所をずらす(ループ初回は必ず"1 - drawCellIndex / 4e3"を減算)
            }

            /*
            *  fillRectX: 描く四角形の左上のx座標
            *  fillRectY: 描く四角形の左上のy座標
            *  fillRectW: 描く四角形の幅(実質的にここが色の濃さを決める(0~1))
            *  fillRectH: 描く四角形の高さ
            */
            const fillRectX = drawCellIndex % width; // 描画する列
            const fillRectY = drawCellIndex / width | 0; // 描画する行(ビット演算にて小数点を切り捨て)
            const fillRectW = 1 - (depth * Z / width) + texture; // 四角形の幅を計算する(実質的にここが色の濃さを決める(0~1))
            const fillRectH = 1; // 高さは1px固定

            canvasId.getContext`2d`.fillRect(fillRectX, fillRectY, fillRectW, fillRectH); // 四角形を描写
        }
    }
</script>

<!-- cssのstyleをwidth:99%にしたうえで、js上でcanvasタグのwidthを99pxにすることで、1pxが拡大されて表示される -->
<canvas style=width:99% id=canvasId onclick=movieCtrl()>

ここから文字数を減らせるか?

実のところ、ここ数日間このコードを削減できないか悩んでいました。
結果として私はなんの成果も得られませんでしたが、誰か有能なコードゴルファーがうまいことできるかもしれないので、ずっと考えていた切り口を書いておきたいと思います。
私はこの考えで上手く削減できませんでした。

  • depth | (...)の部分を上手く他の変数で代替して、depthの初期化部分を削除する。
  • depthでセットされる値がZ / widthなので、Zが99のときdepthは1を取る。それを利用して( ++Z < width )を削除する。(++Zは建物の描画内容計算部分でインクリメントできそう)

最後に

細かいところに作者の文字数削減に対する情熱とこだわりが見えたので、読んでて楽しかったです。
ただ、私の入れたコメントの何処かに誤りがあるかもしれません。もしあった場合はじゃんじゃん教えてください4

  1. サイトによっては"JavaScriptコード"と紹介されていますが、ツイートされている文字列はcanvasタグを含めて256文字なのでここではHTMLと書きます。

  2. setIntervalの第1引数は文字列が許容されており、指定されたミリ秒が経過するたびに文字列をコンパイルして実行するらしい。

  3. この複雑さになると、頭のRAM領域が枯渇して無理でした…

  4. 記事を直すためというのもそうですが、どちらかというと単純に私が「なるほど!」とスッキリしたいので…

25
10
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
25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?