クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】配色ジェネレーターを作った(HSBでメインカラー、アクセントカラー、ベースカラーを割り出す!)

Reactアプリの続きです。前回はアスペクト比ジェネレーターを作りました。 今回もデザイン系のアプリで配色ジェネレーターを作りました!HSBモードでメインカラー、アクセントカラー、ベースカラーの3色を割り出せる配色ジェネレーターです。そのソースコードをここにまとめます(かなりのボリュームになりました…)。それではいきましょう!

【目次】

※参考:前回記事
【React】アスペクト比ジェネレーターを作った(画像の縦横比率を計算するツール) - クモのようにコツコツと

※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com

作ったもの

配色ジェネレーター
f:id:idr_zz:20210816063152j:plain

  • カラーバーの面積比はメインカラー(25%)、アクセントカラー (5%)、ベースカラー(70%)
  • カラーピッカーから色を変更すると他の2色も相関して変更される
  • メインカラーは色相(H)、彩度(S)、明度(B)で変更できる
  • アクセントカラーはメインカラーに対する補色で、表色系により位置が変わる
  • ベースカラーはメインカラーをコントラスト0-99%は薄く、101-200%は濃くした色

作ったアプリはこちら

※参考:配色ジェネレーター

アプリの使い方

※参考:このアプリについて | 配色ジェネレーター

ソースコード

※参考:GitHub - ryo-i/color-scheme-generator

このアプリで実現したかったこと

下記の記事で書いたようなメインカラー、アクセントカラー、ベースカラーの3色を割り出す配色ジェネレーターを作りたかった。

※参考:【配色の基本】面積比(メイン、アクセント、ベース)と色相分割【Adobe Color CC】 - クモのようにコツコツと

そして、配色設定は下記の記事のように、色相(H)、彩度(S)、明度(B)のHSBモードをベースに行う。

※参考:【配色】色相環のH値をいろいろ測ってみた(HSB、マンセル、オストワルト、PCCS、イッテン、NCS、Web配色ツール) - クモのようにコツコツと

アクセントカラー(補色)の色相(H)はいろいろな表色系によって位置がことなるので、その違いが調べられるようにしたかった。さらにキーカラーとキーカラーの間の補色も自動計算で算出したかった。

ソースコード

上記の挙動を実現しているソースコードはこちら。

※参考:GitHub - ryo-i/color-scheme-generator

(基本的なファイル構成は以前作った「Next.jsスターターキット」がベースになっている)

※参考:GitHub - ryo-i/next-app-started

以下、このアプリ固有な内容を書いてく。

データ:data.json

「data.json」の中にアプリにつかうデータの初期値を書いている。

