新增完整色板切换功能,优化颜色排序逻辑,支持按色相排序,更新相关组件以提升用户体验和代码可读性。

This commit is contained in:
zihanjian
2025-05-25 12:30:07 +08:00
parent f8ea1a94af
commit 734851296d
3 changed files with 200 additions and 72 deletions

View File

@@ -25,6 +25,7 @@ import {
convertPaletteToColorSystem,
getColorKeyByHex,
getMardToHexMapping,
sortColorsByHue,
ColorSystem
} from '../utils/colorSystemUtils';
@@ -144,6 +145,9 @@ export default function Home() {
// 新增:高亮相关状态
const [highlightColorKey, setHighlightColorKey] = useState<string | null>(null);
// 新增:完整色板切换状态
const [showFullPalette, setShowFullPalette] = useState<boolean>(false);
const originalCanvasRef = useRef<HTMLCanvasElement>(null);
const pixelatedCanvasRef = useRef<HTMLCanvasElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -196,23 +200,16 @@ export default function Home() {
// 转换为数组并为每个hex值生成对应的色号系统显示
const originalColors = Array.from(uniqueColorsMap.values());
return originalColors.map(color => {
const colorData = originalColors.map(color => {
const displayKey = getColorKeyByHex(color.color.toUpperCase(), selectedColorSystem);
return {
key: displayKey,
color: color.color
};
}).sort((a, b) => {
// 对显示的色号进行排序
if (selectedColorSystem === 'MARD') {
return sortColorKeys(a.key, b.key);
} else {
// 对于数字色号系统,按数字排序
const aNum = parseInt(a.key) || 0;
const bNum = parseInt(b.key) || 0;
return aNum - bNum;
}
});
// 使用色相排序而不是色号排序
return sortColorsByHue(colorData);
}, [mappedPixelData, selectedColorSystem]);
// 初始化时从本地存储加载自定义色板选择
@@ -1195,6 +1192,30 @@ export default function Home() {
setHighlightColorKey(null);
};
// 新增:切换完整色板显示
const handleToggleFullPalette = () => {
setShowFullPalette(!showFullPalette);
};
// 生成完整色板数据(用户自定义色板中选中的所有颜色)
const fullPaletteColors = useMemo(() => {
const selectedColors: { key: string; color: string }[] = [];
Object.entries(customPaletteSelections).forEach(([hexValue, isSelected]) => {
if (isSelected) {
// 根据选择的色号系统获取显示的色号
const displayKey = getColorKeyByHex(hexValue, selectedColorSystem);
selectedColors.push({
key: displayKey,
color: hexValue
});
}
});
// 使用色相排序而不是色号排序
return sortColorsByHue(selectedColors);
}, [customPaletteSelections, selectedColorSystem]);
return (
<>
{/* 添加自定义动画样式 */}
@@ -1553,6 +1574,9 @@ export default function Home() {
isEraseMode={isEraseMode}
onEraseToggle={handleEraseToggle}
onHighlightColor={handleHighlightColor}
fullPaletteColors={fullPaletteColors}
showFullPalette={showFullPalette}
onToggleFullPalette={handleToggleFullPalette}
/>
</div>
</div>

View File

@@ -21,6 +21,10 @@ interface ColorPaletteProps {
onEraseToggle?: () => void;
// 新增高亮相关props
onHighlightColor?: (colorHex: string) => void; // 触发高亮某个颜色
// 新增完整色板相关props
fullPaletteColors?: ColorData[]; // 用户自定义色板中的所有颜色
showFullPalette?: boolean; // 是否显示完整色板
onToggleFullPalette?: () => void; // 切换完整色板显示
}
const ColorPalette: React.FC<ColorPaletteProps> = ({
@@ -31,79 +35,121 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
selectedColorSystem,
isEraseMode,
onEraseToggle,
onHighlightColor
onHighlightColor,
fullPaletteColors,
showFullPalette,
onToggleFullPalette
}) => {
if (!colors || colors.length === 0) {
// Apply dark mode text color
return <p className="text-xs text-center text-gray-500 dark:text-gray-400 py-2"></p>;
}
// 确定要显示的颜色集合
// 如果显示完整色板,需要过滤掉透明颜色(因为完整色板不包含透明色)
const colorsToShow = showFullPalette && fullPaletteColors
? [colors.find(c => transparentKey && c.key === transparentKey), ...fullPaletteColors].filter(Boolean) as ColorData[]
: colors;
return (
// Apply dark mode styles to the container
<div className="flex flex-wrap justify-center gap-2 p-2 bg-white dark:bg-gray-900 rounded border border-blue-200 dark:border-gray-700">
{/* 一键擦除按钮 */}
{onEraseToggle && (
<button
onClick={onEraseToggle}
className={`w-8 h-8 rounded border-2 flex-shrink-0 transition-transform transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-400 dark:focus:ring-blue-500 flex items-center justify-center ${
isEraseMode
? 'border-red-500 bg-red-100 dark:bg-red-900 ring-2 ring-offset-1 ring-red-400 dark:ring-red-500 scale-110 shadow-md'
: 'border-orange-300 dark:border-orange-600 bg-orange-100 dark:bg-orange-800 hover:border-orange-500 dark:hover:border-orange-400'
}`}
title={isEraseMode ? '退出一键擦除模式' : '一键擦除 (洪水填充删除相同颜色)'}
aria-label={isEraseMode ? '退出一键擦除模式' : '开启一键擦除模式'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-5 w-5 ${isEraseMode ? 'text-red-600 dark:text-red-400' : 'text-orange-600 dark:text-orange-400'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
{colors.map((colorData) => {
// 检查当前颜色是否是透明/橡皮擦
const isTransparent = transparentKey && colorData.key === transparentKey;
const isSelected = selectedColor?.key === colorData.key;
return (
<div className="bg-white dark:bg-gray-900 rounded border border-blue-200 dark:border-gray-700">
{/* 色板切换按钮区域 */}
{fullPaletteColors && fullPaletteColors.length > 0 && onToggleFullPalette && (
<div className="flex justify-center p-2 border-b border-blue-100 dark:border-gray-700">
<button
key={colorData.key}
onClick={() => {
onColorSelect(colorData);
// 如果不是透明颜色且有高亮回调,触发高亮效果
if (!isTransparent && onHighlightColor) {
onHighlightColor(colorData.color);
}
}}
className={`w-8 h-8 rounded border-2 flex-shrink-0 transition-transform transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-400 dark:focus:ring-blue-500 ${
isSelected
// Apply dark mode styles for selected state
? 'border-black dark:border-gray-100 ring-2 ring-offset-1 ring-blue-400 dark:ring-blue-500 scale-110 shadow-md'
// Apply dark mode styles for default/hover state
: 'border-gray-300 dark:border-gray-600 hover:border-gray-500 dark:hover:border-gray-400'
} ${isTransparent ? 'flex items-center justify-center bg-gray-100 dark:bg-gray-700' : ''}`} // Add background for transparent button
style={isTransparent ? {} : { backgroundColor: colorData.color }}
title={isTransparent
? '选择橡皮擦 (清除单元格)'
: `选择 ${selectedColorSystem ? getDisplayColorKey(colorData.key, selectedColorSystem) : colorData.key} (${colorData.color})`}
aria-label={isTransparent ? '选择橡皮擦' : `选择颜色 ${selectedColorSystem ? getDisplayColorKey(colorData.key, selectedColorSystem) : colorData.key}`}
onClick={onToggleFullPalette}
className={`px-3 py-1.5 text-xs rounded-md transition-all duration-200 flex items-center gap-1.5 ${
showFullPalette
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-600'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{/* 如果是透明/橡皮擦按钮,显示叉号图标 */}
{isTransparent && (
// Apply dark mode icon color
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{showFullPalette ? (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z" />
</svg>
({fullPaletteColors.length} )
</>
)}
</button>
);
})}
</div>
)}
{/* 颜色按钮区域 */}
<div className="flex flex-wrap justify-center gap-2 p-2">
{/* 一键擦除按钮 */}
{onEraseToggle && (
<button
onClick={onEraseToggle}
className={`w-8 h-8 rounded border-2 flex-shrink-0 transition-transform transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-400 dark:focus:ring-blue-500 flex items-center justify-center ${
isEraseMode
? 'border-red-500 bg-red-100 dark:bg-red-900 ring-2 ring-offset-1 ring-red-400 dark:ring-red-500 scale-110 shadow-md'
: 'border-orange-300 dark:border-orange-600 bg-orange-100 dark:bg-orange-800 hover:border-orange-500 dark:hover:border-orange-400'
}`}
title={isEraseMode ? '退出一键擦除模式' : '一键擦除 (洪水填充删除相同颜色)'}
aria-label={isEraseMode ? '退出一键擦除模式' : '开启一键擦除模式'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-5 w-5 ${isEraseMode ? 'text-red-600 dark:text-red-400' : 'text-orange-600 dark:text-orange-400'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
{colorsToShow.map((colorData) => {
// 检查当前颜色是否是透明/橡皮擦
const isTransparent = transparentKey && colorData.key === transparentKey;
const isSelected = selectedColor?.key === colorData.key;
return (
<button
key={colorData.key}
onClick={() => {
onColorSelect(colorData);
// 如果不是透明颜色且有高亮回调,触发高亮效果
if (!isTransparent && onHighlightColor) {
onHighlightColor(colorData.color);
}
}}
className={`w-8 h-8 rounded border-2 flex-shrink-0 transition-transform transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-400 dark:focus:ring-blue-500 ${
isSelected
// Apply dark mode styles for selected state
? 'border-black dark:border-gray-100 ring-2 ring-offset-1 ring-blue-400 dark:ring-blue-500 scale-110 shadow-md'
// Apply dark mode styles for default/hover state
: 'border-gray-300 dark:border-gray-600 hover:border-gray-500 dark:hover:border-gray-400'
} ${isTransparent ? 'flex items-center justify-center bg-gray-100 dark:bg-gray-700' : ''}`} // Add background for transparent button
style={isTransparent ? {} : { backgroundColor: colorData.color }}
title={isTransparent
? '选择橡皮擦 (清除单元格)'
: `选择 ${selectedColorSystem ? getDisplayColorKey(colorData.key, selectedColorSystem) : colorData.key} (${colorData.color})`}
aria-label={isTransparent ? '选择橡皮擦' : `选择颜色 ${selectedColorSystem ? getDisplayColorKey(colorData.key, selectedColorSystem) : colorData.key}`}
>
{/* 如果是透明/橡皮擦按钮,显示叉号图标 */}
{isTransparent && (
// Apply dark mode icon color
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</button>
);
})}
</div>
</div>
);
};

View File

@@ -115,4 +115,62 @@ export function getColorKeyByHex(hexValue: string, colorSystem: ColorSystem): st
// 如果找不到映射,返回 '?'
return '?';
}
// 将hex颜色转换为HSL
function hexToHsl(hex: string): { h: number; s: number; l: number } {
// 移除 # 符号
const cleanHex = hex.replace('#', '');
// 转换为RGB
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (diff !== 0) {
s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
switch (max) {
case r:
h = ((g - b) / diff + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / diff + 2) / 6;
break;
case b:
h = ((r - g) / diff + 4) / 6;
break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
// 按色相排序颜色
export function sortColorsByHue<T extends { color: string }>(colors: T[]): T[] {
return colors.slice().sort((a, b) => {
const hslA = hexToHsl(a.color);
const hslB = hexToHsl(b.color);
// 首先按色相排序
if (Math.abs(hslA.h - hslB.h) > 5) { // 增加色相容差,让更相近的色相归为一组
return hslA.h - hslB.h;
}
// 色相相近时,按明度排序(从浅到深)
if (Math.abs(hslA.l - hslB.l) > 3) {
return hslB.l - hslA.l; // 浅色(高明度)在前,深色(低明度)在后
}
// 明度也相近时,按饱和度排序(高饱和度在前,让鲜艳的颜色更突出)
return hslB.s - hslA.s;
});
}