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

CSS Preprocessor Advent Calendar 2012の記事の続編…というか完結編です。
3月まで持ち越すつもりはなかったのに……

Part 7: あとはもう、まとめる

今回はPart 4, Part 5, Part 6で作ったmixinをひとつのmixinにします。どのmixinも type-of() 関数で、mixinの最初の引数を調べて処理を分岐する作りになっているので、分岐先の処理を統合するだけですね。

// config
$lg_support_prefixes: '-webkit-';
$lg_support_webkit_gradient: true;
$lg_support_svg_gradient: true;

@mixin linear-gradient( $first, $rest... ) {

    $prefixes: $lg_support_prefixes;
    $support_wk: $lg_support_webkit_gradient;
    $support_svg: $lg_support_svg_gradient;

    $direction: false;
    $legacy_direction: false;
    $webkit_direction: false;
    $svg_direction: false;

    $colorstops: false;

    // for -webkit-gradient()
    $wk_LT: 0 0;
    $wk_LB: 0 100%;
    $wk_RT: 100% 0;
    $wk_RB: 100% 100%;
    $wk_to_B: $wk_LT, $wk_LB;
    $wk_to_L: $wk_LT, $wk_RT;
    $wk_to_T: $wk_LB, $wk_LT;
    $wk_to_R: $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;
    
    // for SVG data: URL
    $x1_0: ''; // '%20x1%3D%220%25%22';   // ' x1="0%"'   // default
    $x1_1: '%20x1%3D%22100%25%22';        // ' x1="100%'
    $x2_0: '%20x2%3D%220%25%22';          // ' x2="0%"'
    $x2_1: ''; // '%20x2%3D%22100%25%22'; // ' x2="100%"' // default
    $y1_0: ''; // '%20y1%3D%220%25%22';   // ' y1="0%"'   // default
    $y1_1: '%20y1%3D%22100%25%22';        // ' y1="100%'
    $y2_0: ''; // '%20y2%3D%220%25%22';   // ' y2="0%"'   // default
    $y2_1: '%20y2%3D%22100%25%22';        // ' y2="100%'
    $svg_to_B: $x2_0 + $y2_1; // ' x2="0%" y2="100%"'
    $svg_to_L: $x1_1 + $x2_0; // ' x1="100%" x2="0%"'
    $svg_to_T: $y1_1 + $x2_0; // ' y1="100%" x2="0%"'
    $svg_to_R: $x1_0 + $x2_1; // ' x1="0%" x2="100%"'
    $svg_to_TL: $x1_1 + $y1_1 + $x2_0 + $y2_0;
    $svg_to_TR: $x1_0 + $y1_1 + $x2_1 + $y2_0;
    $svg_to_BR: $x1_0 + $y1_0 + $x2_1 + $y2_1;
    $svg_to_BL: $x1_1 + $y1_0 + $x2_0 + $y2_1;

    // utilities
    $type_first: type-of( $first );
    $veryfirst: nth( $first, 1 );
    $type_veryfirst: type-of( $veryfirst );

    // colorstop (no direction specified)
    @if $type_first == 'color' or $type_veryfirst == 'color' {
        // gradient goes from top to bottom
        $direction: null;
        $legacy_direction: null;
        $webkit_direction: $wk_to_B;
        $svg_direction: $svg_to_B;

        // join $first and $rest to form $colorstops
        // simply join() or append() doesn't work: they split $first into color and stop
        $colorstops: append( (), $first, comma );
        @each $colorstop in $rest {
            $colorstops: append( $colorstops, $colorstop, comma );
        }
    }
    // keywords
    @else if $type_first == 'list' and $veryfirst == 'to' {
        // which direction?
        $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;
        $webkit_keywords:
            ($wk_to_B), ($wk_to_L), ($wk_to_T), ($wk_to_R),
            ($wk_to_TL), ($wk_to_TR), ($wk_to_BR), ($wk_to_BL),
            ($wk_to_TL), ($wk_to_TR), ($wk_to_BR), ($wk_to_BL);
        $svg_keywords:
            ($svg_to_B), ($svg_to_L), ($svg_to_T), ($svg_to_R),
            ($svg_to_TL), ($svg_to_TR), ($svg_to_BR), ($svg_to_BL),
            ($svg_to_TL), ($svg_to_TR), ($svg_to_BR), ($svg_to_BL);

        // where to go
        $idx: index( $standard_keywords, $first );

        // if a valid keyword(s) passed
        @if $idx >= 1 {
            $direction: if( $idx == 1, null, nth( $standard_keywords, $idx ) );
            $legacy_direction: if( $idx == 1, null, nth( $legacy_keywords, $idx ) );
            $webkit_direction: nth( $webkit_keywords, $idx );
            $svg_direction: nth( $svg_keywords, $idx );

            $colorstops: $rest;
        }
        @else {
            // error
            @warn 'passed direction is not a valid parameter: #{$first}';
        }
    }
    // <angle>
    @else if $type_first == 'number' and unit( $first ) == 'deg' {
    // ISSUE: what about `rad` or `turn` ?
    
        $direction: $first;
        
        // <angle> is neither supported in -webkit-gradient() nor in SVG
        // but some values can be converted to keywords

        // if the gradient goes down
        @if index( (-900, -540, -180, 180, 540, 900), $first ) {
            $direction: null;
            $legacy_direction: null;
            $webkit_direction: $wk_to_B;
            $svg_direction: $svg_to_B;
        }
        // goes left
        @else if index( (-810, -450, -90, 270, 630, 990), $first ) {
            // $direction: to left;
            // $legacy_direction: right;
            $webkit_direction: $wk_to_L;
            $svg_direction: $svg_to_L;
        }
        // goes up
        @else if index( (-1080, -720, -360, 0, 360, 720, 1080), $first ) {
            // $direction: to top;
            // $legacy_direction: bottom;
            $webkit_direction: $wk_to_T;
            $svg_direction: $svg_to_T;
        }
        // goes right
        @else if index( (-990, -630, -270, 90, 450, 810), $first ) {
            // $direction: to right;
            // $legacy_direction: left;
            $webkit_direction: $wk_to_R;
            $svg_direction: $svg_to_R;
        }
        @else {
            // convert <angle> for prefixed linear-gradient()
            $legacy_direction: ( $first + 450 ) % 360;

            // no fallback for legacy webkit and SVG, turning it off
            @if $support_wk {
                @warn 'invalid argument: #{$first}. -webkit-gradient() declaration dropped.';
                $support_wk: false;
            }
            @if $support_svg {
                @warn 'invalid argument: #{$first}. SVG declaration dropped.';
                $support_svg: false;
            }
        }

        $colorstops: $rest;
    }
    @else {
        // error.
        @warn 'invalid argument: #{$first}. it must either be <color> (+ <stop>), keywords, or <angle>.';
    }

    // output gradients
    @if $colorstops {

        // SVG gradient image
        @if $support_svg {

            $svg_colorstops: '';

            $i: 1;
            $len_colorstops: length( $colorstops );
            $okay: is_compatible_colorstop( nth( $colorstops, $i ) );

            @while $i <= $len_colorstops and $okay {
                $current_colorstop: nth( $colorstops, $i );
                $okay: is_compatible_colorstop( $current_colorstop );
                $svg_colorstops: $svg_colorstops + convert_colorstop( $current_colorstop, $format: 'svg' );
                $i: $i + 1;
            }

            @if $okay {
                $svg_data: 'data:image/svg+xml,' +
                    '%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E' +
                        '%3ClinearGradient%20id%3D%22g%22' + $svg_direction + '%3E' +
                            $svg_colorstops +
                        '%3C%2FlinearGradient%3E' +
                    '%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%23g)%22%2F%3E%3C' +
                    '%2Fsvg%3E';
                background-image: url( $svg_data );
            }
            @else {
                @warn 'invalid argument: #{nth( $colorstops, $i )}. declaration dropped.';
            }
        }
        
        // -webkit-gradient
        @if $support_wk {

            $webkit_colorstops: null;

            $i: 1;
            $len_colorstops: length( $colorstops );
            $okay: is_compatible_colorstop( nth( $colorstops, $i ) );

            @while $i <= $len_colorstops and $okay {
                $current_colorstop: nth( $colorstops, $i );
                $okay: is_compatible_colorstop( $current_colorstop );
                $webkit_colorstops: append( $webkit_colorstops, convert_colorstop($current_colorstop, $format: 'webkit' ), comma );
                $i: $i + 1;
            }

            @if $okay {
                background-image: -webkit-gradient(linear, $webkit_direction, $webkit_colorstops);
            }
            @else {
                @warn 'invalid argument: #{nth( $colorstops, $i )}. declaration dropped.';
            }
        }

        // prefixed linear-gradient()s
        @each $prefix in $prefixes {
            background-image: #{$prefix}linear-gradient( ( $legacy_direction, $colorstops ) );
        }
        // standard syntax
        background-image: linear-gradient( ( $direction, $colorstops ) );
    }
}

