Sassでlinear-gradient()のmixinをつくる

このエントリはCSS Preprocessor Advent Calendar 2012、13日目のエントリです。

Sassを本格的に使い始めてもう6ヶ月経つのですが、まだまだだなあと思うこと多々です。
というわけで、勉強のためCSSのグラデーションを出力するmixinを書いてみました。

Part 0 ― linear-gradient() の構成

CSS Image Values仕様linear-gradient() は、グラデーションの「方向」に、色と位置の組からなる「カラーストップ」が続くという構成になってます。

linear-gradient( direction, colorstop1, colorstop2, ... )

さて、今日のImage Values仕様の linear-gradient() と、接頭辞つきの -prefixed-linear-gradient() は、構文が違います。ひと通りブラウザで実装されたあとに、なんと仕様が変わってしまったのです。困ったものです。

そんなわけで、単純に接頭辞だけが違う構文を並べたmixinを作るんでは解決できないのです。

構文がどうが違うかというと「方向の書き方」が違います。ならば、それをなんとかすれば残りは同じ。というわけで、なんとかできないかやってみます。

Part 1 ― 構文が同じなら @each ですっきり

構文が違うと言っておいていきなりですが、同じ場合もあります。方向を省略したときです。
方向が省略されたグラデーションは上から下に向かいます。これならとても簡単です。

@mixin lg-simple($colorstops...) {
    // 出力させたい接頭辞を並べる
    $prefixes: '-webkit-';
    
    @each $prefix in $prefixes {
        background-image: #{$prefix}linear-gradient($colorstops);
    }
    background-image: linear-gradient($colorstops);
}

ただ並べて書いてもいいのですが、出力させたい接頭辞を変数に入れて @each で回すことにしました。これだと重複して書かなくて済みます。

カラーストップの指定には、Sass 3.2から導入された可変長引数を利用しています。何色あっても大丈夫。

Part 2 ― キーワードのマッピングはリスト関数で

上から下ではなく「左から右へ」「右下から左上」といったグラデーションにしたい場合は、方向をキーワードで指定します。

linear-gradient( to top left, #fc0, #fff )

to top left” と「向かう方向」を書くことになります。

さて、ここから接頭辞つきのものと構文が違ってきます。-prefixed-linear-gradient() では、「〜〜から」と「始点」を書くようになってます。

-webkit-linear-gradient( bottom right, #fc0, #fff )

うーん、面倒ですね。ただ、新旧のキーワードをマップできれば解決できます。
ここではリストとリスト関数を使ってなんとかしてみましょう。

@mixin lg-keyword($keyword, $colorstops...) {
    $prefixes: '-webkit-';
    
    // 標準のキーワード
    $standard_keywords:
        to bottom, to left, to top, to right,
        to top left, to top right, to bottom right, to bottom left,
        to left top, to right top, to right bottom, to left bottom;
        
    // 接頭辞版のキーワード
    $legacy_keywords:
        top, right, bottom, left,
        bottom right, bottom left, top left, top right,
        right bottom, left bottom, left top, right top;
    
    // キーワードのインデックスを取得
    $idx: index( $standard_keywords, $keyword );
    
    // キーワードが不正な場合は何も出さない
    @if $idx {
        // マッチする接頭辞版のキーワードを取得
        $legacy_keyword: nth( $legacy_keywords, $idx );
        
        @each $prefix in $prefixes {
            background-image: #{$prefix}linear-gradient(
                $legacy_keyword, $colorstops);
        }
        background-image: linear-gradient($keyword, $colorstops);
    }
}

おえってなりますね。

まず、標準のキーワードと、それに対応する接頭辞版のキーワードのリストを定義します。Sassではカンマもしくは空白で区切られた値がリストとして扱われます。そして、Sassにはこういうリストを扱うリスト関数が用意されています。

というわけで次に、index() 関数でインデックスを取得します。index() はリスト内に値がある場合はそのインデックスを、ない場合は false を返します。
あ、Sassではリストのインデックスが0ではなく、1から始まります。ちょっと注意ですね。

次に、取得したインデックスを使って、該当する接頭辞版のキーワードを取得します。リスト中のあるインデックスに置かれた値をとるには nth() 関数を使えばOKです。

これでキーワードのマッピングができ、mixinが完成です。わー

しかしリストのインデックスを使うというのは泥臭いですね。マップ/ハッシュみたいなものがSassにあれば少しは洗練された感が出るのですが……
と、マップ的なものを追加するという決定はされているようですので、楽しみですね。

Part 3 ― 角度は単純に計算

キーワードで十分だろうとおもいきや、linear-gradient() にはもうひとつ、角度で方向を指定できる機能があります。

linear-gradient( 90deg, #fc0, #fff )

さて、-prefixed-linear-gradient() の構文はどうかというと、なんと、同じです。

-webkit-linear-gradient( 90deg, #fc0, #fff )

しかし、同じでも違います。なんと角度の「解釈」が違うのです。

linear-gradient() では、0deg は上を指し、値が増えると時計回りに回転します。時計と同じ動きです。
いっぽう、-prefixed-linear-gradient() では、0deg は右を指し、値が増えると反時計回りに回転します。IllustratorPhotoshopのグラデーションツールと同じですね。

と、構文は同じでも解釈が違うというひどさです。なんとかしなくてはいけません。

これは簡単で、ただ計算してやればいいだけです。

@mixin lg-angle($angle, $colorstops...) {
    $prefixes: '-webkit-';
    
    // 接頭辞版の角度の解釈にあわせた値に変換
    $legacy_angle: (450 - $angle) % 360;
    
    @each $prefix in $prefixes {
        background-image: #{$prefix}linear-gradient(
            $legacy_angle, $colorstops);
    }
    background-image: linear-gradient($angle, $colorstops);
}

引き算と剰余演算です。単純に 90 - $angle でもいいですが、なんとなく、なるべく負の値を出したくなかったので剰余演算を使ってみました。無駄ですね。

まとめ……?

というわけで、Sassの持ってるディレクティブや関数を使って、linear-gradient()-prefixed-linear-gradient() をmixinにまとめられることが分かりました。

でも、これじゃ足りないですよね。なぜなら世の中には -prefixed-linear-gradient() に対応してないブラウザ(のバージョン)があるからです。具体的に言うとAndroid 2系とIE9です。

はあ、実用的じゃないですよね……

しかーし、実はいろいろやったら、いちおう対応できることがわかりました。
ただもうえらく長くなってしまったので、別途エントリを書こうと思います。では。