anti scroll

ブラウザと小説の新しい関係を模索する

ページを指定して作品を開くことができるようになりました

ページ指定の方法

作品ページのURLの末尾に?p={ページ番号}を付けると、そのページ番号から作品を開けるようになりました。

例えば、夏目漱石の「こころ」の10ページ目を開くリンクは、次のようになります。

https://tb.antiscroll.com/novels/library/6162?p=10

「管理ページ > しおり」ページの改善

「管理ページ > しおり」で表示される、それぞれのリンクをクリックすると、しおりを挟んだページから再開されるようになりました。

これまでそうじゃなかったのが変だったのですが。。。

注意事項

ちなみにページを指定したリンクを他人と共有するのは、オススメできません。

なんでかというと、ページの縦横サイズは、端末の解像度によって変わるからです。

例えばスマホでは、1ページのサイズが、PCに比べて(とても)小さくなりますよね。

なので、PCで2ページ目だった内容が、スマホだと10ページ目ぐらいだったり、みたいなことになります。

というわけで、ツイッターとかで「このページを読んで!」みたいな感じで引用リンクを貼っても、あんまり意味がないことに注意してください。

作品の長さに関係なく、高速に作品が表示されるようになりました

非同期処理への対応

少し前にブログで「wasmではjsと非同期のやり取りをするのが難しい」と書いたのですが、この技術的な課題をなんとか解決できたので、組版の完了したページを(全ページの計算の完了を待たずに)表示できるようになりました。

これによって、すべての作品が0.1 ~ 0.2秒ぐらいで表示されるようになり、大幅に使用感が改善されたと思います。

技術的な部分

実はjsと非同期にやり取りする部分のコードのサンプルは、wasm_bindgenのサンプルに入っています。

https://github.com/rustwasm/wasm-bindgen/tree/main/examples/request-animation-frame

肝要なところを抜き出すと以下のようになります。

pub fn run() {
  let f = Rc::new(RefCell::new(None));
  let g = f.clone();
  let mut i = 0;

  *g.borrow_mut() = Some(Closure::new(|| {
    if i >= 300 {
      let _ = f.take();
      return;
    }
    i += 1;
    request_animation_frame(f.borrow().as_ref().unwrap());
  }));
  request_animation_frame(g.borrow().as_ref().unwrap());
}

fgはともにクロージャーを保持する参照カウント付きのポインタなのですが、gは最初にクロージャーを走らせるためだけに使います。

grunの終了後にクロージャーの参照カウントを減らしますが、もう一方のポインタfクロージャーの中で再帰的に参照され続けるため、何もしないとこのクロージャーに対する参照カウントは永遠にゼロにならず、解放されなくなってしまいます。

なのでreqest_animation_frameの処理を抜けるタイミングで、中身をf.take()で取り出し、スコープを抜けたところで解放されるようにしているわけですね。

基本はまあ理解できるのですが、nehanの場合、上のサンプルにおけるクロージャーの自由変数が、もうちょっと複雑なデータになっていまして、これだけだと動かない部分があり、ハマっていたのです…

しかしまあ、色々と頑張ったら、なんとか動くようになったので、無事に縦書き文庫のビューアーが(以前と同じく)非同期にページを表示できるようになりました。

ちなみに上のrequest_animation_frameはもちろんjsの関数ではなく、rust側のweb_sysパッケージが提供している関数でして、たぶんですがrust側のデータをjs側のデータにいちいち「お直し」する処理が含まれると思われるため、あんまりたくさん呼ぶのはよくなさそうです。

なので、縦書き文庫の場合は、数十ページぐらい組版し、まとめて送信することで、jsとrust間の通信回数を減らすようにしています。

導入の成果

非同期処理に切り替えた結果、全ページを組版するのにかかる時間は、一度にすべて組版してから送信する方式よりは(当たり前ですが)少しだけ遅くなりました。

しかし、せいぜい5% ~ 10%ぐらいの遅延なので、許容範囲だと思います。

なにより作品を開いてから「しばらくお待ち下さい」の表示がほとんどなくなり、一瞬で作品が表示されるメリットのほうが遥かに大きいです。

読者の離脱率も、大きく改善されるものと思われます。

WebAssembly導入の効果をプラットフォーム別に比較してみた

