diff --git a/src/App.tsx b/src/App.tsx index 9176676..2d6d8bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { NewScheme } from './pages/NewScheme'; import { SchemeDetail } from './pages/SchemeDetail'; import { SchemeNotFound } from './pages/SchemeNotFound'; import { Schemes } from './pages/Schemes'; +import { Wheels } from './pages/Wheels'; const routes = createBrowserRouter([ { @@ -25,6 +26,7 @@ const routes = createBrowserRouter([ ], }, { path: 'harmonies', element: }, + { path: 'wheels', element: }, ], }, ]); diff --git a/src/page-components/wheels/ColorColumn.module.css b/src/page-components/wheels/ColorColumn.module.css new file mode 100644 index 0000000..2a28646 --- /dev/null +++ b/src/page-components/wheels/ColorColumn.module.css @@ -0,0 +1,46 @@ +@layer pages { + @keyframes current_blink { + 0% { + opacity: 0.2; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 0.2; + } + } + .color_column { + clip-path: polygon(38.5% 0px, 50% 100%, 61.25% 0px); + position: absolute; + top: 0; + left: 0; + width: 100%; + transform-origin: center bottom; + } + .color_block { + position: relative; + height: 1.5em; + width: 100%; + .bg { + position: absolute; + inset: 0; + } + .dim_overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 70%); + opacity: 0; + } + .show_overlay { + opacity: 0.8; + transition: opacity 500ms ease-in-out; + } + .active { + position: absolute; + inset: 0; + background-color: var(--color-white); + animation: current_blink 2s linear infinite; + } + } +} diff --git a/src/page-components/wheels/ColorColumn.tsx b/src/page-components/wheels/ColorColumn.tsx new file mode 100644 index 0000000..cb1ac24 --- /dev/null +++ b/src/page-components/wheels/ColorColumn.tsx @@ -0,0 +1,56 @@ +import cx from 'clsx'; +import { isEqual } from 'lodash-es'; +import { useMemo } from 'react'; +import { useColorFunction } from '../../ColorFunctionContext'; +import { useCopyColor } from '../../hooks/useCopyColor'; +import styles from './ColorColumn.module.css'; + +type ColorBlockProps = { + dimmed: boolean; + color: string; + isRoot?: boolean; +}; + +function ColorBlock({ dimmed, color, isRoot = false }: ColorBlockProps) { + const copyColor = useCopyColor(); + + return ( +
copyColor(color)}> +
+
+
+
+ ); +} + +type ColorWheelProps = { + actived: boolean; + rotate: number; + rootColor: string; +}; + +export function ColorColumn({ actived, rotate, rootColor }: ColorWheelProps) { + const { colorFn } = useColorFunction(); + const colorSeries = useMemo(() => { + try { + const colors = colorFn?.series(rootColor, 4, 0.16); + return (colors ?? Array.from({ length: 9 }, () => rootColor)).reverse(); + } catch (e) { + console.error('[Generate Color Series]', e); + } + return Array.from({ length: 9 }, () => rootColor); + }, [rootColor]); + + return ( +
+ {colorSeries.map((c, index) => ( + + ))} +
+ ); +} diff --git a/src/page-components/wheels/ColorWheel.module.css b/src/page-components/wheels/ColorWheel.module.css new file mode 100644 index 0000000..158cbd1 --- /dev/null +++ b/src/page-components/wheels/ColorWheel.module.css @@ -0,0 +1,22 @@ +@layer pages { + .wheel_view { + width: 100%; + height: 100%; + padding: 0 var(--spacing-m); + display: flex; + flex-direction: column; + h5 { + padding-block: var(--spacing-m); + font-size: var(--font-size-m); + } + .wheel_place { + flex: 1; + padding: var(--spacing-m) 0; + } + .wheel_container { + position: relative; + aspect-ratio: 1 / 1; + height: 100%; + } + } +} diff --git a/src/page-components/wheels/ColorWheel.tsx b/src/page-components/wheels/ColorWheel.tsx new file mode 100644 index 0000000..a5ec3d2 --- /dev/null +++ b/src/page-components/wheels/ColorWheel.tsx @@ -0,0 +1,67 @@ +import cx from 'clsx'; +import { includes } from 'lodash-es'; +import { useMemo } from 'react'; +import { useColorFunction } from '../../ColorFunctionContext'; +import { ColorColumn } from './ColorColumn'; +import styles from './ColorWheel.module.css'; + +type ColorWheelProps = { + originColor?: string; + highlightMode?: + | 'complementary' + | 'analogous' + | 'analogous_with_complementary' + | 'triadic' + | 'split_complementary' + | 'tetradic' + | 'flip_tetradic' + | 'square'; +}; + +const wheelRotates = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]; +const highlightSeries = { + complementary: [0, 180], + analogous: [0, 30, 330], + analogous_with_complementary: [0, 30, 180, 330], + triadic: [0, 120, 240], + split_complementary: [0, 150, 210], + tetradic: [0, 60, 180, 240], + flip_tetradic: [0, 120, 180, 300], + square: [0, 90, 180, 270], +}; + +export function ColorWheel({ + originColor = '000000', + highlightMode = 'complementary', +}: ColorWheelProps) { + const { colorFn } = useColorFunction(); + const wheelColors = useMemo(() => { + return wheelRotates.map((rotate) => { + try { + const color = colorFn?.shift_hue(originColor, rotate); + return { color: color ?? '000000', rotate }; + } catch (e) { + console.error('[Generate color wheel]', e); + } + return { color: '000000', rotate }; + }); + }, [originColor]); + + return ( +
+
Color Wheel
+
+
+ {wheelColors.map(({ color, rotate }) => ( + + ))} +
+
+
+ ); +} diff --git a/src/pages/Wheels.module.css b/src/pages/Wheels.module.css new file mode 100644 index 0000000..14780ad --- /dev/null +++ b/src/pages/Wheels.module.css @@ -0,0 +1,32 @@ +@layer pages { + .wheels_workspace { + flex-direction: column; + } + .explore_section { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + align-items: stretch; + gap: var(--spacing-m); + } + .function_side { + display: flex; + flex-direction: column; + gap: var(--spacing-m); + font-size: var(--font-size-s); + .mode_navigation { + display: flex; + flex-direction: column; + align-items: stretch; + gap: var(--spacing-s); + } + h5 { + padding-block: var(--spacing-m); + font-size: var(--font-size-m); + } + } + .wheel_side { + flex: 1; + } +} diff --git a/src/pages/Wheels.tsx b/src/pages/Wheels.tsx new file mode 100644 index 0000000..a2cd2a7 --- /dev/null +++ b/src/pages/Wheels.tsx @@ -0,0 +1,49 @@ +import cx from 'clsx'; +import { useAtom } from 'jotai'; +import { useState } from 'react'; +import { ColorPicker } from '../components/ColorPicker'; +import { VSegmentedControl } from '../components/VSegmentedControl'; +import { ColorWheel } from '../page-components/wheels/ColorWheel'; +import { currentPickedColor } from '../stores/colors'; +import styles from './Wheels.module.css'; + +export function Wheels() { + const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); + const [selectedMode, setSelectedMode] = useState('complementary'); + + return ( +
+
+

Color Wheels

+

Choose the required color on the color wheel according to color theory.

+
+
+ +
+ +
+
+
+ ); +}