🍭

【React】快適に使えるカラー変換ツールを作った

2022/03/22に公開

はじめに

先日、ブラウザで使えるカラー変換ツールを探していました。カラーコード⇄RGBを変換したり、HSL⇄HSVを変換したりするツールのことをカラー変換ツールと呼んでいます。
しかし、個人的に欲しいと思っていた以下の4つの条件を満たすものを、Google検索の上位では見つけることができませんでした(僕が知らないだけかもしれませんが…)。

  • 一覧性が高い。
  • すべてのカラーモードで数値の入力・コピーに対応している。
  • マウスクリック等の操作をできるだけ必要としない。
  • 塗り・文字色の両方で色を確認できる。

そこで、ちょうど簡単に作れるWebツールを作ってみたいと思っていたところだったので、この機会に作ってみることにしました。

↓こんな感じのシンプルなサイトを作りました。

↓アクセスはこちらから
https://color.moyotsukai.dev/

この記事では、快適に使えるカラー変換ツールを目指して工夫したポイントと、その実装について紹介していければと思います。

技術構成

  • Next.js (TypeScript)
  • React Context (状態管理)
  • Emotion (CSS)
  • Cloudflare Pages
  • Google アナリティクス

開発には7日かかりました。Google アナリティクスをはじめて使ったので、その設定とプライバシーポリシーに手間取りました。React Contextもはじめて使いました。

こだわりポイントと実装

開発を始める前に

まず、よく使うカラーモードとして、Hex(カラーコード), RGBA, HSLA, HSV, CMYKの5つのカラーモードに対応させようと決めました。

次に、カラー変換ツールとして必須だと思ったのは、すべてのカラーモードの値がレスポンシブに更新されることです。例えばカラーコードを入力した際、Enterキーを押したりボタンをクリックしたりすることなくRGBやHSL等の数値が更新されることです。

それから、今回作るツールは「キーボードでの操作を前提とする」という方針で作ることに決めました。

カラーの状態管理

すべてのカラーモードの値を連動させるにあたって、5つのカラーモードの値とは別に、それらが共有するカラーの変数を作成しました。どれかひとつのカラーモードで値が変更されると、この共有された変数が更新され、他のカラーモードの値にも反映される、という具合です。どのカラーモードとも変換可能である必要があるため、一番扱いやすいRGBAを使って管理することにしました。このRGBA値が、実際に画面に表示されるサンプルの色を指定しています。

今回は、ReactのContextを使って変数を作り、sharedRgbaという名前で使うことにしました。Context用のファイルを作り、RGBA値に関連するContextの部分を切り出しています。

SharedRgbaContext.tsx
const defaultSharedRgbaValue: RGBA = { r: 0, g: 0, b: 0, a: 1 }

const SharedRgbaValueContext = createContext<RGBA>(defaultSharedRgbaValue)

const SharedRgbaDispatchContext = createContext<Dispatch<SetStateAction<RGBA>>>(() => undefined)

type Props = {
  children: React.ReactNode
}

export const SharedRgbaContextProvider: React.FC<Props> = (props) => {
  const [sharedRgb, setSharedRgb] = useState<RGBA>(defaultSharedRgbaValue)

  return (
    <SharedRgbaValueContext.Provider value={sharedRgb}>
      <SharedRgbaDispatchContext.Provider value={setSharedRgb}>
        {props.children}
      </SharedRgbaDispatchContext.Provider>
    </SharedRgbaValueContext.Provider>
  )
}

export const useSharedRgbaValue = () => {
  return useContext(SharedRgbaValueContext)
}

export const useSetSharedRgba = () => {
  return useContext(SharedRgbaDispatchContext)
}

カスタムフックを作ってあるので、各コンポーネントではuseStateと同じような要領で使うことができます。

const [sharedRgba, setSharedRgba] = [useSharedRgbaValue(), useSetSharedRgba()]

動的なカラーの指定

スタイリングにはEmotionを使用しました。Emotionでは、スタイルを関数化することで動的にCSSを適用できます。これを使って、Contextで管理されたsharedRgbaが変更される度に表示する色を更新しています。

IndexPage.tsx
const IndexPage: React.FC = () => {
  const sharedRgba = useSharedRgbaValue()

  return (
    <div>
      <div css={() => colorBlockStyle(sharedRgba)} />
    </div>
  )
}

const colorBlockStyle = (rgba: RGBA) => css`
  background-color: rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a});
`

カラーの更新