※参考:color-scheme-generator/data.json at main · ryo-i/color-scheme-generator · GitHub

    "inner": {
        "colorPicker": {
            "mainHex": "#e61717",
            "accentHex": "#1ee617",
            "baseHex": "#fce6e6"
        },
        "mainColor": {
            "hue": 0,
            "saturation": 90,
            "brightness": 90
        },
        "accentColor": {
            "hue": 118,
            "saturation": 90,
            "brightness": 90,
            "hueCircle": "イッテン表色系",
            "hueCircleKey": "itten"
        },
        "baseColor": {
            "hue": 0,
            "saturation": 9,
            "brightness": 99,
            "contrast": -90
        },
        "hueCircle": {
            "hsb": [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
            "munsell": [23, 44, 75, 165, 180, 198, 211, 300, 332, 352],
            "ostwald": [25, 48, 121, 184, 198, 207, 305, 344],
            "pccs": [19, 37, 53, 66, 153, 181, 201, 209, 251, 300, 330, 344],
            "itten": [3, 11, 22, 41, 59, 85, 122, 186, 214, 228, 265, 336],
            "ncs": [0, 35, 55, 70, 129, 153, 201, 293]
        }
  • colorPickerはカラーピッカーの初期値
  • mainColorはメインカラーの初期値
  • accentColorはアクセントカラーの初期値
  • baseColorはベースカラーの初期値
  • hueCircleは各表色系の色相環のキーカラーの色相(H)

色相環のキーカラーについてはこちらを参照

※参考:【配色】色相環のH値をいろいろ測ってみた(HSB、マンセル、オストワルト、PCCS、イッテン、NCS、Web配色ツール) - クモのようにコツコツと

モジュール:colorConversionフォルダ

何度も実行するような細かい処理は下記のcolorConversionフォルダにモジュールとして切り出した。

※参考:color-scheme-generator/modules/colorConversion at main · ryo-i/color-scheme-generator · GitHub

Hex→RGB変換:hexToRgbモジュール

Hex値(ハッシュ#以下の16進数6桁の値)をRGB(緑、赤、青の3原色)に変換するモジュール

※参考:color-scheme-generator/hexToRgb.ts at main · ryo-i/color-scheme-generator · GitHub

const hexToRgb = (hex: string) => {
    const hexColors: {r: string, b: string, g: string} = {
        r: hex.substring(1, 3),
        g: hex.substring(3, 5),
        b: hex.substring(5)
    };

    const rgbColors: {r: number, b: number, g: number} = {
        r: parseInt(hexColors.r, 16),
        g: parseInt(hexColors.g, 16),
        b: parseInt(hexColors.b, 16)
    };

    return rgbColors;
}

export { hexToRgb };
RGB→HSB変換:rgbToHsbモジュール

RGBの値をHSB(色相(H)、彩度(S)、明度(B))に変換するモジュール

※参考:color-scheme-generator/rgbToHsb.ts at main · ryo-i/color-scheme-generator · GitHub

const setHue = (r: number, g: number, b: number, max: number, min: number) => {
    let hue: number = 0;
    const difference: number = max - min;

    if (difference === 0) {
        hue = 0;
    } else if (max === r) {
        const result = 60 * ((g - b) / (difference));
        hue = Math.round(result);
    } else if (max === g) {
        const result = 60 * ((b - r) / (difference)) + 120;
        hue = Math.round(result);
    } else if (max === b) {
        const result = 60 * ((r - g) / (difference)) + 240;
        hue = Math.round(result);
    }

    if (Math.sign(hue) === -1) {
        hue = hue + 360;
    }

    return  hue;
};

const setSaturation = (max: number, min: number) => {
    let saturation: number = 0;
    const difference: number = max - min;
    if (difference === 0) {
        saturation = 0;
    } else {
        saturation = Math.round((max - min) / max * 100);
    }

    return saturation;
};

const setBrightness = (max: number, min: number) => {
    const brightness: number = Math.round(max / 255 * 100);
    return brightness;
};


const rgbToHsb = (r: number, g: number, b: number) => {
    const max: number = Math.max(r, g, b);
    const min: number = Math.min(r, g, b);

    const hue: number = setHue(r, g, b, max, min);
    const saturation: number = setSaturation(max, min);
    const brightness: number = setBrightness(max, min);

    const hsb: {h: number, s: number, b: number} = {
        h: hue,
        s: saturation,
        b: brightness
    };

    return hsb;
};

export { rgbToHsb };

作成時のイシュー
※参考:HSBとRGBの変換方法 · Issue #3 · ryo-i/color-scheme-generator · GitHub

参考になった記事
※参考:JavaScriptでRGBをHEXに変換する方法
※参考:JavaScriptでHEXをRGBに変換する方法
※参考:【JavaScript】10進数と16進数の相互変換と16進表記の方法 | MaryCore
※参考:Number.parseInt() - JavaScript | MDN
※参考:Number.prototype.toString() - JavaScript | MDN
※参考:【Javascript】0で割ると? at softelメモ
※参考:ゼロ除算(0除算)とは - IT用語辞典 e-Words

RGBとHSBの関係性は今回初めて理解できた。自力ではこの計算式を算出するのは無理で先人のいろいろな記事を参考にさせていただいた。また、最大値と最小値の差分がゼロの時の割り算がエラーになり、「ゼロ除算」は数学のタブーと知った!

HSB→RGB変換:hsbToRgbモジュール

先ほどと逆でHSBをRGBに変換するモジュール

※参考:color-scheme-generator/hsbToRgb.ts at main · ryo-i/color-scheme-generator · GitHub

const hsbToRgb = (h: number, s: number, b: number) => {
    let red: number = 0;
    let green: number = 0;
    let blue: number = 0;
    const max: number = (b / 100) * 255;
    const min: number = Math.round(max - ((s / 100) * max));
    const diff: number = max - min;

    if (h >= 0 && h <= 60) {
        red = max;
        green = (h / 60) * (diff) + min;
        blue = min;
    } else if (h >= 60 && h <= 120) {
        red = ((120 - h) / 60) * (diff) + min;
        green = max;
        blue = min;
    } else if (h >= 120 && h <= 180) {
        red = min;
        green = max;
        blue = ((h - 120) / 60) * (diff) + min;
    } else if (h >= 180 && h <= 240) {
        red = min;
        green = ((240 - h) / 60) * (diff) + min;
        blue = max;
    } else if (h >= 240 && h <= 300) {
        red = ((h - 240) / 60) * (diff) + min;
        green = min;
        blue = max;
    } else if (h >= 300 && h <= 360) {
        red = max;
        green = min;
        blue = ((360 - h) / 60) * (diff) + min;
    }

    const rgb: {r: number, g: number, b: number} = {
        r: Math.round(red),
        g: Math.round(green),
        b: Math.round(blue)
    };

    return rgb;
}

export { hsbToRgb };

作成時のイシュー
※参考:HSBとRGBの変換方法 · Issue #3 · ryo-i/color-scheme-generator · GitHub

参考になった記事
※参考:RGBとHSV・HSBの相互変換ツールと変換計算式 - PEKO STEP
※参考:RGB←→HSB相互変換【Windowsプログラミング研究所】
※参考:Webツール|色の変換(RGB・HSV・HSL・16進数) | Hirota Yano

RBG→Hex変換:rgbToHexモジュール

RGBをHexに変換するモジュール

※参考:color-scheme-generator/rgbToHex.ts at main · ryo-i/color-scheme-generator · GitHub

const rgbToHex = (r: number, g: number, b: number) => {
    const rgbHex: {r: string, g: string, b: string} = {
        r: String(('00' + r.toString(16)).slice(-2)),
        g: String(('00' + g.toString(16)).slice(-2)),
        b: String(('00' + b.toString(16)).slice(-2))
    };

    const hex: string = '#' + rgbHex.r + rgbHex.g + rgbHex.b;

    return hex;
}

export { rgbToHex };

作成時のイシュー
※参考:HSBとRGBの変換方法 · Issue #3 · ryo-i/color-scheme-generator · GitHub

参考になった記事
※参考:https://wa3.i-3-i.info/word14905.html
※参考:JavaScriptで数値の桁数を合わせる(ゼロパディング)方法 - JavaScriptテックラボ - [SMART]
※参考:Array.prototype.slice() - JavaScript | MDN

数字の桁を合わせる「ゼロパディング」便利!

HSB→Hex変換:hsbToHexモジュール

HSBをHexに変換するモジュール

※参考:color-scheme-generator/hsbToHex.ts at main · ryo-i/color-scheme-generator · GitHub

import { hsbToRgb } from './hsbToRgb';
import { rgbToHex } from './rgbToHex';

const hsbToHex = (hue: number, saturation: number, brightness: number,) => {
    const getRgb = hsbToRgb(hue, saturation, brightness);
    const getHex = rgbToHex(getRgb.r, getRgb.g, getRgb.b);
    return getHex;
}

export { hsbToHex };

hsbToRgbモジュールとrgbToHexモジュールを読み込んで同時に実行した結果を返す処理。

作成時のイシュー
※参考:hsbToHex()モジュールを作る · Issue #17 · ryo-i/color-scheme-generator · GitHub

アクセントカラー色相:accentColorHueモジュール

メインカラーの色相(H)からアクセントカラーの色相(H)を算出するモジュール

※参考:color-scheme-generator/accentColorHue.ts at main · ryo-i/color-scheme-generator · GitHub

import { inner } from '../../data/data.json';

const accentColorHue = (mainColorHue: number, hueCircleKey: string) => {
    const keyColor: number[] = inner.hueCircle[hueCircleKey];
    const KeyLength: number = keyColor.length;
    const keyHalfLength: number = KeyLength /2;
    let mainColorNum: number = 0;
    let mainColorKey: number = 0;
    let mainColorDiff: number = 0;
    let nextMeinColorKye: number = 0;
    let nextMainColorDiff: number = 0;

    const setNextMainColor = (keyColor: number, mainColorKey: number) => {
        nextMeinColorKye = keyColor;
        nextMainColorDiff = nextMeinColorKye - mainColorKey;
        if (nextMainColorDiff < 0) {
            nextMainColorDiff = nextMainColorDiff + 360;
        }
    };

    for (let i = 0; i < keyColor.length; i++) {
        if (keyColor[i] > mainColorHue && keyColor[i] !== keyColor[0]) {
            mainColorNum = i - 1;
            mainColorKey = keyColor[mainColorNum];
            mainColorDiff = mainColorHue - mainColorKey;
            setNextMainColor(keyColor[i], mainColorKey);
            break;
        } else if (keyColor[i] > mainColorHue && keyColor[i] === keyColor[0]) {
            mainColorNum = keyColor.length - 1;
            mainColorKey = keyColor[mainColorNum];
            mainColorDiff = (360 - mainColorKey) + mainColorHue;
            setNextMainColor(keyColor[i], mainColorKey);
            break;
        } else if (keyColor[i] <= mainColorHue && keyColor[i] === keyColor[keyColor.length - 1]) {
            mainColorNum = i;
            mainColorKey = keyColor[mainColorNum];
            mainColorDiff = mainColorHue - mainColorKey;
            setNextMainColor(keyColor[0], mainColorKey);
            break;
        }
    }

    let accentColorNum: number = mainColorNum + keyHalfLength;
    let accentColorKey: number = keyColor[accentColorNum];
    if (!accentColorKey) {
        accentColorNum = accentColorNum - KeyLength;
        accentColorKey = keyColor[accentColorNum];
    }

    let nextAccentColorKye: number = keyColor[accentColorNum + 1];
    if (!nextAccentColorKye) {
        nextAccentColorKye = keyColor[0];
    }

    let nextAccentColorDiff: number = nextAccentColorKye - accentColorKey;
    if (nextAccentColorDiff < 0) {
        nextAccentColorDiff = nextAccentColorDiff + 360;
    }

    const accentColorUnit: number = nextAccentColorDiff / nextMainColorDiff;
    const accentColorDiff: number = Math.round(mainColorDiff * accentColorUnit);
    let accentColorHue: number = accentColorKey + accentColorDiff;
    if (accentColorHue > 360) {
        accentColorHue = accentColorHue - 360;
    }

    return accentColorHue;
}

export { accentColorHue };

作成時のイシュー
※参考:H値の色相環上のズレ問題 · Issue #2 · ryo-i/color-scheme-generator · GitHub
※参考:アクセントカラーを動的に変更する · Issue #10 · ryo-i/color-scheme-generator · GitHub

ベースカラー、メインカラーの算出

ここから先のベースカラーとメインカラーの値の算出は数学の基礎力がないため自分の独力では実現が困難だった。理系出身の弟に相談しかなり協力を得てようやく実現できた。

作成時のイシュー
※参考:ベースカラーの計算方法 · Issue #6 · ryo-i/color-scheme-generator · GitHub
※参考:ベースカラーを動的に変更する · Issue #11 · ryo-i/color-scheme-generator · GitHub
※参考:ベースカラーからメインカラーを算出 · Issue #21 · ryo-i/color-scheme-generator · GitHub
※参考:ベースカラーの範囲変更(-100〜100%に) · Issue #22 · ryo-i/color-scheme-generator · GitHub

参考になった記事
※参考:逆算のやり方総まとめ!中学受験やSPI対策にも | そうちゃ式 受験算数(新1号館 数論/特殊算)
※参考:問50の様に□が2つある場合の四則逆算の解き方を教えてください。 - Yahoo!知恵袋
※参考:方程式の解き方をマスターしよう|中学生/数学 |【公式】家庭教師のアルファ-プロ講師による高品質指導

自分は「等式の性質」など基本的な逆算や方程式の方法をほとんど忘れていた。。

ベースカラー彩度:baseColorSaturationモジュール

メインカラーの彩度(S)からベースカラーの彩度(S)を算出するモジュール

※参考:color-scheme-generator/baseColorSaturation.ts at main · ryo-i/color-scheme-generator · GitHub

const baseColorSaturation = (contrast: number, saturation: number) => {
    let baseColorSaturation: number = 0;

    if (contrast == 0) {
        baseColorSaturation = saturation;
    } else if (contrast < 0) {
        baseColorSaturation = saturation * (1 + contrast / 100);
    } else if (contrast > 0) {
        baseColorSaturation = saturation + (100 - saturation) * contrast / 100;
    }

    return Math.round(baseColorSaturation);
}

export { baseColorSaturation };
ベースカラー明度:baseColorBrightnessモジュール

メインカラーの明度(B)からベースカラーの明度(B)を算出するモジュール

※参考:color-scheme-generator/baseColorBrightness.ts at main · ryo-i/color-scheme-generator · GitHub

const baseColorBrightness = (contrast: number, brightness: number) => {
    let baseColorBrightness: number = 0;

    if (contrast == 0) {
        baseColorBrightness = brightness;
    } else if (contrast < 0) {
        baseColorBrightness = brightness - (100 - brightness) * (contrast / 100)
    } else if (contrast > 0) {
        baseColorBrightness = brightness * (1 - (contrast / 100));
    }

    return Math.round(baseColorBrightness);
}

export { baseColorBrightness };
メインカラー彩度:mainColorSaturationモジュール

ベースカラーの彩度(S)からメインカラーの彩度(S)を算出するモジュール

※参考:color-scheme-generator/mainColorSaturation.ts at main · ryo-i/color-scheme-generator · GitHub

const mainColorSaturation = (contrast: number, saturation: number) => {
    let resultSaturation: number = 0;
    const absContrast: number = Math.abs(contrast);

    if (contrast == 0) {
        resultSaturation = saturation;
    } else if (contrast < 0) {
        resultSaturation = saturation / (1 - absContrast / 100);
    } else if (contrast > 0) {
        resultSaturation = (saturation - absContrast) / (1 - absContrast / 100);
    }

    return Math.round(resultSaturation);
}

export { mainColorSaturation };

作成時のイシュー ※参考:ベースカラー→メインカラー変換で彩度Sと明度Bがオーバーフロー · Issue #23 · ryo-i/color-scheme-generator · GitHub

ベースカラー→メインカラーの算出ではコントラストの位置が保てないオーバーフロー問題が発生。「絶対値」を用いてプラスマイナスではなくゼロからの相対的な位置関係を元に計算する必要があった。

参考になった記事
※参考:絶対値とは何か?なんの意味があるのか?簡単に解説してみた
※参考:JavaScript | 絶対値を取得する(Math.abs)

メインカラー明度:mainColorBrightnessモジュール

ベースカラーの明度(B)からメインカラーの明度(B)を算出するモジュール

※参考:color-scheme-generator/mainColorBrightness.ts at main · ryo-i/color-scheme-generator · GitHub

const mainColorBrightness = (contrast: number, brightness: number) => {
    let resultBrightness: number = 0;
    const absContrast: number = Math.abs(contrast);

    if (contrast == 0) {
        resultBrightness = brightness;
    } else if (contrast < 0) {
        resultBrightness = (brightness - absContrast) / (1 - absContrast / 100);
    } else if (contrast > 0) {
        resultBrightness = brightness / (1 - absContrast / 100);
    }

    return Math.round(resultBrightness);
}

export { mainColorBrightness };

こちらも同様に絶対値を用いる。

ベースカラーのコントラスト:changeConstractモジュール

ベースカラーのコントラスト比を変更するモジュール

※参考:color-scheme-generator/changeConstract.ts at main · ryo-i/color-scheme-generator · GitHub

ベースカラー→メインカラーの算出でオーバーフローが起きたときにコントラスト比を変更してオーバーフローが治らない範囲に収めている。

const changeConstract = (contrast: number, saturation: number, brightness: number) => {
    const absContrast: number = Math.abs(contrast);
    const absSaturation: number = Math.abs(saturation);
    const absBrightness: number = Math.abs(brightness);
    let checkSaturationVal: number = 0;
    let checkBrightnessionVal: number = 0;
    let saturationFlag: boolean = false;
    let brightnessFlag: boolean = false;
    let saturationDiff: number = 0;
    let brightnessDiff: number = 0;
    let resultContrast: number = contrast;


    // Check Saturation Diff
    if (contrast < 0) {
        checkSaturationVal = absSaturation + absContrast;
        saturationFlag = (checkSaturationVal) <= 100 ? true : false;
        saturationDiff = saturationFlag ? 0 : checkSaturationVal - 100;
    } else if (contrast > 0) {
        checkSaturationVal = absSaturation - absContrast;
        saturationFlag = 0 <= (checkSaturationVal) ? true : false;
        saturationDiff = saturationFlag ? 0 : -(checkSaturationVal);
    }

    // Check Brightnession Diff
    if (contrast < 0) {
        checkBrightnessionVal = absBrightness - absContrast;
        brightnessFlag = 0 <= (checkBrightnessionVal) ? true : false;
        brightnessDiff = brightnessFlag ? 0 : -(checkBrightnessionVal);
    } else if (contrast > 0) {
        checkBrightnessionVal = absBrightness + absContrast;
        brightnessFlag = (checkBrightnessionVal) <= 100 ? true : false;
        brightnessDiff = brightnessFlag ? 0 : checkBrightnessionVal -100;
    }

    // Change Flag (Saturation or Brightness)
    if (saturationDiff > brightnessDiff && !brightnessFlag) {
        brightnessFlag = true;
    } else if (saturationDiff < brightnessDiff && !saturationFlag) {
        saturationFlag = true;
    }


    // Result Contrast
    if (saturationFlag && brightnessFlag) {
        resultContrast = absContrast;
    } else if (!saturationFlag && contrast < 0) {
        resultContrast = 100 - saturation;
    } else if (!saturationFlag && contrast > 0) {
        resultContrast = saturation;
    } else if (!brightnessFlag && contrast < 0) {
        resultContrast = brightness;
    } else if (!brightnessFlag && contrast > 0) {
        resultContrast = 100 - brightness;
    }


    // Change Contrast (Plus or Minus)
    if (contrast < 0 && resultContrast > 0) {
        resultContrast = -(resultContrast);
    }

    return resultContrast;
}

export { changeConstract };

Innerコンポーネント

InnerコンポーネントでReactコンポーネントを実行している。上記のデータやモジュールを読み込んで実行している。

※参考:color-scheme-generator/Inner.tsx at main · ryo-i/color-scheme-generator · GitHub

このアプリの本丸なので作成時のイシューはたくさん…

※参考:History for components/Inner.tsx - ryo-i/color-scheme-generator · GitHub

モジュール類のimport

Reactフック(useState、useEffect)、CSS in JS(styled-components)や上記のデータ、モジュール類をインポート

import React, { useState, useEffect }  from 'react';
import styled from 'styled-components';
import { inner } from '../data/data.json';
import { hexToRgb } from '../modules/colorConversion/hexToRgb';
import { rgbToHsb } from '../modules/colorConversion/rgbToHsb';
import { hsbToRgb } from '../modules/colorConversion/hsbToRgb';
import { rgbToHex } from '../modules/colorConversion/rgbToHex';
import { hsbToHex } from '../modules/colorConversion/hsbToHex';
import { accentColorHue } from '../modules/colorConversion/accentColorHue';
import { baseColorSaturation } from '../modules/colorConversion/baseColorSaturation';
import { baseColorBrightness } from '../modules/colorConversion/baseColorBrightness';
import { mainColorSaturation } from '../modules/colorConversion/mainColorSaturation';
import { mainColorBrightness } from '../modules/colorConversion/mainColorBrightness';
import { changeConstract } from '../modules/colorConversion/changeConstract';
CSS in JS設定

styled-componentsで後述するJSXタグのCSSを設定している。

JSXResultタグに適用されるスタイル

// Style
const Result = styled.div`
  margin: 0 0 30px;
  .colorPalette {
    display: flex;
    width: 100%;
    margin 0 0 10px;
    border: 1px #ddd solid;
    div {
      height: 80px;
    }
    .mainColor {
      width: 25%;
    }
    .accentColor {
      width: 5%;
      border-left: 1px #ddd solid;
      border-right: 1px #ddd solid;
    }
    .baseColor {
      width: 70%;
    }
  }
  .colorPicker {
    input, label {
      :hover {
        cursor: pointer;
      }
    }
    label {
      margin: 0 1em 0 0;
      display: inline-block;
    }
    input[type="color"] {
      margin: 0 0.25em 0 0;
      padding: 0;
      border: none;
      background: none;
      appearance: none;
      width: 2em;
      height: 1em;
      ::-webkit-color-swatch {
        border: #eee 1px solid;
        border-radius: 3px;
      }
      ::-webkit-color-swatch-wrapper {
        margin: 0;
        padding: 0;
        width: 2em;
        height: 1em;
        border: none;
      }
    }
  }
`;

JSXGeneratorタグに適用されるスタイル

const Generator = styled.div`
  margin: 0 0 30px;
  h2 span {
    font-size: 14px;
    font-weight: normal;
    color: #000;
  }
  p {
    margin: 0;
  }
  .colorPalette {
    border: 1px solid #eee;
  }
  input, label {
    :hover {
      cursor: pointer;
    }
  }
  input[type='color'] {
    font-size: 16px;
  }
  input[type='range'] {
    width: 100%;
  }
  label {
    margin: 0 0.5em 0 0;
    display: inline-block;
  }
`;

styled-componentsについてはこちらを参照

※参考:【React】styled-componentsでCSS in JSを事始める - クモのようにコツコツと

Innerコンポーネント全体

このコンポーネントファイルの主要な部分。下記のようなパートに分かれる。

// Component
function Inner() {
  // Color Picker Hooks
  // 中略(カラーピッカーのフック)

  // Main Color Hooks
  // 中略(メインカラーのフック)

  // Accent Color Hooks
  // 中略(アクセントカラーのフック)

  // base Color Hooks
  // 中略(ベースカラーのフック)


  // Change Color Picker
  const changeColorPicker = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略(カラーピッカー変更時の処理)
    }
  };


  // Change Main Color
  const changeMainColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略(メインカラー変更時の処理)
  };


  // Change Accent Color
  const changeAccentColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略(アクセントカラー変更時の処理)
  };


  // Change Base Color
  const changeBaseColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 中略(ベースカラー変更時の処理)
  };


  // Change CSS
 // 中略(動的なCSS変更)


  // JSX
  return (
    // 中略(JSXタグの設定)
  );
}

