PhantomJSでサムネイルの取得を自動化する

前に作ったウェブサイトギャラリーで使ってるサムネイル画像の取得を自動化した。
それまでは、URLを渡すとサムネイル画像を提供してくれる外部のAPIを使ってたけど、Node.jsでフォルダ内の全HTMLをキャプチャし画像化という記事を見て、おもしろそうだったのでやってみた。
実際にはこの記事のサンプルより少し複雑になってしまった。

PhantomJSコマンドラインから実行できるブラウザで、これを利用してサイトのスクリーンショットを取得した。
そのスクリーンショットを、EasyImageというモジュールを使って縮小してる。
最初からスクリーンショットのサイズを指定して生成することもできるけど、大きめの画面でスクリーンショットを撮影して縮小したほうがきれいに撮れる。

PhantomJSの操作を非同期で行ってるんだけど、並行タスクの数が増えてくるとかなりマシンの負荷が増えてくるので、とりあえずタスクを10ずつに分割して、その10のタスクが終了したら次の10のタスクを実行するようにした。
本当は並行タスクの数を10までに制限するというような形にするべきだけど、あんまりきれいな実装が思いつかなかったのでひとまずこれにした。
bluebirdにそんなAPIがあるらしいので後で調べる。

既存のものを使えばだいたいなんでもできるんだなあと少し感動した。

Velocityのイージングを拡張する

Velocityっていうアニメーション用のライブラリがある。
最初からいろんなイージングが使えて、jQuery UIのイージングの一部が入ってるんだけど、Back系、Elastic系、Bounce系のやつは入ってない。
Velocityは独自にイージングを拡張することもできるので、jQuery UIで使えるイージングは全部使えるようにしてみた。

'use strict';
import Velocity from 'velocity-animate';

let baseEasings = {};

baseEasings.Elastic = p => {
  return p === 0 || p === 1
    ? p
    : -Math.pow(2, 8 * (p - 1)) *
        Math.sin (((p - 1) * 80 - 7.5) * Math.PI / 15);
};

baseEasings.Back = p => p * p * (3 * p - 2);

baseEasings.Bounce = p => {
  let pow2;
  let bounce = 4;
  while (p < ((pow2 = Math.pow(2, --bounce)) - 1) / 11) {}

  return 1 / Math.pow(4, 3 - bounce) - 7.5625 *
    Math.pow((pow2 * 3 - 2) / 22 - p, 2);
};

Object.keys(baseEasings).forEach(name => {
  Velocity.Easings[`easeIn${name}`] = baseEasings[name];

  Velocity.Easings[`easeOut${name}`] = p => 1 - baseEasings[name](1 - p);

  Velocity.Easings[`easeInOut${name}`] = p => {
    return p < 0.5
      ? baseEasings[name](p * 2) / 2
      : 1 - baseEasings[name](p * -2 + 2) / 2;
  }
});

export default Velocity;

以上のコードで、

  • easeInElastic
  • easeOutElastic
  • easeInOutElastic
  • easeInBack
  • easeOutBack
  • easeInOutBack
  • easeInBounce
  • easeOutBounce
  • easeInOutBounce

を拡張できた。
これをimportしたらイージングを拡張したVelocityが使える。

2016/8/15 追記

ソースはこの辺参考
あと、イージングは一度拡張すれば、ファイルを分けてrequireする必要はない。
つまり以下のようにして利用できる。

main.js

const Velocity = require('velocity-animate');

// イージングを拡張
Velocity.Easings.anyEasing = () => {};

require('./animation');

animation.js

const Velocity = require('velocity-animate');

Velocity(document.getElementById('el'), {opacity: 0}, 'anyEasing');

ウェブサイトギャラリーを作り直した

気に入ったサイトを集めるだけのページが欲しくて前に作ったものがある。
けどそれがあんまり使いやすくないので作り直した。

前のやつは、管理用のページを用意して、そこにURLとパスワード入力したらデータベースに登録して、実際のリストに反映されるという形にしてた。
でも、そのデータベースを置いてたサーバーの契約更新時期が来たのをきっかけに、これくらいのことで自分でデータベースを使うのは大袈裟なような気がして、もっと簡易的な仕組みにしたいと思った。

