anti scroll

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

トップページなどから直接しおりを開くことができるようになりました

f:id:convertical:20150922194901p:plain

PCページだけですが、トップページなどのヘッダーバーから、直接しおりを開くことができるようになりました。

スマホで無効化されているのは、単に画面のサイズに収まりにくかったからです…

なにか良いレイアウトを思いついたら、スマホからも使えるようにしたいと思います。

ビューアーにページ送りのシークバーが付きました

ビューアーにシークバーが付きました。

つまんで動かしてページ移動できます。

f:id:convertical:20150920132136p:plain

縦書きの時は右から左に、横書きの時は左から右に動かします。

ただしIEだと、縦書きの時に変な数字がポップアップします。

どういうことかというと、html5rangeは左がminで右がmaxという仕様で固定化されているので、縦書きの時はこれを逆にするために「逆順の数値」を内部的にセットしているのですが、これがそのままポップ・アップされてしまうんですね。

その他のブラウザだと、こういう数値は表に出てこないのですが…

今のところ、この表示を制御する方法はなさそうなので、なんともかんとも、という感じです。

ビューアーを更新し、スマホからもPCと同じ機能が使えるようになりました

先日の管理画面に続き、ビューアーも全ての環境で統一し、スマホからもPCと同じ機能が使えるようになりました。

これでスマホからも、しおりを挟んだり、お気に入りに登録したり、コメントを投稿したりすることができます。

その他の新しい機能

まずコメントに返信機能が付きました。

f:id:convertical:20150912213417p:plain

ビューアーのしおりボタンから、しおりのセーブとロードが両方できるようになりました。

f:id:convertical:20150912213443p:plain

最後に細かいことですが、画面の左右をタップすることでページ送りできるようになりました(スマホのみ)。

エディターに字下げ、引用、改ページ、セリフ、補足ボタンを追加

エディターに字下げ、引用、台詞、補足、改ページボタンなどを追加しました。

f:id:convertical:20150826204020p:plain

改ページについては説明不要かと思いますので、それ以外について少し補足します。

字下げ、引用の違い

字下げ、引用は選択範囲を選んでから押してみてください。

「字下げ」と「引用」の表示の違いはこんな感じです。

f:id:convertical:20150826204139p:plain

「補足」(チップリンク)について

補足を使うと、次のようにクリックで説明ダイアログが出るリンクを表示できます。

f:id:convertical:20150826204555p:plain

ここで「パソコン」をクリックすると

f:id:convertical:20150826204617p:plain

のようなダイアログが出ます。

使い方としては、対象のテキストを選択してから「補足」ボタンを押します。

すると次のようなダイアログが出るので、そこに補足説明を入力して下さい。

f:id:convertical:20150826204717p:plain

台詞(セリフ)ボタンについて

キャラクタを作成して、作品に「キャスティング」すると、エディタに次のようなドロップダウンボタンが出現します。

f:id:convertical:20150826204825p:plain

台詞を言わせたいキャラクタをクリックして、文章の部分を追加すると、次のように脚本形式のような表示になります。

f:id:convertical:20150826205008p:plain

これで各キャラクタをクリックすると、キャラクタの説明が出ます。

f:id:convertical:20150826205108p:plain

制限事項

外部への埋め込み作品では、台詞表示とチップ表示が出来ません。

いつの間にかFileAPIのreadAsBinaryStringがオワコンになっていた。今後はreadAsArrayBufferで。

タイトルで全てを言い尽くしてしまいましたが、経緯をば。

不具合報告で「IEで画像のアップロードができない」ってのがあったので調査してみたら、readAsBinaryStringでコケてました。

調べてみると、どうもIE系列ではreadAsBinaryStringそのものが存在しないらしく…

なんでなのかというと、そもそもこのAPI、将来的になくなる方向らしいのです(実際にw3cのドキュメントからも消えてる)。

というわけで、readAsBinaryStringreadAsArrayBufferに置き換えたら、IE11でも(もちろんChrome/Firefoxでも)問題なく動いたのですが…

非推奨になった理由

で、なんで非推奨となったのかというと、単に効率が悪いからだそうで。

https://lists.w3.org/Archives/Public/public-webapps/2011OctDec/1497.html

要約すると「このメソッドは型付きの配列(ArrayBuffer)が標準化される前に作られた実装で、バイナリデータを文字列に変換するから効率が悪い」とのこと。

