完成色轮选色的基础功能。
This commit is contained in:
parent
b206a58be1
commit
10f885526e
|
@ -8,6 +8,7 @@ import { NewScheme } from './pages/NewScheme';
|
||||||
import { SchemeDetail } from './pages/SchemeDetail';
|
import { SchemeDetail } from './pages/SchemeDetail';
|
||||||
import { SchemeNotFound } from './pages/SchemeNotFound';
|
import { SchemeNotFound } from './pages/SchemeNotFound';
|
||||||
import { Schemes } from './pages/Schemes';
|
import { Schemes } from './pages/Schemes';
|
||||||
|
import { Wheels } from './pages/Wheels';
|
||||||
|
|
||||||
const routes = createBrowserRouter([
|
const routes = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
@ -25,6 +26,7 @@ const routes = createBrowserRouter([
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: 'harmonies', element: <Harmonies /> },
|
{ path: 'harmonies', element: <Harmonies /> },
|
||||||
|
{ path: 'wheels', element: <Wheels /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
46
src/page-components/wheels/ColorColumn.module.css
Normal file
46
src/page-components/wheels/ColorColumn.module.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/page-components/wheels/ColorColumn.tsx
Normal file
56
src/page-components/wheels/ColorColumn.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className={styles.color_block} onClick={() => copyColor(color)}>
|
||||||
|
<div className={styles.bg} style={{ backgroundColor: `#${color}` }} />
|
||||||
|
<div className={cx(styles.dim_overlay, dimmed && styles.show_overlay)} />
|
||||||
|
<div className={cx(isRoot && styles.active)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.color_column} style={{ transform: `rotate(${rotate}deg)` }}>
|
||||||
|
{colorSeries.map((c, index) => (
|
||||||
|
<ColorBlock
|
||||||
|
key={`${c}-${index}`}
|
||||||
|
dimmed={!actived}
|
||||||
|
color={c}
|
||||||
|
isRoot={isEqual(c, rootColor) && isEqual(rotate, 0)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
22
src/page-components/wheels/ColorWheel.module.css
Normal file
22
src/page-components/wheels/ColorWheel.module.css
Normal file
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
src/page-components/wheels/ColorWheel.tsx
Normal file
67
src/page-components/wheels/ColorWheel.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className={styles.wheel_view}>
|
||||||
|
<h5>Color Wheel</h5>
|
||||||
|
<div className={cx('center', styles.wheel_place)}>
|
||||||
|
<div className={styles.wheel_container}>
|
||||||
|
{wheelColors.map(({ color, rotate }) => (
|
||||||
|
<ColorColumn
|
||||||
|
key={`${color}-${rotate}`}
|
||||||
|
rootColor={color}
|
||||||
|
rotate={rotate}
|
||||||
|
actived={includes(highlightSeries[highlightMode], rotate)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
src/pages/Wheels.module.css
Normal file
32
src/pages/Wheels.module.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
49
src/pages/Wheels.tsx
Normal file
49
src/pages/Wheels.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className={cx('workspace', styles.wheels_workspace)}>
|
||||||
|
<header>
|
||||||
|
<h3>Color Wheels</h3>
|
||||||
|
<p>Choose the required color on the color wheel according to color theory.</p>
|
||||||
|
</header>
|
||||||
|
<section className={styles.explore_section}>
|
||||||
|
<aside className={styles.function_side}>
|
||||||
|
<div>
|
||||||
|
<h5>Basic color</h5>
|
||||||
|
<ColorPicker color={selectedColor} onSelect={(color) => setSelectedColor(color)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.mode_navigation}>
|
||||||
|
<h5>Color selection method</h5>
|
||||||
|
<VSegmentedControl
|
||||||
|
onChange={setSelectedMode}
|
||||||
|
options={[
|
||||||
|
{ label: 'Complementary', value: 'complementary' },
|
||||||
|
{ label: 'Analogous', value: 'analogous' },
|
||||||
|
{ label: 'Analogous with Complementary', value: 'analogous_with_complementary' },
|
||||||
|
{ label: 'Triadic', value: 'triadic' },
|
||||||
|
{ label: 'Split Complementary', value: 'split_complementary' },
|
||||||
|
{ label: 'Tetradic', value: 'tetradic' },
|
||||||
|
{ label: 'Flip Tetradic', value: 'flip_tetradic' },
|
||||||
|
{ label: 'Square', value: 'square' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div className={styles.wheel_side}>
|
||||||
|
<ColorWheel originColor={selectedColor} highlightMode={selectedMode} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user