// check if a <colorstop> can be converted to WebKit or SVG ones
@function is_compatible_colorstop( $colorstop ) {
    @return (length( $colorstop ) == 2 and
             type-of( nth( $colorstop, 1 ) ) == 'color' and
             type-of( nth( $colorstop, 2 ) ) == 'number' and
             unit( nth( $colorstop, 2 ) ) == '%');
}

// convert CSS <colorstop> to WebKit's or SVG's
@function convert_colorstop( $colorstop, $format ) {
    // invalid condition
    @if is_compatible_colorstop( $colorstop ) {
        @if $format == 'webkit' {
            // e.g. color-stop(50%, #fc0)
            @return color-stop(nth( $colorstop, 2 ), nth( $colorstop, 1 ));
        }
        @if $format == 'svg' {
            // e.g. <stop offset="50%" stop-color="rgb(255,204,00)"/>
            @return '%3Cstop%20offset%3D%22' + nth( $colorstop, 2 ) +
                '25%22%20stop-color%3D%22' + convenc_rgba( nth( $colorstop, 1 ) ) +'%22%2F%3E';
        }
    }
    @else {
        @warn 'invalid argument: #{$colorstop}.';
        @return 'error';
    }
}

// converts <color> to rgb() or rgba() then percent-encode
@function convenc_rgba( $color ) {
    $r: red( $color );
    $g: green( $color );
    $b: blue( $color );
    $a: alpha( $color );
    @if $a == 1 { // rgb()
        @return 'rgb%28' + $r + '%2C' + $g + '%2C' + $b + '%29';
    }
    @else { // rgba()
        @return 'rgba%28' + $r + '%2C' + $g + '%2C' + $b + '%2C' + $a + '%29'; 
    }
}

ふう。
Mixin統合だけでは面白くなかったので、もうちょっといろいろやってます。

  • サポートしたいvariants(接頭辞、-webkit-gradient(), SVG)を設定可能に
  • <angle> の場合でもキーワードに変換可能な場合はそうするように
  • -webkit-gradient(), SVG で使えないカラーストップが出た場合はふたつを出力させないように

