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 (
+
+
+
+
+
+
+
+
+
+ );
+}