読者です 読者をやめる 読者になる 読者になる

Develop and Design Note

フロントエンドなデザイナーの覚書

compassのsprite-mapっぽい記法でgulp.spritesmithを使ってretina対応する方法

最近はGulpに代表される様々なタスクランナーがnpmで管理できるようになり、SassもGulpで扱えるようになりました。
そのSassのフレームワークとして、ひと昔前はCompassが人気でした。特にCSSスプライトを簡単にやってくれる「sprite-map」の機能はなかなか便利でしたね。
今回は、そのsprite-mapの代わりとなるGulpパッケージ「gulp.spritesmith」の使用例と、ちょっとコツがいるRetina対応の方法について紹介します。

Compassからgulp-sassへの移行

2014/8/19を最後にCompassの更新が滞り*1、最近は、Sassのコンパイル作業にはgulp-sassを使う機会が増えました。

したがって、もともとCompassで作っていたCSSをgulp-sassに移行するという作業が発生したりします。
しかし、もしもsprite-mapを使っていると、gulp-sassでコンパイルができません(sprite-mapを使っていなければできる)。

gulp.spritesmithの導入

そこで、sprite-mapと同じ機能を持つGulpパッケージ、gulp.spritesmithを使います。
前提として、Gulpの環境を構築している必要があります。

それができていたら、gulp-sassとgulp.spritesmithをインストールします。

npm install gulp-sass --save-dev
npm install gulp.spritesmith --save-dev
スプライト用画像の作成

ここまでできたら、スプライトする用の画像を作成するのですが、通常サイズの画像と高解像度の画像は別々のフォルダにいれます

今回はこのようなディレクトリ構成で、「sprite」フォルダの中に、赤、黄、緑の4辺が50pxの画像を格納します。

f:id:designnote:20151220190849p:plain

次に「sprite_2x」フォルダを作成し、そこに先ほどと同じ画像の高解像度版(4辺が100px)を格納します。

f:id:designnote:20151220190856p:plain


gulp.spritesmithでRetina対応をカスタマイズ

最近は高解像度の端末やディスプレイがだいぶ増えてきましたが、次のようなCSS記法で、通常解像度と高解像度、両方の対応をするのがまだ現実だと思います。

.hoge {
 background:url(fuga.png);
}
@media screen and (-webkit-device-pixel-ratio:2) {
 .hoge {
  background:url(fuga_x2.png);
  background-size:25px 25px;
 }
}

spritesmithもこの記法に対応しているようです。

・・・が、なぜか、ここで紹介されている方法だと上手くいきません^^;
この方法で解像度の違う2種類のスプライト画像を作成するとこまではできたのですが、どういうわけか、scssが上手く生成されません(詳しい人教えてください)。

Retina対応する場合、「cssFormat」オブジェクトの値が「scss_retina」でないと、スプライト用SCSSがうまく生成されない。設定できる値はこちら。

しかし、この方法の場合、mixinの中身が自動生成されるので、カスタマイズできません。そこで、(人によっては馴染みある)sprite-mapっぽい書き方でretina対応してみます。
ちょっと複雑なことをしなければいけないので、理解のために、まずは、通常画像だけの場合で、spritesmithによるCSSスプライトの対応をしてみましょう。


Retinaじゃないサイズの画像でCSSスプライト

と言っても、この方法は、他の方がけっこう率先して取り組んでたりします。

簡単に概要を紹介します。

gulpfile.jsの編集

まずは、gulpfile.jsにspritesmith用の記述を追加します。こんな感じです。

// プラグインの読みこみ
var gulp = require('gulp');
var sass = require('gulp-sass');
var spritesmith = require('gulp.spritesmith');

// CSSスプライト
gulp.task( 'sprite', function() {
    var spriteData = gulp.src('./images/sprite/*.png')// 合体させる画像群
      .pipe(spritesmith({
        imgName:'sprite.png',// 生成されるスプライト画像のファイル名
        imgPath:'../images/sprite.png',// 生成されるスプライト用SCSSに記載されるスプライト画像パス
        cssName:'_sprite.scss',// 生成されるスプライト用SCSSのファイル名
        cssFormat:'scss',// 生成されるスプライト用SCSSの拡張子
        algorithm:'left-right',// 画像の並ぶ方向を横一列に
        algorithmOpts : {// 画像の並ぶ順をファイル名順に
          sort: false
        },
        padding:10//画像と画像の間の余白
      }));
    spriteData.img.pipe(gulp.dest('./images'));// 生成されるスプライト画像の保存先
    spriteData.css.pipe(gulp.dest('./sass'));// 生成されるスプライト用SCSSの保存先
});

※gulp.spritesmithの「imgName」などのオブジェクトは以下サイトで、リファレンスとして紹介されています。

このgulpfile.jsでスプライト用SCSS「_sprite.scss」と、合体したスプライト画像「sprite.png」が生成されます。

この_sprite.scssには、「合体したスプライト画像での、各画像の位置、縦幅、横幅」と「そのmixin」が記載されます。

style.scssの編集

次に、生成された「_sprite.scss」をstyle.scssでimportします。
そして、「_sprite.scss」の中で定義されたmixinをincludeします。引数には、各画像のファイル名を$付きで書くのがポイントです。

@import 'sprite';
ul {
  li {
    &:nth-child(1) {
      @include sprite($red);
    }
    &:nth-child(2) {
      @include sprite($yellow);
    }
    &:nth-child(3) {
      @include sprite($green);
    }
  }
}

これをコンパイルすると、以下のようなstyle.cssが生成されます。