あとは内部的なところですが、カラーストップを -webkit-gradient()SVG<stop> 要素(パーセントエンコード済)にする関数もなんとなく統合したり、SVG 用に使う色変換の関数を独自の関数にしたりしてます。

誰が嬉しいかわかりませんが、GitHubに置いときました。

バージョンが0.5になってますが、これは書くの5回目だからです……

感想1: でっかいmixinは避けよう

さて、いけてないと思うところ多々ありますが、とりあえず目的達成です。やー、しんどかった……

書いてみて思いましたが、大規模なmixinはデバッグやメンテが大変ですね。書いている途中に意図しない挙動に出くわすこと幾度となくありました。簡単なテストを書くもあまり使えず、開発に慣れてないなあと悲しくなったりもしました。

大きなmixinにしてしまったのは、「ひとつのmixinにまとめるほうがポータブルかな」とか思ったからなのですが、あとあと関数が必要になったりしたので、もう少し前に気づいて細かいものにすべきだったと思ってます。

まあそもそも、Sassだけでどうにかしようというのもアレですね。

そんなわけで、変に限られたコンテキストから頑張って凝ったmixinを定義するより、ちゃっちゃとfunctionとして自由なコンテキストで処理するほうがラクだったりするケースもあるんじゃないでしょーか。

まったくその通りだと思います……

感想2: 型あると便利

ただ、RubyJavaScriptを使えたとしても、グラデーションのmixinを書くのは面倒かなと思います。というのも、linear-gradient() 構文はかなり自由なので、いろんな書き方に対応しようとすると複雑になるのは避けられない気がしています。Compassなどのライブラリでもグラデーションが満足に対応できていないですし、普通に複雑なのでしょう。

SassでここまでやれちゃったのはSassの値が型を持ってたというのが大きいのかなと思いました。あ、Sass固有ってわけではたぶんなくて、Stylusのtypeof()LESSのGuardsを使えばたぶん移植できるかなと。

感想3: いろいろ

あとはメモです。

  • Sassのinteractive shellが便利だった
    • nullやリスト絡みはCSSの出力とシェルの出力が違うのでちょっと戸惑ったりも
  • ''trueに評価されてハマったりした
    • これはRubyの仕様なのですかね。WAT感。
  • リストを操作する関数が欲しい
    • 分割系の関数がないんですよね
  • エラーを投げたい
    • @warnは処理が止まらないので
  • マップ(ディクショナリ)的なものが欲しい

というわけで、これにて自分のCSS Preprocessor Advent Calendar 2012は終了とします。 誰がために書いたのかまったくわからないこのシリーズに「ヤバい」「引いた」「尖ってる」などのコメントをいただけました。ほんとごめんなさい。

あ、Mixinについて何かあったらGitHubの方によろしくお願いいたします。

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

2013年2月も終わりかけですが、CSS Preprocessor Advent Calendar 2012の記事の続編です。

今回は linear-gradient() から、SVGのグラデーション画像を生成するmixinをつくります。なんでそんなことをするのかというと、IE9linear-gradient() を実装しておらず、またグラデーションを生成できる独自フィルタもIE9標準モードで使えないからです。面倒ですねえ。Windows 7版のIE10の登場とそれへの移行が早く済むとよいのですが。

Part 6 ― SVGはパーセントエンコードされたdata: URLで表現

今回やるのは、異なるCSS構文の変換ではなく、CSSSVGというフォーマットの変換が主です。IE9からサポートされたSVGを利用し、グラデーションを含んだSVGファイルを、background-image で参照させます。

background-image: url(SVGファイルへのパス);

これでSVGファイルを適用できますが、Sassにはファイル生成をする関数なんて用意されてません。なので今回はファイルではなくdata: URLを使い、CSSファイルに直にデータを埋め込みます。

data: URLですが、こんな仕組みになっています。

data:image/png;base64,iVBORw0KGgoAAAANS...

data: スキームに続けてフォーマットのMIME(この場合はPNGのもの), それに続けてエンコードされたデータをつなげると、それがリソース自体のURL表現になります。

SVGMIMEimage/svg+xml です。さて、データですが、先ほどのPNGの場合は base64, という文字が示す通り、データをBase64エンコードし埋め込んでいます。今回のSVGでもそうできればよいのですが、SassにはBase64エンコードするような関数はやはりありません。

そこに登場するのがパーセントエンコードです。URLでコンポーネントに使えない文字列は%xxといった形にエンコードされます。Googleなどで検索した時、q=%E7%9F%A2%E5%80%89%E7%9C%9E%E9%9A%86Mなどという文字列が検索結果のURLにくっついてるかと思いますが、あれです。

要はURLで使える文字列になればOKなので、パーセントエンコーディングでもdata: URLがつくれます。というわけでSVGファイルをパーセントエンコードしdata: URLにしましょう。

ふう。ようやくSVGファイルの構造までたどり着きました。上から下のグラデーションを定義するSVGはこんな感じになります。

<svg xmlns="http://www.w3.org/2000/svg">
    <linearGradient id="g" x2="0%" y2="100%">
        <stop offset="0%" stop-color="#fff"/>
        <stop offset="100%" stop-color="#fc0"/>
    </linearGradient>
    <rect width="100%" height="100%" fill="url(#g)"/>
</svg>

軽く説明すると、こんなことをしています。

  • グラデーション(<linearGradient> 要素)を定義し、それを <rect> 要素に適用(塗りつぶし)する
  • グラデーションの始点と終点は <linearGradient> 要素の属性 (x1, x2, y1, y2) で表す
  • カラーストップは <linearGradient> 要素の子要素に <stop> 要素をストップの数ぶん指定する
  • <stop> 要素には、オフセット (offset) と色 (stop-color) というふたつの属性を指定する