export default Inner;

最後にInnerコンポーネント全体をエクスポートしている。

フック設定

フックとそこに入る初期値を設定

  // Color Picker Hooks
  const [mainHex, setMainHex] = useState(inner.colorPicker.mainHex);
  const [accentHex, setAccentHex] = useState(inner.colorPicker.accentHex);
  const [baseHex, setBaseHex] = useState(inner.colorPicker.baseHex);

  // Main Color Hooks
  const [mainHue, setMainHue] = useState(inner.mainColor.hue);
  const [mainSaturation, setMainSaturation] = useState(inner.mainColor.saturation);
  const [mainBrightness, setMainBrightness] = useState(inner.mainColor.brightness);

  // Accent Color Hooks
  const [accentHue, setAccentHue] = useState(inner.accentColor.hue);
  const [accentSaturation, setAccentSaturation] = useState(inner.accentColor.saturation);
  const [accentBrightness, setAccentBrightness] = useState(inner.accentColor.brightness);
  const [hueCircle, setHueCircle] = useState(inner.accentColor.hueCircle);
  const [hueCircleKey, setHueCircleKey] = useState(inner.accentColor.hueCircleKey);

  // base Color Hooks
  const [baseHue, setBaseHue] = useState(inner.baseColor.hue);
  const [baseSaturation, setBaseSaturation] = useState(inner.baseColor.saturation);
  const [baseBrightness, setBaseBrightness] = useState(inner.baseColor.brightness);
  const [contrast, setContrast] = useState(inner.baseColor.contrast);

