【CSS x JavaScript】HLS(カラーコード)を活用して美しくアニメーションするグラデーションカラーを作ってみる

wakamsha
178

CSS における色指定で最も一般的なのは HEX 方式でしょうか。CSS3 が定着してからは RGB の方が多くなってきてるでしょうかね。アルファ値 (透明度) も一緒に設定できることですし。ちょっとした検証には Color name を使うこともあります (僕の場合)。

HEX RGB Color name
#ffffff rgb(255, 255, 255) white

この他にも CSS3 からは HSL という形式の色指定が出来るようになりました。あまり普及していない印象を受けますが、これはこれで中々のポテンシャルを持った機能なので、ひとつ調べてみることにしました。

HSL - CSS3から導入された色指定方法

色相 ( Hue )彩度 ( Saturation )輝度 ( Luminance / Lightness )の3つの要素1)色の三属性といいます。からなる指定方法のことです。写真やイラストなどグラフィック分野に明るい人であればご存知かもしれませんね。Photoshop や Illustrator にもこの指定方法があります。CSS ではこのように記述します。

.demo {
  background: hsl(色相, 彩度, 輝度);
}

// アルファ値 (透明度) を含めた hsla というのもあります
.demo--skeleton {
  background: hsla(色相, 彩度, 輝度, 透明度);
}

HEX や RGB に慣れた人だとはじめはなかなかとっつきにくいかもしれませんが、キチンと意味を理解すればとても使い勝手の良い指定方法であることがお分かりいただけるはずです。ひとつひとつの値についてもう少し詳しく掘り下げてみるとしましょう。

色相 ( Hue )

color_h

簡単に言うと、赤、橙、黄、緑、青、藍、紫という言葉で区別できる色の性質を色相といいます。いま挙げたのは虹の色ですね。虹は一般的に7つの色で表現しますが、世の中にはもっとたくさんの色が存在します。黄と緑を混ぜれば黄緑ができ、緑と青を混ぜれば青緑が出来ます。そうです。色はそれぞれ独立したものではなくお互いが連続して繋がり合っており、それはひとつの色相の輪を作り出します。これを色相環と呼びます。

その色相ですが、プログラム上では 0 ~ 360 の値で指定します。これは先程の色相環の角度を意味します。0を指定すると赤となり、120を指定すると緑、240は青といった具合になります。環であることから値は一周させて同じ色を指定することも出来ます。つまり360720と指定しても結果は0と同じ赤となります。

彩度 ( Saturation )

色の鮮やかさを表します。例えばレモンの色は黄です。とても鮮やかな色をしていますね。一方で梨の色は黄土というものですが、これも黄色の一種です。同じ黄でも鮮やかさが違います。彩度が高ければより鮮やかな色 (純色)となり、低ければ無彩色 (白、黒、灰)になります

Munsell_Color

プログラム上では 0 ~ 100 (%) で指定し、数値が高いほど鮮やかな色になります。これを色空間という図で表すとこのようになります。中央軸を無彩色軸と呼び、この中央軸から色相環までの距離が彩度となります。

輝度 ( Luminance / Lightness )

色の発光度合いを表します。カラーで撮影した写真や映像を白黒にする時はこの輝度を調整することで実現するのが一般的です。彩度を 0 にして白黒化することも出来ますが、彩度による調整は全体的にのっぺりした印象になってしまうので普通は輝度を調整します。

luminance

プログラム上では 0 ~ 100 (%) で指定し、0%だと黒、100%で白となります。純色を表現したい時は50%を指定し、これが基準となります。

ブラウザのサポート状況

Can I use でブラウザサポート状況を確認してみると、IE9 からサポートされているということなので安心して使うことが出来ます。

can_i_use-hsl

DEMO 1 - CSS でグラデーションカラーを指定してみる

HSLという色指定方法の特徴が分かったところで、早速これを利用したデモを作ってみるとします。とりあえず最初は CSS で background-color を指定するのに使ってみるとします。ただ単色を指定するだけではつまらないので、HSL の利点を感じるためにグラデーションカラーを指定してみるとします。

.demo {
    background: radial-gradient(farthest-side ellipse at 10% 0, hsl(300, 100%, 95%), hsl(190, 50%, 70%) 80%, hsl(226, 40%, 60%) 120%);
}

実際の動作はこちらです。

See the Pen Gradient color by HSL by wakamsha (@wakamsha) on CodePen.

左上から右下に向かって円形状に色が変化しているというグラデーションですが、色相だけでなく彩度や輝度も徐々に変化させています。このデモで指定した色は以下の通りですが、同じ色を RGB で表してみると HSL の方が色の移り変わり具合がより直感的に見て取れるのがお分かりいただけるかと思います。

HSL RGB
カラー1 hsl(300, 100%, 95%) rgb(255, 229, 255)
カラー2 hsl(190, 50%, 70%) rgb(140, 204, 217)
カラー3 hsl(226, 40%, 60%) rgb(112, 131, 194)

DEMO 2 - 色相のみを変化させてトーンの揃ったカラーパターンを作ってみる

彩度と輝度を固定して色相を変化させるだけで同じトーンのカラーパターンを量産することが出来ます。例えば iOS は実に多彩な色を持ったトーンマナーにも関わらず統一感が失われていないのは、この彩度と輝度が一定に保たれているからです2)実際は微妙に調整されているのですが、それでも殆ど同じだったりします。。そういったカラーパターンも HSL の色相を変化させるだけで簡単に作り出すことが出来ます。以下はそのSCSS の例です。