ということでまず、自分のタイミングで更新できるページなので、サーバーサイドとかAjaxでデータ取ってきてレンダリングするというのはやめて、ローカルでテンプレートエンジンを使って静的なページにすることに決めた。
それで、ローカルでビルドするときに基になるデータが必要なので、その管理をどうするかで悩んだ。BaaS使おうかなと思ったけどそれでは前と同じ手間だし、登録しやすくするにはどうしたらいいか考えて、いつもページをブックマークするときに使ってるPocketを使うのがよさそうだと思った。chrome拡張機能を使ってページをすぐに登録できるので便利。

ということで、気に入ったサイトはPocketに専用のタグをつけて登録して、ページを更新するときはPocketのAPIからそのタグのついたデータを取ってきてビルドするという形にした。API使うのに最初だけOAuth認証しないといけないのがめんどくさかった。

Website Gallery

サムネイルはHeartRails Captureってやつがあったので使わせてもらった。
毎回手作業で作るのは手間すぎるし、それを自動化するのもなかなか大変そうだったので既存のもので対応。

GitHub Pagesに置いた。
yuheiy/gallery.yhey.me

図形による画像のクリップをクロスブラウザで実現する

この記事の内容は全体的に古いので、クロスブラウザ周りの情報はあまりあてにしないでください。

Webサイト上で、画像を六角形にクリップしたいということがあった。
やり方はいろいろあるけど、クロスブラウザで動作するようにと考えるとSVGのclipPathを使った方法がよさそうだった。
ただ、ブラウザによる解釈の違いではまるところもあったので備忘録として書いておく。

まず、clipPathを定義するSVGをHTML内にインラインで書く。
外部ファイルに記述することもできるけど、クロスブラウザでは動作しない。
以下のコードは六角形にくり抜くclipPath。

<svg width="0" height="0" style="position: absolute;">
  <defs>
    <clipPath id="hexagon" clipPathUnits="objectBoundingBox">
      <polygon points=".5,0 .933,.25 .933,.75 .5,1 .067,.75 .067,.25"/>
    </clipPath>
  </defs>
</svg>

clipPathUnits="objectBoundingBox"という属性をつけると、要素に対して相対的にクリップできる。
これを使わない場合だと、クリップするためのSVG要素をpxで指定することになって、伸縮する要素に対応できない。
インラインスタイルでposition: absolute;を指定しているのは、これがないとこのSVG要素分の空間ができてしまうので、この指定で高さを無くしている。
あと、このclipPathを参照できるようにIDをつけておく。
この記述を、HTML内のどこに書いても問題なさそうだけど、bodyの終了直前に書くのが多いっぽい。

次に、実際にクリップされた要素を配置する場所にSVGを記述する。

<svg width="500" height="500">
  <image class="clipped" xlink:href="./img/photo.jpg" width="100%" height="100%"/>
</svg>

クリップする要素にCSSから参照する用のクラスをつけておく。 複数の要素をクリップするときはg要素とかで囲ってやるといい。

<svg width="500" height="500">
  <g class="clipped">
    <image xlink:href="./img/photo.jpg" width="100%" height="100%"/>
    <rect width="100%" height="100%" style="fill: rgba(0,0,0,.8);"/>
  </g>
</svg>

そして、次のようなCSSを書く。

.clipped {
  clip-path: url("#hexagon");
}

-webkit-をつけるとSafariで動作しないので外しておく。
Autoprefixerとかを使ってると勝手につけてくれるのでそれも考慮しておかないといけない。
PostCSSの設定で特定のプロパティだけ無視するみたいなことができるかもしれないけど、やり方がわからないので、僕はAutoprefixerの実行後に不要なベンダープリフィックスを削除するようにしてる。

あと、Firefoxではまるところがあって、clipPathのurlとして指定するIDの前に、CSSファイルの位置から見たHTMLファイルまでのパスを書く必要がある。

css/
- style.css

index.html

みたいな階層だと、

.clip-path {
  clip-path: url("..#hexagon");
}

という風に書いたりする。ルートまでの指定だったら

.clip-path {
  clip-path: url("/#hexagon");
}

とか。
この辺で複数ファイルがあったときの挙動とかは調べてないけど、どうしても問題があればHTML内にインラインCSSで書いてやれば動くはず。

