CSS Sprite を自動生成する - Gulp で作る Web フロントエンド開発環境 #8

wakamsha
126

前置き - Compass から Bourbon へ

preview

Sass ( SCSS ) や Stylus といった CSS プリプロセッサを使う上で更に効率を高めるために Compass や Bourbon といった Sass ( SCSS ) のフレームワークを導入している方もたくさんいるかと思います。僕自身も SCSS を習得するとほぼ同時に Compass を愛用するようになりました。CSS のミニファイ化やベンダープレフィックス対応してくれる Mixin などとても多機能なのが魅力ですが、一方で動作が重くコンパイルに結構な時間が掛かる、Web ブラウザの CSS サポートが進むにつれて生成される CSS が冗長なものになりつつあるというのが気になるようになってきました。公式ブログを見る限り2014年8月に Ver.1.01 をリリースして以来メンテナンスされている様子もないことから1)Mac OS X - El Capitan では動作しなくなるとの報告も出ており、こちらのリンクにある対処をする必要があるようです。、現在は軽量かつ動作の安定した後発のフレームワークである Bourbon を Gulp 上で使っています。

bourbon-logo-e1426359702775

Bourbon は Compass の Mixin 定義部分のみを抽出したようなフレームワークであり、各種 Mixin の呼び出し方も Compass とほぼ同じなので殆ど双方の違いを意識することなく使うことが可能です。

【悲報】Bourbon には CSS Sprite 生成機能がない

Compass は単に便利 Mixin の寄せ集めやミニファイ化をしてくれるだけでなく、複数の画像ファイルを結合して CSS Sprite を生成する事も出来るのが強みです。僕自身はあまり CSS Sprite を使ってきませんでしたが、それ目的で Compass を使うという人も多いでしょう。Bourbon は先に述べた通り便利 Mixin 機能のみで CSS Sprite 生成機能などはありません。もし Compass の使用をやめようにも CSS Sprite を使っているようなプロジェクトであれば事案においてはおいそれと移行することは難しいでしょう。何か他の手段で CSS Sprite を自動生成出来るようにする必要があります。

本題 - CSS Sprite を自動生成する Gulp タスクを作成

そんなわけで、Gulp で CSS Sprite を自動生成するタスクを作成してみるとします。

前提条件

  • Mac OS X El Capitain
  • node.js インストール済み ( v5.4.0 ~ )

サンプルコードはこちらからどうぞ

gulp.spritesmith

gulp.spritesmith はCSS Sprite とそれに対応した CSS ファイルを生成する Node モジュールです。もともと spritesmith と Grunt 対応版の grunt-spritesmith というモジュールがありますが、今回使用するのはそれを Gulp に対応させたものになります。

以下のコマンドを実行してパッケージをインストールします。

$ npm install --save-dev gulp.spritesmith

無事にインストール出来たら Gulp タスクを定義します。

var gulp = require('gulp');
var spritesmith = require('gulp.spritesmith');

gulp.task('sprite', () => {
    let spriteData = gulp.src('./app/images/sprite/*.png')
        .pipe(spritesmith({
            imgName: 'sprite.png',                        // スプライト画像
            cssName: '_sprite.scss',                      // 生成される CSS テンプレート
            imgPath: './public/assets/images/sprite.png', // 生成される CSS テンプレートに記載されるスプライト画像パス
            cssFormat: 'scss',                            // フォーマット拡張子
            cssVarMap: (sprite) => {
                sprite.name = "sprite-" + sprite.name;      // 生成される CSS テンプレートに変数の一覧を記述
            }
    }));
    spriteData.img
        .pipe(gulp.dest('./public/assets/images'));     // imgName で指定したスプライト画像の保存先
    return spriteData.css
        .pipe(gulp.dest('./app/styles/commons'));       // cssName で指定した CSS テンプレートの保存先
});

gulp.task('default', ['sprite']);

spritesmith() に上記のようなオプションを渡します。コメントでも書いてますがここで指定したオプションは以下のとおり。

プロパティ 概要 Optional
imgName String 生成するスプライト画像ファイルを指定。対応フォーマットは.png.jpg/jpegの二種類。
cssName String 生成するCSSファイルを指定。対応フォーマットは.cssだけでなく.sass/scss.less.styl/.stylusといったプリプロセッサや.jsonなどがある。
imgPath Sring 生成されるCSSファイルに記載されるスプライト画像のパスを指定。デフォルトはファイル名そのままとなる。
cssFormat String 生成CSSのフォーマットを指定。未指定だとcssNameに指定したファイルの拡張子で生成される。
cssVarMap Function スプライト画像の各要素 ( 変数 ) に対して実行したい処理を定義するマッピング関数を指定。

