📖

色相環を可視化!カラーバリエーション生成ツールで学ぶ色彩理論の実装

に公開

はじめに

Webデザインやアプリ開発で、カラースキームを決める作業って悩みますよね。

「この色の補色ってどれ?」「類似色を使いたいけど、どの角度がいいかな?」「もっと明るいバージョンが欲しい」...こういった疑問に対して、毎回色相環を見ながら手作業で調整するのは大変です。

そこで、ベースカラーを選ぶだけで補色・類似色・明度/彩度のバリエーションを自動生成し、円形レイアウトで視覚的に確認できるツールを作りました。

この記事では、ツールの設計思想と実装の詳細、そして色彩理論をコードに落とし込む過程で得た学びを共有します。

ツールURL: https://tools.easegis.jp/ja/tools/image/color-variations

なぜこのツールを作ったのか

背景にある課題

デザインシステムを構築する際、プライマリカラーを1色決めても、実際に使う色はその何倍も必要になります。

  • ボタンのホバー状態(少し明るい色)
  • アクティブ状態(少し暗い色)
  • セカンダリカラー(類似色)
  • アクセントカラー(補色)
  • 背景色やテキスト色(明度調整)

これらを毎回手動で調整するのは、非効率ですし、一貫性を保つのも難しいんです。

設計の方向性

このツールを作る際、以下の3つの設計方針を重視しました。

色彩理論を可視化する

補色や類似色という概念を、単に数値として出力するだけでなく、円形レイアウトで色相環の関係性を視覚的に理解できるようにしました。

実用性を優先する

デザイナーがすぐに使える形(HEX/RGB/HSL)で出力し、CSS変数形式での一括コピーにも対応。実際のプロジェクトにそのまま貼り付けられることを意識しています。

レスポンシブで直感的に

デスクトップでもモバイルでも快適に使えるよう、画面サイズに応じてUIを最適化。タップ操作でも色の情報を確認しやすくしました。

色彩理論の基礎知識

実装に入る前に、色彩理論の基本を整理しておきましょう。

色相環(Color Wheel)

色相環は、色を円形に配置したもので、色同士の関係性を理解するための基本ツールです。

  • 0度: 赤
  • 120度: 緑
  • 240度: 青

この360度の環を使って、様々な配色パターンを作り出せます。

補色(Complementary Colors)

色相環で180度反対に位置する色同士を補色と言います。補色は最もコントラストが強く、お互いを引き立て合う関係です。

青(217度) → 補色は黄橙(37度)

類似色(Analogous Colors)

色相環で隣り合う色を類似色と言います。一般的には±30度以内の色が類似色とされています。

青(217度)の類似色:
  - 187度(青緑)
  - 202度(青)
  - 232度(青紫)
  - 247度(紫)

HSL色空間の利点

このツールでは、HSL(Hue, Saturation, Lightness)色空間を活用しています。

  • Hue(色相): 0〜360度
  • Saturation(彩度): 0〜100%
  • Lightness(明度): 0〜100%

HSLの最大の利点は、色の調整が直感的にできることです。色相を変えれば別の色に、彩度を変えれば鮮やかさを、明度を変えれば明るさを調整できます。

実装の詳細

補色の生成アルゴリズム

補色の計算は、色相を180度回転させるだけです。

const generateComplementary = (hex: string) => {
  const rgb = hexToRgb(hex)
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)

  // 色相を180度回転
  const complementaryHue = (hsl.h + 180) % 360

  // HSL → RGB → HEXに変換
  const complementaryRgb = hslToRgb(complementaryHue, hsl.s, hsl.l)
  return rgbToHex(complementaryRgb.r, complementaryRgb.g, complementaryRgb.b)
}

% 360を使って、360度を超えた場合に0度に戻るようにしています。

類似色の生成

類似色は、色相を±15度、±30度ずらすことで生成します。