今は型付きの配列は既に実装されていて、それを読み取る関数(readAsArrayBuffer)も実装済み。したがって、非効率的な古いAPIに存在意義はない(から削除するべきだ)、ということみたいです。

というわけで、これからはreadAsArrayBufferなのです。

Blobを作らなくても、fileオブジェクトならそのまま渡せます。

RxJSとvirtual-domで、拡張可能なreactiveアプリケーションを作るリアクティブ・アダプター - inga

inga(因果)は、軽量で拡張可能なリアクティブ・アダプターです。RxJSとvirtual-domを使っています。

github.com

よくあるTodo-MVCは100行ぐらいです。

inga/todo-mvc.js

特徴

  • 軽量なこと。全ソース合わせても100行ちょっとしかないです。
  • プラグインで拡張可能です。
  • 副作用の処理をthunkで扱うストリームがデフォルトで付いているので、大抵のイベント処理はこれで済みます。

簡単なサンプル

例えばクリックする度にカウンタが増える、みたいなのは次のようになります。

var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{clickCount:0}
  }),
  virtualView:function(ctx){
    return h("button", {
      "ev-click":function(ev){
        ctx.action.emitUpdater(function(state){
          state.clickCount++;
        });
      }
    }, ctx.state.clickCount);
  }
});

プラグインを使ったサンプル

例えばinga/plugins/scroll-posを組み込むと、こんな感じでレンダリング・コンテキストの状態オブジェクトにscrollPosが合成されます。

var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{scrollPos:{x:0, y:0}},
    // plugin 'scroll-pos' combines 'scrollPos' to status object.
    plugins:[
      {module:require("inga/plugins/scroll-pos"), options:{combine:true}}
    ]
  }),
  virtualView:function(ctx){
    return h("div", [
      h("p", "x = " + ctx.state.scrollPos.x),
      h("p", "y = " + ctx.state.scrollPos.y)
    ]);
  }
});

自前のストリームを合成するサンプル

デフォルトのストリームに、自分で作ったストリームを組み込む場合は次のようにcombineアクションを定義します。

var Rx = require("rx");
var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{width:window.innerWidth},
    actions:{
      // extend default stream by your own source.
      combine:function(initial_state, upstream$){
        var width$ = new Rx.Observable.fromEvent(window, "resize")
          .debounce(250)
          .map(function(event){
          return window.innerWidth
        })
        .startWith(window.innerWidth);

        return upstream$.combineLatest(width$, function(state, width){
          state.width = width;
          return state;
        });
      }
    }
  }),
  virtualView:function(ctx){
    return h("p", "current window width = " + ctx.state.width);
  }
});

その他

ライセンスはMITです。

管理画面がスマホにも対応しました

管理画面リニューアルのお知らせです。

スマホとPCでUIを統一し、どちらからも同じUIで使用できるようになりました。

エントリーの一覧には、カードUIを使用しています。

PCユーザーからすると、元のテーブルレイアウトのほうが一覧性が高いかもしれませんが、どうしてもスマホとの共存が難しくなってしまうので、今回はカードUIを採用しました。

リニューアルした背景

最近はアクセスの半分近くがスマホ経由で、執筆作業もスマホで行う人が増えてきていたので、それに対応した感じです。

注意事項

新管理画面には、まだスタイル編集画面が入っていません。

バグを見つけたら

通常のメジャーなブラウザは動作を確認していますが、古いAndroid系のブラウザなどでは、わかりません。

ちなみにIE8以下、Safari5以下のサポートはやめました

もし不具合等がありましたらメールフォームから連絡くださると助かります。

nehan.jsのセレクターマッチング処理を高速化

nehan.jsのセレクタのマッチング処理を高速化しました。

先日サポートした行末揃えは、有効にすると20%ほど遅くなりましたが、今回の修正によって15%ほど速くできたので、少し戻すことができました。

前々から、機会があったらやっておこうと思っていた処理でもあったし、ちゃんと効果があって良かったです。

最適化の内容

やっていることは単純で、セレクタのマッチング結果を(キャッシュできるものに限り)キャッシュしただけです。

ただしマッチ結果には、キャッシュしやすいものとしにくいものがあるので、その辺のことについてちょっと説明します。

属性セレクタのマッチング結果はキャッシュが困難

属性セレクタは、マークアップや親が同じでも、マークアップが現れる相対的な文脈によってマッチしたりしなかったりするので、常に同じ値が返ってくるとは限りません。