全てのプロパティを知りたいという方は以下のドキュメントを参照ください。

CSS Sprite の生成内容を定義した後はスプライト画像と CSS ファイルをそれぞれ出力します。一般的な Gulp タスクは Stream の作成からgulp.dest()でファイル等を保存するまでをチェーンメソッドで順に定義していくものですが、gulp.spritesmith は Stream を変数に格納し、img()メソッドとcss()メソッドに対して個別にgulp.dest()を実行するのが特徴です。

Gulp タスクを実行する

タスクを実行するとアイコンが1つになった Sprite 画像が生成されます。

$ gulp

[14:42:42] Requiring external module coffee-script/register
[14:42:42] Using gulpfile ~/Documents/sandbox/gulp/try_csssprite/gulpfile.coffee
[14:42:42] Starting 'sprite'...
[14:42:43] Finished 'sprite' after 854 ms
[14:42:43] Starting 'default'...
[14:42:43] Finished 'default' after 48 μs

出来上がった画像ファイルはこちら。

sprite

同時に SCSS ファイルも出来ました。

$sprite-cat-name: 'sprite-cat';
$sprite-cat-x: 0px;
$sprite-cat-y: 0px;
$sprite-cat-offset-x: 0px;
$sprite-cat-offset-y: 0px;
$sprite-cat-width: 150px;
$sprite-cat-height: 150px;
$sprite-cat-total-width: 450px;
$sprite-cat-total-height: 300px;
$sprite-cat-image: './public/assets/images/sprite.png';
$sprite-cat: (0px, 0px, 0px, 0px, 150px, 150px, 450px, 300px, './public/assets/images/sprite.png', 'sprite-cat', );
... ( 中略 ) ...
$spritesheet-width: 450px;
$spritesheet-height: 300px;
$spritesheet-image: './public/assets/images/sprite.png';
$spritesheet-sprites: ($sprite-cat, $sprite-crow, $sprite-deer, $sprite-penguin, $sprite-rabbit, $sprite-raccoon, );
$spritesheet: (450px, 300px, './public/assets/images/sprite.png', $spritesheet-sprites, );

/*
The provided mixins are intended to be used with the array-like variables

.icon-home {
  @include sprite-width($icon-home);
}

.icon-email {
  @include sprite($icon-email);
}
*/
@mixin sprite-width($sprite) {
  width: nth($sprite, 5);
}

@mixin sprite-height($sprite) {
  height: nth($sprite, 6);
}

@mixin sprite-position($sprite) {
  $sprite-offset-x: nth($sprite, 3);
  $sprite-offset-y: nth($sprite, 4);
  background-position: $sprite-offset-x  $sprite-offset-y;
}

@mixin sprite-image($sprite) {
  $sprite-image: nth($sprite, 9);
  background-image: url(#{$sprite-image});
}

@mixin sprite($sprite) {
  @include sprite-image($sprite);
  @include sprite-position($sprite);
  @include sprite-width($sprite);
  @include sprite-height($sprite);
}

/*
The `sprites` mixin generates identical output to the CSS template
  but can be overridden inside of SCSS

@include sprites($spritesheet-sprites);
*/
@mixin sprites($sprites) {
  @each $sprite in $sprites {
    $sprite-name: nth($sprite, 10);
    .#{$sprite-name} {
      @include sprite($sprite);
    }
  }
}

スプライト画像を使いたい箇所で上記の Mixins を呼び出せば画像を表示することができます。

i {
    display: block;
    @include sprite($sprite-cat);
}

以下の様なCSSが出来上がります。

i {
    display: block;
    background-image: url(../images/sprite.png);
    background-position: 0px 0px;
    width: 150px;
    height: 150px;
}

実際にマークアップしてみたイメージはこちら。

cap-demo_csssprite

.container
  .row
    .col-sm-8.col-xs-offset-2
      ul.media-list
        each val in ['cat', 'crow', 'deer', 'penguin', 'rabbit', 'raccoon']
          li.media
            .media-left
              a(href="#")
                .media-object(class="media-object--#{val}")
            .media-body
              h4.media-heading
                |Sprite 
                span.text-primary=val.toUpperCase()
              p Cras sit amet nibh libero, in gravida nulla. ...
              p Cras sit amet nibh libero, in gravida nulla. ...