というわけで、Sass側ではlinear-gradient()の引数から次の3つを変換し生成すればよいわけです。

  • グラデーションの方向(<linearGradient>x1, x2, y1, y2 属性の組み)
  • カラーストップの位置(<stop> 要素の offset属性)
  • カラーストップの色(<stop> 要素の stop-color属性)

方向ですが、CSSのキーワードに対応するSVGの方向(属性の組み)を変数にし、それをリストにでもしときます。-webkit-gradient() を生成した時とだいたい同じです。

カラーストップの位置ですが、offset 属性は -webkit-gradient() と同じく、0から1までの数値もしくはパーセンテージしか受け付けません。ですので -webkit-gradient() と同じ制約が必要になってしまいます。今回はパーセンテージのみに絞ります。

カラーストップの色はCSS<color> となっています。SVG 1.1はCSS 2.1を参照していますが、CSS3で定義された rgba() なども利用可能です。これは特に何も変換が必要なさそうです…あっ。

そうでした。生成しないといけないのはパーセントエンコード後の値です。
では、見づらいですが、エンコードされた <linearGradient> 要素(の開始タグ)と <stop> 要素を見てみましょう。

<linearGradient id="g" x2="0%" y2="100%">
%3ClinearGradient%20id%3D%22g%22%20x2%3D%220%25%22%20y2%3D%22100%25%22%3E
<stop offset="100%" stop-color="#fc0"/>
%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23fc0%22%2F%3E

強調箇所で、パーセントエンコードされた文字は次のとおりです。

半角スペース %20
" %22
# %23
% %25
= %3D

まず、方向はパーセントエンコード済の文字列のリストを持っておけばOKそうです。

ただ、あとの2つが困ったことになりました。0%0%25 に、#fff%23fff にしないといけませんが、Sassには文字列置換をする関数がありません。うーん……

やっぱだめかなーと悩んでいたのですが、ちょっと考えたところ、迂回策が見つかりました。

  • %25 は文字列として付加するようにして、変換に使う変数には % を取り除いた数値を使えばよい
  • stop-color 属性はCSS<color> を取るので、# を使わない(文字列置換を必要としない)別の <color> に変換すればよい

ずるい!けどこれでなんとかなりそうです。

パーセンテージの除去は、1% で割ればよいです。

@function removePercentage($percentage) {
    @return $percentage / 1%;
}

色は、rgb() もしくは rgba() 表記に変換します。Sassには色関連の関数が沢山用意されているので、それを駆使します。

@function convertToRGBa($color) {
    $r: red($color);
    $g: green($color);
    $b: blue($color);
    $a: alpha($color);

    // rgb()/rgba()表記をエンコード
    @return if($a == 1, 'rgb%28'+$r+'%2C'+$g+'%2C'+$b+'%29', 'rgba%28'+$r+'%2C'+$g+'%2C'+$b+'%2C'+$a+'%29');
}

おお、いけそうです。あとはちょっと長いmixinを書くだけです。書くだけです……

