概要
SassとStylus、両CSSプリプロセッサには lighten()
という同名の関数がありながら、その処理は実は違う。
結果の違いや原理の違いを把握し、ついでなのでそれぞれに互換する独自関数を作ってみる。
関数紹介
lighten(color, amount)
-
color
:明るくする前の色。red
とか、#ff0000
とか。 -
amount
:明るくする量。本記事では簡単のために、単位を10%
とかのパーセントのみ、範囲を0~100のみとする。
Sassの挙動
*
color: lighten(red, 50%)
// => color: white;
Stylusの挙動
*
color: lighten(red, 50%)
// => color: #ff8080;
※ 執筆時点で、QiitaのシンタックスハイライトはStylusに未対応
違うね…
// => color: white; // Sass
// => color: #ff8080; // Stylus
違う。
違いに直面した当初は、自分が何をミスってるのかと焦った。
色空間の違いか? と思ったりもしたけど、調べたところどちらもHSL色空間の輝度(Lightness)を増やす関数なので、値の扱いは同じはず1。
結果的には、どのように明るくするかという仕様そのものの違いだった。
なお、併せて述べていくが、暗くする darken()
や、彩度を変える saturate()
と desaturate()
も、やはり両言語間で仕様が違う。
何が違うか
以下、公式情報を概訳しながら拾い上げていく。
Sassの lighten()
公式ドキュメント:
https://sass-lang.com/documentation/modules/color#lighten
ご丁寧に目立つ注意書きがある。
⚠️ ちょっと待った!
lighten()
は、輝度の値を固定的に増加させる関数です。お望みの効果ではない場合があります。// #e1d7d2の輝度は85%なので、lighten()関数で30%の輝度を足すとただの白になる @debug lighten(#e1d7d2, 30%) // white
// 輝度 46% → 66% @debug lighten(#6b717f, 20%) // #a1a5af // 輝度 20% → 80% @debug lighten(#036, 60%) // #99ccff // 輝度 85% → 100% @debug lighten(#e1d7d2, 30%) // white
つまり、引数がパーセント値でありながらその実態は「旧パーセント値そのものを amount
だけ底上げする」であり、「旧パーセント値を amount
の割合だけ増加させる」ではない(伝わるかな…)。
お望みではないかも、というのはこういうことだろう。
なお一般的に、パーセント値そのものが差分的に変動する場合、その差分の単位の名称はパーセントではなくポイント(パーセントポイント、percentage point)となる。
例えば20%が25%になったとしたら、それは「5%増えた」ではなく「5ポイント増えた」という。
何せ、値としての20が5%増えたら $20 \times 1.05 = 21$ になるので、その紛らわしさを回避するためにはそりゃ必要な呼び分け方だろうなという感じがする。
…いやしかし、CSS的には大きさの単位に pt
(ポイント)があるからやっぱ紛らわしいな……。
ということで、Sassの lighten()
は元の色の輝度に対してポイント的処理をしている、といえる。
タイトルの例について改めて触れると、
*
color: lighten(red, 50%)
// => color: white;
HSL色空間では red
等の原色はその時点で既に50%の輝度を持つため、Sassの lighten(red, 50%)
で輝度が更に50ポイント足された結果、輝度100%の white
が得られる。
お仲間の関数について
暗くする darken()
や、彩度を変える saturate()
と desaturate()
についても同じであり、ドキュメントにも全く同様の注意書きがある。
Stylusの lighten()
公式ドキュメント:
https://stylus-lang.com/docs/bifs.html#lightencolor-amount
特に注意書きは無い。
オープンソースなので、ソースを覗いてみる。
https://github.com/stylus/stylus/blob/master/lib/functions/index.styl#L125
index.styl// 与えられた amount で暗くする darken(color, amount) adjust(color, 'lightness', - amount) // 与えられた amount で明るくする lighten(color, amount) adjust(color, 'lightness', amount)
暗くする方と共に adjust()
という関数で定義されており、その差は amount
の正負にある模様。
更に adjust()
のソースを覗いてみる。
https://github.com/stylus/stylus/blob/master/lib/functions/adjust.js#L13
adjust.js(抜粋)function adjust(color, prop, amount){ utils.assertColor(color, 'color'); utils.assertString(prop, 'prop'); utils.assertType(amount, 'unit', 'amount'); var hsl = color.hsla.clone(); prop = { hue: 'h', saturation: 's', lightness: 'l' }[prop.string]; if (!prop) throw new Error('invalid adjustment property'); var val = amount.val; if ('%' == amount.type){ val = 'l' == prop && val > 0 ? (100 - hsl[prop]) * val / 100 : hsl[prop] * (val / 100); } hsl[prop] += val; return hsl.rgba; };
輝度操作のコア部分の処理をグラフで表すとこんな感じ。
https://www.desmos.com/calculator/m5j1th0unt
緑のラインは元の色の輝度。
入力値が正の領域は lighten()
に、負の領域は darken()
に用いられている。
見ての通り、これはSassとは違って「旧パーセント値を amount
の割合だけ増加させる」ような計算になっている。
この計算なら、元の色によらず常に真っ白や真っ黒になるようなケースは amount
を 100%
と指定した場合のみとなる。
ということで、Stylusの lighten()
は元の色の輝度に対して割合的処理をしている、といえる。
タイトルの例について改めて触れると、
*
color: lighten(red, 50%)
// => color: #ff8080;
HSL色空間では red
等の原色はその時点で既に50%の輝度を持つため、Stylusの lighten(red, 50%)
で「輝度50%から100%までの道のりを50%進む」ようにされた結果、輝度75%の #ff8080
が得られる。
お仲間の関数について
暗くする darken()
については既に述べた。
彩度を変える saturate()
と desaturate()
については、少し状況が異なる。
上記コア部分の 'l' == prop && val > 0
という場合分けの左辺(?)が示しているように、彩度関数では入力値が正の領域でも負の方と同じ1次関数が使われているため、amount
を 100%
と指定してもせいぜい元の2倍の彩度にしかならない。
この仕様の意図は正直よくわからない。まぁそういうもんなんだろう。
See the Pen Stylus HSL by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ Try Stylus!)
「ご覧の通り、下げる方向なら輝度でも彩度でも同じなのに上げる方向では違うよね」…ってやろうとしたんだけど、下げる方向もそれはそれで意味わからん誤差があるな……。
といってもパーセント値の一の位で四捨五入すれば同じだし、この程度なら許容されてるのかな…(なお、rate = 43%
にすると四捨五入ですら不一致になる)。
あとなんかこんな挙動もある→ Try Stylus!
どちらが良いのか
良いも悪いも無い。
無いが、どちらかのCSSプリプロセッサを主に使いながらもう片方のものにも多少首を突っ込むような使い方をする人の場合、そういう差があるということだけは知っておく必要がある。
…と思う。
一応の比較考察
Sass側のポイント加算方式は、正直言って私達が「割合」の操作に期待するものではないところがある。
何せSassでは「#FF0000
を半分くらい明るくしたい」と思って lighten(red, 50%)
と書いたら #FFFFFF
が返ってくるわけで、そりゃ公式ドキュメントにも警告が載るわなという感じがする。
ただ一方で、「サイコロを6回振ったら1回は6が出るはず」とか「ハサミギロチン2を3回撃ったら命中率90%」とか思ってしまうのもまた人情であり、ポイント加算方式はそういう思考にはむしろマッチするのが面白い。
あと、HSL色空間は
- 色相(Hue):0~360 [°]
- 彩度(Saturation):0~100 [%]
- 輝度(Lightness):0~100 [%]
という定義上、彩度と輝度は元々がパーセント値なわけで、HSL色空間についてそこまで把握している人なら「そもそもパーセント値を更に割合操作する方がおかしい」と感じるところがあっても不思議ではないと思う。
じゃあどっちも使おうぜ!
違いはわかった。原理もわかった。
それならいっそ、両言語にある独自関数作成機能で、お相手のそれらに相当する関数を自作してしまおうじゃあないか。
そうすれば、言語を問わずいつでも好きな方を選んで使えるようになる。最強。
以下、各CodePenの右側のResult画面が、どちらの言語の例でもおおよそ同じ結果になるのを確認されたし。
誤差は仕方無し。
SassでStylusの lighten()
を再現
概訳時にこっそり省いていたが、公式ドキュメントの lighten()
等の注意書きには、割合的な変更の実現方法として scale-color()
が紹介されている。
color.scale($color, $red: null, $green: null, $blue: null, $saturation: null, $lightness: null, $alpha: null) scale-color(...) //=> color
$color
の1つ以上の成分を流動的に拡縮します。
使わない手は無い。
See the Pen Sass HSL Control: vs Stylus by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ SassMeister)
StylusでSassの lighten()
を再現
単純な足し引きなので楽ちん。
See the Pen Stylus HSL Control: vs Sass by 森の子リスのミーコの大冒険 (@phroneris) on CodePen.
(もしくはこちらから→ Try Stylus!)
既知の誤差として、Kirakiratterのボタンの色を再現すべく sass-lighten(sass-desaturate(#6a4643, 5%), 12%)
とすると、現物は #8a6461
なのにこの自作関数では #8a6460
になる。
まとめ
SassとStylusという、どちらも同じCSSプリプロセッサの役目を担う言語の、同じ名前の関数であり、「HSL色空間に準じた輝度を増やす」という同じような機能を持つ、lighten(color, amount)
。
しかし、その輝度をどのように増やすかという点で、両言語のそれには決定的な違いがある。
Sass | Stylus | |
---|---|---|
処理 | 元の輝度に単純に加算 | 元の輝度から割合的に増加 |
利点 | 元の輝度を知っていれば 計算結果が明白 |
真っ白になるのはamount = 100% の時だけ |
欠点 | 元の輝度を知らない場合は 予想外に白飛びする |
割合の割合を操作する という疑問が残る |
例 |
lighten(red, 50%) → white
|
lighten(red, 50%) → #ff8080
|
また、輝度を下げる darken()
、彩度を上げる saturate()
、彩度を下げる desaturate()
も同様。
ただし、Stylusの saturate()
だけはまた少し違う仕様を持つので注意。
そして、頑張ればどちらも自作関数で再現することができる。
ただし、どうしても細かい誤差が出る場合もあり、完全な互換性は望むべくもないので強く生きる。
余談
なんでこんなことを気にして調べるに至ったかというと、Stylus(ブラウザ拡張機能の方)で使えるUserCSSとして、マストドンの見た目をカスタムするものを作っている過程で気付いたため。
- ブラウザ拡張機能「Stylus」: https://add0n.com/stylus.html
- Stylus用UserCSS(森): https://github.com/Phroneris/StylusUserCSS-Phroneris
↑この「Mastodon未収載アイコン変更」に色変え機能を付けようと思って…。
私はもっぱらStylus使い3だが、マストドンにはSass(SCSS)が採用されており、両者は記法とか変数とか関数とかが似ていたり違ったりする部分が当然色々ある。
そういう色々について色々格闘していた中で、特に色調整関数の挙動の違いにはかなり困惑し、またそれについて紹介する既存の資料も見当たらなかったので、自分自身のためも兼ねてここにまとめたのであった。
おわり