カラーピッカー、メインカラー、アクセントカラー、ベースカラーの順で設定。useState()inner(data.json)から初期値を読み込んでいる。

フックについてはこちらを参照

※参考:【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと

カラーピッカー変更時の処理:changeColorPicker()

ここからinputタグの変更時に行われる処理。基本的にはに各モジュールを実行して、その結果をフックに登録している。

カラーピッカーの値が変わったときに、そのカラーピッカーがメインカラー、アクセントカラー、ベースカラーのどれかによって違う処理をしている。それぞれ、他の2色も連動して変更する。ベースカラー変更時にメインカラーがオーバーフローする場合はベースカラーのコントレストも変更する。

  // Change Color Picker
  const changeColorPicker = (e: React.ChangeEvent<HTMLInputElement>) => {
    const getName:string = String(e.target.name);
    const getValue: string = String(e.target.value);
    const rgb = hexToRgb(getValue);
    const hsb = rgbToHsb(rgb.r, rgb.g, rgb.b);

    if (getName === 'mainHex') {
      // Main Color
      setMainHue(hsb.h);
      setMainSaturation(hsb.s);
      setMainBrightness(hsb.b);
      setMainHex(getValue);

      // Accent Color
      const getAccentHue: number = accentColorHue(hsb.h, hueCircleKey);
      const getAccentHex: string = hsbToHex(getAccentHue, hsb.s, hsb.b);
      setAccentHue(getAccentHue);
      setAccentSaturation(hsb.s);
      setAccentBrightness(hsb.b);
      setAccentHex(getAccentHex);

      // Base Color
      const getBaseSaturation: number = baseColorSaturation(contrast, hsb.s);
      const getBaseBrightness: number = baseColorBrightness(contrast, hsb.b);
      const getBaseHex: string = hsbToHex(hsb.h, getBaseSaturation, getBaseBrightness);
      setBaseHue(hsb.h);
      setBaseSaturation(getBaseSaturation);
      setBaseBrightness(getBaseBrightness);
      setBaseHex(getBaseHex);
    } else if (getName === 'accentHex') {
      // Accent Color
      setAccentHue(hsb.h);
      setAccentSaturation(hsb.s);
      setAccentBrightness(hsb.b);
      setAccentHex(getValue);

      // Main Color
      const getMainHue: number = accentColorHue(hsb.h, hueCircleKey);
      const getMainHex: string = hsbToHex(getMainHue, hsb.s, hsb.b);
      setMainHue(getMainHue);
      setMainSaturation(hsb.s);
      setMainBrightness(hsb.b);
      setMainHex(getMainHex);

      // Base Color
      const getBaseSaturation: number = baseColorSaturation(contrast, hsb.s);
      const getBaseBrightness: number = baseColorBrightness(contrast, hsb.b);
      const getBaseHex: string = hsbToHex(getMainHue, getBaseSaturation, getBaseBrightness);
      setBaseHue(getMainHue);
      setBaseSaturation(getBaseSaturation);
      setBaseBrightness(getBaseBrightness);
      setBaseHex(getBaseHex);
    } else if (getName === 'baseHex') {
      // Base Color
      setBaseHue(hsb.h);
      setBaseSaturation(hsb.s);
      setBaseBrightness(hsb.b);
      setBaseHex(getValue);

      // Contrast
      const getConstract: number = changeConstract(contrast, hsb.s, hsb.b);
      setContrast(getConstract);

      // Main Color
      const getMainSaturation: number = mainColorSaturation(getConstract, hsb.s);
      const getMainBrightness: number = mainColorBrightness(getConstract, hsb.b);
      const getMainHex: string = hsbToHex(hsb.h, getMainSaturation, getMainBrightness);
      setMainHue(hsb.h);
      setMainSaturation(getMainSaturation);
      setMainBrightness(getMainBrightness);
      setMainHex(getMainHex);

      // Accent Color
      const getAccentHue: number = accentColorHue(hsb.h, hueCircleKey);
      const getAccentHex: string = hsbToHex(getAccentHue, getMainSaturation, getMainBrightness);
      setAccentHue(getAccentHue);
      setAccentSaturation(getMainSaturation);
      setAccentBrightness(getMainBrightness);
      setAccentHex(getAccentHex);
    }
  };
