React使ってみた　～配色ジェネレータ～

## はじめに
こんにちは。
MYJLab Advent Calendar 2024の10日目担当します、3年佐藤です。
何しようか迷っているうちにあっという間に来てしまったのでとりあえずReact使ってみようかな～と思い、使ってみました。私自身何かをつくるとき色合いをどうしようかとても迷うので配色ジェネレータっぽいものを作ってみました。

完成図としてはこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3957055/23fd77ac-621d-1acc-877f-c333f422c58e.png)


## 準備
Reactアプリを作成し必要なライブラリをインストールします。
色の操作と描写用です。

 ```rb
npx create-react-app color-generator

npm install chroma-js
npm install react-konva
npm install react-color
```

https://gka.github.io/chroma.js/

## コードの説明
### 1.カラーホイールの表示

カラーホイールを表示し、選択した色が下に表示されるようにします。
今回はグラデーションではなく12個に分けます。

```rb:ColorWheel.js
import React, { useState } from "react";
import { Stage, Layer, Arc } from "react-konva";
import chroma from "chroma-js";

const ColorWheel = ({ size = 300, segments = 12, onColorsChange }) => {
  const [selectedIndices, setSelectedIndices] = useState([]);

  const radius = size / 2;
  const segmentAngle = 360 / segments;

  // 色の配列を生成
  const colors = chroma
    .scale(["red", "yellow", "green", "cyan", "blue", "magenta", "red"])
    .colors(segments);

  // セグメントをクリックした際の処理
  const handleClick = (index) => {
    const newSelection = selectedIndices.includes(index)
      ? selectedIndices.filter((i) => i !== index)
      : [...selectedIndices, index];

    setSelectedIndices(newSelection);

    if (onColorsChange) {
      const selectedColors = newSelection.map((i) => colors[i]);
      onColorsChange(selectedColors);
    }
  };

  // カラーホイールを描画
  const renderWheel = () => {
    return colors.map((color, index) => {
      const startAngle = segmentAngle * index;
      const endAngle = startAngle + segmentAngle;

      return (
        <Arc
          key={index}
          x={radius}
          y={radius}
          innerRadius={radius / 2}
          outerRadius={radius}
          fill={color}
          angle={segmentAngle}
          rotation={startAngle}
          onClick={() => handleClick(index)}
          stroke={selectedIndices.includes(index) ? "black" : null}
         />
       );
     });
   };

return (
    <div>
      <Stage width={size} height={size}>
        <Layer>{renderWheel()}</Layer>
      </Stage>
    </div>
  );
};

export default ColorWheel;

```

```rb:App.js
import React, { useState } from "react";
import ColorWheel from "./ColorWheel";

const App = () => {
  const [selectedColors, setSelectedColors] = useState([]);

  return (
    <div style={{ textAlign: "center", padding: "20px" }}>
      <h1>Color Palette</h1>
      <div style={{display: "flex", justifyContent: "center"}}>
          <ColorWheel
            size={400}
            segments={12} 
            onColorsChange={setSelectedColors}/>
      </div>
      {selectedColors.length > 0 && (
        <div>
          <h2>Selected Colors</h2>
          <div style={{ display: "flex", justifyContent: "center", gap: "10px" }}>
            {selectedColors.map((color, index) => (
              <div 
                key={index}
                style={{
                  backgroundColor: color,
                  width: "100px",
                  height: "100px",
                }}
              ></div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

export default App;

```
ここまで以下のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3957055/c698d763-547f-8b39-b0dc-d922be1b0cef.png)

### 2.色選択の種類
次に補色や類似色に選べるようにします。

