pixiv insideは移転しました! ≫ http://inside.pixiv.blog/

トップページを刷新した話

こんにちは。Redmineバックログ・プラグインが好きで「Redmine使いやすいですよね!」と言ったら「Redmineを使いやすいって言ってる人を初めて見た」と言われたedvakfです。今年はバックログの年にしたいです。

最近pixivのログイン前トップページが新しくなりましたね。イラストを全面に押し出したクールなデザインと、ウィンドウをリサイズしたりするとウニウニ動くアニメーションは新鮮だったのではないでしょうか。常にログインする設定の人も一度はログアウトして見てみてください。(アニメーションはCSSアニメーションに対応してるブラウザだけですが)

僕はその部分のリリースには関わってないんですが、コードを読んだりしてどんなことをしているのか解説したいと思います。

(注:2013年1月末時点での実装についての解説です)

イラストデータ

使用するイラストは、現在は直近のランキングからランダムで30件ほど取得しています。

サーバー側はHTMLの中にこのようにデータを入れています。(読みやすいように改行を加えたりして整形しています)

1
2
3
4
5
6
<ul class="ui-brick">
  <li><a href="member_illust.php?..."><img src="http://i2.pixiv.net/img30/works/600x300/....jpg" alt="" data-width="1700" data-height="1200"></a></li>
  <li><a href="member_illust.php?..."><img src="http://i1.pixiv.net/img119/works/600x300/....png" alt="" data-width="644" data-height="700"></a></li>
  <li><a href="member_illust.php?..."><img src="http://i1.pixiv.net/img11/works/600x300/....png" alt="" data-width="700" data-height="980"></a></li>
  <li><a href="member_illust.php?..."><img src="http://i1.pixiv.net/img71/works/600x300/....png" alt="" data-width="550" data-height="670"></a></li>
  ...

data-width/data-heightは元画像のサイズです。pixivではサイズによる検索も提供していますので、元画像のサイズ情報をデータベースに保存しています。

サムネイルのURLにある/600x300/Apacheモジュールのmod_small_lightに渡されるオプションで、600x300の枠に合わせて画像がリサイズされます。Chromeデバッグツールなどでみると600x300の枠に合わせてリサイズされているのがわかります。

ここらへんに興味のある方は当ブログのYAPC::Asia 2012で「pixivのサムネイル事情」について発表してきました発表の動画をご覧ください。

また、極端な縦横比のサムネイルはこの段階で弾いています。

レンガUI

いよいよ本題です。

ページのロード時やウィンドウのリサイズ時に、これらの情報を使って綺麗に画像を敷き詰めています。社内ではbrick(レンガ)UIと呼ばれていたりします。

一行縦幅300px固定で画像を並べていき、最初に画面幅を超えたら、今度は行の縦幅を縮めることで一行が画面幅いっぱいになるようにし、次の画像はその次の行に流れるという仕様みたいです。

去年の年末にykskさん一晩でやってくれました

ソースは pre-login.js というファイルに書かれているこちらの箇所です。

1
2
3
4
5
6
7
8
9
var brick = {
  container : null,
  baseHeight: 300,
  margin    : 10,
  setup: function(target) {
    this.container = $('.ui-brick');
    if (!this.container.length) return;
    $(window).on('resize.brick', pixiv.throttle(this.update, this, 500));
    this.update();
  },

ロード時とリサイズ時にupdateというのを実行していますね。updateというのはその直下の60行の関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  update: function() {
    var position = 0,
      margin = this.margin,
      base_height = this.baseHeight,
      container = this.container,
      container_width = container.width(),
      container_height = 0,
      lists = $('li', container),
      items = $('img', container),
      item_length = items.length;
    loop();
    function loop() {
      var start_position = position, i = 0, width = 0, height, r = , w = , list, item, data, right_margin, target, current_width = 0, top;
      while (width < container_width) {
        item = items.eq(position);
        if (!item.length) break;
        data = item.dataset();
        r[i] = data.width / data.height;
        w[i] = base_height * r[i] + margin;
        width += w[i];
        ++i;
        ++position;
      }
      right_margin = width - container_width;
      height = ((w[0] - right_margin * (w[0] / width))) / r[0] - margin / r[0];
      items.slice(start_position, position).height(height);
      target = lists.slice(start_position, position).each(function(n) {
        var target = $(this);
        var css = {
          display: '',
          top    : container_height,
          left   : '',
          right  : ''
        };
        if (n === i - 1) {
          css.right = 10;
        }
        else {
          css.left = current_width;
          current_width += Math.floor(height * r[n]) + margin;
        }
        target.css(css);
      });
      if (position < item_length) {
        container_height += height + margin;
        loop();
      }
      else {
        target.hide();
        container.height(container_height);
      }
    }
  }
};

詳しく見ていきましょう。

updateの中ではクロージャー変数をいくつか定義した後、loopというクロージャー関数を呼んでいます。

loop関数の頭のwhileループでは、i番目の画像のアスペクト比(幅÷高さ)rと、 行の仮の高さbase_height(300px)を元にした画像の仮の幅wという配列を作っています。(marginは画像と画像の間のマージン:10pxみたいです)

wを順々に足していった値widthが、行の幅(container_width)を超えるとbreakするようです。当然行の幅は仮ではなく固定です。

次に、はみ出した部分の幅right_marginを元に画像の高さheightを決定しています。

ここまで読んで分かったのですが、よく見るとこの計算式ちょっとミスってますね…詳細は省略しますが、僕だったらこうします。あとでpull reqしておきます。

アルゴリズムの部分はほぼここまでで、あとはそれぞれ画像の入っているli要素のleftとtopをセットしています。行の最後の画像だったら左揃えではなく右揃えにしているようです。前述のミスにより余計な隙間ができてしまうためなのでしょう。

最後の行でなければloopを再度呼んでいます。最後の行なら中途半端な枚数になってしまうため、画像を非表示にしているようです。

このぐらい短い行でここまでの表現が実現できるのはすごいですね。

こぼれ話

トップページの刷新をリリースしたその日に、何度やっても他の画像にかぶってしまう画像が一枚だけありました。

てっきりこのJSのバグやImageMagickで画像の高さと幅を取得する部分のバグを疑ったのですが、実際には再投稿されて元画像のサイズが変わってしまったために起こったことでした。

元画像の高さと幅を含む当日のランキングの情報は1日1回KVSに保存され、それを参照していたのですが、キャッシュの生成後に再投稿されてサイズが変わってしまう画像があることを忘れていたのでした。

これを受けて、現在はランキング入りしたイラストの情報を短い間隔で定期的に更新するようになっています。

pixivでは細部にこだわるUIエンジニアを募集しています

この記事を読んで、「JSONとかじゃなくてHTMLに画像を入れてるのはページのロード中に画像のロードを開始させたいからだな」とか「naturalHeightを使わずにdata-hegihtとかを入れるのはonloadを待たずに実行できるからだな」と思ったあなたは正解です。pixivのUIエンジニアとして飽くなきUI談義に花を咲かせませんか?

職種紹介 | ピクシブ株式会社 採用情報