先日、縦書き文庫組版エンジン(nehan)をRustで書き直し、WebAssembly化したと報告しました。

tategakibunko.hatenablog.com

その際に「約3倍の速度向上があった」と書いたのですが、あれから約2週間分の利用者のログをもとに、プラットフォーム別に速度を比較したところ、ちょっと違う結果が出ました。

比較したプラットフォーム

とても大雑把ですが、PC(Windows)とPC(Mac)とiPhoneAndroidに分けて計測しました。

プラットフォーム別の速度

以下は速度比較のグラフです。縦軸は秒速の組版字数で、大きいほど速いことになります。

赤がnehan7(js)の速度で、青がnehan8(wasm)の速度です。

nehan7(js) vs nehan8(wasm)

プラットフォーム別の速度向上率は、こんな感じでした。

結果について

メモリが潤沢と思われるPCにおいては、WindowsMac組版速度が秒速30万字を超えているので、大抵の作品で1秒以内に組版が完了し、ストレスなく作品が表示されるものと思われます。

iPhoneも秒速16万字ぐらいの速度が出ているようなので、長い作品では2〜3秒は待たされてしまうかもしれませんが、概ねストレスなく表示されそうです。

最後にAndroidですが、これは古い機種(Android 10とか)のスマホが平均値を押し下げているみたいで、秒速7万字ぐらいになってしまいました(統計を取るときに、Android 12以降、みたいな縛りをつけたら良かったかもしれません)。

ちなみに秒速7万字ぐらいだと、40万字超えの作品(例えば夏目漱石の「こころ」とか)は5〜6秒ぐらい待たされることになり、そこそこストレスだと思います。

しかしそれでもjsよりは約8倍速いので、WebAssmbly化の効果は大きかったと言えるでしょう。

その他のプラットフォームでも、だいたい6〜9倍の速度向上があり、概ね満足な結果と言えます。

これでwasmからjsに非同期にデータを送信する方法があったら、もっと快適な読者体験を提供できるのですが…

nehan(縦書き文庫の組版エンジン)をWebAssembly化することで、約3倍の高速化を達成しました

縦書き文庫組版エンジンであるnehan(js製)をRustで書き換え、WebAssemblyで実行したところ、約3倍の高速化に成功しました。

現時点ですでに運用されています。

感想としては「本当は10倍ぐらい速くなって欲しかったけど、そこまでは速度が出ずにトホホ…」という感じです。

3倍なら良いではないか、と思われる方もいるかもしれませんが、青空文庫の長編小説なんかは、だいたい40万字ぐらいあり、そのjsでの組版時間は(すごく遅い端末だと)27秒に達することもあります。

ちなみに手元のノートPC(メモリ8G)でjsに組版させると5秒ぐらいです。

この5秒が1.7秒ぐらいになるのは嬉しくても、27秒が9秒になっても、あんまり嬉しくないですよね。

だから、ずっと10倍を目標にしてきたのですが…

まあこれから頑張って、Rust側のソースを最適化していこうと思います。

ビューアーの変更点

  • 全ページの計算が3倍速くなった
  • 作品を開いたとき、自動で前回に開いたページまで移動するようになった
  • 全ページ計算してから表示されるようになった(以前は先頭ページが計算できたら、すぐに表示されていた)
  • 数式の表示やコードハイライトの機能は削除した

注意してほしいのは、全ページの計算が終わるまで、先頭ページが表示されない、という新しい仕様です。

(2022/09/10 追記:解決しました!)

作品の長さに関係なく、高速に作品が表示されるようになりました - anti scroll

これが問題になるのは、文字数のすごく多い作品(100万字以上とか)を、すごく遅い端末(古いスマホとか)で開いた場合です。

古い組版エンジンだと、たとえ遅い端末でも、とりあえず先頭ページ「だけ」は高速に表示されたため、そんなに「待たされた感」はなかったと思います。

しかし新しいビューアーだと、全ページの計算が終わらないと作品が表示されないので、遅い端末だと待たされ感が凄いことになりそうです。

なんでこんな作りになってしまったかというと、ウェブアプリ(js)と組版エンジン(wasm)の間で、非同期にデータをやり取りするのが難しいからです。

正確には、 js側のデータを非同期でwasmに送信することはできそうですが、wasm側のデータを非同期でjsに送信するのが難しいのです。