@mixin lg-svg($first, $rest...) {

    $direction: false;
    $svg_direction: false;

    $colorstops: false;

    // SVGの位置キーワード(x1, x2, y1, y2属性)
    $x1_0: ''; // '%20x1%3D%220%25%22';   // ' x1="0%"'   // default
    $x1_1: '%20x1%3D%22100%25%22';        // ' x1="100%'
    $x2_0: '%20x2%3D%220%25%22';          // ' x2="0%"'
    $x2_1: ''; // '%20x2%3D%22100%25%22'; // ' x2="100%"' // default
    $y1_0: ''; // '%20y1%3D%220%25%22';   // ' y1="0%"'   // default
    $y1_1: '%20y1%3D%22100%25%22';        // ' y1="100%'
    $y2_0: ''; // '%20y2%3D%220%25%22';   // ' y2="0%"'   // default
    $y2_1: '%20y2%3D%22100%25%22';        // ' y2="100%'

    $svg_down:  $x2_0 + $y2_1; // ' x2="0%" y2="100%"'
    $svg_left:  $x1_1 + $x2_0; // ' x1="100%" x2="0%"'
    $svg_up:    $y1_1 + $x2_0; // ' y1="100%" x2="0%"'
    $svg_right: $x1_0 + $x2_1; // ' x1="0%" x2="100%"'

    $svg_to_TL: $x1_1 + $y1_1 + $x2_0 + $y2_0;
    $svg_to_TR: $x1_0 + $y1_1 + $x2_1 + $y2_0;
    $svg_to_BR: $x1_0 + $y1_0 + $x2_1 + $y2_1;
    $svg_to_BL: $x1_1 + $y1_0 + $x2_0 + $y2_1;

    // 色だった場合
    @if type-of(nth($first, 1)) == 'color' {
        $direction: $first;
        $svg_direction: $svg_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;

        $svg_keywords:
            ($svg_down), ($svg_left), ($svg_up), ($svg_right),
            ($svg_to_TL), ($svg_to_TR), ($svg_to_BR), ($svg_to_BL),
            ($svg_to_TL), ($svg_to_TR), ($svg_to_BR), ($svg_to_BL);

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

        @if $idx {
            $direction: $first;
            $svg_direction: nth($svg_keywords, $idx);

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

    // いよいよ出力
    @if ($colorstops) {
        $svg_colorstops: '';

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

        // カラーストップを変換してはくっつけ、を繰り返す
        @each $cs in $colorstops {
            $svg_colorstops: $svg_colorstops + conv_svg_colorstop($cs);
        }
        // data: URL
        $svg_data: 'data:image/svg+xml,' +
            '%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E' +
                '%3ClinearGradient%20id%3D%22g%22' + $svg_direction + '%3E' +
                    $svg_colorstops +
                '%3C%2FlinearGradient%3E' +
                '%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22url(%23g)%22%2F%3E%3C' +
            '%2Fsvg%3E';
        background-image: url($svg_data);
        background-image: linear-gradient($direction, $colorstops);
    }
}

// color-stop() に変換する関数
@function conv_svg_colorstop($colorstop) {
    // $colorstop は <color> と <percentage> のリスト
    $color: nth($colorstop, 1);
    $stop: nth($colorstop, 2);

    @if unit($stop) != '%' {
        $msg: 'パーセンテージで指定しよう: #{$colorstop}';
        @warn $msg;
    }
    $svg_stop: $stop / 1%;

    $r: red($color);
    $g: green($color);
    $b: blue($color);
    $a: alpha($color);
    $svg_color: if($a == 1, 'rgb%28' + $r + '%2C' + $g + '%2C' + $b + '%29', 'rgba%28' + $r + '%2C' + $g + '%2C' + $b + '%2C' + $a + '%29');

    @return '%3Cstop%20offset%3D%22' + $svg_stop + '%22%20stop-color%3D%22' + $svg_color + '%22%2F%3E'
}

できてしまいました。

パーセントエンコードのため、Base64エンコードなdata: URLよりも長くなってしまうところがあまりよろしくないですね。とはいえ、パーセントエンコードが基本的には文字列置換だったから今回のMixinが実現できたというのもあるので、これはOKにしておきましょう。

悔しいのが、色の出力をrgb(), rgba()形式に固定してしまったところですね。ただ、いま気づいたんですが、10進数を16進数に変換する関数を自分で用意すれば、いったんred(), green(), blue()で10進数の値を取って、そこから16進数表記に戻すこともできますね。

ふう。SVGのへの変換もできました。このままもう終えたいのですが、これまで定義したmixinではlinear-gradient()-webkit-linear-gradient(), linear-gradient()-webkit-gradient(), linear-gradient()SVGと出力を分けてしまっているので、実用性がありません。

というわけで、もうだいぶ書くのしんどいのですが、次回はlinear-gradient()から他のvariantすべてを生成するmixinをつくります。次でたぶん終われる…と思う。

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() にも変換可能だとわかりました。

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

誰がうれしいのかわからない、CSS Preprocessor Advent Calendar 2012の記事の第2弾です。
今回は -webkit-gradient() への変換について書こうと思ったのですが、ひとつ忘れてたことがあったので、それについて書きます。

Part 4 ― type_of() で処理を振り分け

前回は「方向なし」「キーワード」「角度」別に3つのmixinを定義しました。分けるとそれぞれ何をやってるかがわかりやすいのですが、やはり使うことを考えると一つにまとめたいもの。そこで、ひとつのmixin内で処理を振り分けることにします。

やり方ですが、Sassの type_of() 関数を使います。mixinの引数を第一引数と可変長引数のふたつにわけ、第一引数の型を判定します。

@mixin lg-compound($first, $rest...) {
    $prefixes: '-webkit-';

    // 出力用の変数
    $direction: false;
    $legacy_direction: false;
    $colorstops: false;
    
    // $first が color を含む → 方向がない → パート1
    @if type_of(nth($first, 1)) == 'color' {
        // direction って名前よくないね
        $direction: $first;
        $legacy_direction: $first;
        
        $colorstops: $rest;
    }
    // $first がリスト → キーワードかも → パート2
    @else if type_of($first) == 'list' {

        // 標準のキーワード
        $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, $first );

        @if $idx {
            $direction: $first;
            $legacy_direction: nth($legacy_keywords, $idx);
            $colorstops: $rest;
        }
    }
    // $first が number → 角度かも
    @else if type_of($first) == 'number' {
        // unit() 関数で角度 (deg) かを確認
        // deg の場合 → パート3
        @if unit($first) == 'deg' {
            $direction: $first;
            $legacy_direction: (450 - $first) % 360;
            $colorstops: $rest;
        }
    }
    // それ以外はエラー
    @else { /* エラーですよん */ }
    
    // カラーストップが確認できたら出力
    @if ($colorstops) {
        @each $prefix in $prefixes {
            background-image: #{$prefix}linear-gradient(
                $legacy_direction, $colorstops);
        }
        background-image: linear-gradient($direction, $colorstops);
    }
}

前回はあまり引数のチェックをしてなかったのですが、ちょっと細かくしてみました。

まず color のチェックは、#fff 0% など、カラーストップに位置指定があることを考えて type_of(nth($first, 1)) と、「最初の最初」の型をチェックしています。

キーワードのチェックでは、list であるかをまず確認します。その上でリストの最初に 'to' があるかを確認しています。

角度の場合は、まず number かをチェックし、その上で unit() 関数を使い単位が 'deg' かを確認しています。これは、Sassにおいて数値と単位からなるものは number に属することを利用しています。

まとめ

type_of() を使って引数の型を調べることで、型にあわせて処理を振り分けられるようになりました。また、値のチェックを詳しくしてみました。

というわけで、つぎこそ -webkit-gradient() の出し方について書こうと思います。

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です。

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

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

:-moz-placeholderから::-moz-placeholderに

コロンに注目ね。Mozillaはこれまでplaceholder属性にスタイルをつけるために、:-moz-placeholderという擬似クラスを使っていたんだけど、Firefox 19からは::-moz-placeholderという擬似要素になる。

