深夜、ブラウザの画面を1本の帯だけにして、ずっと眺めていました。
本文は関係ありません。見ていたのは上のアドレスバー、それも # から右の12マスだけです。
先に、たどり着いたところを書いておきます。
横一列の画面が直接表現できる関係は、ひとつしかありません。近づいているか、離れているか。だから物語は、距離そのものについての話にするしかありませんでした。ここに気づくまで、絵をきれいに動かすことばかり考えて2日ぶんくらい遠回りしています。
できあがったのは、街のポストからおばあちゃんの家まで手紙が渡っていく107秒の無声映画です。hashgk.com のアドレスバーで、いまも一周し続けています。

川に来ると船になります

敷いて、置いて、つなげるだけです
この記事は、その設計の話です。1次元しかない画面で、どうやって前後関係と時間と心情を出したか。
1マスずつ絵文字を置くのをやめる
最初に読んだのは、location.hash を setTimeout で書き換えるだけの8行のコードでした。配列に月の満ち欠けを8コマ入れて、順に流す。それだけで満ち欠けします。
ただ、この作りだと1コマが絵文字1個です。表現が8種類で尽きます。
そこで、1コマを「12マスぶんの配列」にしました。背景を敷いて、その上に置いて、最後につなげる。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var W = 12; // 背景で埋めた1行を作る function fill(c) { var a = [], i; for (i = 0; i < W; i++) { a.push(c); } return a; } // x 番目のマスに置く。画面外は捨てる。あとから呼んだものが手前 function put(a, x, c) { var i = Math.round(x); if (i >= 0 && i < W) { a[i] = c; } return a; } |
これだけで前後関係が出ます。
put はあとから呼んだほうが勝つので、太陽を描いたあとに雲を描けば雲が手前を横切ります。速度を変えて雲を2枚流せば視差になります。1行しかない画面に、奥行きが1段だけ生まれました。
書いていて気づいたのですが、これはスプライトを重ねる古い作り方そのものです。横1×縦12ドットのフレームバッファがあって、そこに毎フレーム描き直している。画面が細いだけで、やっていることは変わりません。
世界を動かさない
最初は幕ごとに背景を作り直していました。夜の幕は全部黒、海の幕は全部青、というふうに。
これが失敗でした。マスの意味が毎回変わるので、見ている側がどこを見ればいいのか分からなくなります。動いているだけで、旅になっていない。
作り直して、12マスを固定の地図として扱うことにしました。
| 1 2 3 4 5 | // 12マスの地図。全部の幕がこれを複製して使う var MAP = [E.office, E.store, E.post, E.tree2, E.water, E.water, E.water, E.tree, E.mount, E.rice, E.boxOff, E.home]; function map() { return MAP.slice(); } |
左端が街、2番目が街のポスト、真ん中が川、8番目が山、右端がおばあちゃんの家。10番目にもうひとつポストがあります。全部の幕がこの同じ地図を複製して使い、その上をスプライトが動くだけになりました。
効果はすぐ出ました。マスに意味が固定されると、位置そのものが情報になります。8番目に何かが置かれていれば「山にいる」と読める。両端のポストを物語の極に据えたので、あいだの8マスが距離として効いてきます。
この話でいちばん大事な1コマは、10番目が 📪 から 📫 に変わるところです。閉じたポストが、ふたを開けている絵に切り替わるだけ。地図が固定されているから、その1マスの変化が読めます。
幕という単位を持つ
長さを伸ばすために、シーンを構造にしました。秒数と毎秒のコマ数と描画関数を持つオブジェクトを並べるだけです。
| 1 2 3 4 5 6 7 8 9 10 | { no: '五', ttl: 'まいにちポストをみる', sec: 10, fps: 8, render: function (f) { var a = map(); put(a, 8, E.train); // 手紙は山で止まったまま var night = (f % 8) >= 4; // 8コマ = 1秒 = 1日 put(a, 0, night ? E.moon : E.sun); put(a, 10, E.boxOff); // ポストは空のまま put(a, 11, night ? E.zzz : E.gran); return a.join(''); } } |
これが五幕、手紙が山で止まっている場面です。10秒あります。
やっていることは単純で、8コマを1日と決めて、前半4コマを昼、後半4コマを夜にしています。0番目のマスが 🌞 と 🌙 で入れ替わり、11番目のおばあちゃんが 👵 と 💤 になる。1秒で1日が過ぎます。手紙は8番目に置かれたまま、10日ぶん動きません。

字幕がないと切れ目が分からなくなりました
横一列しかない画面でも、時間が飛ぶことは描けるらしい。作ってみるまで知りませんでした。動かないものを画面に残したまま、まわりだけ速く回す。映画のモンタージュと同じ手です。
もうひとつ効いたのが、字幕の幕でした。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function card(text, sec) { return { sec: sec || 3, fps: 5, render: function (f) { var a = text.split(''); return a.slice(0, Math.min(f + 1, a.length)).join('') + (f % 2 ? '\u258D' : ''); // ▍ を明滅させてカーソルに見せる } }; } // 使い方 card('ところが', 3) card('あきのはじめ', 3) |
絵の幕と文字の幕を交互に置くと、無声映画の字幕と同じ働きをしてくれます。「ところが」の3文字が入るだけで、場面が変わったことが伝わる。絵が語れない部分を、まるごと引き受けてくれました。
全部で17幕、うち7幕が字幕です。107秒のうち23秒が文字だけの時間になります。多いように見えますが、ここを削ると話の切れ目が分からなくなりました。
使える文字が決まっている
設計と関係ないところで、いちばん時間を取られたのが文字選びでした。
絵文字のなかには、うしろに U+FE0F という見えない1文字を必要とするものがあります。⛰️ や ☁️ や 🕊️ がそれで、アドレスバーがURLを組み直すときにこの2文字が引き離されます。本体は白黒の記号に戻り、はぐれた合図が %EF%B8%8F という9文字になって表に出てきます。
地図の山だけが記号になって、その右に意味不明な文字列が生えていました。
Unicode 16.0 で数えたところ、この合図を必要とする絵文字は207字ありました。詳しい経緯と一覧は別の記事に分けてあります。地図の12マスは、そこで作った表を見ながら組み直しました。
もうひとつ、字幕に空白が使えません。半角スペースは %20 になります。「あきの はじめ」と書いたら「あきの%20はじめ」になったので、詰めて「あきのはじめ」にしました。ひらがなだけなら詰めても読めます。
制約はもうひとつあって、これは最後まで解決していません。表示先がアドレスバーなので、自動テストが書けません。コンソールに出せば数は数えられますが、実際にどう見えているかはブラウザに聞くしかない。全856コマを目視した回数は数えていません。
骨格だけのコード
映写の部分は、幕の配列を順に回すだけです。
| 1 2 3 4 5 6 7 8 9 10 | scenes.forEach(function (s) { s.F = Math.round(s.sec * s.fps); }); var si = 0, fi = 0; function step() { var s = scenes[si]; location.hash = s.render(fi, s.F); if (++fi >= s.F) { fi = 0; si = (si + 1) % scenes.length; } setTimeout(step, 1000 / s.fps); } |
location.hash に直接書いているのは、履歴を汚さない書き方を試して失敗したからです。history.replaceState() を使うと戻るボタンが死なずに済むのですが、そちらは別の理由でつまずきました。素朴な書き方のほうが確実に動きます。
離脱前に # を消す処理だけは入れておいたほうがいいです。12マスの絵文字はURLの形にすると140文字を超えるので、その状態のリンクを誰かに送ると、ただの文字化けにしか見えません。
もう一度、帯だけの画面で
hashgk.com を開いて、上の帯だけを見てください。
タイトルが出て、街とおばあちゃんの家がいちばん離れたところに置かれます。子どもがポストまで歩いて、手紙が旅に出る。山で止まって、何日か過ぎて、また動きだす。返事が街へ向かってきて、着いたのは手紙ではありません。
最後に「1マスになった」と出て、また最初に戻ります。
12マスの距離が1マスになるだけの話です。横一列の画面で言えることは、結局それくらいでした。


コメント