その他のオプションを試してみる

gulp.spritesmith には他にも幾つかの便利機能があります。

Spritesheet 名を指定する

スプライト画像に対する変数名の接頭辞を指定します。デフォルトはspritesheet-と汎用的な名前ですが、cssSpritesheetNameオプションを指定することで好きな名前に変更することが出来ます。

let spriteData = gulp.src('./app/images/sprite/*.png').pipe(spritesmith({
    imgName: 'sprite.png',
    cssName: '_sprite.scss',
    imgPath: './public/assets/images/sprite.png',
    cssFormat: 'scss',
    cssVarMap: (sprite) => {
        sprite.name = "sprite-" + sprite.name;
    },
    cssSpritesheetName: 'animals'
}));

生成されるSCSSはこちら

$animals-width: 900px;
$animals-height: 600px;
$animals-image: './public/assets/images/sprite.png';
$animals-sprites: ($sprite-cat, $sprite-crow, $sprite-deer, $sprite-penguin, $sprite-rabbit, $sprite-raccoon, );
$animals: (900px, 600px, './public/assets/images/sprite.png', $spritesheet-sprites, );

Mixins を除去する

cssOptsオプションのfunctionsfalseを指定すると@mixin sprite-width()といった Mixins を全て除去した変数のみのテンプレートを生成することが出来ます。

let spriteData = gulp.src('./app/images/sprite/*.png')
    .pipe(spritesmith({
        imgName: 'sprite.png',
        cssName: '_sprite.scss',
        imgPath: './public/assets/images/sprite.png',
        cssFormat: 'scss',
        cssVarMap: (sprite) => {
            sprite.name = "sprite-" + sprite.name;
        },
        cssOpts: {
            functions: false
        }
}));

スプライト画像内の並び順を指定する

algorithmalgorithmOptsオプションを指定することで、画像の並びやソートを指定することが出来ます。デフォルトは先程の例にあるようなbinary-treeという並びで、アイコンのファイル名順に配置されます。
Algorithms についてはこちらで詳しく解説されています。

spriteData = gulp.src './app/images/sprite/*.png'
    .pipe spritesmith
        imgName: 'sprite.png'
        cssName: '_sprite.scss'
        imgPath: './public/assets/images/sprite.png'
        cssFormat: 'scss'
        cssVarMap: (sprite) ->
            sprite.name = "sprite-#{sprite.name}"
            return
        algorithm: 'diagonal'
        algorithmOpts:
            sort: false

algorithm にdiagonalを指定してみました。するとこのような並びでスプライト画像が生成されます。

sprite_diagonal

Retina 対応について

Retina ディスプレイ用のスプライト画像を通常サイズのそれとは別に生成することが出来ます。ここまでの例で使用したapp/images/sprite以下のアイコンファイルに加えて Retina サイズ用の*@2x.pngを用意します。

Gulp タスクを修正

let spriteData = gulp.src('./app/images/sprite/*.png')
    .pipe(spritesmith({
        imgName: 'sprite.png',
        cssName: '_sprite.scss',
        imgPath: './public/assets/images/sprite.png',
        // cssFormat: 'scss',
        cssVarMap: (sprite) => {
            sprite.name = "sprite-" + sprite.name;
        },
        retinaSrcFilter: './app/images/sprite/*@2x.png',
        retinaImgName: 'sprite@2x.png',
        retinaImgPath: './public/assets/images/sprite@2x.png'
    }));

3つのオプションを追加しました。retinaSrcFilterでRetina用のスプライト画像に使うアイコンを抽出する条件を指定します。retinaImgNameオプションはその名の通りRetina用に生成するスプライト画像ファイル名、retinaImgPathオプションはCSSテンプレートに記載するRetina用スプライト画像のパスとなります。

注意点としてcssFormatを指定するとRetina用の変数や Mixins が生成されなくなるというのがあります。ドキュメントを読む限りそのような記載が見当たらない & スプライト画像だけは生成されてることから不具合の可能性がありますが、現時点ではそのオプションを無くすことで回避することが可能です。

生成されたテンプレートはこちら。