変更した理由については、プレースホルダまわりのバグを修正するにあたってのことらしい。

直接関係ないけど、フォーカス時にもプレースホルダが出るのを止めるためのprefもついたdom.placeholder.show_on_focusから変更可能。

さて、独自拡張だからなのか、これまでの:-moz-placeholderを残すわけでもなく::-moz-placeholderに置き換えられるので、プレースホルダのスタイルをいじってるひとは注意かな。コロンいっこ足すくらいでいいけど、プレースホルダが半透明になってるので、不透明にしたい場合はopacityを書き足さないといけない。

さて、Mozillaは擬似クラスになった。他はどうか。WebKitはもともと::-webkit-input-placeholderで、MicrosoftはIE10で:-ms-input-placeholderという擬似クラスを導入している。ややこしいね。www-styleだと擬似クラスの方に傾いていたような記憶があるけれど、ちゃんと標準化されるんだろうか。

さらばmozとoのdevice-pixel-ratio

そういえば、Firefox 16とOpera 12.10からメディアクエリーのresolutiondppxが使えるのを思い出した。
これらを使うと、Retina displayはじめ高密度なディスプレイに対応するコードがけっこう短くなる。

これまではこんな感じ。

@media (-webkit-min-device-pixel-ratio: 2),
    (min--moz-device-pixel-ratio: 2),
    (-o-min-device-pixel-ratio: 2/1) {
        .foo {
            background-image: url(image-2x.png);
        }
    }
}

device-pixel-ratioはもともとWebKitの拡張だったもの。それをMozillaOperaも取り入れたのだけど、ベンダー接頭辞の面倒さに加えてmin-/max-のつき方、値の書き方がばらばらというカオスになっていた。どちらも各々のパーサに都合のよい、もしくは「適切」だと考えた構文を実装した結果なのだろうけど、センスよい感じではない。

そんな時代も終わり。resolutiondppxでこんなにすっきり。

@media (-webkit-min-device-pixel-ratio: 2),
    (min-resolution: 2dppx) {
        ...
    }
}

-webkit-device-pixel-ratio: 2resolution: 2dppxだけ。すっきり。

というわけでみんな書きかえとこう。Firefoxはもう17が出るし、Opera Mobileも12.1が出てからひと月以上経つ。古いバージョンを気にする必要はそんなないでしょうう。device-pixel-ratioを使うコードなんてほとんどスマートフォンが対象だろうし、そこでのFirefoxOperaのシェアとかコンテンツ側の対応状況を想像するに…うん、いいでしょうよ。

device-pixel-ratioを標準化すればよかったんじゃ」っていう素朴な疑問については、CSS WG Blogのポストを。回答になってるかは怪しいけれど。

resolution10月にWebKitでも実装されたdppxは6月に実装)のだけれど、Mac portでもChromium portでも有効にはなってなさげ。

他にもいろいろ思い出してきたけど面倒なので書くのはやめておく。

FirefoxのFlexbox実装、進行中

MozillaのBug 666041がRESOLVED FIXEDになった。また一歩CSS Flexboxのサポートに近づいた。

バグのタイトル的にはメタバグっぽいんだけど、そうじゃなくて、基本的なところの実装みたい。なのでMultiline Flexboxや、結局名前が変わらなかったorderプロパティvisibility: collapseなどまだ実装されてないのがいろいろあるみたい。

というのもあってか、まだビルドで有効にされてない。トラッカーとして使われているBug 783409にぶらさがってるバグの進捗を見る限り、もうちょっとかかりそう。

まだかなあ。楽しみだなあ。

追記 (2012-10-17) ビルドの有効と接頭辞の削除も

使えるのはもう少し先かと思ったら、10/3にビルドで有効にされた。

有効とはいっても、全部の機能がそろったわけでも、すぐに使える状態にあるわけでもない。使える状態にあるというのは、デフォルトで無効にされているから。なのでabout:configからlayout.css.flexbox.enabledをtrueにしないと動かない。まあでも、試せるのはとても素敵。

遊んでると楽しい。

で、昨日は接頭辞も外れた。

デフォルトで有効にされるものにはmultilineが入らない感じなのでちょっと残念だけど、その分有効になるのは早くなるかなあ。

 

接頭辞外しと-webkit-サポート

もう書くのだるいんだけど、またまたベンダー接頭辞的なおはなし。

Opera 12.50では標準と-webkit-のみ (-o-さよなら)

今月に入って、Opera 12.50で使われるPrestoのバージョンが2.12になった。

まず3日のsnapshotでは、接頭辞なしのTransitions, 2D Transforms, Animationsのサポートが行われ、さらに接頭辞付きの実装が削除された。

続いて10日のsnapshotで、接頭辞なしのグラデーションもサポートされ、3日のと同じように接頭辞付きの実装が削除された。

つまり、12.50からは、-o-fooなコードは単に無視されることになる。

-o-のサポートを消した理由は、standards compliance的なところもあるんだろうけれど、Opera 12.50で-webkit-なものをエイリアスとしてサポートするのが大きいんだろう。-o-を書いてるなら-webkit-も書かれてるってことなのか、-o-なコードはあまりなくて、-webkit-のサポートのが互換性が高いってことだったんだろうか。嗚呼デファクト標準。

Firefox-webkit-サポートは?

-webkit-を(消極的にだけど)サポートしないとと考えているのはOperaだけではなく、MozillaもMicrosoftもそうなはず。ではこの2つはどうなったのか。

Mozillaについては、ちょっと前にMozillaのLawrence MandelがWebKitとの互換性について触れている。

