概要
タイトルの通り、色をソートしてみたかったという話です。
以下の記事をがっつり参考にさせてもらいました。ありがとうございます!
あとタグでネタバレしてますが、この話のそもそもの動機については余談に書いてます。(Qiita的にはメインで語るような話でもない気がするので...)
はじめに
色のソート
ソートというと、例えば数字を昇順・降順にソートしたり、日付値をカレンダー順にソートするといった処理があります。
では色をソートするときは何を基準にするかというと、1つに、色相を基準にするという方法があります。
色相が何かという説明はWikipediaさんに任せるとして、色相の並びについては下図の 色相環 が視覚的にわかりやすいかと思います。
引用元:https://ja.wikipedia.org/wiki/色相
いわゆる虹色の順番ですね。
今回はこの色相をベースとしたHSV色空間を基準に色をソートしてみます。
HSVとは
HSVは
- Hue : 色相
- Saturation : 彩度
- Value : 明度
で表現される色空間です。
色相は上に書いている通りで、彩度は色の鮮やかさ、明度は色の明るさを表す値です。
文字だけだとわかりづらいと思うのでアニメーションを用意しました。
使用させていただいたツール
https://yanohirota.com/color-converter/
まず色相を変化させるとこんな感じで色の様相が変わります。
次に彩度を変化させるとこんな感じで色の鮮やかさが変わって、彩度を下げるほどいわゆるモノクロに近づいていきます。
最後に明度を変化させるとこんな感じで色の明るさが変わって、明度を下げるほど黒に近づいていきます。
ソートアルゴリズム
今回HSVを基準にソートすると決めたわけですが、参考にさせてもらった記事では
- RGBをHSV色空間に変換し、色相+彩度を使って並び替え
- 基準となる色を用意し、それぞれ照らしあわせて最も近い色で並び替え
という2つの手法でソートし、その結果を見比べていました。
それで、この記事では
HSVでは他に明度をパラメータとして持っていますが、実際に試してみると結果が微妙な感じだったので今回は外してみました。
という理由で明度を使ってなかったのですが、明度を使わない場合、明度が低い色(≒黒)が鮮やかなグラデーションの中に混じってしまい見栄えが悪くなるケースが生じるのかと考えました。
途中に明度の低い色が混じるケース
■■■■■ :#00ff2a
: HSV(130,100,100)
■■■■■ :#004d1a
: HSV(140,100,30)
■■■■■ :#00ff80
: HSV(150,100,100)
■■■■■ :#000000
: HSV(160,100,0)
■■■■■ :#00ffd5
: HSV(170.100,100)
なので今回は、まず母集団を明度別にグルーピングして、各グループ内でソートして結合するという方法を試してみることにしました。
各グループごとにソートした場合両端は赤系統に近くなるため結合しても違和感はそこまで大きくないはずです。
コード
JSDocが混じってたりクラスメソッドでソートを実装してたりでわかりにくいかもしれないですが、ざっくり以下のような感じで作ってみました。
class Color {
/* ~中略~ */
/**
* @param {Array<Color>} colorList
* @param {number} valueSplitCount
* @returns {Array<Color>}
*/
// valueSplitCount = 1 の場合はグループ化なしの単純ソート
static sorted(colorList, valueSplitCount = 1) {
/** @type {Array<Array<Color>>} */
const colorListSorteds = new Array(valueSplitCount)
for(let i = 0; i < valueSplitCount; i++) {
colorListSorteds[i] = new Array()
}
for(const c of colorList) {
// 色を明度別にグループ分けする
const i = Math.floor(valueSplitCount * c.HSV.V / (c.HSV.V_MAX + 1))
colorListSorteds[i].push(c)
}
// 各グループの中で色相+彩度でソートする
for(const cls of colorListSorteds) {
cls.sort((a,b) => {
if (a.HSV.H < b.HSV.H) return 1
if (a.HSV.H > b.HSV.H) return -1
if (a.HSV.S < b.HSV.S) return 1
if (a.HSV.S > b.HSV.S) return -1
return 0
})
}
// グループを結合
return colorListSorteds[0].concat(...colorListSorteds.slice(1))
}
}
上記コードの全容は以下リポジトリに置いてあるので詳細が気になる方はこちらを見てください。
検証
このソートアルゴリズムを試すためにデモを作って軽く検証してみました。
デモ
以下リンクからデモを試せます。
デモページでは以下のように色の数と明度の分割数を設定できて、それぞれランダムに生成する色の数と明度別にグループ化するときの粒度の値です。
各値を設定して描画を押すと
- オリジナルの色配列
- 色相+彩度で単純にソートした結果
- 明度別にソートした結果
が表示されるので、みなさんもぜひ何回か試してみてください。
結果
結果としては、明度別の方がだいぶ綺麗さというか流れの自然さが増した気がします。
(以下例のように特に黒に近い色が多い場合に効果的)
明度の分割数については色の総数や母集団の色にもよるところはあるんですが、だいたい2か3で十分見栄えは良くなる印象です。
大きくしすぎると局所的にはきれいな並びになるんですけど、全体を見ると色相の循環が多すぎて逆に見栄えが悪いように感じますね。
余談
そもそもなぜ色をきれいにソートしたいと思ったかというと
- 某リズムゲーム "ミリ〇タ" にて
- "Thank You!" の39人ライブで
- 衣装を "インフィニット・スカイ 彩 アナザーver" にして
- スペシャルアピールのウェーブを虹色にしたい
というのが始まりでした。
なのでソートアルゴリズムを使って各アイドルのイメージカラーをソートするデモページ②も作りました。
以下は明度の分割数を2に設定したときの結果です。1,2,3を試した感じ2がちょうどよかったです。
あと39人ライブが目的ですが一応ASも入れてます。
こうして見るとけっこう均等に設定されているんですね。
とまあいい感じの順番がわかったので、MV鑑賞に生かしたいと思います。もし需要があれば皆さんも活用してください。
というわけで余談でした。
さいごに
いかがでしたか?
もし色をそれっぽくきれいに並び替えたくなった時は参考にしてみてください。
最後まで読んでいただきありがとうございました。