CSS Sprite を自動生成する - Gulp で作る Web フロントエンド開発環境 #8
wakamsha
前置き - Compass から Bourbon へ
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 は 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
出来上がった画像ファイルはこちら。
同時に 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;
}
実際にマークアップしてみたイメージはこちら。
.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
オプションのfunctions
にfalse
を指定すると@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
}
}));
スプライト画像内の並び順を指定する
algorithm
とalgorithmOpts
オプションを指定することで、画像の並びやソートを指定することが出来ます。デフォルトは先程の例にあるような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
を指定してみました。するとこのような並びでスプライト画像が生成されます。
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 等を使った開発環境へ徐々に移行していくことを検討してみるのも良いかもしれません。