新增颜色替换功能,支持选择源颜色和目标颜色的替换逻辑,更新调色板组件以集成颜色替换状态和交互,提升用户体验和代码可读性。

This commit is contained in:
zihanjian
2025-05-26 14:53:42 +08:00
parent 06a379ea9f
commit a1ea5dfffd
2 changed files with 281 additions and 2 deletions

View File

@@ -147,6 +147,16 @@ export default function Home() {
// 新增:完整色板切换状态
const [showFullPalette, setShowFullPalette] = useState<boolean>(false);
// 新增:颜色替换相关状态
const [colorReplaceState, setColorReplaceState] = useState<{
isActive: boolean;
step: 'select-source' | 'select-target';
sourceColor?: { key: string; color: string };
}>({
isActive: false,
step: 'select-source'
});
const originalCanvasRef = useRef<HTMLCanvasElement>(null);
const pixelatedCanvasRef = useRef<HTMLCanvasElement>(null);
@@ -337,6 +347,15 @@ export default function Home() {
return;
}
// 如果当前在颜色替换模式,先退出替换模式
if (colorReplaceState.isActive) {
setColorReplaceState({
isActive: false,
step: 'select-source'
});
setHighlightColorKey(null);
}
setIsEraseMode(!isEraseMode);
// 如果开启擦除模式,取消选中的颜色
if (!isEraseMode) {
@@ -913,6 +932,19 @@ export default function Home() {
if (i >= 0 && i < N && j >= 0 && j < M) {
const cellData = mappedPixelData[j][i];
// 颜色替换模式逻辑 - 选择源颜色
if (isClick && colorReplaceState.isActive && colorReplaceState.step === 'select-source') {
if (cellData && !cellData.isExternal && cellData.key && cellData.key !== TRANSPARENT_KEY) {
// 执行选择源颜色
handleCanvasColorSelect({
key: cellData.key,
color: cellData.color
});
setTooltipData(null);
}
return;
}
// 一键擦除模式逻辑
if (isClick && isEraseMode) {
if (cellData && !cellData.isExternal && cellData.key && cellData.key !== TRANSPARENT_KEY) {
@@ -1197,6 +1229,127 @@ export default function Home() {
setShowFullPalette(!showFullPalette);
};
// 新增:处理颜色选择,同时管理模式切换
const handleColorSelect = (colorData: { key: string; color: string; isExternal?: boolean }) => {
// 如果选择的是橡皮擦(透明色)且当前在颜色替换模式,退出替换模式
if (colorData.key === TRANSPARENT_KEY && colorReplaceState.isActive) {
setColorReplaceState({
isActive: false,
step: 'select-source'
});
setHighlightColorKey(null);
}
// 选择任何颜色(包括橡皮擦)时,都应该退出一键擦除模式
if (isEraseMode) {
setIsEraseMode(false);
}
// 设置选中的颜色
setSelectedColor(colorData);
};
// 新增:颜色替换相关处理函数
const handleColorReplaceToggle = () => {
setColorReplaceState(prev => {
if (prev.isActive) {
// 退出替换模式
return {
isActive: false,
step: 'select-source'
};
} else {
// 进入替换模式
// 只退出冲突的模式,但保持在手动上色模式下
setIsEraseMode(false);
setSelectedColor(null);
return {
isActive: true,
step: 'select-source'
};
}
});
};
// 新增:处理从画布选择源颜色
const handleCanvasColorSelect = (colorData: { key: string; color: string }) => {
if (colorReplaceState.isActive && colorReplaceState.step === 'select-source') {
// 高亮显示选中的颜色
setHighlightColorKey(colorData.color);
// 进入第二步:选择目标颜色
setColorReplaceState({
isActive: true,
step: 'select-target',
sourceColor: colorData
});
}
};
// 新增:执行颜色替换
const handleColorReplace = (sourceColor: { key: string; color: string }, targetColor: { key: string; color: string }) => {
if (!mappedPixelData || !gridDimensions) return;
const { N, M } = gridDimensions;
const newPixelData = mappedPixelData.map(row => row.map(cell => ({ ...cell })));
let replaceCount = 0;
// 遍历所有像素,替换匹配的颜色
for (let j = 0; j < M; j++) {
for (let i = 0; i < N; i++) {
const currentCell = newPixelData[j][i];
if (currentCell && !currentCell.isExternal &&
currentCell.color.toUpperCase() === sourceColor.color.toUpperCase()) {
// 替换颜色
newPixelData[j][i] = {
key: targetColor.key,
color: targetColor.color,
isExternal: false
};
replaceCount++;
}
}
}
if (replaceCount > 0) {
// 更新像素数据
setMappedPixelData(newPixelData);
// 重新计算颜色统计
if (colorCounts) {
const newColorCounts: { [hexKey: string]: { count: number; color: string } } = {};
let newTotalCount = 0;
newPixelData.flat().forEach(cell => {
if (cell && !cell.isExternal && cell.key !== TRANSPARENT_KEY) {
const cellHex = cell.color.toUpperCase();
if (!newColorCounts[cellHex]) {
newColorCounts[cellHex] = {
count: 0,
color: cellHex
};
}
newColorCounts[cellHex].count++;
newTotalCount++;
}
});
setColorCounts(newColorCounts);
setTotalBeadCount(newTotalCount);
}
console.log(`颜色替换完成:将 ${replaceCount}${sourceColor.key} 替换为 ${targetColor.key}`);
}
// 退出替换模式
setColorReplaceState({
isActive: false,
step: 'select-source'
});
// 清除高亮
setHighlightColorKey(null);
};
// 生成完整色板数据(用户自定义色板中选中的所有颜色)
const fullPaletteColors = useMemo(() => {
const selectedColors: { key: string; color: string }[] = [];
@@ -1568,7 +1721,7 @@ export default function Home() {
<ColorPalette
colors={[transparentColorData, ...currentGridColors]}
selectedColor={selectedColor}
onColorSelect={setSelectedColor}
onColorSelect={handleColorSelect}
transparentKey={TRANSPARENT_KEY}
selectedColorSystem={selectedColorSystem}
isEraseMode={isEraseMode}
@@ -1577,6 +1730,9 @@ export default function Home() {
fullPaletteColors={fullPaletteColors}
showFullPalette={showFullPalette}
onToggleFullPalette={handleToggleFullPalette}
colorReplaceState={colorReplaceState}
onColorReplaceToggle={handleColorReplaceToggle}
onColorReplace={handleColorReplace}
/>
</div>
</div>

View File

@@ -10,6 +10,13 @@ interface ColorData {
isExternal?: boolean; // 添加 isExternal 属性以支持透明/橡皮擦功能
}
// 新增:颜色替换相关接口
interface ColorReplaceState {
isActive: boolean;
step: 'select-source' | 'select-target'; // 替换步骤:选择源颜色 | 选择目标颜色
sourceColor?: ColorData; // 被替换的颜色
}
interface ColorPaletteProps {
colors: ColorData[];
selectedColor: ColorData | null;
@@ -25,6 +32,10 @@ interface ColorPaletteProps {
fullPaletteColors?: ColorData[]; // 用户自定义色板中的所有颜色
showFullPalette?: boolean; // 是否显示完整色板
onToggleFullPalette?: () => void; // 切换完整色板显示
// 新增颜色替换相关props
colorReplaceState?: ColorReplaceState; // 颜色替换状态
onColorReplaceToggle?: () => void; // 切换颜色替换模式
onColorReplace?: (sourceColor: ColorData, targetColor: ColorData) => void; // 执行颜色替换
}
const ColorPalette: React.FC<ColorPaletteProps> = ({
@@ -38,7 +49,10 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
onHighlightColor,
fullPaletteColors,
showFullPalette,
onToggleFullPalette
onToggleFullPalette,
colorReplaceState,
onColorReplaceToggle,
onColorReplace
}) => {
if (!colors || colors.length === 0) {
// Apply dark mode text color
@@ -84,6 +98,81 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
</div>
)}
{/* 颜色替换状态提示 */}
{colorReplaceState?.isActive && (
<div className="p-3 border-b border-purple-100 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
<span className="text-sm font-medium text-purple-700 dark:text-purple-300"></span>
</div>
{colorReplaceState.step === 'select-source' ? (
<div className="text-xs text-purple-600 dark:text-purple-400">
<p className="mb-1"> 1/2</p>
<p className="text-gray-500 dark:text-gray-400"></p>
</div>
) : (
<div className="text-xs text-purple-600 dark:text-purple-400">
<p className="mb-1"> 2/2</p>
<div className="flex items-center justify-center gap-2 mt-2">
<span className="text-gray-500 dark:text-gray-400"></span>
<div className="flex items-center gap-1">
<span
className="inline-block w-4 h-4 rounded border border-gray-400 dark:border-gray-500"
style={{ backgroundColor: colorReplaceState.sourceColor?.color }}
></span>
<span className="font-mono text-xs">
{selectedColorSystem ? getDisplayColorKey(colorReplaceState.sourceColor?.color || '', selectedColorSystem) : colorReplaceState.sourceColor?.key}
</span>
</div>
</div>
</div>
)}
</div>
</div>
)}
{/* 一键擦除状态提示 */}
{isEraseMode && (
<div className="p-3 border-b border-orange-100 dark:border-orange-800 bg-orange-50 dark:bg-orange-900/20">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 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>
<span className="text-sm font-medium text-orange-700 dark:text-orange-300"></span>
</div>
<div className="text-xs text-orange-600 dark:text-orange-400">
<p className="mb-1"></p>
<p className="text-gray-500 dark:text-gray-400">使</p>
</div>
</div>
</div>
)}
{/* 橡皮擦选中状态提示 */}
{selectedColor?.key === transparentKey && !isEraseMode && !colorReplaceState?.isActive && (
<div className="p-3 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-600 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>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300"></span>
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
<p className="mb-1"></p>
<p className="text-gray-500 dark:text-gray-400"></p>
</div>
</div>
</div>
)}
{/* 颜色按钮区域 */}
<div className="flex flex-wrap justify-center gap-2 p-2">
{/* 一键擦除按钮 */}
@@ -111,6 +200,31 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
</button>
)}
{/* 颜色替换按钮 */}
{onColorReplaceToggle && (
<button
onClick={onColorReplaceToggle}
className={`w-12 h-12 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 ${
colorReplaceState?.isActive
? 'border-purple-500 bg-purple-100 dark:bg-purple-900 ring-2 ring-offset-1 ring-purple-400 dark:ring-purple-500 scale-110 shadow-md'
: 'border-purple-300 dark:border-purple-600 bg-purple-100 dark:bg-purple-800 hover:border-purple-500 dark:hover:border-purple-400'
}`}
title={colorReplaceState?.isActive ? '退出颜色替换模式' : '颜色替换 (将图中A颜色全部替换为B颜色)'}
aria-label={colorReplaceState?.isActive ? '退出颜色替换模式' : '开启颜色替换模式'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-5 w-5 ${colorReplaceState?.isActive ? 'text-purple-600 dark:text-purple-400' : 'text-purple-600 dark:text-purple-400'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
)}
{colorsToShow.map((colorData) => {
// 检查当前颜色是否是透明/橡皮擦
const isTransparent = transparentKey && colorData.key === transparentKey;
@@ -137,7 +251,16 @@ const ColorPalette: React.FC<ColorPaletteProps> = ({
<button
key={colorData.key}
onClick={() => {
// 颜色替换模式下的特殊处理
if (colorReplaceState?.isActive && colorReplaceState.step === 'select-target' && !isTransparent && onColorReplace && colorReplaceState.sourceColor) {
// 步骤2选择目标颜色并执行替换
onColorReplace(colorReplaceState.sourceColor, colorData);
return;
}
// 正常的颜色选择逻辑
onColorSelect(colorData);
// 如果不是透明颜色且有高亮回调,触发高亮效果
if (!isTransparent && onHighlightColor) {
onHighlightColor(colorData.color);