5つのカラーモードの中からHSLAを例にとって、カラーの更新の方法について書いていきます。

①HSLA→sharedRgba

HSLAのパラメーターを持つコンポーネント内部で、H, S, L, Aの値が変更された時に、ContextのsharedRgbaを更新します。

ConverterHSLA.tsx
const ConverterHSLA: React.FC = () => {
  const [sharedRgba, setSharedRgba] = [useSharedRgbaValue(), useSetSharedRgba()]
  const [hsla, setHsla] = useState<HSLA>({ h: 0, s: 0, l: 0, a: 1 })
  const [hslaText, setHslaText] = useState<string>("0, 0%, 0%, 1")

  const onChangeH = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newHsla: HSLA = { h: parseInt(e.target.value), s: hsla.s, l: hsla.l, a: hsla.a }
    setChanged(newHsla)
  }
  
  const setChanged = (newHsla: HSLA) => {
    setHsla(newHsla)
    setHslaText(toHslaText(newHsla))
    const newRgba: RGBA = toRgbFromHsla(newHsla)
    setSharedRgba({ ...newRgba, editedFrom: "Hsla" })
  }
  
  return (
    <div>
      <div>
        <span>H</span>
        <RangeInput min={0} max={360} step={1} value={hsla.h} onChange={onChangeH} />
        <NumberInput min={0} max={360} step={1} value={hsla.h} onChange={onChangeH} />
      </div>
      {/* S, L, Aも同じ */}
      <div>
        <span>HSLA</span>
        <TextInput value={hslaText} onChange={onHslaTextChange} />
      </div>
    </div>
  )
}

<input type="range" />でスライダーからの入力を、<input type="number" />100のような値を、<input type="text" />"100, 80%, 50%, 1"のようなひと繋がりの文字列を受け取るというように、3種類の方法で入力できます。

sharedRgba→HSLA

HexやRGBAなどのパラメーターを持つ外部のコンポーネントによってContextのsharedRgbaが更新された時、変更を検知して自身のコンポーネントに値を反映します。

ConverterHSLA.tsx
//To hsla from sharedRgba
useEffect(() => {
  if (sharedRgba.editedFrom === "Hsla") { return }
  const newHsla = toHslaFromRgb(sharedRgba)
  setHsla(newHsla)
  setHslaText(toHslaText(newHsla))
}, [sharedRgba])

ポイントは、sharedRgbaeditedFromプロパティを持たせ、どのカラーモードから値が変更されたか判別していることです。これによって、自身のパラメーターの変更によって再度自身が更新されることを防いでいます。

その他の工夫点

高い一覧性

すべてのカラーモードを一覧できるようにし、マウスクリック等でカラーモードを切り替える必要なく使えるようにしました。

入力とコピーをやりやすく

例えばHSLAであれば、H, S, L, Aそれぞれの値を調整できるだけでなく、"100, 80%, 50%, 1"のような横一列での入力・コピーをできるようにしました。CSSと行ったり来たりしやすくするためです。

Tabキーでの移動

キーボードで快適に入力するために欠かせないのがTabキーでの移動です。
<input type="number" /><input type="text" />にはtabIndexを指定し、Tabキーを押した時に適切な順番でフォーカスされるようにしました。
逆に、<input type="range" />にはtabIndex={-1}を指定し、Tabキーを押してもフォーカスされないようにすることで、スムーズな数値の入力を邪魔しないように設計しました。

<RangeInput tabIndex={-1} />
<NumberInput tabIndex={5} />
<TextInput tabIndex={6} />

inputをクリックした際に全選択

const onFocus = (e: React.FocusEvent<HTMLInputElement>) => {
  e.target.select()
}

塗り・文字色の両方で色を確認

人間の目は、面積が小さいと色が暗く見えてしまうので、塗り・文字色の両方で色を確認できるようにしました。

背景色を変更

カラーコードで背景色も変更できるようにしました。ダークモードでの見え方の検証などにご活用ください。

柔軟なテキストバリデーション

テキストのバリデーションを柔軟にし、inputからフォーカスが外れた時に入力値を正しい形式に直すようにしました。たとえばHSLで%が抜けていている、数値が範囲を超えている、入力された文字が数字でない、などの場合にEnterを押すかフォーカスを外すとフォーマットされます。


快適に使えるカラー変換ツールを目指して工夫したので、ぜひ使ってみてください!

https://color.moyotsukai.dev/

Discussion

ログインするとコメントできます