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

2013年1月も後半ですが、CSS Preprocessor Advent Calendar 2012の記事の続編です。

さて、数日前にWebKitが接頭辞なしのグラデーションをサポートしました。Chrome 26やSafari 7?になれば幸せになりそうです。

でも、GingerbreadのAndroidブラウザーなど、-webkit-gradient() しかサポートしてない環境に対応しないといけないこともあるでしょう。というわけで、今回は linear-gradient() から -webkit-gradient()を生成するmixinを作ります。

Part 5 ― 構文の違いは制約と関数でなんとかする

-webkit-gradient()linear-gradient()と構文が大きく違います。

-webkit-gradient( linear, start, end, colorstops ... )

第一引数っぽいものに linearという、線形グラデーションを示すキーワードを指定します。そのあとに始点と終点、そしてカラーストップを続けます。

始点と終点ですが、定義済みのキーワードもしくはパーセンテージのみが使えます。linear-gradient()のように角度を使うことはできません。

カラーストップですが、色と位置を並べるだけの linear-gradient() と異なり、color-stop()という関数表記が使われます。

color-stop( stop, color )

linear-gradient() と色と位置の書き順が逆になります。stopには0から1までの数値、もしくはパーセンテージが指定できます。省略はできません。

まとめると、linear-gradient() に比べて -webkit-gradient()では次のことができません。

  • 角度が使えない
  • カラーストップの位置には数値もしくはパーセンテージのみ
  • カラーストップの位置が省略できない

というわけで、mixinとしてふたつの構文を満足させるためには、以下の制約が必要です。

  • グラデーションの方向に角度は使わない
  • カラーストップはパーセンテージで指定し、必ず書く

これさえ守ってくれるなら、変換はそんな難しくはありません。面倒ですが。
ということで、こんな感じになりました。

@mixin lg-webkit($first, $rest...) {
    
    $direction: false;
    $wk_direction: false;

    $colorstops: false;

    // 始点/終点キーワードの変換
    $wk_LT: 0 0;
    $wk_LB: 0 100%;
    $wk_RT: 100% 0;
    $wk_RB: 100% 100%;

    $wk_down:  $wk_LT, $wk_LB;
    $wk_left:  $wk_LT, $wk_RT;
    $wk_up:    $wk_LB, $wk_LT;
    $wk_right: $wk_RT, $wk_LT;

    $wk_to_TL: $wk_RB, $wk_LT;
    $wk_to_TR: $wk_LB, $wk_LT;
    $wk_to_BR: $wk_LT, $wk_RB;
    $wk_to_BL: $wk_RT, $wk_LB;

    // 色だった場合
    @if type-of(nth($first, 1)) == 'color' {
        $direction: $first;
        $wk_direction: $wk_down;

        $colorstops: $rest; // ISSUE
    }
    // 'to' からはじまるキーワードの場合
    @else if type-of($first) == 'list' and nth($first, 1) == 'to' {

        $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;

        $webkit_keywords:
            ($wk_down), ($wk_left), ($wk_up), ($wk_right),
            ($wk_to_TL), ($wk_to_TR), ($wk_to_BR), ($wk_to_BL),
            ($wk_to_TL), ($wk_to_TR), ($wk_to_BR), ($wk_to_BL);

        $idx: index( $standard_keywords, $first );

        @if $idx {
            $direction: $first;
            $wk_direction: nth( $webkit_keywords, $idx );

            $colorstops: $rest;
        }
    }
    // 角度かそれ以外か
    @else {
        $msg: '-webkit-gradient() に変換できないよ: #{$first}';
        @warn $msg;
        /* #{$msg} */
    }

    // いよいよ出力
    @if ($colorstops) {
        $wk_colorstops: null;

        // いけてないけど……
        @if type-of(nth($first, 1)) == 'color' {
            $wk_colorstops: convert_colorstop($first);
        }

        // カラーストップを変換してはくっつけ、を繰り返す
        @each $cs in $colorstops {
            $wk_colorstops: join($wk_colorstops, convert_colorstop($cs), comma);
        }

        background-image: -webkit-gradient(linear, $wk_direction, $wk_colorstops );
        background-image: linear-gradient($direction, $colorstops);
    }
}

// color-stop() に変換する関数
@function convert_colorstop($colorstop) {
    @if unit(nth($colorstop, 2)) != '%' {
        @warn 'パーセンテージで指定しよう: #{$colorstop}';
    }
    @return color-stop(nth($colorstop, 2), nth($colorstop, 1));
}
 

ふう。

キーワードのあたりが何やら騒がしいですが、マッピングの考えはパート2と同じです。準備が必要なだけです。むつかしくないです。

個々のカラーストップを color-stop() へ変換するために convert_colorstop() という関数を定義しました。@eachで回して都度変換し、出力用の変数にくっつけると。だいぶコスト高そうですが、map() 関数みたいなものがないので……

ちょっといけてないのが、最初の(最初の)引数が color 型だったときの処理でしょうか。$colorstop$first, $rest という感じにできるとよいのですがそうもいきません。可変長引数の型は arglist という、それ自体がひとつのリストになっています。なので、$first, $rest とすると長さが2つのリストになっちゃいます。$rest@eachで回して…というのもできなくはない気がしますが、だるいのでここは個別に処理します。

あと、もしも角度が入ってしまった場合に、こういうことができます。

    // もし角度だった場合
    @else if type_of($first) == 'number' and unit($first) == 'deg' {

        // ちょっとは受け入れて、deg をキーワードに変換
        @if index((-900, -540, -180, 180, 540, 900), $first) {
            $direction: null;
            $wk_direction: $wk_down;
        }
        @else if index((-810, -450, -90, 270, 630, 990), $first) {
            // $direction: to left;
            $wk_direction: $wk_left;
        }
        @else if index((-1080, -720, -360, 0, 360, 720, 1080), $first) {
            // $direction: to top;
            $wk_direction: $wk_upward;
        }
        @else if index((-990, -630, -270, 90, 450, 810), $first) {
            // $direction: to right;
            $wk_direction: $wk_right;
        }
    }

キーワードに変換してあげるやさしさ。リストで指定した範囲より広い角度はダメですが、まあだれが 1080degとか書くんだって話です。

使う側の制約は大きいですが、いちおう -webkit-gradient() にも変換可能だとわかりました。