デモ作った

で、実際に作ったものとソース)はもうちょっと複雑だったけど、CSSのpositionを駆使すれば実現できるレベルだった。
とはいえ若干手間でわかりにくいので、IEとかもclipPathの実装はがんばってほしいとしか言いようがない。

$.animate()でのコールバック地獄を回避する

jqueryで、あるアニメーションの終了後に次のアニメーションを続けたいということがあった。
普通に書くとコールバック地獄に陥ってしまうので、Promiseっぽくthenチェーンで書きたい。
jquery的に書くなら、毎回$.Deferredでラップして書いていかないとダメなのかなと思ってたら、$.animate()$.promise()をつなげると$.Deferredが返ってくるので、それを使ったら簡潔に書けた。

$el.animate({ left: 100 }).promise()
  .then(function () {
    return $el.animate({ left: 200 });
  })
  .then(function () {
    return $el.animate({ left: 300 });
  });

もうちょい複雑なデモ

thenの中で返してるオブジェクトは、promiseを持ってたら次のthenまでいくので、thenで返すオブジェクトにpromiseをつなげる必要はない。
コードこの辺

入院してる

1月1日の夜、突如肺に激痛がした。
尋常でない痛みだと感じたので救急車を呼ぼうとしたが、あまりに体が痛くて動けない。
僕は所々で意識を失いながらもなんとか携帯を手に取り、119番へ連絡した。
それから病院まで搬送され、緊急手術を行った。

原因は、もともと僕が気胸を持っていて、前に発症したときの穴を塞いでいた、かさぶたのようなものが剥がれたこと。
それがきっかけになって血管が切れ、1リットル以上の出血をした。

今は病室からこの記事を書いていて、かなり元気に回復してる。
数時間前に体に入っていた管が抜けて、さっき自分でシャワーを浴びることもできた。
ただ、あのとき助けを呼べなかったら確実に死んでいたと考えると、恐怖でしかない。
誰しも常に、死と隣り合わせになることはあり得るのだと認識して、もう少し対策を考えなければならないと実感した。

ビルド時にデータベースのデータをJSファイルに埋め込む

webpackでビルドするときに、外部データベースのデータをJSファイル内に埋め込む方法。
機密にする必要がなくて、ほぼ変更しないデータがあったので、毎回サーバーから受けとるよりもJSファイルのなかに埋め込んでしまえば、通信するコストがかからないと思ったのでやってみた。

最初、gulp-dataを使えばできるんじゃないかと思ったけど、どうやらそれでは無理らしく、webpackのDefinePluginを使えばやりたいことができた。

webpackのDefinePluginは、webpackでビルドするときに設定値に値を注入することができるプラグイン
例えば、モジュールに含まれるprocess.env.NODE_ENVproductionに置き換えたりということができる。

こんな感じのコードでうまいこといった。

'use strict';
const MongoClient = require('mongodb').MongoClient;
const webpack = require('webpack');

const DATABASE_URL = 'mongodb://for_webpack:pass@ds061974.mongolab.com:61974/heroku_k4vml408';

const fetchData = query => {
  return new Promise((resolve, reject) => {
    MongoClient.connect(DATABASE_URL, (err, db) => {
      if (err) {
        return reject(err);
      }

      db.collection('posts').find(query).toArray((err, docs) => {
        if (err) {
          return reject(err);
        }

        resolve(docs);
        db.close();
      });
    });
  });
};

const build = data => {
  return new Promise((resolve, reject) => {
    webpack({
      entry: {
        app: './src/app.js'
      },

      output: {
        path: './dist',
        filename: '[name].js'
      },

      plugins: [
        new webpack.DefinePlugin({
          FETCHED_DATA: JSON.stringify(data)
        })
      ]
    }, (err, stats) => {
      if (err) {
        return reject(err);
      }

      console.log(stats.toString({ colors: true }));
      resolve();
    });
  });
};

fetchData({})
  .then(build);

これで、モジュール中のFETCHED_DATAが取得したデータに置き換えられる。

mongodbのURLは、以前作ったブログのデータベース。
一応読み取り専用のユーザーにしてる。

全コードはここに置いた。