以前の組版エンジンであるnehan7(js製)では、全体の組版速度は遅くとも、先頭ページが組版できたら、さっさとアプリ側に表示させることができました。

どっちもjsなので、非同期なデータのやり取りがしやすかったのです。

読者が先頭ページを読んでいる間、裏でこっそり残りページを計算し続ける、みたいなこともできました。

しかしWebAssemblyだと「ちょっとずつ計算した結果を、ちょっとずつアプリ側(js)に渡す」みたいな非同期処理が難しいので、全ページ計算してjsに渡すしかありません。

今後の展望

とまあ、いくつか頭の痛い問題はありますが、しばらく新しいエンジンを運用してみます。

とりあえず、新し目のスマホやPCで開くぶんには、ほとんどのケースで快適になるのも事実ですので。

nehan8のコードは、いずれGithubに公開したいと思っていますが、時期はもうちょい先になりそうです。

というのも、仮にソースを公開したところで、中身でRustの実験的な機能を使っている関係上、普通のRustコンパイラではコンパイルできないからです(最新の開発者版コンパイラが必要)。

そのへんのことで無用な混乱を生むのも面倒なので、Rust側が安定してから公開することにします。

開発後記

nehanの大きなバージョンアップは今回で8回目になりますが、今回ばかりは本当に心が折れそうでした。

いつも組版エンジンの書き直しは地獄なのですが、今回は言語がRustに変わったので、なにもかもが新しいことだらけで、しんどかったです。

去年の8月から着手して、今年の7月末に完了ですから、まるまる1年かかった計算になります。

jsからTypeScriptにしたときですらしんどかったですが、今回はその比じゃありませんでした。

ただRustは良い言語だなと思いました。慣れるまでは大変でしたが。少なくともC++よりはずっと書きやすい言語だと思います。

nehan-player v0.1.1を公開しました

nehan-playerの0.1.1を公開しました。

tategakibunko.github.io

変更点

  • NehanPlayer.initializeにて、cssTextオプションを指定できるようになりました。
  • ページ番号(ノンブル)の表示位置が、ページ送りメニューバーの中ではなく、本文画面の中に表示されるようになりました。

cssTextオプションについて

nehan-player要素のスタイルはcssFilesオプションで読み込むスタイルシート群を指定できるのですが…

NehanPlayer.initialize({
  cssFiles: ['path/to/my-style.css']
});

しかし、外部ファイルをわざわざ用意したくない場合は、cssTextオプションが便利です。

NehanPlayer.initialize({
  // メニューバーは非表示
  cssText: "#menu { display: none }"
});

ページ番号の表示位置の変更

これまでのページ番号は、ページ送りボタンと一緒にメニューバー内に表示されていました。

しかしこれを、本文画面の要素として表示させることにしました。

f:id:convertical:20210417152803p:plain
本文画面にページ番号を表示

このデザイン変更により「メニューバーは必要ないかな」というときに、安心して(スタイル定義で)消去できるようになりました。

f:id:convertical:20210417152831p:plain
メニューバーを消したデザイン

縦書き文庫のビューアーをリニューアルしました

縦書き文庫のビューアーを久しぶりにリニューアルしました。

いくつかあった不満点を、それなりに改善できたと思います。

旧ビューアーの不満点

  • 目次、登場人物、コメントなどの各種情報が確認しにくかった(下方向へのスクロールが必要だった)
  • 解像度の大きいPCで横幅が余ってしまっていた
  • 横書きが読みにくかった(縦書きを初期設定にしているので、横幅がでかくて読みにくかった)
  • 表示設定を変えると、先頭ページに戻されてしまっていた

新しいビューアーの改善点

  • 目次、登場人物、読者分析、コメント、お気に入りユーザー、しおりなどの情報が、上部のタブから簡単に確認できるようになりました。

f:id:convertical:20210328222945p:plain
上部のタブから各種情報が確認できるように

  • 見開きに対応したので、横書きが読みやすくなりました。

f:id:convertical:20210329063153p:plain
見開きで横書きも読みやすく

  • 目次の情報が作品の左上に表示されるようになりました。