メインカラー変更時の処理

メインカラーのrangeバーを変えた時に、そのバーが色相(H)、彩度(S)、明度(B)かによって異なる処理をしている。アクセントカラー、ベースカラーも連動して変更する。

  // Change Main Color
  const changeMainColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    const getName: string = String(e.target.name);
    const getValue: number = Number(e.target.value);
    let mainRgb: {r: number, g: number, b: number};
    let accentRgb: {r: number, g: number, b: number};
    let baseRgb: {r: number, g: number, b: number};

    if (getName === 'hue') {
      const getAccentHue: number = accentColorHue(getValue, hueCircleKey);
      setMainHue(getValue);
      setAccentHue(getAccentHue);
      setBaseHue(getValue);
      mainRgb = hsbToRgb(getValue, mainSaturation, mainBrightness);
      accentRgb = hsbToRgb(getAccentHue, accentSaturation, accentBrightness);
      baseRgb = hsbToRgb(getValue, baseSaturation, baseBrightness);
    } else if (getName === 'saturation') {
      const getBaseSaturation: number = baseColorSaturation(contrast, getValue);
      setMainSaturation(getValue);
      setAccentSaturation(getValue);
      setBaseSaturation(getBaseSaturation);
      mainRgb = hsbToRgb(mainHue, getValue, mainBrightness);
      accentRgb = hsbToRgb(accentHue, getValue, accentBrightness);
      baseRgb = hsbToRgb(baseHue, getBaseSaturation, baseBrightness);
    } else if (getName === 'brightness') {
      const getBaseBrightness: number = baseColorBrightness(contrast, getValue);
      setMainBrightness(getValue);
      setAccentBrightness(getValue);
      setBaseBrightness(getBaseBrightness);
      mainRgb = hsbToRgb(mainHue, mainSaturation, getValue);
      accentRgb = hsbToRgb(accentHue, accentSaturation, getValue);
      baseRgb = hsbToRgb(baseHue, baseSaturation, getBaseBrightness);
    }

    const getMainHex: string = rgbToHex(mainRgb.r, mainRgb.g, mainRgb.b);
    const getAccentHex: string = rgbToHex(accentRgb.r, accentRgb.g, accentRgb.b);
    const getBaseHex: string = rgbToHex(baseRgb.r, baseRgb.g, baseRgb.b);
    setMainHex(getMainHex);
    setAccentHex(getAccentHex);
    setBaseHex(getBaseHex);
  };