これに対応するためには、属性クラスの情報をキャッシュキーに盛り込む必要があります。

しかし例えば次のようなマークアップを考えてください。

<div>
  <p class="foo">text0</p>
  <p class="foo">text1</p>
  <p class="foo">text2</p>
</div>

<div>
  <p class="foo">text3</p>
  <p class="foo">text4</p>
  <p class="foo">text5</p>
  <p class="foo">text6</p>
</div>

ここでtext2text6を内容に含むpタグをそれぞれp2p6と名付けることにします。

さて、p2p6のキャッシュキーはどうなるでしょうか。

両者の特徴をフルに含んだキャッシュキーを設計するとなれば(擬似的な記述ですが)こんなかんじになりそうです。

// p2のキャッシュキー
div>p.foo[nth-child=2, first-of-type=false, last-child=true, last-of-type=true, ...]

// p6のキャッシュキー
div>p.foo[nth-child=3, first-of-type=false, last-child=true, last-of-type=true, ...]

殆ど同じキャッシュキーですが、nth-childの値だけが異なります。

しかし両者は木構造の位置として、性質的には殆ど同じものです(divの直下にあるpタグの最後の要素)。

なので、p2p6のどちらも、次のセレクタにマッチします。

div>p.foo:last-child{ font-size:2em }

しかし、nth-childの値が違うだけで、違うキャッシュキーとなるわけです。

これを同じキャッシュキーにするわけにはいきません。なぜなら属性クラスはnth-childを使っている「かも」しれないからです。 属性クラスをキャッシュして(他の場所で)再利用するなら、確実に属性クラスが表現しうる全ての特徴が一致している、という保証が必要なのです。

というわけで、仮にp2のマッチ結果をキャッシュしたところで、同じ結果がp6のマッチング時に再利用されることはありません。

キャッシュが効果的なのは、どれだけ再利用されるかにかかっているのですが、こうやってキャッシュが分散すると、ヒット率が落ちる上に容量も食うわけで、かえって性能を損ねるおそれが出てきます。

というわけで、キャッシュキーが分散しがちな属性セレクタについては、都度マッチする方針で統一しました。

ただし、属性クラスを含むセレクタは別の領域に保存しておくなどして、前もって検索範囲を狭めておく、というようなことはしています。

キャッシュできる属性クラスのセレクタを書く方法

しかし、上記のようなp2p6で、同じマッチ結果がキャッシュされるようなセレクタが書きたい! と思いませんか?

nehan.jsならこれができます。

nehan.jsでは、cssプロパティに関数値を設定できるので、先ほどと同じ場所(div>p.foo:last-child)を、属性クラスを使わずに指定することがでるのです。

例えば以下のように、属性クラスの判定を遅延評価の関数に追い出すことで、セレクタの詳細度が減った「キャッシュヒット率の高い」セレクタを宣言することができます。

"div>p.foo":{
  onload:function(ctx){
    if(ctx.isLastChild()){ // is last-child?
      return {fontSize:"2em"};
    }
    return null;
  }
}

大まかなものでマッチさせておくことでセレクタのマッチング結果をキャッシュさせ、last-childのような詳細で決まるスタイルについては、遅延評価で取得させるわけです。

一見すると、スタイル値の取得に際し関数呼び出しのオーバーヘッドが発生するように見えますが、これは属性クラスの条件判定をセレクタ検索時に計算するか、検索後に計算するかの差でしか無いので、性能差には結びつきません。

属性クラスバージョンと遅延評価バージョンの性能差

とはいえ、セレクタ数が少ないケースでは、どちらの書き方をしても、たいして性能の差はありませんでした。

字数が40万字、セレクタ数が5000個ぐらいのラインを超えると、さすがにキャッシュの効く遅延評価バージョンの方が22%ぐらい速かったですが…

マージ後のスタイル値はキャッシュ出来ないのか

マッチしたセレクタは、specificityの順にソートし、最終的なスタイル値へとマージされます。

この最終的なスタイル値をキャッシュできると更に良いのは言うまでもないことですが、nehan.jsではcssの値に関数を指定できる仕様がある関係上、これが難しくなっています。

関数値で指定された値は実行時に値が決まるサンクなので、ある特定のタイミングで呼ばれて返ってきた結果を、永続的な最終値としてキャッシュすることはできないからです。

そもそも仮にキャッシュしたとしても、スタイル構築で時間がかかるのは圧倒的にセレクタ検索の部分なので、大した効果は期待できないと思われます。