f:id:convertical:20210328223030p:plain
目次の情報が左上に表示されるように

  • 画面サイズに合わせて、ビューアーの幅が勝手に伸張するようになった(レスポンシブになった)ので、解像度の大きいPCでも横幅が有効に使えるようになりました。
  • 表示設定を変えても、先に見ていた内容のページ(になるべく近いところ)に戻れるようになりました。
  • ページ送りボタンのUIを削除したので、縦幅を増やすことができました。
  • UIをマテリアルデザインに統一したので、スマホでの操作がより直感的になりました。

とまあ、こんな感じです。

その他、注意点

  • 新しいビューアーの表示設定は、右上の歯車アイコンから行います。

f:id:convertical:20210329092856p:plain
表示設定の変更

  • ビューアーを新しくした影響で、エディタ画面での「プレビュー」機能が(現在のところ)使えなくなっています。

プレビューは、いずれ復活させたいのですが、対応時期は未定です。

だれでも簡単にページ組版が扱えるウェブ・コンポーネント「nehan-player」を公開しました

nehanによるページ組版を簡単に扱えるウェブコンポーネントを作ってみました。

導入

テキストの内容を、サーバーからsrc属性で読み込む場合は、こんな感じになります。

<nehan-player writing-mode="horizontal-tb" width="responsive" src="/path/to/your-novel.html" height="500" font-size="16" layout="1x2">
</nehan-player>

<script src="https://cdn.jsdelivr.net/npm/nehan-player@0.0.2/dist/nehan-player.min.js"></script>
<script>
  NehanPlayer.initialize(); // 初期設定(オプションなし)
</script>

タグの中にそのまま内容を記述したい場合は、<div slot="content"> ~ </div>の中に本文を記述します。

<nehan-player writing-mode="horizontal-tb" width="responsive" height="500" font-size="16" layout="1x2">
  <div slot="content">
    <h1>はじめてのnehan-player</h1>
    <p>はじめてのテキスト</p>
  </div>
</nehan-player>
  
<script src="https://cdn.jsdelivr.net/npm/nehan-player@0.0.2/dist/nehan-player.min.js"></script>
<script>
  NehanPlayer.initialize(); // 初期設定(オプションなし)
</script>

ブログやホームページでも、本文にHTMLをそのまま打ち込める環境なら、問題なく導入できると思います。

はてなブログの場合は、管理画面から「デザイン設定」→「カスタマイズ」→「フッター」と進んで、<script> ~ </script>の部分をコピペしておくと、<nehan-player> ~ </nehan-player>の部分を入力するだけでOKです。ただHTMLをそのまま入力できるのはProプランのユーザーのみらしいですので、無料ユーザーはnehan-playerを記事中では使えないということになりますね…

ちなみにウェブコンポーネントということで、親ページからの影響を受けませんし、親ページに影響を与えることもありません。

完全に独立した表示要素として、安心して導入できます。

またレスポンシブにも対応しているので(nehan-playerタグのwidth属性を"responsive"にするだけ)、スマホ対応も簡単です。

なおnpmから使うときは、

npm install nehan-player --save

のようにインストールし、

import { NehanPlayer } from 'nehan-player';

NehanPlayer.initialize();  // 初期設定で使う(オプションなし)

で、呼び出せます。

initializeに与えるオプションついてはドキュメントを参照してください。

デモ

さて、ここで、はてなブログにウェブコンポーネントを貼り付けたかったのですが、どうやらHTMLをそのまま記述できるモードは、現在Proユーザーに限定されているようです。

なので、ちょっと裏技的ですが、はてなブログaboutページ(HTMLを入力できる)に、nehan-playerを貼り付けて見ました。

ひと手間が必要な場合

1. 配色を変えたい

一応、nehan-playerのページに、こちらで用意したテーマがありますので、利用するなり、編集するなりしてください。

使い方は、nehan-playerタグのtheme属性に、テーマファイルへのパスを記述するだけです。

<nehan-player (省略) theme="/path/to/your-theme.css"></nehan-player>

ただし、公式のテーマはどれも内部で、default-layout.cssっていう別のファイルを読み込んでいるので、テーマファイル単体だけじゃなく、default-layout.cssも忘れずにサーバーに設置してください。

2. 画面の左右をクリックしたらページ送りしたい

initializeを以下のように呼び出すと、画面の左右クリックでそれぞれのページに移動するようになります。