アクセントカラー変更時の処理

アクセントカラーのチェックボックスの表色系を変更した時にアクセントカラー の色相(H)を変更する処理を実行する。

  // Change Accent Color
  const changeAccentColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    const getValue: string = String(e.target.value);
    const hueCircleKey: string = e.target.dataset.hueCircle;
    const mainColorHue: number = mainHue;
    const getAccentHue = accentColorHue(mainColorHue, hueCircleKey);
    const getAccentHex = hsbToHex(getAccentHue, accentSaturation, accentBrightness);
    setAccentHue(getAccentHue);
    setAccentHex(getAccentHex);
    setHueCircle(getValue);
    setHueCircleKey(hueCircleKey);
  };
ベースカラー変更時の処理

ベースカラーのアクセント比を変更した時にベースカラーの彩度(S)、明度(B)を変更する。メインカラーの彩度(S)、明度(B)も変更する。

  // Change Base Color
  const changeBaseColor = (e: React.ChangeEvent<HTMLInputElement>) => {
    const getValue: number = Number(e.target.value);
    const getBaseSaturation = baseColorSaturation(getValue, mainSaturation);
    const getBaseBrightness = baseColorBrightness(getValue, mainBrightness);
    const getBaseHex = hsbToHex(baseHue, getBaseSaturation, getBaseBrightness);
    setContrast(getValue);
    setBaseSaturation(getBaseSaturation);
    setBaseBrightness(getBaseBrightness);
    setBaseHex(getBaseHex);
  };