/*
SCSS variables are information about icon's compiled state, stored under its original file name

.icon-home {
  width: $icon-home-width;
}

The large array-like variables contain all information about a single icon
$icon-home: x y offset_x offset_y width height total_width total_height image_path;

At the bottom of this section, we provide information about the spritesheet itself
$spritesheet: width height image $spritesheet-sprites;
*/
$sprite-cat-name: 'sprite-cat';
$sprite-cat-x: 0px;
$sprite-cat-y: 0px;
$sprite-cat-offset-x: 0px;
$sprite-cat-offset-y: 0px;
$sprite-cat-width: 150px;
$sprite-cat-height: 150px;
$sprite-cat-total-width: 450px;
$sprite-cat-total-height: 300px;
$sprite-cat-image: './public/assets/images/sprite.png';
$sprite-cat: (0px, 0px, 0px, 0px, 150px, 150px, 450px, 300px, './public/assets/images/sprite.png', 'sprite-cat', );
$sprite-crow-name: 'sprite-crow';
$sprite-crow-x: 150px;
$sprite-crow-y: 0px;
$sprite-crow-offset-x: -150px;
$sprite-crow-offset-y: 0px;
$sprite-crow-width: 150px;
$sprite-crow-height: 150px;
$sprite-crow-total-width: 450px;
$sprite-crow-total-height: 300px;
$sprite-crow-image: './public/assets/images/sprite.png';
$sprite-crow: (150px, 0px, -150px, 0px, 150px, 150px, 450px, 300px, './public/assets/images/sprite.png', 'sprite-crow', );
... ( 中略 ) ...
$sprite-cat-2x-name: 'sprite-cat@2x';
$sprite-cat-2x-x: 0px;
$sprite-cat-2x-y: 0px;
$sprite-cat-2x-offset-x: 0px;
$sprite-cat-2x-offset-y: 0px;
$sprite-cat-2x-width: 300px;
$sprite-cat-2x-height: 300px;
$sprite-cat-2x-total-width: 900px;
$sprite-cat-2x-total-height: 600px;
$sprite-cat-2x-image: './public/assets/images/sprite@2x.png';
$sprite-cat-2x: (0px, 0px, 0px, 0px, 300px, 300px, 900px, 600px, './public/assets/images/sprite@2x.png', 'sprite-cat@2x', );
$sprite-crow-2x-name: 'sprite-crow@2x';
$sprite-crow-2x-x: 300px;
$sprite-crow-2x-y: 0px;
$sprite-crow-2x-offset-x: -300px;
$sprite-crow-2x-offset-y: 0px;
$sprite-crow-2x-width: 300px;
$sprite-crow-2x-height: 300px;
$sprite-crow-2x-total-width: 900px;
$sprite-crow-2x-total-height: 600px;
$sprite-crow-2x-image: './public/assets/images/sprite@2x.png';
$sprite-crow-2x: (300px, 0px, -300px, 0px, 300px, 300px, 900px, 600px, './public/assets/images/sprite@2x.png', 'sprite-crow@2x', );
... ( 中略 ) ...
sprite@2x.png', 'sprite-raccoon@2x', );
$spritesheet-width: 450px;
$spritesheet-height: 300px;
$spritesheet-image: './public/assets/images/sprite.png';
$spritesheet-sprites: ($sprite-cat, $sprite-crow, $sprite-deer, $sprite-penguin, $sprite-rabbit, $sprite-raccoon, );
$spritesheet: (450px, 300px, './public/assets/images/sprite.png', $spritesheet-sprites, );
$retina-spritesheet-width: 900px;
$retina-spritesheet-height: 600px;
$retina-spritesheet-image: './public/assets/images/sprite@2x.png';
$retina-spritesheet-sprites: ($sprite-cat-2x, $sprite-crow-2x, $sprite-deer-2x, $sprite-penguin-2x, $sprite-rabbit-2x, $sprite-raccoon-2x, );
$retina-spritesheet: (900px, 600px, './public/assets/images/sprite@2x.png', $retina-spritesheet-sprites, );

