NIR for Retina Images

擬似要素と content プロパティを利用した画像置換テクニックの NIR (Nash Image replacement) と、いわゆる Retina ディスプレイ向けの高解像度画像 (sprites@2x.png みたいなやつ) を組み合わせる方法について。まずは NIR についておさらい:

.nir {
    overflow: hidden;
    width: 160px;
    height: 50px;
    *background: url(sprites.png) no-repeat 0 -25px;
    *text-indent: -9999px;
}

.nir:before {
    content: url(sprites.png);
    display: inline-block;
    font-size: 0;
    line-height: 0;
    margin-left: 0;
    margin-top: -25px;
}

要素の幅と高さと overflow: hidden を指定した上で、:before 擬似要素の content プロパティで画像を生成し、テキストをボックスのそとに追い出して見えなくする、というテクニック。スプライト画像の場合は左上からの座標を margin-leftmargin-top で指定すれば対応できる。擬似要素をサポートしない IE7 以下では背景画像と text-indent による昔ながらの画像置換でフォールバック。

これを 2 倍のサイズで書き出した高解像度の画像でやるにはちょっと工夫が必要になる。というのも、画像をブラウザで半分に縮小して表示する必要があるわけだけど、content プロパティで生成した画像のサイズは width/height プロパティなどではコントロールできないからだ。背景画像なら background-size が使えるけどもちろんそれも無理。そこで CSS Transformsscale() 関数を使う:

.nir {
    overflow: hidden;
    width: 160px;
    height: 50px;
}

.nir:before {
    content: url(sprites@2x.png);
    display: inline-block;
    font-size: 0;
    line-height: 0;
    margin-left: 0;
    margin-top: -25px;
    -webkit-transform: scale(0.5);
    -ms-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: 0 0;
    -ms-transform-origin: 0 0;
    transform-origin: 0 0;
}
 
.no-csstransforms .retina-nir {
    background: url(sprites.png) no-repeat 0 -25px;
    text-indent: -9999px;
}
 
.no-csstransforms .retina-nir:before {
    display: none;
}

このように transform プロパティに scale(0.5) を指定することで画像が半分のサイズで表示される。ちなみに background-size プロパティではこのように画像のサイズを基準にした相対的な指定はできず、画像の実際のサイズを元にした数値を CSS に反映させる必要があるけど、これはシンプルに比率を渡せるのでその点でも便利。

見落としがちなのが transform-origin プロパティで、この初期値が 50% 50% であるため、画像の左上を基点に座標を指定するためには 0 0 を指定する必要がある。

フォールバックとしては transform が使えるかどうかを基準にする必要があるので、Modernizr を利用して、もしサポートしていなければ擬似要素は利用せず「等倍」の画像を背景として表示している。具体的には IE8 以下がその対象。もちろんできれば CSS だけで判別したいけどちょっと思いつかなかった。

これを Sass のミックスインにするとこんな感じ:

// NIR for Retina image
@mixin retina-nir (
    $x:      0,
    $y:      0,
    $width:  null,
    $height: null,
    $path:   "/img/sprites",
    $mod:    "@2x",
    $ext:    ".png",
    $ratio:  2
) {
    @if $x > 0 { $x: -$x; }
    @if $y > 0 { $y: -$y; }
    overflow: hidden;
    @if $width {
        width: if(type-of($width) == number and unit($width) != '%', $width/$ratio, $width);
    }
    @if $height {
        height: if(type-of($height) == number and unit($height) != '%', $height/$ratio, $height);
    }
    &:before {
        content: url($path + $mod + $ext);
        display: inline-block;
        font-size: 0;
        line-height: 0;
        margin-left: $x/$ratio;
        margin-top: $y/$ratio;
        @include transform(scale(1/$ratio));
        @include transform-origin(0 0);
    }
    .no-csstransforms & {
        background: url($path + $ext) no-repeat $x/$ratio $y/$ratio;
        text-indent: -9999px;
        &:before {
            display: none;
        }
    }
}

// transform
@mixin transform ($transform-function) {
    $prefixes: webkit, ms;
    @each $prefix in $prefixes {
        -#{ $prefix }-transform: $transform-function;
    }
    transform: $transform-function;
}

 // transform-origin
@mixin transform-origin ($origin) {
    $prefixes: webkit, ms;
    @each $prefix in $prefixes {
        -#{ $prefix }-transform-origin: $origin;
    }
    transform-origin: $origin;
}

// usage
.nir {
    @include retina-nir($y: -50px, $width: 320px, $height: 100px);
}

実際の作業では Retina サイズの画像をもとに CSS を書くことになると思うので、サイズや位置の引数は縮小した結果ではなく高解像度画像の実際のサイズを基準にした数値を渡し、それぞれ 2 で割るようにしてある (パーセント値の場合を除く)。座標に正の数値を渡した場合に負に変換するのはおせっかいな気もするけど、個人的によく間違うので入れた。transform 関係はほかでも使うので別ミックスインに分けてある。

もちろん、画像置換に限らず CSS generated content には同じ手が使える。