canvas と Web 仕掛け絵本

明けましておめでとうございます。 🙂

昨年のご愛顧も込めまして、「WordPress デザインワークブック」共著のコモモさんこと高橋朋代さんと Web 仕掛け絵本のサイトをつくりました。とてもかわいい感じになりましたので、良ければ見てください!

バッタになりたい魔法使い – pararaehon.com

PC/Mac はモダンブラウザ(IE8 以下はごめんなさいです)に対応しています。

モバイル系は iOS は 6 以降の Mobile Safari、Android は 4 以降の Chrome が良い感じで見れます。Android 2 の方は Firefox 17 を使うとゆっくりですが動作します。3G 回線の場合は、ちょっと読み込みに時間かかりますので、白くなったりしたらリロードしてみてください。キャッシュするときれいに動きます。

iPhone/iPad は 4S 以降(Apple A5)でフルスペックで動くはずです。すごい性能。iPhone の場合は Retina ででます。絵本的になりますので iPad3 もおすすめです。ベンチマークにもどうぞ。

自分の本職はバックエンド側の処理、業務系アプリの処理やインフラなどなのですが、WordPress を含めこういった Web サイトをつくる場合には、芸術性のあるデザインやブラウザフロントエンド処理が不可欠です。

WordPress デザインワークブック” の執筆ではこの部分にコモモさんの全面支援をもらっていますが、”バッタになりたい魔法使い”では絵をコモモさんに、ひろましゃはお話と、なんと初 JavaScript をやっております。 動きも面白いですので良ければその辺もご覧ください。 🙂

さて、去年はこのブログで「WordPress 徹底解析」シリーズを週1でやろうと始めておりましたが、少し忙しくなってしまい止まってしまってごめんなさい。2月くらいからまた時間が出来ると思いますので再開したいと思います。

今年もどうぞよろしくお願いいたします!


WordPress ではないですが、せっかく Web 系のネタですのでこの仕掛け絵本の仕組みを解説したいと思います。 まずは動作見て頂ければ。スクロールを上下するとぴょこぴょこ動くのが分かると思います。 🙂

バッタになりたい魔法使い

Web 仕掛け絵本で実現したかったことは2つでした。

  1. 60FPS のフルフレームで動作させる
  2. スマートフォン・タブレットでも動かす

絵本という前提があるので、コンピュータ的なフレーム落ちしたガタガタした動作は物語の世界を壊すと考え、ブラウザに 1/60 秒フレーム描画(canvas & requestAnimationFrame)をさせています。また、スマートフォン、タブレットでみれたら絵本っぽいなというのも考えました。

このため、canvas と requestAnimationFrame(もしくはベンダープレフィックス)が使えないブラウザでは動作しません。IE で言えば IE9 以降に対応します。ちなみに IE9 は結構速かったです。スマートフォン系では iOS6 以降、Android 4(標準ブラウザもしくは Chrome、Firefox17)が動作します。iOS5 の方、ごめんなさい…

この仕組みにパララ絵本という名称をつけていて、いわゆるパララックス効果も入っているのですが、通常のパララックスとは異なります。というのも、普通のパララックスはブラウザの縦座標(scrollTop)に対して、描画オブジェクトの位置を決めますが、パララ絵本ではスクロール位置を動かすタイミングとしてだけ使って、あとはタイマー処理しています。これは 60 フレーム描画をするためと、スマートフォン(Mobile Safari)がスクロール中動作にイベントが発生しないことに対する仕様です。

実際の実装ですが、絵本のコマに対して canvas タグを対応させ、CSS では canvas を display: block; してブラウザの描画領域を全て埋め、横幅、余白を含め全ての描画制御を JavaScript から行っています。たとえば、背景画像横幅が足りなければ CSS で真ん中にもっていくのではなく、”白”を画像の左右に描画しているイメージです。

ブラウザにやらせるよりも自前で制御したほうが、必ず見せたい領域を中央にもってくる処理や、 devicePixelRate(retina)の対応など、描いてもらった絵を忠実に再現させるのが楽だったというのが理由です。(このためちょっとブラウザリサイズの処理は遅いです)