13日の時点では、-webkit-な機能にエイリアスを張るかまだ決定していないと。その次にこう書いてある。

As our current research has shown, the problems with the mobile Web extend beyond CSS prefixing to user agent sniffing and Webkit specific functionality. It is unclear that aliasing Webkit CSS prefixes in Firefox will provide much benefit to mobile Web compatibility.

モバイルWebの問題はCSSだけではなく、UA SniffingWebKitの独自拡張にも及んでおり、CSSエイリアスがどれだけ意味を成すかがわからないと。

で、その翌週。

After our initial discussion last week, David Baron created a build of Firefox for Android that includes a number of Webkit CSS property aliases. John Jensen put together a short list of sites to test based on market analysis and top site data and Aaron Train and Jason Smith got to work testing these sites using David’s build to see what impact, if any, aliasing CSS properties has on site compatibility. [...]

The partial test results show that the experience of the majority of the sites in this set did not improve by adding Webkit CSS aliases. In one case the experience of the site was actually worse after adding the aliases. There are a couple of notable exceptions where the experience did significantly improve. In both of these cases the sites make heavy use of animations and transformations. Our next step is to investigate more sites that make heavy use of these two categories of CSS properties to see if the impact is more notable on this category of sites. We plan to review the new test results at the end of this week.

-webkit-をサポートした実験的なビルドでいくつかのサイトをテストしたところ、互換性が向上したサイトはあまりなかった。それどころか逆に下がるところもあったと。

もちろん、互換性が上がったところもあって、そういうサイトはAnimations, Transformsを使っているところだったと。これら2つの互換性に対する影響はどれくらいなのかというのは、またこれから調査するらしい。

互換性が逆に下がったというのが面白い。なんだろう。Firefoxはけっこうなシェアがあるから、-mozも一定割合書かれていて、それが継承や上書きで悪影響を及ぼしたんだろうか。Animationsなどのコードで互換性が上がったのは、@-webkit-keyframesに加えて@-moz-keyframesなんて書いてらんないから数が少なくて、ってことかなあ。

IEはどうなるんだろう

今回の接頭辞削除のきっかけになった、IE10でのunprefixing。強引ではあったけど、他のベンダーも追従して、リリーズ時期もいい具合に重なりそうで、よさげな感じになっている。

さて、MicrosoftはIEで-webkit-エイリアスを設けるんだろうか。接頭辞に関してはほぼ無視されてるであろうIEだから、-webkit-を解釈すると互換性は上がりそうな気がするけれど、素直に実装するかというと、違うかなあと。

あくまで予想だけれど、互換表示リストを使うんじゃないか。せっかくそういう仕組みがあるし、ドメインやパス単位で細かいカスタマイズも可能だから、エンジン側でどんな時でも-webkit-を-ms-にマップさせるのではなく、互換表示リストに指定されたサイトのみ-webkit-なコードを解釈させるというのは十分に考えられるかなと。Windows 8のRTMが配布され始めたら、もっと何かわかるかな。

追記 (2012-08-16): Firefoxエイリアスの方針

Twitterでの言及で、Mozillaの該当バグを教えてもらう。ありがとうございます。

ざっと読むと、2つの方針があるようだ。

  • すべての-webkit-な機能をサポートするわけではない
  • Mozillaで接頭辞なしの実装が行われてから、エイリアスを張るか検討する

前者については前からそうだった気がする。後者については、やることやってからにしようってところか。

ただ、何の機能についてエイリアスを張るかなどについては言及なし。

追記 (2012-08-19): FirefoxWebKit互換性調査、仮報告

Lawrenceが新しいエントリを起こしていた。

曰く、現時点では-webkit-エイリアスとしてマップしてもあまり益にはならないと。なんと。

ただ、これは「CSSのみをマップしても」ということらしい。

The results thus far indicate that there is a very small benefit of adding Webkit CSS aliases to Gecko. However, our research is not yet complete, so we will refrain from making any definitive decisions until all our research has been carried out.

The consensus from dbaron, jet, jsmith, tchung, aaronmt, jjensen, and me is that the value of aliasing Webkit CSS properties in Gecko alone appears to be pretty low and the benefit does not warrant its inclusion in the platform at this time.

Lawrenceのニュースグループへの投稿にもう少し詳しい事やデータが書いてある。今回のテストではUA SniffingとAliasingをしたけれど、それだけではあまり効果は見込めず、DOMのaliasingやGeckoで未サポートの機能を含めた調査が必要と。

ちょっと意外だった。どうなんだろう。Firefoxについてはそこまで無視されてないってことなんだろうか。ふうむ。

@supports ― CSSのFeature Queries

Twitter殺しなタイトルだ。Firefox NightlyにCSS Conditional Rules@supportsが入った。

仕様がまだ初期段階なので、他のベンダーや仕様の動きが思わしくない場合は設定でオフにされる可能性もあるらしいけれど、嬉しいね。

Feature Queries

@supportsCSSのat-ruleで、あるプロパティと値のペアをサポートしている/いない状態にだけ、ブロック内の宣言を適用するというもの。仕様書の例を見るほうが早いか。

@supports ( display: flexbox ) {
  body, #navigation, #content { display: flexbox; }
  #navigation { background: blue; color: white; }
  #article { background: white; color: black; }
}

っていう。メディアクエリーと似ていて、and, or, not が使えるので複雑な条件も指定できる。ただ、メディアクエリーと違って、値のないクエリーは書けない。つまり「〇〇プロパティをサポートしてる」とはできない。あくまでプロパティと値。

あと、セレクタや別のat-ruleをクエリーには入れられない。at-ruleは別にしても、セレクタはちょっと仕組みがほしい気もする。