const generateAnalogousColors = (hex: string) => {
  const rgb = hexToRgb(hex)
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)

  const analogous = []
  for (let offset = -30; offset <= 30; offset += 15) {
    if (offset === 0) continue // ベースカラー自体は除外

    const analogousHue = (hsl.h + offset + 360) % 360
    const analogousRgb = hslToRgb(analogousHue, hsl.s, hsl.l)

    analogous.push({
      name: `Analogous ${offset > 0 ? '+' : ''}${offset}deg`,
      hex: rgbToHex(analogousRgb.r, analogousRgb.g, analogousRgb.b)
    })
  }

  return analogous
}

+ 360を加えることで、負の値になった場合でも正しく計算できるようにしています。

明度のバリエーション

明度は、HSLのLightness値を調整します。

const generateLightnessVariations = (hex: string) => {
  const rgb = hexToRgb(hex)
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)

  const variations = []
  for (let i = -40; i <= 40; i += 20) {
    if (i === 0) continue

    // 0〜100の範囲に制限
    const adjustedL = Math.max(0, Math.min(100, hsl.l + i))
    const adjustedRgb = hslToRgb(hsl.h, hsl.s, adjustedL)

    variations.push({
      name: `${i > 0 ? 'Lighter' : 'Darker'} ${Math.abs(i)}%`,
      hex: rgbToHex(adjustedRgb.r, adjustedRgb.g, adjustedRgb.b)
    })
  }

  return variations
}

Math.max(0, Math.min(100, value))で、明度が0%未満や100%を超えないように制限しています。

彩度のバリエーション

彩度も同様に、Saturation値を調整します。

const generateSaturationVariations = (hex: string) => {
  const rgb = hexToRgb(hex)
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)

  const variations = []
  for (let i = -40; i <= 40; i += 20) {
    if (i === 0) continue

    const adjustedS = Math.max(0, Math.min(100, hsl.s + i))
    const adjustedRgb = hslToRgb(hsl.h, adjustedS, hsl.l)

    variations.push({
      name: `Saturation ${i > 0 ? '+' : ''}${i}%`,
      hex: rgbToHex(adjustedRgb.r, adjustedRgb.g, adjustedRgb.b)
    })
  }

  return variations
}

円形レイアウトの実装

色相環を視覚的に表現するため、バリエーションを円形に配置しました。

座標計算

三角関数を使って、円周上の座標を計算します。

const calculateCirclePosition = (
  index: number,
  totalItems: number,
  radius: number
) => {
  // -90度から始めることで、12時の位置からスタート
  const angle = (index * 360) / totalItems - 90

  const x = Math.cos((angle * Math.PI) / 180) * radius
  const y = Math.sin((angle * Math.PI) / 180) * radius

  return { x, y }
}

各色を円周上に均等に配置するため、360 / totalItemsで角度の間隔を計算しています。

レスポンシブ対応

画面サイズに応じて円の半径を変更し、モバイルでも見やすくしました。

const useResponsiveRadius = () => {
  const [windowWidth, setWindowWidth] = useState(0)

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth)
    handleResize()

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  // ブレークポイントに応じた半径を返す
  if (windowWidth < 640) return 120  // モバイル
  if (windowWidth < 768) return 160  // タブレット
  return 200  // デスクトップ
}

モバイル向けのタップインタラクション

モバイルでは、ホバーが使えないため、タップでツールチップを表示する仕組みを実装しました。

const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)

const handleColorClick = (index: number, hex: string) => {
  // 640px未満(モバイル)では、1回目のタップで詳細表示
  if (windowWidth < 640 && hoveredIndex !== index) {
    setHoveredIndex(index)
  } else {
    // 2回目のタップ、またはデスクトップではコピー
    copyToClipboard(hex)
    setHoveredIndex(null)
  }
}

ライト/ダークテーマのパレット生成

ベースカラーから、完全なUIテーマを自動生成する機能も実装しました。