ul li:nth-child(1) {
  background-image: url(../images/sprite.png);
  background-position: -60px 0px;
  width: 50px;
  height: 50px;
}
ul li:nth-child(2) {
  background-image: url(../images/sprite.png);
  background-position: -120px 0px;
  width: 50px;
  height: 50px;
}
ul li:nth-child(3) {
  background-image: url(../images/sprite.png);
  background-position: 0px 0px;
  width: 50px;
  height: 50px;
}

以上で出来上がったデモページがこちらです。

フォルダやファイルもGitHubで公開しているので、参考にしてみてください。

これで、通常解像度の画像でのCSSスプライトは完成です。

spritesmithでRetina込みで対応する方法

ここからが本題です。

gulpfile.jsの編集

先ほどのgulpfile.jsを、高解像度の画像群を合体させたスプライト画像と、それ用のSCSSを作成できるように、以下のように書き換えます。

// プラグインの読みこみ
var gulp = require('gulp');
var sass = require('gulp-sass');
var spritesmith = require('gulp.spritesmith');

// CSSスプライト
gulp.task( 'sprite', function() {
  function spriteCustom(retinaName) {// スプライト生成用の記述を関数化
    var retinaName = retinaName || '';// 引数の初期値を空に設定
    if (retinaName) {var spacingSize=20} else{var spacingSize=10};// Retina画像の場合は画像間の余白を2倍に
    var spriteData = gulp.src('./images/sprite'+retinaName+'/*'+retinaName+'.png')// 合体させる画像群
      .pipe(spritesmith({
        imgName:'sprite'+retinaName+'.png',// 生成されるスプライト画像のファイル名
        imgPath:'../images/sprite'+retinaName+'.png',// 生成されるスプライト用SCSSに記載されるスプライト画像パス
        cssName:'_sprite'+retinaName+'.scss',// 生成されるスプライト用SCSSのファイル名
        cssFormat:'scss',// 生成されるスプライト用SCSSの拡張子
        cssOpts: {// スプライト用SCSS内のmixinの記述をなくす
          functions: false
        },
        algorithm:'left-right',// 画像の並ぶ方向を横一列に
        algorithmOpts : {// 画像の並ぶ順をファイル名順に
          sort: false
        },
        padding:spacingSize,//画像と画像の間の余白
        cssSpritesheetName: 'sprite'+retinaName//@mixinするための変数名
      }));
    spriteData.img.pipe(gulp.dest('./images'));// 生成されるスプライト画像の保存先
    spriteData.css.pipe(gulp.dest('./sass'));// 生成されるスプライト用SCSSの保存先
  }
  spriteCustom();
  spriteCustom('_2x');//'_'は'-'に変換されるので注意
});

なにやら複雑になりましたね。
それぞれの記述が何を意味しているのか、細かいことはコメントアウトで書きましたが、修正前との大きな違いとして、以下があります。

  1. スプライトの処理を関数にして、通常画像と高解像度画像で、2回実行
  2. 解像度による、フォルダ名やファイル名の違いは関数の引数として記述
  3. スプライト用SCSS内のmixinの記述をなくす
  4. @mixinするための変数名を指定

このgulpfile.jsで生成されるのは「_sprite.scss」「_sprite_2x.scss」「sprite.png」「sprite_2x.png」の4つです。

生成されたSCSS内のmixinをなくしていますが、これは、独自で書き直す必要があるためです。
そして、その書き直したmixinは、style.scssに書きます。

style.scssの編集

前回同様に、生成されたscssをimportするのですが、「_sprite.scss」に加えて「_sprite_2x.scss」もimportします。
そして、各セレクタでincludeするためのmixinを書くのですが、各プロパティの値は変数でなく、nth($color,5)などの配列値で書くのがポイントです。
background-imageやbackground-sizeなど、セレクタによって値が変わらないものは変数値で大丈夫です。

@import 'sprite','sprite_2x';
@mixin sprite_color($color) {
  width: nth($color,5);
  height: nth($color,6);
  background-image: url($sprite-image);
  background-position: nth($color,3) nth($color,4);
  @media screen and (-webkit-min-device-pixel-ratio:2.0){
    background-image: url($sprite-2x-image);
    background-size: $sprite-2x-width/2 $sprite-2x-height/2;
  }
}

ul {
  li {
    &:nth-child(1) {
      @include sprite_color($red);
    }
    &:nth-child(2) {
      @include sprite_color($yellow);
    }
    &:nth-child(3) {
      @include sprite_color($green);
    }
}

これをコンパイルすると、以下のようなstyle.cssが生成されます。

ul li:nth-child(1) {
  width: 50px;
  height: 50px;
  background-image: url("../images/sprite.png");
  background-position: -60px 0px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2) {
  ul li:nth-child(1) {
    background-image: url("../images/sprite_2x.png");
    background-size: 170px 50px;
  }
}
ul li:nth-child(2) {
  width: 50px;
  height: 50px;
  background-image: url("../images/sprite.png");
  background-position: -120px 0px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2) {
  ul li:nth-child(2) {
    background-image: url("../images/sprite_2x.png");
    background-size: 170px 50px;
  }
}
ul li:nth-child(3) {
  width: 50px;
  height: 50px;
  background-image: url("../images/sprite.png");
  background-position: 0px 0px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2) {
  ul li:nth-child(3) {
    background-image: url("../images/sprite_2x.png");
    background-size: 170px 50px;
  }
}

出来上がったデモページと、全フォルダやファイル構成はこちらです。

これで、gulp.spritesmithを使った、通常解像度と高解像度、両方のCSSスプライトに対応できました。
プロジェクトや人によっては、高解像度のファイル名やフォルダ名の命名規則が違うかもしれません。効かせたいスタイルも違うと思います。
そういった場合は、gulpfile.js内に記述した関数だったり、style.scssのmixinの中身を自分なりにアレンジして、いろいろと応用してみて下さい。