Sassでlinear-gradient()のmixinをつくる その4
2013年2月も終わりかけですが、CSS Preprocessor Advent Calendar 2012の記事の続編です。
今回は linear-gradient()
から、SVGのグラデーション画像を生成するmixinをつくります。なんでそんなことをするのかというと、IE9が linear-gradient()
を実装しておらず、またグラデーションを生成できる独自フィルタもIE9標準モードで使えないからです。面倒ですねえ。Windows 7版のIE10の登場とそれへの移行が早く済むとよいのですが。
Part 6 ― SVGはパーセントエンコードされたdata: URLで表現
今回やるのは、異なるCSS構文の変換ではなく、CSS→SVGというフォーマットの変換が主です。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表現になります。
SVGのMIMEは image/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をつくります。次でたぶん終われる…と思う。