完成Tints & Shades功能。
This commit is contained in:
		| @@ -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 { TintsShades } from './pages/TintsShades'; | ||||
| import { Tones } from './pages/Tones'; | ||||
| import { Wheels } from './pages/Wheels'; | ||||
|  | ||||
| @@ -29,6 +30,7 @@ const routes = createBrowserRouter([ | ||||
|       { path: 'harmonies', element: <Harmonies /> }, | ||||
|       { path: 'wheels', element: <Wheels /> }, | ||||
|       { path: 'tones', element: <Tones /> }, | ||||
|       { path: 'tints-shades', element: <TintsShades /> }, | ||||
|     ], | ||||
|   }, | ||||
| ]); | ||||
|   | ||||
							
								
								
									
										59
									
								
								src/page-components/TintsShades/shades.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/page-components/TintsShades/shades.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { last } from 'lodash-es'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useColorFunction } from '../../ColorFunctionContext'; | ||||
| import { FlexColorStand } from '../../components/FlexColorStand'; | ||||
|  | ||||
| type ShadesListProps = { | ||||
|   color: string; | ||||
|   shades: number; | ||||
|   mix: 'progressive' | 'linear' | 'average'; | ||||
|   step?: number; | ||||
|   maximum?: number; | ||||
|   copyMode: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'; | ||||
| }; | ||||
|  | ||||
| export function Shades({ color, shades, mix, step, maximum, copyMode }: ShadesListProps) { | ||||
|   const { colorFn } = useColorFunction(); | ||||
|   const colors = useMemo(() => { | ||||
|     try { | ||||
|       if (!colorFn) { | ||||
|         return Array.from({ length: shades + 1 }, () => color); | ||||
|       } | ||||
|       const genColors = [color]; | ||||
|       switch (mix) { | ||||
|         case 'progressive': | ||||
|           for (let i = 1; i <= shades; i++) { | ||||
|             const shade = colorFn!.shade(last(genColors), step); | ||||
|             genColors.push(shade); | ||||
|           } | ||||
|           break; | ||||
|         case 'linear': | ||||
|           for (let i = 1; i <= shades; i++) { | ||||
|             const shade = colorFn!.shade(color, step * i); | ||||
|             genColors.push(shade); | ||||
|           } | ||||
|           break; | ||||
|         case 'average': { | ||||
|           const interval = maximum / shades / 100; | ||||
|           for (let i = 1; i <= shades; i++) { | ||||
|             const shade = colorFn!.shade(color, interval * i); | ||||
|             genColors.push(shade); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       return genColors.reverse(); | ||||
|     } catch (e) { | ||||
|       console.error('[Generate Shades]', e); | ||||
|     } | ||||
|     return Array.from({ length: shades + 1 }, () => color); | ||||
|   }, [color, shades, mix, step, maximum]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {colors.map((c, index) => ( | ||||
|         <FlexColorStand key={`${c}-${index}`} color={c} valueMode={copyMode} /> | ||||
|       ))} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										59
									
								
								src/page-components/TintsShades/tints.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/page-components/TintsShades/tints.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { last } from 'lodash-es'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useColorFunction } from '../../ColorFunctionContext'; | ||||
| import { FlexColorStand } from '../../components/FlexColorStand'; | ||||
|  | ||||
| type TintsListProps = { | ||||
|   color: string; | ||||
|   tints: number; | ||||
|   mix: 'progressive' | 'linear' | 'average'; | ||||
|   step?: number; | ||||
|   maximum?: number; | ||||
|   copyMode: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'; | ||||
| }; | ||||
|  | ||||
| export function Tints({ color, tints, mix, step, maximum, copyMode }: TintsListProps) { | ||||
|   const { colorFn } = useColorFunction(); | ||||
|   const colors = useMemo(() => { | ||||
|     try { | ||||
|       if (!colorFn) { | ||||
|         return Array.from({ length: tints + 1 }, () => color); | ||||
|       } | ||||
|       const genColors = [color]; | ||||
|       switch (mix) { | ||||
|         case 'progressive': | ||||
|           for (let i = 1; i <= tints; i++) { | ||||
|             const tint = colorFn!.tint(last(genColors), step); | ||||
|             genColors.push(tint); | ||||
|           } | ||||
|           break; | ||||
|         case 'linear': | ||||
|           for (let i = 1; i <= tints; i++) { | ||||
|             const tint = colorFn!.tint(color, step * i); | ||||
|             genColors.push(tint); | ||||
|           } | ||||
|           break; | ||||
|         case 'average': { | ||||
|           const interval = maximum / tints / 100; | ||||
|           for (let i = 1; i <= tints; i++) { | ||||
|             const tint = colorFn!.tint(color, interval * i); | ||||
|             genColors.push(tint); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       return genColors; | ||||
|     } catch (e) { | ||||
|       console.error('[Generate Tints]', e); | ||||
|     } | ||||
|     return Array.from({ length: tints + 1 }, () => color); | ||||
|   }, [color, tints, mix, step, maximum]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {colors.map((c, index) => ( | ||||
|         <FlexColorStand key={`${c}-${index}`} color={c} valueMode={copyMode} /> | ||||
|       ))} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										55
									
								
								src/pages/TintsShades.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/pages/TintsShades.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| @layer pages { | ||||
|   .tints_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); | ||||
|     } | ||||
|   } | ||||
|   .tints_content { | ||||
|     flex: 1; | ||||
|     padding: 0 var(--spacing-m); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: var(--spacing-s); | ||||
|     h5 { | ||||
|       padding-block: var(--spacing-m); | ||||
|       font-size: var(--font-size-m); | ||||
|     } | ||||
|     .color_value_mode { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       align-items: center; | ||||
|       gap: var(--spacing-s); | ||||
|       font-size: var(--font-size-s); | ||||
|     } | ||||
|     .colors_booth { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       justify-content: center; | ||||
|       align-items: stretch; | ||||
|       gap: var(--spacing-xs); | ||||
|       min-height: 12em; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										134
									
								
								src/pages/TintsShades.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/pages/TintsShades.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| import cx from 'clsx'; | ||||
| import { useAtom } from 'jotai'; | ||||
| import { isEqual } from 'lodash-es'; | ||||
| import { useState } from 'react'; | ||||
| import { ColorPicker } from '../components/ColorPicker'; | ||||
| import { HSegmentedControl } from '../components/HSegmentedControl'; | ||||
| import { Labeled } from '../components/Labeled'; | ||||
| import { LabeledPicker } from '../components/LabeledPicker'; | ||||
| import { ScrollArea } from '../components/ScrollArea'; | ||||
| import { VSegmentedControl } from '../components/VSegmentedControl'; | ||||
| import { Shades } from '../page-components/TintsShades/shades'; | ||||
| import { Tints } from '../page-components/TintsShades/tints'; | ||||
| import { currentPickedColor } from '../stores/colors'; | ||||
| import styles from './TintsShades.module.css'; | ||||
|  | ||||
| export function TintsShades() { | ||||
|   const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); | ||||
|   const [steps, setSteps] = useState(10); | ||||
|   const [tints, setTints] = useState(3); | ||||
|   const [shades, setShades] = useState(3); | ||||
|   const [maximum, setMaximum] = useState(90); | ||||
|   const [mixMode, setMixMode] = useState<'progressive' | 'average'>('progressive'); | ||||
|   const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); | ||||
|  | ||||
|   return ( | ||||
|     <div className={cx('workspace', styles.tints_workspace)}> | ||||
|       <header> | ||||
|         <h3>Tints & Shades</h3> | ||||
|         <p>By proportionally mixing black and white, generating a series of varying colors.</p> | ||||
|       </header> | ||||
|       <ScrollArea enableY> | ||||
|         <section className={styles.explore_section}> | ||||
|           <aside className={styles.function_side}> | ||||
|             <div> | ||||
|               <h5>Basic color</h5> | ||||
|               <ColorPicker color={selectedColor} onSelect={(color) => setSelectedColor(color)} /> | ||||
|             </div> | ||||
|             <div> | ||||
|               <h5>Series Setting</h5> | ||||
|               <LabeledPicker | ||||
|                 title="Tints" | ||||
|                 min={1} | ||||
|                 max={10} | ||||
|                 step={1} | ||||
|                 value={tints} | ||||
|                 onChange={setTints} | ||||
|               /> | ||||
|               <LabeledPicker | ||||
|                 title="Shades" | ||||
|                 min={1} | ||||
|                 max={10} | ||||
|                 step={1} | ||||
|                 value={shades} | ||||
|                 onChange={setShades} | ||||
|               /> | ||||
|               <Labeled label="Generate Mode"> | ||||
|                 <VSegmentedControl | ||||
|                   options={[ | ||||
|                     { label: 'Progressive', value: 'progressive' }, | ||||
|                     { label: 'Linear', value: 'linear' }, | ||||
|                     { label: 'Average', value: 'average' }, | ||||
|                   ]} | ||||
|                   value={mixMode} | ||||
|                   onChange={(value) => { | ||||
|                     setMixMode(value as 'progressive' | 'average'); | ||||
|                     console.debug('[Mix Mode Switch]', value); | ||||
|                   }} | ||||
|                 /> | ||||
|               </Labeled> | ||||
|               {(isEqual(mixMode, 'progressive') || isEqual(mixMode, 'linear')) && ( | ||||
|                 <LabeledPicker | ||||
|                   title="Step" | ||||
|                   min={10} | ||||
|                   max={25} | ||||
|                   step={1} | ||||
|                   value={steps} | ||||
|                   onChange={setSteps} | ||||
|                 /> | ||||
|               )} | ||||
|               {isEqual(mixMode, 'average') && ( | ||||
|                 <LabeledPicker | ||||
|                   title="Maximum" | ||||
|                   min={10} | ||||
|                   max={100} | ||||
|                   step={1} | ||||
|                   value={maximum} | ||||
|                   onChange={setMaximum} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|           </aside> | ||||
|           <div className={styles.tints_content}> | ||||
|             <h5>Tints</h5> | ||||
|             <div className={styles.colors_booth}> | ||||
|               <Tints | ||||
|                 color={selectedColor} | ||||
|                 tints={tints} | ||||
|                 step={steps / 100} | ||||
|                 maximum={maximum} | ||||
|                 mix={mixMode} | ||||
|                 copyMode={mode} | ||||
|               /> | ||||
|             </div> | ||||
|             <h5>Shades</h5> | ||||
|             <div className={styles.colors_booth}> | ||||
|               <Shades | ||||
|                 color={selectedColor} | ||||
|                 shades={shades} | ||||
|                 step={steps / 100} | ||||
|                 maximum={maximum} | ||||
|                 mix={mixMode} | ||||
|                 copyMode={mode} | ||||
|               /> | ||||
|             </div> | ||||
|             <div className={styles.color_value_mode}> | ||||
|               <label>Copy color value in</label> | ||||
|               <HSegmentedControl | ||||
|                 options={[ | ||||
|                   { label: 'HEX', value: 'hex' }, | ||||
|                   { label: 'RGB', value: 'rgb' }, | ||||
|                   { label: 'HSL', value: 'hsl' }, | ||||
|                   { label: 'LAB', value: 'lab' }, | ||||
|                   { label: 'OKLCH', value: 'oklch' }, | ||||
|                 ]} | ||||
|                 valu={mode} | ||||
|                 onChange={setMode} | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </section> | ||||
|       </ScrollArea> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user