<body id=&quot;parara&quot;>
<canvas></canvas>
<canvas></canvas>
...
</body>
body {
    margin: 0;
    padding: 0;
}

canvas {
    display: block;
}

JavaScript のソースはオーソドックスな、いわゆるゲーム描きです。requestAnimationFrame(render) と通常タイマー(update)の二つをつかった 1/60 処理です。

/**
 * Event initialize.
 */
Scene.prototype.start = function() {
    this.hack();
    window.addEventListener('load', this.init.bind(this), false);
    window.addEventListener('resize', this.init.bind(this), false);
    window.addEventListener('orientationchange', this.init.bind(this), false);
    window.addEventListener('scroll', this.update.bind(this), false);
    window.setInterval(this.update.bind(this), 1000 / ANIMATION_FPS);
    this.render();
};

/**
 * render.
 */
Scene.prototype.render = function() {
    requestAnimFrame(this.render.bind(this));
    // viewport range render.
    ...
    ...

...
/**
 * requestAnimationFrame wrapper.
 * context invalid?: Illegal operation on WrappedNative prototype object
 */
window.requestAnimFrame = (function() {
    return window.requestAnimationFrame
    || window.webkitRequestAnimationFrame
    || window.mozRequestAnimationFrame
    || window.oRequestAnimationFrame
    || window.msRequestAnimationFrame
    || function(/* function */callback, /* DOMElement */element) {
        window.setTimeout(callback, 1000 / ANIMATION_FPS);
    };
})();

オブジェクトの設計は、複数あるコマ(Scene)、コマ(Page)、コマ内の描画オブジェクト(Actor)と分割し、Scene ではブラウザのリサイズやスクロール位置処理と update render タイミングをとる処理、Page では Actor の実座標や動作タイマー(tick)制御、Actor は描画処理(canvas.drawImage)という感じになっています。

描画オブジェクトも全てビットマップ(Image)ですので、これらに対する DOM の操作はありません。全て canvas コンテキストに描いています。

Actor の座標はそれぞれに設定された動作開始スクロール位置からタイマーで +1 増加を始める tick 値によって決まり、逆スクロールでは時間(tick)を巻き戻すことで元の配置に戻るようにしています。つまり x += 1; とやるわけではなく x = tick とするイメージです。

隠しコマンドで「d」キーを押すとデバッグ表示になるようにしています。たぶん見るとすぐ分かると思います。

また、Actor の座標に関しては PararaEhon.js 初期化時にコンストラクタで定義できるようになっており、update や efect(drawImage する直前)をメソッドを処理時に差し込み、仕掛け絵本ライブラリとして使えるようにしています。

/**
 * バッタになりたい魔法使い.
 *
 * @author komomo and hiromasa
 * @version 1.0
 */
new PararaEhon.Scene({
    id : 'parara',
    width: 960,
    height: 600,
    book : [
        /**
         * ページ01
         */
        {
             bg: {
                 image: 'images/page01/01_bg.png',
                 fixed: true
             },
             wizard: {
                 image: 'images/page01/01_m.png',
                 x: 50,
                 y: 25
             },
             .....
        },
        /**
         * ページ02
         */
        {
             .....
             hitsuji01: {
                 image: 'images/page02/02_hitsuji01.png',
                 x: 15,
                 //y: 320,
                 start: 200,
                 update: function() {
                     this.y = 600 + (1 - Math.exp(-6 * (this.tick / 60))) * -280;
                     if(this.tick > 60) this.complete = true;
                 }
             },
             .....
/**
 * Constructor.
 */
Actor = function(context, object) {
    .......
    this.update = this.update ? object.update.bind(this) : null;
    this.effect = this.effect ? object.effect.bind(this) : null;
    .......
};

さて、キモとなる canvas.drawImage ですがブラウザによっていろいろ面白い動作がありました。Gecko や Webkit のソースまで見ていないので本当に見た目だけの判断です。間違っている可能性大きいです。

  1. Firefox17 と IE9 は スクロールで見えなくなっている canvas にもまじめに drawImage している?(見えているコマだけ render したら速くなりました)
  2. Firefox17 の Mac と Windows 版は描画が少し遅い。Linux 版(しかも性能の劣る機械)だとフルルフレームで描画される。(本当にこれはなんとなくなのですが Linux 以外は Image が GPU じゃなくてメインメモリから転送されているような。うまく VRAM に乗らなかった?気のせい?)
  3. Firefox では drawImage の引数で画像範囲外を誤って転送させても動くが、他のブラウザだと動作しない。(動かない動作は OpenGL で同様の間違いをしたときの動作に似ている)
  4. Mobile Safari に過激に複数の canvas をレンダリングさせると落ちる時がある
  5. Android Firefox 17 が結構がんばって canvas 描ける。(遅いですが)
  6. 速度の印象は Safari < Chrome < Firefox17 = IE9 という感じでした。がんばれ Firefox!(もうすぐでるであろう Firefox18 / IonMonkey が楽しみです)

なにぶんにもモダンな JavaScript をかくこともブラウザもほとんど初めてで、押し迫る期限の中座標処理でうひょ〜となってしまった部分もあり、まだまだ不出来ですがもし興味ありましたらソースを見てみてください。 🙂

PararaEhon.js

maho.js

というわけで。

[tegaki]みんなバッタになっちゃえ![/tegaki]

canvas と Web 仕掛け絵本」への12件のフィードバック

  1. すごいです><
    僕も今似たようなものを作りたいと思っているのですが、なかなかうまくいかず、悩ましい日々が続いています。
    バッタになってどっかに飛んでいきたいでっす!!
    見て楽しかったです^^
    ありがとうございます!!

  2. 見てくださってありがとうございます! バッタになりたい・・・。(>_<)

    ちぇきちぇきさんの作品できたら教えて頂ければと思います。
    とても楽しみですっ 🙂

  3. この場所でのコメントではないのですが、許していただけると思い、
    以下ヒントを戴ければと思い、お願いを書いてみました。

    『プラグインwp-otenkiが魅力的で便利だったのでずっと使っていました。
    しかし、先月末にライブドアの天気情報のRSS送信が終了したそうで、
    現在はお天気アイコンが表示されない状況です。
    http://weather.livedoor.com/help/restapi_close?city=113&day=tomorrow

    近日中にRSSからJSDN形式などへのバージョンアップされるのでしょうか

    たいへん恐縮ですが、現在の見通しをコメントいただければ幸いです』

  4. Dachs飼主さん、こんにちは。:)

    仕様変更確認しました。
    それほど難しくない修正ですので、今月中くらいには修正してリリースしたいと思います。

    ご連絡ありがとうございました!

  5. ひろまさ さま

     おはようございます。wp-otenkiでご質問させていただきました
    Dachs飼主です。
     今回の疑問に早速ご対応いただき、ありがとうございました。
    http://wppluginsj.sourceforge.jp/wp-otenki/
    上記URLのご説明内容にしたがい、otenki.phpをインストールし
    PLUGIN設定し、wp-otenki ディレクトリを全て削除して、
    テーマテンプレート(index.html)のdo_actionを the_otenki 
    に変更して、地域コード設定しましたら無事にお天気アイコンが
    表示されました。うまく、できたのですが・・・・
     
     私の不手際からか、3月までの旧wp_otenkiで表示されていた
    お天気アイコンが消えてしまったのですが、
    これは救えないのでしょうか。

    以下のご説明最下行は、わたしの判断ミスでしょうか。

    # 旧バージョン(wp-otenki 1.12)をお使いの方は、お手数ですが上記の部分の引数を ‘wp-otenki’ から the_otenki に変更してください。
    変更することで新旧バージョンの情報が双方とも表示されるようになります。

  6. ひろまさ さま

     毎度お騒がせしております。
    3月以前のお天気アイコンがでない件、
    以下のように復元しましたら、新しいアイコンとともに併記して表示されました。
    ◎プラグインwp-otenkiを復元
    ◎テーマテンプレート(index.php)のdo_actionをthe_otenki にするとともに、
     wp-otenki のdo_actionも追加して記述して実行した

     ら、表示成功しました。

    日付判断チェックをテーマテンプレートの該当部分に入れれば、
    解決すると思います。
    ありがとうございました。

  7. ご連絡ありがとうございました!

    ご指摘の通り、otenki.php の do_action(‘the_otenki’); のみで新旧両方出力される意図でつくっておりましたのですが、どうも下位互換の機能がうまく動作せず、古い形式のものが出力されなかったようです。

    WordPress や PHP に精通されているとお察しいたしまして、もし良ければなのですが、otekin.php の 192 行目以降に compat という下位互換用のメソッド(旧データを取得して表示する部分)があるのですが、見て頂いても良いでしょうか。

    var_dump($otenki); を追加しまして変数が取れているか、その内容によって if の分岐がうまくしたまでたどり着いているかが確認したい部分です。

        function compat() {
            $otenki = get_post_custom_values('wpotenki');
            if(!is_array($otenki)) return;
            var_dump($otenki); /* 追加 */
            /* 以下略 */
        }
    

    実はこちらのサイトでは旧データもうまく表示していまして、何かしら値の取得か正当性の確認処理で差がでていると考えています。(もし可能であれば return をしている if を取り外すなど、動きをみていただければと思います)

    新旧両方のプラグインを有効にして動作しているということで、了解いたしました! もしお手すきの際がありましたら、上記、ご確認お願いできたらと思います。

    (実は旧プラグインが有効の状態であると、記事公開時に LWWS に古い形式の値を取得しにいき結果エラーとなる、若干無駄な処理が動いてしまうため、できれば新だけで動けばよいなぁと思っております。)

    以上、よろしくお願いいたします!

  8. ひろまさ さま

     うまくいきました。
    パラメーターのダンプをみてもわからなかったので、みてはいませんが
    ご指示のリターンを省略したら、wp-otenkiを削除してもotekin.phpだけ
    で、先月以前も昨日以降もお天気アイコンが表示されました。
    これで、すっきりしました。ありがとうございました。
     速やかで親切な回答アドバイスに改めて感謝いたします。

    * (下位互換用)
    */
    function compat() {
    $otenki = get_post_custom_values(‘wpotenki’);
    if(!is_array($otenki)) ;  
    $otenki = unserialize($otenki[0]);
    if(!(isset($otenki[‘mark’])   /* 削除 return */
    && isset($otenki[‘otenki’]))) return;

  9. ご連絡ありがとうございました!

    以前より少し格納されているデータに対してエラーチェックをきつくしていたのですが、そちらでひっかかっていたようです。

    こちらでも 3月末より前の表示を見ることが出来ました。旧お天気データは増えることはありませんので、問題ないと思います!

    お手数おかけしましてすいませんでした。
    今後ともよろしくお願いいたします。 😀

  10. ひろまさ さま

     追加です。
    お役に立つかどうかわかりませんが、
    パラメータのダンプを新旧表示しておきます。
    *****************************************************************************
    昨日(新形式)のダンプ
    array(1) { [0]=> string(82) “a:5:{s:6:”otenki”;s:0:””;s:3:”max”;N;s:3:”min”;N;s:4:”mark”;N;s:5:”title”;s:0:””;}” }
    *****************************************************************************
    旧(旧型式)のダンプ
    array(1) { [0]=> string(187) “a:5:{s:6:”otenki”;s:6:”曇り”;s:3:”max”;s:0:””;s:3:”min”;s:0:””;s:4:”mark”;s:48:”http://image.weather.livedoor.com/img/icon/8.gif”;s:5:”title”;s:34:”千葉県 千葉 – 今日の天気”;}” }
    *****************************************************************************

  11. どうもありがとうございます!

    とても解析に役立ちます。 調査しまして何か気がつきましたら、ブログのほうに再度ご連絡したいと思います 🙂

  12. ピンバック: スクロールすると「しかけ絵本」のように物語が動き出すサイト「バッタになりたい魔法使い」 | web sign*

コメントを残す