const generateThemePalette = (
  baseColor: string,
  theme: 'light' | 'dark'
) => {
  const rgb = hexToRgb(baseColor)
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)

  if (theme === 'light') {
    return {
      primary: baseColor,
      secondary: hslToHex((hsl.h + 30) % 360, hsl.s, hsl.l),
      accent: hslToHex((hsl.h + 180) % 360, hsl.s, hsl.l),
      background: '#FFFFFF',
      surface: '#F5F5F5',
      text: '#212121',
      textSecondary: '#757575',
      border: '#E0E0E0'
    }
  } else {
    return {
      primary: baseColor,
      // ダークテーマでは彩度と明度を下げて目に優しく
      secondary: hslToHex(
        (hsl.h + 30) % 360,
        Math.max(hsl.s - 10, 0),
        Math.max(hsl.l - 10, 0)
      ),
      accent: hslToHex((hsl.h + 180) % 360, hsl.s, hsl.l),
      background: '#121212',
      surface: '#1E1E1E',
      text: '#FFFFFF',
      textSecondary: '#B0B0B0',
      border: '#333333'
    }
  }
}

ダークテーマでは、セカンダリカラーの彩度と明度を少し下げることで、目への負担を軽減しています。

CSS変数の一括出力

生成したカラーパレットをCSS変数として一括コピーできる機能も実装しました。

const generateCSSVariables = (
  baseColor: string,
  variations: ColorVariation[]
) => {
  const lightPalette = generateThemePalette(baseColor, 'light')
  const darkPalette = generateThemePalette(baseColor, 'dark')

  let css = ':root {\n'

  // ライトテーマの色
  Object.entries(lightPalette).forEach(([key, value]) => {
    css += `  --color-${key}: ${value};\n`
  })

  // ベースカラー
  css += `  --color-base: ${baseColor};\n`

  // バリエーション
  variations.forEach((variation) => {
    css += `  --color-${variation.cssName}: ${variation.hex};\n`
  })

  css += '}\n\n'

  // ダークモード
  css += '@media (prefers-color-scheme: dark) {\n'
  css += '  :root {\n'

  Object.entries(darkPalette).forEach(([key, value]) => {
    css += `    --color-${key}: ${value};\n`
  })

  css += '  }\n'
  css += '}'

  return css
}

開発を通じて学んだこと

色彩理論は数学で表現できる

補色や類似色という抽象的な概念も、HSL色空間を使えば数値計算で表現できることを実感しました。

特に、色相を角度として扱うことで、色の関係性を幾何学的に理解できるのが面白かったですね。

ユーザー体験の重要性

最初は単純なリスト表示でしたが、円形レイアウトに変えたことで、色の関係性が一目でわかるようになりました。

見た目だけでなく、情報の理解しやすさにも影響するUIデザインの重要性を学びました。

レスポンシブデザインの奥深さ

デスクトップとモバイルでは、単にサイズを変えるだけでなく、インタラクション方法も変える必要があることを学びました。

ホバーをタップに置き換えるだけでなく、ツールチップの表示タイミングや位置も最適化する必要があったのは、良い経験でした。

今後の展望

現在の機能に加えて、以下のような拡張を検討中です。

トライアド・テトラード配色

120度間隔のトライアド配色、90度間隔のテトラード配色など、より高度な配色パターンにも対応したいと考えています。

アクセシビリティチェック

生成した色のコントラスト比を自動計算し、WCAG基準に適合しているか確認できる機能を追加予定です。

パレットの保存と共有

生成したカラーパレットをURLで共有できる機能や、JSON/SCSS形式でエクスポートできる機能も実装したいですね。

まとめ

カラーバリエーション生成ツールの開発を通じて、色彩理論をコードに落とし込む面白さを体験できました。

特にHSL色空間の柔軟性は、デザインシステム構築において非常に有用だと実感しています。色相・彩度・明度を個別に調整できることで、一貫性のある配色を簡単に作り出せるようになりました。

このツールが、デザイナーや開発者の配色作業を少しでも効率化できれば嬉しいです。

ツールURL: https://tools.easegis.jp/ja/tools/image/color-variations

Discussion