基本完成Q Scheme的Builder功能。
This commit is contained in:
		
							
								
								
									
										37
									
								
								src/page-components/scheme/q-scheme/Builder.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/page-components/scheme/q-scheme/Builder.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| @layer pages { | ||||
|   .builder_layout { | ||||
|     padding: var(--spacing-s) var(--spacing-m); | ||||
|     font-size: var(--font-size-s); | ||||
|     line-height: 1.3em; | ||||
|     display: grid; | ||||
|     grid-template-columns: 200px 200px 200px; | ||||
|     gap: var(--spacing-xs); | ||||
|     .label { | ||||
|       max-width: 200px; | ||||
|       grid-column: 1; | ||||
|       padding-inline-end: var(--spacing-m); | ||||
|       text-align: right; | ||||
|     } | ||||
|     .color_picker_row { | ||||
|       grid-column: 2 / span 2; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: var(--spacing-s); | ||||
|       .error_msg { | ||||
|         color: var(--color-danger); | ||||
|         font-size: var(--font-size-xs); | ||||
|       } | ||||
|     } | ||||
|     .segment_title { | ||||
|       grid-column: 1 / span 2; | ||||
|       text-align: center; | ||||
|     } | ||||
|     .parameter_input { | ||||
|       max-width: 8em; | ||||
|     } | ||||
|     h5 { | ||||
|       font-size: var(--font-size-m); | ||||
|       line-height: 1.7em; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										378
									
								
								src/page-components/scheme/q-scheme/Builder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								src/page-components/scheme/q-scheme/Builder.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,378 @@ | ||||
| import { every, isEmpty, isNil } from 'lodash-es'; | ||||
| import { useActionState, useMemo } from 'react'; | ||||
| import { | ||||
|   ColorExpand, | ||||
|   ColorShifting, | ||||
|   SchemeSetting, | ||||
|   WACGSetting, | ||||
| } from '../../../color_functions/color_module'; | ||||
| import { useColorFunction } from '../../../ColorFunctionContext'; | ||||
| import { FloatColorPicker } from '../../../components/FloatcolorPicker'; | ||||
| import { ScrollArea } from '../../../components/ScrollArea'; | ||||
| import { VSegmentedControl } from '../../../components/VSegmentedControl'; | ||||
| import { SchemeContent } from '../../../models'; | ||||
| import { QSchemeSource, QSchemeStorage } from '../../../q-scheme'; | ||||
| import { useUpdateScheme } from '../../../stores/schemes'; | ||||
| import { defaultEmptyFormData } from '../../../utls'; | ||||
| import styles from './Builder.module.css'; | ||||
|  | ||||
| type QSchemeBuilderProps = { | ||||
|   scheme: SchemeContent<QSchemeStorage>; | ||||
| }; | ||||
|  | ||||
| export function QSchemeBuilder({ scheme }: QSchemeBuilderProps) { | ||||
|   const { colorFn } = useColorFunction(); | ||||
|   const updateScheme = useUpdateScheme(scheme.id); | ||||
|   const defaultSetting = useMemo(() => { | ||||
|     try { | ||||
|       if (!colorFn) throw 'Web Assembly functions is not available'; | ||||
|       const defaultValues = colorFn.q_scheme_default_settings(); | ||||
|       if (!scheme.schemeStorage.source?.setting) | ||||
|         return { | ||||
|           hover: { | ||||
|             chroma: | ||||
|               scheme.schemeStorage.source?.setting?.hover.chroma ?? defaultValues.hover.chroma, | ||||
|             lightness: | ||||
|               scheme.schemeStorage.source?.setting?.hover.lightness ?? | ||||
|               defaultValues.hover.lightness, | ||||
|           }, | ||||
|           active: { | ||||
|             chroma: | ||||
|               scheme.schemeStorage.source?.setting?.active.chroma ?? defaultValues.active.chroma, | ||||
|             lightness: | ||||
|               scheme.schemeStorage.source?.setting?.active.lightness ?? | ||||
|               defaultValues.active.lightness, | ||||
|           }, | ||||
|           focus: { | ||||
|             chroma: | ||||
|               scheme.schemeStorage.source?.setting?.focus.chroma ?? defaultValues.focus.chroma, | ||||
|             lightness: | ||||
|               scheme.schemeStorage.source?.setting?.focus.lightness ?? | ||||
|               defaultValues.focus.lightness, | ||||
|           }, | ||||
|           disabled: { | ||||
|             chroma: | ||||
|               scheme.schemeStorage.source?.setting?.disabled.chroma ?? | ||||
|               defaultValues.disabled.chroma, | ||||
|             lightness: | ||||
|               scheme.schemeStorage.source?.setting?.disabled.lightness ?? | ||||
|               defaultValues.disabled.lightness, | ||||
|           }, | ||||
|           dark_convert: { | ||||
|             chroma: | ||||
|               scheme.schemeStorage.source?.setting?.dark_convert.chroma ?? | ||||
|               defaultValues.dark_convert.chroma, | ||||
|             lightness: | ||||
|               scheme.schemeStorage.source?.setting?.dark_convert.lightness ?? | ||||
|               defaultValues.dark_convert.lightness, | ||||
|           }, | ||||
|           expand_method: | ||||
|             scheme.schemeStorage.source?.setting?.expand_method ?? defaultValues.expand_method, | ||||
|           wacg_follows: | ||||
|             scheme.schemeStorage.source?.setting?.wacg_follows ?? defaultValues.wacg_follows, | ||||
|         } as SchemeSetting; | ||||
|       return defaultValues; | ||||
|     } catch (e) { | ||||
|       console.error('[Q scheme builder]', e); | ||||
|     } | ||||
|     return null; | ||||
|   }, [scheme]); | ||||
|   const expandingMethods = useMemo(() => { | ||||
|     try { | ||||
|       if (!colorFn) throw 'Web Assembly functions is not available'; | ||||
|       return colorFn.q_scheme_color_expanding_methods(); | ||||
|     } catch (e) { | ||||
|       console.error('[Q scheme builder]', e); | ||||
|     } | ||||
|     return []; | ||||
|   }, []); | ||||
|   const wacgFollowStrategies = useMemo(() => { | ||||
|     try { | ||||
|       if (!colorFn) throw 'Web Assembly functions is not available'; | ||||
|       return colorFn.q_scheme_wacg_settings(); | ||||
|     } catch (e) { | ||||
|       console.error('[Q scheme builder]', e); | ||||
|     } | ||||
|     return []; | ||||
|   }, []); | ||||
|  | ||||
|   const [errMsg, handleSubmitAction] = useActionState((state, formData) => { | ||||
|     const errMsg = new Map<string, string>(); | ||||
|     const requiredFields = [ | ||||
|       'primary', | ||||
|       'danger', | ||||
|       'success', | ||||
|       'warn', | ||||
|       'info', | ||||
|       'foreground', | ||||
|       'background', | ||||
|     ]; | ||||
|     for (const field of requiredFields) { | ||||
|       if (!formData.get(field)) { | ||||
|         errMsg.set(field, 'This color is required for scheme generating.'); | ||||
|       } | ||||
|     } | ||||
|     if (!isEmpty(errMsg)) return errMsg; | ||||
|     try { | ||||
|       const schemeSetting = new SchemeSetting( | ||||
|         new ColorShifting( | ||||
|           Number(formData.get('hover_chroma')) / 100, | ||||
|           Number(formData.get('hover_lightness')) / 100, | ||||
|         ), | ||||
|         new ColorShifting( | ||||
|           Number(formData.get('active_chroma')) / 100, | ||||
|           Number(formData.get('active_lightness')) / 100, | ||||
|         ), | ||||
|         new ColorShifting( | ||||
|           Number(formData.get('focus_chroma')) / 100, | ||||
|           Number(formData.get('focus_lightness')) / 100, | ||||
|         ), | ||||
|         new ColorShifting( | ||||
|           Number(formData.get('disabled_chroma')) / 100, | ||||
|           Number(formData.get('disabled_lightness')) / 100, | ||||
|         ), | ||||
|         new ColorShifting( | ||||
|           Number(formData.get('dark_chroma')) / 100, | ||||
|           Number(formData.get('dark_lightness')) / 100, | ||||
|         ), | ||||
|         Number(formData.get('expanding')) as ColorExpand, | ||||
|         Number(formData.get('wacg')) as WACGSetting, | ||||
|       ); | ||||
|  | ||||
|       const source: QSchemeSource = { | ||||
|         primary: defaultEmptyFormData(formData, 'primary', null), | ||||
|         secondary: defaultEmptyFormData(formData, 'secondary', undefined), | ||||
|         tertiary: defaultEmptyFormData(formData, 'tertiary', undefined), | ||||
|         accent: defaultEmptyFormData(formData, 'accent', undefined), | ||||
|         danger: defaultEmptyFormData(formData, 'danger', null), | ||||
|         success: defaultEmptyFormData(formData, 'success', null), | ||||
|         warning: defaultEmptyFormData(formData, 'warn', null), | ||||
|         info: defaultEmptyFormData(formData, 'info', null), | ||||
|         foreground: defaultEmptyFormData(formData, 'foreground', null), | ||||
|         background: defaultEmptyFormData(formData, 'background', null), | ||||
|         setting: schemeSetting, | ||||
|       }; | ||||
|       console.debug('[collected]', source); | ||||
|       const generatedScheme = every([source.secondary, source.tertiary, source.accent], isNil) | ||||
|         ? colorFn?.generate_q_scheme_automatically( | ||||
|             source.primary, | ||||
|             source.danger, | ||||
|             source.success, | ||||
|             source.warning, | ||||
|             source.info, | ||||
|             source.foreground, | ||||
|             source.background, | ||||
|             source.setting, | ||||
|           ) | ||||
|         : colorFn?.generate_q_scheme_manually( | ||||
|             source.primary, | ||||
|             source.secondary ?? undefined, | ||||
|             source.tertiary ?? undefined, | ||||
|             source.accent ?? undefined, | ||||
|             source.danger, | ||||
|             source.success, | ||||
|             source.warning, | ||||
|             source.info, | ||||
|             source.foreground, | ||||
|             source.background, | ||||
|             source.setting, | ||||
|           ); | ||||
|       updateScheme((prev) => { | ||||
|         prev.schemeStorage.source = source; | ||||
|         prev.schemeStorage.scheme = generatedScheme[0]; | ||||
|         prev.schemeStorage.cssVariables = generatedScheme[1]; | ||||
|         prev.schemeStorage.scssVariables = generatedScheme[2]; | ||||
|         prev.schemeStorage.jsVariables = generatedScheme[3]; | ||||
|         return prev; | ||||
|       }); | ||||
|       console.debug('[generated]', generatedScheme); | ||||
|     } catch (e) { | ||||
|       console.error('[build q scheme]', e); | ||||
|     } | ||||
|  | ||||
|     return errMsg; | ||||
|   }, new Map<string, string>()); | ||||
|  | ||||
|   return ( | ||||
|     <ScrollArea enableY> | ||||
|       <form action={handleSubmitAction} className={styles.builder_layout}> | ||||
|         <h5 className={styles.segment_title}>Original Colors</h5> | ||||
|         <label className={styles.label}>Primary Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="primary" color={scheme.schemeStorage.source?.primary} /> | ||||
|           {errMsg.has('primary') && ( | ||||
|             <span className={styles.error_msg}>{errMsg.get('primary')}</span> | ||||
|           )} | ||||
|         </div> | ||||
|         <label className={styles.label}>Secondary Color</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="secondary" color={scheme.schemeStorage.source?.secondary} /> | ||||
|         </div> | ||||
|         <label className={styles.label}>Tertiary Color</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="tertiary" color={scheme.schemeStorage.source?.tertiary} /> | ||||
|         </div> | ||||
|         <label className={styles.label}>Accent Color</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="accent" color={scheme.schemeStorage.source?.accent} /> | ||||
|         </div> | ||||
|         <label className={styles.label}>Danger Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="danger" color={scheme.schemeStorage.source?.danger} /> | ||||
|           {errMsg.has('danger') && <span className={styles.error_msg}>{errMsg.get('danger')}</span>} | ||||
|         </div> | ||||
|         <label className={styles.label}>Success Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="success" color={scheme.schemeStorage.source?.success} /> | ||||
|           {errMsg.has('success') && ( | ||||
|             <span className={styles.error_msg}>{errMsg.get('success')}</span> | ||||
|           )} | ||||
|         </div> | ||||
|         <label className={styles.label}>Warning Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="warn" color={scheme.schemeStorage.source?.warning} /> | ||||
|           {errMsg.has('warn') && <span className={styles.error_msg}>{errMsg.get('warn')}</span>} | ||||
|         </div> | ||||
|         <label className={styles.label}>Info Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="info" color={scheme.schemeStorage.source?.info} /> | ||||
|           {errMsg.has('info') && <span className={styles.error_msg}>{errMsg.get('info')}</span>} | ||||
|         </div> | ||||
|         <label className={styles.label}>Foreground Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="foreground" color={scheme.schemeStorage.source?.foreground} /> | ||||
|           {errMsg.has('foreground') && ( | ||||
|             <span className={styles.error_msg}>{errMsg.get('foreground')}</span> | ||||
|           )} | ||||
|         </div> | ||||
|         <label className={styles.label}>Background Color*</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker name="background" color={scheme.schemeStorage.source?.background} /> | ||||
|           {errMsg.has('background') && ( | ||||
|             <span className={styles.error_msg}>{errMsg.get('background')}</span> | ||||
|           )} | ||||
|         </div> | ||||
|         <h5 className={styles.segment_title}>Automated parameters</h5> | ||||
|         <label style={{ gridColumn: 2 }}>Chroma shifting</label> | ||||
|         <label style={{ gridColumn: 3 }}>Lightness shifting</label> | ||||
|         <label className={styles.label}>Hover</label> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="hover_chroma" | ||||
|             defaultValue={((defaultSetting?.hover.chroma ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="hover_lightness" | ||||
|             defaultValue={((defaultSetting?.hover.lightness ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <label className={styles.label}>Active</label> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="active_chroma" | ||||
|             defaultValue={((defaultSetting?.active.chroma ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="active_lightness" | ||||
|             defaultValue={((defaultSetting?.active.lightness ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <label className={styles.label}>Focus</label> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="focus_chroma" | ||||
|             defaultValue={((defaultSetting?.focus.chroma ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="focus_lightness" | ||||
|             defaultValue={((defaultSetting?.focus.lightness ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <label className={styles.label}>Disabled</label> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="disabled_chroma" | ||||
|             defaultValue={((defaultSetting?.disabled.chroma ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="disabled_lightness" | ||||
|             defaultValue={((defaultSetting?.disabled.lightness ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <label className={styles.label}>Convert to Dark scheme</label> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="dark_chroma" | ||||
|             defaultValue={((defaultSetting?.dark_convert.chroma ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <div className="input_wrapper"> | ||||
|           <input | ||||
|             type="number" | ||||
|             name="dark_lightness" | ||||
|             defaultValue={((defaultSetting?.dark_convert.lightness ?? 0) * 100).toFixed(2)} | ||||
|             className={styles.parameter_input} | ||||
|           /> | ||||
|           <span>%</span> | ||||
|         </div> | ||||
|         <h5 className={styles.segment_title}>Settings</h5> | ||||
|         <label className={styles.label}>Color Expanding Method</label> | ||||
|         <div style={{ gridColumn: '2 / span 2' }}> | ||||
|           <VSegmentedControl | ||||
|             options={expandingMethods} | ||||
|             name="expanding" | ||||
|             defaultValue={defaultSetting?.expand_method} | ||||
|           /> | ||||
|         </div> | ||||
|         <label className={styles.label}>Follow WACG Standard</label> | ||||
|         <div style={{ gridColumn: '2 / span 2' }}> | ||||
|           <VSegmentedControl | ||||
|             options={wacgFollowStrategies} | ||||
|             name="wacg" | ||||
|             defaultValue={defaultSetting?.wacg_follows} | ||||
|           /> | ||||
|         </div> | ||||
|         <div style={{ gridColumn: '2 / span 2' }}> | ||||
|           <button type="submit" className="primary"> | ||||
|             Build Scheme | ||||
|           </button> | ||||
|         </div> | ||||
|       </form> | ||||
|     </ScrollArea> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user