読者です 読者をやめる 読者になる 読者になる

anti scroll

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

nehan.jsで拡張タグを作る

拡張タグを作るサンプルとして、次のようなタグを作ってみます。

<circular>
  <div>1時ですよ!</div>
  <div>2時ですよ!</div>
(省略)
  <div>11時ですよ!</div>
  <div>12時ですよ!</div>
</circular>

で、例えばこのタグを表示した時刻が1時だったら、次のようになるものとします。

f:id:convertical:20150408134943p:plain

動作デモ

実際に動作させてみた結果は、こんな感じになります。

デモを表示する、を押すと文字がくるっと回転しながら表示され、現在時刻だけが赤くなります。

上手く動作しない場合はページをリロードしてみてください。

基本方針

  1. Nehanのグローバルスタイルにてcircularタグをブロックタグとして登録
  2. circularの子供エレメントの中で、現在時刻に該当する子の文字色を赤に
  3. それぞれの子を円の中心に移動させてから、時刻数に応じて回転させる

すべてのソースは以下のリポジトリに登録しておきました。

github.com

なので、この記事ではポイントだけを解説します。

タグの登録

Nehan.setStyleを使うと、グローバルタグを登録できます。

組版エンジンごとに登録することもできるのですが、今回は解説しません。

circularをブロックタグとして登録したいので、次のようにしてみます。

Nehan.setStyle("circular", {
  display:"block",
  background:"wheat",
  measure:"280px",
  extent:"280px",
  "border-radius":"280px",
  margin:{after:"2em"},
  // 円の直径(280px)を表示する余白がなければ改ページ
  onload:function(ctx){
    var items = [];
    var rest_extent = ctx.getRestExtent();
    var extent = parseInt(ctx.getMarkup().getAttr("extent", "280px"), 10);
    if(rest_extent < extent){
      ctx.setCssAttr("break-before", "always");
    }
  }
});

上のonloadは、circularセレクタの読み出し完了をフックする関数です。

ここで「十分な余白があるかどうか」をチェックする処理が埋め込まれています。

なぜなら、このタグを表示するには、12個ある子供のdivがすべて表示されないといけないからです。

10時までしか表示されなかったら時計になりませんので。

というわけで、十分な余白がなければ、スタイルに改ページを追加する、という処理がされています。

時刻を記述した行の処理

各時刻(「〜時ですよ!」の行)を指すセレクタcircular divです。

Nehan.setStyle("child div", {
  "line-height":"1em",
  color:function(ctx){
    var child_index = ctx.getChildIndex();
    var cur_hour = new Date().getHours() % 12;
    return ((child_index + 1) % 12 === cur_hour)? "red" : "black";
  }
});

colorのところでは、各子供が現在時刻に該当する時刻を表す行なら赤を返すような「関数値」が設定されています。

各時刻を回転

あとは、各行が時計上で位置する場所へと回転させるだけです。

各行を親のボックスサイズのブロックレベルサイズの半分(*1)だけ移動し、そこからそれぞれ30度(=360/12)ずつずらして表示させたらよさそうです。

*1 - 正確に言うと親ブロックのブロックサイズの半分から、さらに行そのものの高さの半分を引きます。

Nehan.setStyle("child div", {
  onblock:function(ctx){
    var parent_style = ctx.getParentStyleContext();
    var is_vert = ctx.isTextVertical();
    var child_index = ctx.getChildIndex();
    var child_count = parent_style.getChildCount();
    var line_height = ctx.getStyleContext().getFontSize(); // line-height:"1em"
    var parent_extent = ctx.getParentBox().getContentExtent();
    var trans_extent = Math.floor((parent_extent - line_height) / 2);
    var unit_degree = Math.floor(360 / child_count);
    var rotate_degree = child_index * unit_degree + (is_vert? 30 : 120);
    var $dom = $(ctx.dom);
    var translate = is_vert? {translateX:trans_extent + "px"} : {translateY:trans_extent + "px"};
    var rotate = {
       opacity:1,
       rotateZ:rotate_degree + "deg"
    };
    $dom
    .css("position", "absolute")
    .css("opacity", 0)
    .velocity(translate)
    .velocity(rotate);
  }  
});

ちなみに、回転処理をアニメーションさせたかったので、その部分についてはvelocity.jsを利用しました。

onblockについては説明が必要でしょう。

circle divセレクタに対するonblockは、このセレクタを表すエレメントがブロックレベルとしてDOM化されたタイミングをフックしています。

どういうことかというと、実はブロックレベルの下には、匿名ラインボックスという更に下のレベルのDOM化があり、そちらはonlineとして読み出されるのですが、それと区別しているわけです。

例えば<p>hoge</p>というマークアップがあった場合、pはブロックの中にさらにhogeというテキストを表す匿名ラインボックスを含んでいます。

ちなみにoncreateを呼ぶと、ブロックレベルでもラインレベルでも共通で呼ばれます。

今回は行エレメントがブロックレベルで作成された時だけに興味があったので、onblockを使いました。

まとめ

このサンプルを見ればわかると思いますが、レイアウトエンジンレベルでセレクタ定義が出来ると、セレクタの定義とそのセレクタに対するアクションが同時に書けます。

jQueryなどでは、画面に既に表示されたものに対してセレクタ検索をかけますが、このセレクタの検索はレイアウトエンジンからすれば(スタイルを読み出す際に)既に検索済みなので、そのタイミングでjsのコードを埋め込めるのは効率面からも有利でしょう。

さらにスタイルの設定を読み出す際に、スタイルプロパティの値を関数にできるのも大きな強みです。

このサンプルでもcolorの設定は、new Dateして、現在時刻を見た上で色を決定していますね?

circleタグも、表示前に組版状況を見て、余白がなければ動的に改ページしています。

このようにjsと協調しながら、動的にスタイル設定を切り替えることができるのは非常に面白いと思うのですが、どうでしょうか。