.gradient {
  $colors:(
    red      : (start: 11,  end: 343),
    orange   : (start: 35,  end: 11),
    yellow   : (start: 48,  end: 40),
    green    : (start: 135, end: 124),
    teal     : (start: 165, end: 200),
    blue     : (start: 190, end: 220),
    violet   : (start: 282, end: 241),
    magenda  : (start: 321, end: 282),
    lime     : (start: 102, end: 164),
    lavender : (start: 288, end: 287),
    sunset   : (start: 342, end: 11),
    skyblue  : (start: 214, end: 185),
  );

  @mixin variant-color($name, $start-hue, $end-hue) {
    &--#{$name} {
      background: linear-gradient(hsl($start-hue, 100%, 60%), hsl($end-hue, 100%, 55%));
    }
  }

  height: 160px;
  margin-bottom: 30px;
  padding: 15px 0 0;
  border-radius: 8px;

  @each $name, $color in $colors {
        @include variant-color($name, map-get($color, start), map-get($color, end));
    }
}

配列で定義した色情報を @each で回して @mixin を実行することで各色のスタイルを定義しました。

実際の動作とソースコード全体ははこちら。

See the Pen Color palette by wakamsha (@wakamsha) on CodePen.

ちなみに start, endと二色の色相を指定するのではなく、色相を一色にして彩度を変更することでもグラデーションは実現出来ます。こちらの方が何かと楽かもしれませんね。

DEMO 3 - EaselJS と組み合わせて美しく変化するグラデーションカラーを作ってみる

従来のRGB指定の難点として、色の連続性 (グラデーション) をプログラム化し難いというのがあります。例えば赤、橙、黄、緑をそれぞれRGBで表現するとrgb(255, 0, 0)rgb(255, 165, 0)rgb(255, 255, 0)rgb(120, 100, 50)といった具合なります。これだけならまだどうにか出来なくもないですが、鮮やかさや明るさから感じる印象は色相によって異なるため、自然で美しい色合いを表現しようとするとそれらも考慮しなくてはなりません。

そこでHSLを使ってみます。HSLは色の構成が色相、彩度、輝度と分かれているため、彩度と輝度の値を固定して色相値だけを連続的に変化させれば自然で美しいグラデーションを表現出来ます。

更に色相値は環のようにループしているので単純に数値を上げていくだけでも綺麗に色の変化を循環させることが出来ます。この特性を活かして時間とともにグラデーションの色が徐々に変化していくというデモを作ってみました。絶えず色を変化させるとなると CSS では処理が重すぎて描画処理が追いつかないため、ここは Canvas を使うことにします。Canvas の操作は CreateJS の EaselJS を使うと非常に便利です。

class GradientBackground {

  constructor(stage) {
    this.stage = stage;
    this.gradientFill = new createjs.Shape();
    this.stage.addChild(this.gradientFill);
    
    createjs.Ticker.timingMode = createjs.Ticker.RAF;
    let me = this;
    createjs.Ticker.addEventListener('tick', () => me.tick());
  }
  
  tick() {
    // グラデーション用カラーを生成します。
    // 色は HSL 形式で生成し、H (色相) を現在時刻 ( getTime() ) を使って算出することで自然な色変化を演出します。
    let colorStart = createjs.Graphics.getHSL(new Date().getTime() / 50, 80, 80);
    let colorEnd = createjs.Graphics.getHSL((new Date().getTime() + 50 * 60) / 50, 100, 70);
    
    this.gradientFill.graphics
      .clear()
      .beginLinearGradientFill([colorStart, colorEnd], [0, 1], 0, 0, this.stage.canvas.width, this.stage.canvas.height)
      .drawRect(0, 0, this.stage.canvas.width, this.stage.canvas.height);
    this.stage.update();
  }
}

ES6 のクラス機能を使ってみました。コンストラクタメソッドで stage オブジェクトを受け取り、グラデーションを描画するためのオブジェクト (Shape) を配置します。グラデーションの再描画タイミングはフレームレート値ではなく、requestAnimationFrame に合わせたいので createjs.Ticker.RAF を設定しました。

tick() はグラデーションの描画処理を行います。まず色を定義するわけですが、今回は彩度と輝度はそのままに色相だけを変化させたいので H (色相) の値を動的に生成するようにしました。ここでは new Date().getTime() を利用して生成しています。50 で割るなど細々とした計算処理がありますが、ここは感覚でいい感じの色合いが出るように調整した結果となります。

また、CSS では彩度と輝度の指定に % を付ける必要がありましたが、getHSL() では % を付ける必要はありません。

実際の動作とソースコード全体ははこちら。

See the Pen The color changes guradually by wakamsha (@wakamsha) on CodePen.

締め

ずっと RGB で色定義してきた人からすると、はじめのうちはなかなか完成する色が想像できずとっつきにくいかもしれません。しかし Chrome の Dev tool には強力なカラーピッカーが搭載されており、RGB <=> HSL といった変換も簡単に出来てしまいます。RGB に置き換わるものでは決してありませんが、使い所によってはとても便利な機能なので、是非ともマスターしておきたいところです。

脚注   [ + ]

1. 色の三属性といいます。
2. 実際は微妙に調整されているのですが、それでも殆ど同じだったりします。