```rb:ColorWheel.js
- const ColorWheel = ({ size = 300, segments = 12, onColorsChange }) => {
-  const [selectedIndices, setSelectedIndices] = useState([]);

+ const ColorWheel = ({ size = 300, segments = 12, harmonyType = "Complementary", onColorsChange }) => {
+  const [selectedIndex, setSelectedIndex] = useState(null);

// カラーハーモニーを計算する関数
+  const getHarmonyIndices = (baseIndex) => {
+    switch (harmonyType) {
+      case "Complementary":
+        return [baseIndex, (baseIndex + segments / 2) % segments];
+      case "Triad":
+        return [
+          baseIndex,
+          (baseIndex + segments / 3) % segments,
+          (baseIndex + (2 * segments) / 3) % segments,
+        ];
+      case "Analogous":
+        return [
+          baseIndex,
+          (baseIndex - 1 + segments) % segments,
+          (baseIndex + 1) % segments,
+        ];
+      default:
+        return [baseIndex];
+    }
+  };


//セグメントをクリックした際の処理
   const handleClick = (index) => {
-     const newSelection = selectedIndices.includes(index)
-       ? selectedIndices.filter((i) => i !== index)
-       : [...selectedIndices, index];
-     setSelectedIndices(newSelection);
-     if (onColorsChange) {
-       const selectedColors = newSelection.map((i) => colors[i]);
-       onColorsChange(selectedColors);
-     }

+    const harmonyIndices = getHarmonyIndices(index);
+    setSelectedIndex(index);

+    const selectedColors = harmonyIndices.map((i) => colors[i]);
+    if (onColorsChange) onColorsChange(selectedColors);
  };


-  const endAngle = startAngle + segmentAngle;
+  const isSelected = selectedIndex !== null && getHarmonyIndices(selectedIndex).includes(index);  


-  stroke={selectedIndices.includes(index) ? "black" : null}
+  stroke={isSelected ? "black" : null}

```
```rb:App.js
const App = () => {
   const [selectedColors, setSelectedColors] = useState([]);
+  const [harmonyType, setHarmonyType] = useState("Complementary");

  return (
    <div style={{ textAlign: "center", padding: "20px" }}>
      <h1>Color Palette</h1>


//ハーモニータイプの選択
+  <div style={{ marginBottom: "20px" }}>
+   <select value={harmonyType} onChange={(e) => setHarmonyType(e.target.value)}>
+     <option value="Complementary">Complementary</option>
+     <option value="Triad">Triad</option>
+     <option value="Analogous">Analogous</option>
+     </select>
+  </div>
    <ColorWheel
        size={400}
        segments={12} 
+       harmonyType={harmonyType}
        onColorsChange={setSelectedColors}
    />
```

2色、3色、類似色と選べるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3957055/592076b9-ba98-9977-109f-6095aa04d0b0.png)

### 3.寒色、暖色
最後に寒色、暖色と先ほどのように選択できるようにして、似たような色を表示するようにしてみます。
最終的なコードは以下です。

```rb:ColorWheel.js
import React, { useState, useEffect } from "react";
import { Stage, Layer, Arc } from "react-konva";
import chroma from "chroma-js";

const ColorWheel = ({ size = 300, segments = 12, scheme = "full",  harmonyType = "Complementary",
    onColorsChange }) => {
  const [selectedIndex, setSelectedIndex] = useState(null); // 選択中のセグメント

  const radius = size / 2;
  const segmentAngle = 360 / segments;

 // 色スキームを生成
 const generateColors = () => {
    let scale;
    if (scheme === "warm") {
      scale = chroma.scale(["red", "yellow", "orange"]);
    } else if (scheme === "cool") {
      scale = chroma.scale(["blue", "cyan", "purple"]);
    } else {
      scale = chroma.scale(["red", "yellow", "green", "cyan", "blue", "magenta", "red"]);
    }
    return scale.colors(segments);
  };

  const [colors, setColors] = useState(generateColors());
  useEffect(() => {
    setColors(generateColors());
  },[scheme]);

  // カラーハーモニーを計算する関数
  const getHarmonyIndices = (baseIndex) => {
    switch (harmonyType) {
      case "Complementary":
        return [baseIndex, (baseIndex + segments / 2) % segments];
      case "Triad":
        return [
          baseIndex,
          (baseIndex + segments / 3) % segments,
          (baseIndex + (2 * segments) / 3) % segments,
        ];
      case "Analogous":
        return [
          baseIndex,
          (baseIndex - 1 + segments) % segments,
          (baseIndex + 1) % segments,
        ];
      default:
        return [baseIndex];
    }
  };

  // セグメントをクリックした際の処理
  const handleClick = (index) => {
    const harmonyIndices = getHarmonyIndices(index);
    setSelectedIndex(index);

    const selectedColors = harmonyIndices.map((i) => colors[i]);
    if (onColorsChange) onColorsChange(selectedColors);
  };

  // カラーホイールを描画
  const renderWheel = () => {
    return colors.map((color, index) => {
      const startAngle = segmentAngle * index;
      const isSelected = selectedIndex !== null && getHarmonyIndices(selectedIndex).includes(index);

      return (
        <Arc
          key={index} 
          x={radius} y={radius} innerRadius={radius / 2} outerRadius={radius}
          fill={color} 
          angle={segmentAngle} 
          rotation={startAngle}
          onClick={() => handleClick(index)} stroke={isSelected ? "black" : null}
        />
      );
    });
  };

  return (
    <div>
      <Stage width={size} height={size}>
        <Layer>{renderWheel()}</Layer>
      </Stage>
    </div>
  );
};

export default ColorWheel;

```