/*
These "retina group" variables are mappings for the naming and pairing of normal and retina sprites.

The list formatted variables are intended for mixins like `retina-sprite` and `retina-sprites`.
*/
$sprite-cat-group-name: 'sprite-cat';
$sprite-cat-group: ('sprite-cat', $sprite-cat, $sprite-cat-2x, );
$sprite-crow-group-name: 'sprite-crow';
$sprite-crow-group: ('sprite-crow', $sprite-crow, $sprite-crow-2x, );
$sprite-deer-group-name: 'sprite-deer';
$sprite-deer-group: ('sprite-deer', $sprite-deer, $sprite-deer-2x, );
$sprite-penguin-group-name: 'sprite-penguin';
$sprite-penguin-group: ('sprite-penguin', $sprite-penguin, $sprite-penguin-2x, );
$sprite-rabbit-group-name: 'sprite-rabbit';
$sprite-rabbit-group: ('sprite-rabbit', $sprite-rabbit, $sprite-rabbit-2x, );
$sprite-raccoon-group-name: 'sprite-raccoon';
$sprite-raccoon-group: ('sprite-raccoon', $sprite-raccoon, $sprite-raccoon-2x, );
$retina-groups: ($sprite-cat-group, $sprite-crow-group, $sprite-deer-group, $sprite-penguin-group, $sprite-rabbit-group, $sprite-raccoon-group, );

/*
The provided mixins are intended to be used with the array-like variables

.icon-home {
  @include sprite-width($icon-home);
}

.icon-email {
  @include sprite($icon-email);
}
*/
@mixin sprite-width($sprite) {
  width: nth($sprite, 5);
}

@mixin sprite-height($sprite) {
  height: nth($sprite, 6);
}

@mixin sprite-position($sprite) {
  $sprite-offset-x: nth($sprite, 3);
  $sprite-offset-y: nth($sprite, 4);
  background-position: $sprite-offset-x  $sprite-offset-y;
}

@mixin sprite-image($sprite) {
  $sprite-image: nth($sprite, 9);
  background-image: url(#{$sprite-image});
}

@mixin sprite($sprite) {
  @include sprite-image($sprite);
  @include sprite-position($sprite);
  @include sprite-width($sprite);
  @include sprite-height($sprite);
}

/*
The `retina-sprite` mixin sets up rules and a media query for a sprite/retina sprite.
  It should be used with a "retina group" variable.

The media query is from CSS Tricks: https://css-tricks.com/snippets/css/retina-display-media-query/

$icon-home-group: ('icon-home', $icon-home, $icon-home-2x, );

.icon-home {
  @include retina-sprite($icon-home-group);
}
*/
@mixin sprite-background-size($sprite) {
  $sprite-total-width: nth($sprite, 7);
  $sprite-total-height: nth($sprite, 8);
  background-size: $sprite-total-width $sprite-total-height;
}

@mixin retina-sprite($retina-group) {
  $normal-sprite: nth($retina-group, 2);
  $retina-sprite: nth($retina-group, 3);
  @include sprite($normal-sprite);

  @media (-webkit-min-device-pixel-ratio: 2),
         (min-resolution: 192dpi) {
    @include sprite-image($retina-sprite);
    @include sprite-background-size($normal-sprite);
  }
}

/*
The `sprites` mixin generates identical output to the CSS template
  but can be overridden inside of SCSS

@include sprites($spritesheet-sprites);
*/
@mixin sprites($sprites) {
  @each $sprite in $sprites {
    $sprite-name: nth($sprite, 10);
    .#{$sprite-name} {
      @include sprite($sprite);
    }
  }
}

/*
The `retina-sprites` mixin generates a CSS rule and media query for retina groups
  This yields the same output as CSS retina template but can be overridden in SCSS

@include retina-sprites($retina-groups);
*/
@mixin retina-sprites($retina-groups) {
  @each $retina-group in $retina-groups {
    $sprite-name: nth($retina-group, 1);
    .#{$sprite-name} {
      @include retina-sprite($retina-group);
    }
  }
}

Retina 用の変数と Mixins が新たに追加されているのが分かります。

締め

CSS3 の普及によってかつてほど画像を使う機会は減ってきましたが、グラフィカルな Web ページであれば相応の画像を使うことになります。CSS Sprite は HTTP リクエストを削減するというパフォーマンスチューニングの基本的なテクニックですが、画像作成や修正の手間などを理由に導入を躊躇してしまう人もいることでしょう。今回ご紹介した仕組みは画像の差し替えや修正といった変更にとても強くできているので、開発効率を下げることなく気楽に導入することが出来るのではないでしょうか。

また、これまで Compass を使っていたという方も今回のような Gulp 等を使った開発環境へ徐々に移行していくことを検討してみるのも良いかもしれません。

脚注

脚注
1 Mac OS X - El Capitan では動作しなくなるとの報告も出ており、こちらのリンクにある対処をする必要があるようです。