さて、CSSには不明なプロパティや値を無視する仕組みがあるのに、どうして@supportsが提案されたのか。

ひとつは、CSSが複雑になったから。たとえば、CSS RegionsCSS Grid Layoutみたいな仕様は、そこで使われるプロパティが相互に依存し、時には別のモジュールのプロパティからも影響を受けるので、宣言単位で無視されるだけだとうまくフォールバックがきかない。レイアウトが大幅に崩れる可能性が多分にある。作りようによっては無視されても後方互換になるんだろうけれど、「Regionsサポートしてる」「Grid Layoutサポートしてる」みたく、ルールセットの塊でわけられる方が、たぶんだいぶ楽。

あとは、各ブラウザーの対応状況やプライオリティの付け方が違ったり、ベンダー拡張が増えてきたというのもあるだろう。

Operaも実装、WebKitには前からパッチが

さて、Mozillaが実装したのをうけてOperaのFlorianがTwitterでこう反応している。

@bz_moz @heycam @davidbaron @supports' time has come. Soon available in #opera as well.

Soonとな。Opera 12.50のCoreがPresto 2.12になったばかりだけれど、2.12に入ってきたりするんだろうか。

しかし、これを実装してほしいのは、やっぱりWebKitと、あとTridentだったりする。MozillaOperaが大きめのCSS仕様を提案をすることがあまりないのと、Mozillaのみ、Operaのみサポートしてるプロパティ(拡張含む)はそこまで数も利用例もないので、@supportsを使ってまでっていう状況に出くわさない。

いっぽうWebKitはいろんなベンダーがいろんなものを追加しているし、MicrosoftもIE10から結構アグレッシブになってきている。

ベンダー固有の拡張だからよくない、というわけじゃないんだけど、標準化のスピードや他のブラウザへの実装が不透明になりがちなので、独自実装が入りやすいエンジンにこそ、オプトインでCSSを記述できるような仕組みが入って欲しい。

.foo {
  /* WebKit拡張もしくはMicrosoft拡張のプロパティ */
  bar: baz;
  ...
  /* 全ブラウザで対応してるプロパティだけど、bar: bazを前提にした宣言 */
  qux: quux;
}

@supports not (bar: baz) {
  .foo {
    ...
    qux: foobar; /* 上書き */
  }
}

こんな感じでちまちま上書きするのは不毛なので、これ以上仕様が増えて実装される前に、@supportsが入って欲しいなと。

で、WebKitには実はパッチが上がっている。

レビューがされてないので、すぐ実装されるというわけではなさそうだけど。

Tridentについては、Windows 8がRTMになってしまったし、次を待たないといけない。

その他

機能が機能なので、Mozillaの実装では@-moz-supportsなんて馬鹿げたことはしていなくて、単に@supportsとなっている。

あと、@supportsのOM版も考えられていて、今のところwindow.supportsCSS('property', 'value')なんてのが考案されている。名前がjQueryでもつけなそうな安直さ(かつ紛らわしさ)なのでなんとかなんないかなあ。あと、Windowにぶら下げるのはどうよ的な話が出ているので、それは変わるかもしれない。

CondRulesには@-moz-document由来の@documentもあるので、ユーザースタイルシート書きとしてはそちらもぜひ欲しいところ。

Firefox (Nightly)で接頭辞なしのCSS Transitions, CSS Animationsも実装

うっ、一日待っとけばよかった。
TransitionsとAnimationsにも接頭辞なしのが実装された。

全部入ったらFirefox 16は楽しいなあ。

しかし、たまたま重なっているだけなのか、何か意味があるのかな。やっぱりFirefox OSのリリース時期とかが関係していたりするだろうか。接頭辞依存の機能がプラットフォームの基盤にあると、どうしても省きにくくなってしまうだろうし。

あとは、次のESR版がFirefox 17だから、それまでには安定したものを入れておきたいとかってことなのだろうか。そんな関係ない気もするけど。

あとは-webkit-のサポートだけれど、どうなんだろう。Animations, (3D) Transformsあたりはしとくとよさそうな感じがなんとなくする。Transitionsは動いたりするものでなければそんな問題ないというか。というか.5sとか長すぎるdurationで遷移させるような邪魔臭いスタイルが多そうな気がする(偏見)から、ないほうが嬉しいなんてことがあったりしそうな…

Firefox (Nightly)で接頭辞なしのCSS TransformsとGradientsが実装

接頭辞なしのCSS TransformsとCSS GradientsがFirefox Nightlyに実装された。

バグのタイトルにはunprefixやdrop prefixとあるけれど、今のところは接頭辞なしのバージョン(つまり標準)を実装するのみにとどめている。-moz-なものを省くわけではない。どれくらい残るんだろうなあ。

バックアウトされなければ、このまま来週にAuroraに移動予定で、リリースされるのは10月半ば(だと思う)。

さて、接頭辞を落とすきっかけになったIE10では、もちろん接頭辞なしのバージョンが実装される。Opera 12.50では-webkit-な機能の一部が実装されるし、たぶん接頭辞を省いたバージョンもそう遠くないうちに実装するだろう。

そう考えると、これらがリリースされる(だろう)今年後半には、TransformsとGradientsについては-webkit-と接頭辞なしの記述だけで済むようになるのかもしれない。

あ、2D TransformsはIE9でも入ってるから、-ms-transformはあってもいいのか。

ただグラデーションについては結構不安が残る。構文変更について知られていないのか面倒だと思われているのか、あまりちゃんと対応してるコードを見たことがない。どうなるのかなあ。