NehanPlayer.initialize({
  onClickLeftPage(player){
     player.gotoLeftPage();
  },
  onClickRightPage(player){
     player.gotoRightPage();
  }
});

3. 読み込んだテキストを、ちょっと編集したい

initializeonFetchContentを使います。

NehanPlayer.initialize({
     onFetchContent(src: string, content: string){
         if (src.endsWith(".txt")) {
            return content.replace(/^\n+/, "").replace(/\n/g, "<br>");
         }
         return content;
     }
});

4. Androidで縦書きを扱いたい

これはちょっと少し工夫が必要です。

というのも、Androidには縦書きに対応したフォントが入ってないこともあり、その場合は自分で提供しないといけないからです。

やることは2つで、

  • 縦書きに対応したフォント(例えばIPA明朝とか)をサーバーに置く
  • それを@font-faceで読み込むcssを「headタグ」に入れる(ウェブコンポーネントなのに)

です。

ちなみに2つ目で「cssをheadタグに入れる(ウェブコンポーネントなのに)」と書きましたが、どういうことでしょうか。

実はこのcss、本当はウェブコンポーネントの「内部」に入れないといけないのです(じゃないと外部のページに影響を与えないというウェブコンポーネントの趣旨と矛盾するから)。

が、どうも現状のブラウザは、どれもshadom DOMから@font-faceが読み込めないようなのですね。

上のリンクは2014年の議論ですが、どうやら技術的に、とても難しいことらしいのです。

で「ものすごい強い要請があるまでは、このままにしとこうや」って感じになってるみたいでして…

というわけで、これを回避するため、@font-faceを指定したファイルは、普通にHTMLのヘッダに読み込む必要があります。ようするに、

  • @font-faceを含まない普通のcss(テーマとか)は、ウェブコンポーネントの内部に読み込まないといけない(まあそうじゃないとウェブコンポーネントにする意味がないので、当たり前ですが)
  • けど、@font-faceを含むcssは例外。shadow DOMの外側(shadow host側って言うそうですが)じゃないと読み込めない。

ということです。

数式の表示とプログラムの色付け表示に対応しました

縦書き文庫で数式とプログラムの自動色付け表示ができるようになりました。

数式

数式は文の途中に差し込む場合(インライン表示)と、行全体を使って表示する場合(ブロック形式)のそれぞれで書き方が異なります。

文の途中に差し込む場合は、次のように書きます。

[math $sin$theta]は、日本語で「正弦」といいます。

書き方は[math 数式]です。

実際に表示するとこんな感じになります。

f:id:convertical:20210226170956p:plain

行全体を使って表示する場合はmathタグを使います。

<math>
c = $pm$sqrt{a^2 + b^2}
</math>

実際に表示するとこんな感じになります。

f:id:convertical:20210226171033p:plain

色々な数式の記述についてはKaTexのサイトを参照してください。

ちなみにKaTexでは\(バックスラッシュ)で書く記号を、縦書き文庫では$(ドルマーク)で書くことに注意してください。

プログラムの色付け表示

こんな感じでプログラムを書くと、言語(下の場合は javascript)に応じた色付けがされます。

<pre><code class="lang-javascript">
let a = "foo";
console.log("a is %s", a);
</code</pre>

もちろん javascript の部分は、プログラムの中身がhtmlなら htmlcssなら css などと書き換えてください。

実際に表示させると、こんな感じになります。

f:id:convertical:20210226171256p:plain

ちなみに投稿フォームにて、入力フォーマットの項目を「マークダウン記法」にすると、もう少し簡単に書けます。

```javascript
let a = "foo";
console.log("a is %s", a);
```

またプログラムについて別のページから参照させたい場合は、id属性をつけて、別ページからアンカーリンクを貼ると便利ですが、次のように書くとプログラムにid属性を与えることができます。

```javascript#program1
let a = "foo";
console.log("a is %s", a);
```

上の例ではprogram1というid属性を与えています。

まとめ

小説投稿サイトで数式やプログラムを記述することは多分ないでしょうが、技術的な内容や学術的な内容も書けるようになると面白いかなあと思って、対応してみました。

なお数式についてはKaTex、プログラムの色付けについてはhighlight.jsを利用しました。

どちらも素晴らしく使いやすいライブラリで、作者様には感謝の気持ちでいっぱいです。