【Javascript】RGB⇔HSL色空間変換クラス

# 概要
この記事では、Javascriptを用いたRGBとHSLの変換クラスを紹介します。色の変換の他にカラーコードの出力もできます。

# RGBをカラーコードに変換する
コード部分が長いので、特に使用頻度の高そうな、RGBからカラーコードを出力する部分を抜粋しました。

```js
get code(){
    return '#' + this.hex;
}
get hex(){
    return ('000000'+ this.integer.toString(16).toUpperCase() ).slice(-6);
}
get integer(){
    return this.r * 256 * 256 + this.g * 256 + this.b;
}
```

これを一つの関数にすると、このようになります。

```js
function code(r, g, b) {
    let integer = r * 256 * 256 + g * 256 + b;
    let hex = ('000000'+ integer.toString(16).toUpperCase() ).slice(-6);
    return '#' + hex;
}
```

# クラス
## クラス図
![無題.png](https://qiita-image-store.s3.amazonaws.com/0/231330/2490f24d-8809-2f86-ca1a-6b67c1e11f04.png)

## 機能説明

### Range（範囲）クラス
このクラスは、範囲を持った値を保持します。範囲外の値を代入しようとした場合、範囲内の値に丸めます。引数 scale に値を入力すると、その割合で計算した値を入出力してくれます。例えば、0～255の値の場合、scale = 255 を入力すると、0～1に変換してくれます。

### Angle（角度）クラス
このクラスは、角度を計算します。内部フィールドの値域は、0 ≦ _degree ＜ 360 です。単位が、[°]と[rad]の値を入出力できます。

### Hue（色相）クラス
このクラスは、色相の計算と保持を行います。他にも、RGB⇔HSL色空間の変換に必要な値の出力も行います。

### RGBColorSpace（RGB色空間）クラス
R（赤）, G（緑）, B（青） の各値を保持し、カラーコードを出力します。内部フィールドの値域は、0 ≦ _r, _g, _b ≦ 1 ですが、入出力する値域は、0 ≦ r, g, b ≦ 255 です。

### HSLColorSpace（HSL色空間）クラス
H（色相）, S（彩度）, L（輝度） の各値を保持し、RGB⇔HSL色空間の変換を行います。内部フィールドの値域は、0 ≦ _s, _l ≦ 1 で、入出力する値域と同じです。


## コード
### Range（範囲）クラス
```Range.js
class Range {
	constructor(value, max = 1, min = 0){
		this._value;
		this._max = max;
		this._min = min;
		this.set(value);
	}
	set(newValue, scale = 1){
		let quotient = newValue / scale;
		
		if(quotient > this._max){
			this._value = this._max;
		} else if(quotient < this._min) {
			this._value = this._min;
		} else {
			this._value = quotient;
		}
	}
	get(scale = 1){
		if(scale == 1) {
			return this._value;
		}
		
		let result = scale * this._value;
		if(scale > 1 && scale % 1 == 0) {
			return Math.round(result);
		}
		return result;
	}
	isOver(value, scale = 1){
		return this._value / scale > value;
	}
}
```

### Angle（角度）クラス
```Angle.js
class Angle{
	constructor(degree = 0){
		this._degree = 0;
		this.degree = degree;
	}
	set radian(newValue){
		this.degree = newValue * 180 / Math.PI;
	}
	get radian(){
		return this.degree * Math.PI / 180;
	}
	set degree(newValue){
		let value = newValue;
		while(value < 0) value += 360;
		value %= 360;
		this._degree = value;
	}
	get degree(){
		return this._degree;
	}
}
```

### Hue（色相）クラス
```Hue.js
class Hue extends Angle{
	constructor(r = 0, g = 0, b = 0){
		super(0);
		this.setRGB(r, g, b);
	}
	get degree(){// setterとgetterはセットでOverrideしなければならない
		return super.degree;
	}
	set degree(newValue){// Override
		super.degree = Math.round(4 * newValue) / 4; // 0.25ごとに丸め
	}
	setRGB(r, g, b){
		const max = Math.max(r, g, b);
		const min = Math.min(r, g, b);

		if(min != max) {
			let h;
			if(r == max)
				h = (g - b) / (max - min);
			else if(g == max)
				h = (b - r) / (max - min) + 2;
			else
				h = (r - g) / (max - min) + 4;
			if(h < 0)
				h += 6;
			this._h = h;
		}
	}
	set _h(newValue){
		this.degree = newValue * 60;
	}
	get _h(){
		return this.degree / 60;
	}
	get interval(){
		return Math.floor(this._h % 6);
	}
	get different(){
		let d = this._h % 1;
		return (this.interval % 2 == 1) ? 1 - d : d;
	}
}
```

### RGBColorSpace（RGB色空間）クラス
```RGBColorSpace.js
class RGBColorSpace {
	constructor(r = 0, g = 0, b = 0) {
		this.initialize();
		this.setRGB(r, g, b);
	}
	initialize() {
		this._r = new Range(0);// red
		this._g = new Range(0);// green
		this._b = new Range(0);// blue
	}
	set code(newValue) {
		this.hex = newValue.substr(1);
	}
	get code() {
		return '#' + this.hex;
	}
	get hex() {
		return ('000000'+ this.integer.toString(16).toUpperCase() ).slice(-6);
	}
	set hex(newValue) {
		if(this.isColorCode("#" + newValue))
			this.integer = parseInt(newValue, 16);
	}
	get integer() {
		return this.r * 256 * 256 + this.g * 256 + this.b;
	}
	set integer(newValue) {
		let num;
		if(newValue < 0)
			num = 0;
		else if(newValue > Math.pow(256, 3))
			num = Math.pow(256, 3);
		else
			num = newValue;

		let r = Math.floor(num / 256 / 256);
		let g = Math.floor(num / 256 % 256);
		let b = Math.floor(num % 256);
		this.setRGB(r, g, b);
	}
	isColorCode(code) {
		return new RegExp("#[0-9A-F]{6}", "i").test(code);
	}
	get r() {
		return this._r.get(255);
	}
	set r(newValue) {
		this._r.set(newValue, 255);
		this.transformation();
	}
	get g() {
		return this._g.get(255);
	}
	set g(newValue) {
		this._g.set(newValue, 255);
		this.transformation();
	}
	get b() {
		return this._b.get(255);
	}
	set b(newValue) {
		this._b.set(newValue, 255);
		this.transformation();
	}
	get y() {
		let pr = 0.298912;
		let pg = 0.586611;
		let pb = 0.114478;
		return pr * this._r.get() + pg * this._g.get() + pb * this._b.get();
	}
	setRGB(r, g, b) {
		this._r.set(r, 255);
		this._g.set(g, 255);
		this._b.set(b, 255);
		this.transformation();
	}
	copy(color) {
		this._r.set(color._r.get());
		this._g.set(color._g.get());
		this._b.set(color._b.get());
		this.transformation();
	}
	random() {
		this.integer = Math.round(Math.pow(256, 3) * Math.random());
	}
	transformation() {
		// abstract
	}
	inverseTransformation() {
		// abstract
	}
}
```

### HSLColorSpace（HSL色空間）クラス
```HSLColorSpace.js
class HSLColorSpace extends RGBColorSpace {
	constructor(r, g, b) {
		super(r, g, b);
	}
	// @Override
	initialize() {
		this._h = new Hue(0, 0, 0);
		this._s = new Range(0);// saturation
		this._l = new Range(0);// luminosity
		super.initialize();
	}
	get radian() {
		return this._h.radian;
	}
	set radian(newValue) {
		this._h.radian = newValue;
		this.inverseTransformation();
	}
	get degree() {
		return this._h.degree;
	}
	set degree(newValue) {
		this._h.degree = newValue;
		this.inverseTransformation();
	}
	get s() {
		return this._s.get();
	}
	set s(newValue) {
		this._s.set(newValue);
		this.inverseTransformation();
	}
	get l() {
		return this._l.get();
	}
	set l(newValue) {
		this._l.set(newValue);
		this.inverseTransformation();
	}
	setHSL(h, s, l) {
		if(typeof h !== "undefined") this._h.degree = h;
		if(typeof s !== "undefined") this._s.set(s);
		if(typeof l !== "undefined") this._l.set(l);
		this.inverseTransformation();
	}
	copy(color) {
		super.copy(color);
		let isAchromaticColor = (color._s === 0 || color._l === 0 || color._l === 1);
		if(isAchromaticColor) this._h.degree = color._h.degree;
	}
	transformation() {// @Override
		super.transformation();
		
		this._h.setRGB(this._r.get(), this._g.get(), this._b.get());
		
		let max = Math.max(this._r.get(), this._g.get(), this._b.get());
		let min = Math.min(this._r.get(), this._g.get(), this._b.get());

		let l = (max + min) / 2;
		this._l.set(l);
		
		if(max != min) {
			if(max + min < 1)
				this._s.set((l - min) / l);
			else
				this._s.set((max - l) / (1 - l));
		}
	}
	inverseTransformation() {// @Override
		super.inverseTransformation();
		let s = this._s.get();
		let l = this._l.get();
		let range = this._l.isOver(0.5) ? (1 - l) : l;
		
		let max, mid, min;
		max = l + s * range;
		min = l - s * range;
		mid = this._h.different * (max - min) + min;
		
		this._r.set([max,mid,min,min,mid,max][this._h.interval]);
		this._g.set([mid,max,max,mid,min,min][this._h.interval]);
		this._b.set([min,min,mid,max,max,mid][this._h.interval]);
	}
}
```

# おわりに
このクラス群は、私がよく使う機能を収めたものです。何度もリファクタリングをしておりますが、もし、よりスマートなコードを思いつく、またはご存知の方がおりましたらコメントいただけましたら幸いです。