```rb:App.js
import React, { useState } from "react";
import ColorWheel from "./ColorWheel";
import chroma from "chroma-js";

const App = () => {
  const [selectedColors, setSelectedColors] = useState([]);
  const [harmonyType, setHarmonyType] = useState("Complementary");
  const [scheme, setScheme] = useState("full");
  const [similarColors, setSimilarColors] = useState([]);

  const styles = {
    container: {
      display: "inline-block",
    },
    select: {
      appearance: "none",
      borderRadius: "8px",
      padding: "10px 30px 10px 15px",
      margin: "5px",
      fontSize: "16px",
      cursor: "pointer",
    },
  };

// 選択した色に基づくランダムな彩度の類似色を生成
 const generateSimilarColors = (colors) => {
    const similar = colors.map((color) => {
      // 彩度をランダムに設定
      return Array.from({ length: 5 }, () => {
        const randomSaturation = Math.random(); // ランダムな彩度（0.0～1.0）
        return chroma(color).set('hsl.s', randomSaturation).hex();
      });
    });
    setSimilarColors(similar);
  };
  
  // カラーホイールで色が選択された時の処理
  const handleColorChange = (colors) => {
    setSelectedColors(colors);
    generateSimilarColors(colors);
  };


  return (
    <div style={{ textAlign: "center", padding: "20px" }}>
      <h1>Color Palette</h1>

        {/* スキーム切り替え */}
        <div style={styles.container}>
          <select value={scheme} onChange={(e) => setScheme(e.target.value)} style={styles.select}>
            <option value="full">Full</option>
            <option value="warm">Warm</option>
            <option value="cool">Cool</option>
          </select>
      </div>

      {/* ハーモニータイプの選択 */}
      <div style={styles.container}>
          <select value={harmonyType} onChange={(e) => setHarmonyType(e.target.value)} style={styles.select}>
            <option value="Complementary">Complementary</option>
            <option value="Triad">Triad</option>
            <option value="Analogous">Analogous</option>
          </select>
      </div>

      {/* カラーホイール */}
      <div style={{display: "flex", justifyContent: "center"}}>
      <ColorWheel
        size={400}
        segments={12}
        scheme={scheme}
        harmonyType={harmonyType}
        onColorsChange={handleColorChange}
      />
      </div>

      {/* 選択した色と類似色を表示 */}
      <div style={{ marginTop: "20px" }}>
        <h2>Selected Colors and Similar Colors</h2>
        <div style={{ display: "flex", justifyContent: "center", gap: "20px" }}>
          {selectedColors.map((color, index) => (
            <div key={index}>
              <div style={{ backgroundColor: color,width: "100px", height: "100px", borderRadius: "8px", marginBottom: "10px"}}
              ></div>
              {similarColors[index]?.map((similarColor, subIndex) => (
                <div key={subIndex}
                  style={{backgroundColor: similarColor, width: "100px", height: "100px", borderRadius: "8px", margin: "10px 0px"}}
                ></div>
              ))}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default App;


```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3957055/3f60a33d-65ab-2786-d673-5b81ec77d890.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3957055/bbb3fdfb-e6c5-e0d5-9cf7-cc307bdf68d8.png)

## おわりに
こんな感じで作ってみました～
色コードの表示や保存などは全然できていないのでいつかできたらと思います。