見出し画像

貸し本棚における「文字打ち」機能の実装

こんにちは、貸し本棚開発者の加藤憲保です。魔人加藤ではありません。サイコパワーでスパゲッティコードを書きます。
貸し本棚にはいくつかの奇妙な機能がありますが、その中でも使用頻度の高い「文字打ち」機能というものがあります。ノベルゲームのように本文を一文字ずつ打ち出す機能で、その実装の概要を書きたいと思います。

貸し本棚とは

貸し本棚ポータル

貸し本棚は小説投稿サイトの体を成した、小説保管サービスです。宣伝、コミュニティ、ランキングやレスポンスを強制するような騒がしい機能がありません。
多くの人に読んでもらうためのサイトではなく、そういった投稿サイトに公開する前、あるいは公開後、あるいは公開しなかった作品を保管しておいて、任意に宣伝したりしなかったりできる場所として提供しています。

「文字打ち」機能

「文字打ち」という機能は、小説本文のサイドバーからレイアウト設定を開き、「文字出力」の速度を全文以外に設定することで動きます。

レイアウト設定

上記ツイート時点ではテスト段階で、現在の仕様とは少し違いますが、概ねこのようにノベルゲーム風に文字を打ち出します。多くのモダンブラウザであれば、文字出力時に「ココココ……」というSEも鳴ります。

見た目が面白いというだけでなく、テスト段階でも「読む気のない試験用の作品を強制的に読まされる」感覚がありました。一種のリーディングトラッカーとしての働きもあるようです(リーディングトラッカーは別途実装されています)。

文字打ち実装(の概要)

あらかじめ文字打ち設定されている状態で、新規ページを開いたとき、以下の処理が順番に実行されます。

  1. init()…初期化。本文ページを開いたとき1回だけ実行。

  2. start()…文字打ち開始。incrementを呼びます。

  3. increment(interval)…文字を一文字出力します。繰り返し実行し、ある条件でstopを呼びます。

  4. stop()…文字打ち停止。

initで現在の状態を設定します。
ブックマークから既読範囲を読み込み、文字打ちのSEデータをサーバから読み込みます。この初期化処理が完了すると、本文パネルにフォーカスを移します。そのままキーボードでEnterやSpaceを押下したり、クリック・タップすることで次のstartが呼ばれます。

startは文字打ちの開始です。
文字打ちは
start → increment x 文字数 → stop
の順番で動作します。その起点となる処理で、キーボード押下やクリック・タップで呼ばれます。

incrementは実際に文字を打ち出します。
基本的に1文字出力すると、設定された間隔で次のincrementを呼び出し、繰り返し実行します。
ある条件――2文を出力するか、改行に到達するか――を満たすと、stopを呼びます。

stopでタイマー等をクリアして文字打ちを停止します。ここで既読範囲を保存します。

4つのバッファ

文字打ちの構造は4つのバッファで実現しています。UI上の動き自体はフロントのフレームワークに丸投げして、

  • 本文全体(body)

  • いま出力中(curr)

  • 出力完了(done)

  • 過去の文字列(archive)

のバッファ間で文字列をやりとりします。当初2つで実装したところ、文字打ちの後半に重くなる問題がありました。

文字打ちを開始すると、現在位置の文字を本文全体から取得します。これをいま出力中に追加します。

let char = this.section.body.substring(this.pos, this.pos + 1);
// ...
this.curr += char;

これを繰り返して、文字打ちが停止したとき、いま出力中の文字列を出力完了に吐き出します。

this.done += this.curr;
this.curr = "";

そして、出力完了文字が5,000字を超えたら、過去の文字列に吐き出します。

this.archive = this.done;
this.done = "";

この処理は一見意味がわからないですが、これがないと、出力文字数が5,000字を超えるとみるみる出力速度が低下します。

画面表示

画面上の本文の表示エリアは、
過去の文字列、出力完了、いま出力中
の順番で並んでいます。

書き換え回数は、いま出力中>出力完了>過去の文字列、の順番で多く、最も頻繁に書き換えられる「いま出力中」のバッファをできるだけ短く保っています。

文字打ちをしない設定の場合、すべての文字列を「過去の文字列」にコピーしています。これが長文を開いたときに発生する若干のオーバーヘッドの原因です。

効果音

効果音はAudioContextを使っています。若干手間ですが、Audioと違って多くのモダンブラウザでサポートされており、動作は軽量です。

予め生の効果音を読み込み、文字打ち開始時に初期化します。

window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audio = new AudioContext();
this.audio.decodeAudioData(this.buffer, (buf) => {
    this.buffer = buf;  // デコード済み
    this.source.buffer = buf;
    this.source.loop = false;
});
this.source = this.audio.createBufferSource();
this.source.connect(this.audio.destination);

その上で、文字を打つたびに以下の処理を呼びます。

this.source.start(0);
this.source = this.audio.createBufferSource();
this.source.buffer = this.buffer;
this.source.connect(this.audio.destination);

ここまで大雑把に処理の流れと実装の肝となる4つのバッファについて説明しましたが、細かいことを言うと実際はもう少し複雑です。

例えば、文字打ちの最中は常にページ最下部に自動スクロールし、ルビ文字や強調表示があれば一括出力、句読点のところで出力速度を抑えたりします。出力中に更にキーが押されると、最後まで一気に出力する処理もあります。

すべての説明を網羅すると書ききれないので、骨子に絞って書いてみました。わかりづらい部分もあると思いますが、基礎構築はそれほど難しくありません。

課題

実際に使ってみると、次の文を出力開始するのにキーボードを打ったりマウスクリックするのが少し面倒です(スマホはタップなので気にならないです)。スクロールにも反応できたら良いと思いますが、調整が難しいです。マウスやタッチパッドによってスクロール量が大きく異なります。

貸し本棚は2〜9文字の全角スペースで字下げを行いますが、10文字以上の全角スペースで地付きの行にします。
この地付きの行の文字打ちは、地から文字が湧き出てくるように現れるので、文字打ちの読みやすさが損なわれます。



この記事が気に入ったらサポートをしてみませんか?