動的なCSS変更

上記の関数によってメインカラー、アクセントカラー 、ベースカラーが変更された時に、画面上のタグの色も動的に変更する。下記のJSXタグに設定する。

  // Change CSS
  const mainColorPalette = {
    background: mainHex
  }
  const accentColorPalette = {
    background: accentHex
  }
  const baseColorPalette = {
    background: baseHex
  }
JSX設定

画面上に表示されるJSXタグ設定と、そのタグの属性やテキストの変更、関数を実行するイベントなどを設定している。

  // JSX
  return (
    <>
      <Result>
        <div className="colorPalette">
          <div className="mainColor" style={mainColorPalette}></div>
          <div className="accentColor" style={accentColorPalette}></div>
          <div className="baseColor" style={baseColorPalette}></div>
        </div>
        <p className="colorPicker">
          カラーピッカー:
          <label><input type="color" name="mainHex" value={mainHex} onChange={changeColorPicker} />{mainHex}</label>
          <label><input type="color" name="accentHex" value={accentHex} onChange={changeColorPicker} />{accentHex}</label>
          <label><input type="color" name="baseHex" value={baseHex} onChange={changeColorPicker} />{baseHex}</label>
        </p>
      </Result>
      <Generator>
        <section className="mainColor">
          <h2>メインカラー<span>(H:{mainHue}, S:{mainSaturation}, B:{mainBrightness})</span></h2>
          <p>色相(H):{mainHue}</p>
          <input type="range" name="hue" value={mainHue} min="0" max="360" onChange={changeMainColor} />
          <p>彩度(S):{mainSaturation}</p>
          <input type="range" name="saturation" value={mainSaturation} min="0" max="100" onChange={changeMainColor} />
          <p>明度(B):{mainBrightness} </p>
          <input type="range" name="brightness" value={mainBrightness} min="0" max="100" onChange={changeMainColor} />
        </section>
        <section className="accentColor">
          <h2>アクセントカラー<span>(H:{accentHue}, S:{accentSaturation}, B:{accentBrightness})</span></h2>
          <p>色相環:{hueCircle}</p>
          <label><input type="radio" name="hueCircle" value="HSB表色系" data-hue-circle="hsb" onChange={changeAccentColor} />HSB</label>
          <label><input type="radio" name="hueCircle" value="マンセル表色系" data-hue-circle="munsell" onChange={changeAccentColor} />マンセル</label>
          <label><input type="radio" name="hueCircle" value="オストワルト表色系" data-hue-circle="ostwald" onChange={changeAccentColor} />オストワルト</label>
          <label><input type="radio" name="hueCircle" value="PCCS表色系" data-hue-circle="pccs" onChange={changeAccentColor} />PCCS</label>
          <label><input type="radio" name="hueCircle" value="イッテン表色系" data-hue-circle="itten" onChange={changeAccentColor} defaultChecked />イッテン</label>
          <label><input type="radio" name="hueCircle" value="NCS表色系" data-hue-circle="ncs" onChange={changeAccentColor} />NCS</label>
        </section>
        <section className="baseColor">
          <h2>ベースカラー<span>(H:{baseHue}, S:{baseSaturation}, B:{baseBrightness})</span></h2>
          <p>コントラスト:{contrast}%</p>
          <input type="range" name="contrast" value={contrast} min="-100" max="100" onChange={changeBaseColor} />
        </section>
      </Generator>
    </>
  );

参考になった記事
※参考:data属性の名前に使える文字列 | cly7796.net
※参考:HTMLElement.dataset - Web API | MDN

data-setの値に使える表記のルールに詳しくなった。

最後に

とういことで駆け足で書きましたがそれでもかなりのボリュームになりました。今回はこれまで作ったものよりもかなり混み入った計算をいくつも実行しており、これを手作業で計算しようとすると膨大な作業になるはずです!

また、やりたいことを実現するにあたり、等式の性質、絶対値、ゼロ除算、など数学の基礎的な能力が自分は欠如していることがわかり、とても勉強になりました。今後のために数学の基礎力も上げていきたいと思います。

それではまた!


※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com