为专心模式添加去杂色功能,新增画布颜色面板以管理颜色,优化颜色移除和恢复逻辑,提升用户操作体验。

This commit is contained in:
zihanjian
2025-06-27 16:42:14 +08:00
parent a3cf833c65
commit 6da2997a02
3 changed files with 320 additions and 151 deletions

View File

@@ -30,6 +30,7 @@ import CompletionCard from '../../components/CompletionCard';
import PreviewToolbar from '../../components/PreviewToolbar';
import EditToolbar from '../../components/EditToolbar';
import ColorSystemPanel from '../../components/ColorSystemPanel';
import CanvasColorPanel from '../../components/CanvasColorPanel';
import { getColorKeyByHex, ColorSystem, getMardToHexMapping, getAllHexValues } from '../../utils/colorSystemUtils';
// 定义编辑模式类型
@@ -140,6 +141,11 @@ export default function FocusMode() {
const [history, setHistory] = useState<MappedPixel[][][]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
// 去杂色模式状态
const [showCanvasColorPanel, setShowCanvasColorPanel] = useState(false);
const [canvasPalette, setCanvasPalette] = useState<Set<string>>(new Set());
const [removedColors, setRemovedColors] = useState<string[]>([]);
// 计算状态
const hasSelection = selectedCells.size > 0;
const canUndo = historyIndex > 0;
@@ -1011,6 +1017,148 @@ export default function FocusMode() {
}
}, [history, historyIndex]);
// 获取画布中的所有颜色信息
const getCanvasColors = useCallback(() => {
if (!mappedPixelData) return [];
const colorCounts: { [hex: string]: number } = {};
mappedPixelData.forEach(row => {
row.forEach(pixel => {
if (pixel.color && pixel.color !== 'transparent' && !pixel.isExternal) {
colorCounts[pixel.color] = (colorCounts[pixel.color] || 0) + 1;
}
});
});
return Object.entries(colorCounts).map(([hex, count]) => ({
hex,
key: getColorKeyByHex(hex, selectedColorSystem),
count
}));
}, [mappedPixelData, selectedColorSystem]);
// 去杂色:启动模式
const handleRemoveNoise = useCallback(() => {
if (!mappedPixelData) return;
// 获取当前画布颜色
const canvasColors = getCanvasColors();
setCanvasPalette(new Set(canvasColors.map(c => c.hex)));
setRemovedColors([]); // 重置已移除颜色列表
setShowCanvasColorPanel(true);
}, [mappedPixelData, getCanvasColors]);
// 去杂色:移除颜色
const handleRemoveCanvasColor = useCallback((hexToRemove: string) => {
if (!mappedPixelData) return;
// 更新画布色板
const newCanvasPalette = new Set(canvasPalette);
newCanvasPalette.delete(hexToRemove);
setCanvasPalette(newCanvasPalette);
// 添加到已移除颜色列表
setRemovedColors(prev => [...prev, hexToRemove]);
// 保存历史
saveToHistory();
// 获取剩余的颜色
const remainingColors = Array.from(newCanvasPalette);
if (remainingColors.length === 0) {
console.warn('无法移除所有颜色');
return;
}
// 创建新的像素数据
const newPixelData = mappedPixelData.map(row =>
row.map(pixel => {
if (pixel.color === hexToRemove && !pixel.isExternal) {
// 找到最接近的替换颜色
let closestColor = remainingColors[0];
let minDistance = Infinity;
const removedRgb = hexToRgb(hexToRemove);
if (removedRgb) {
remainingColors.forEach(hex => {
const rgb = hexToRgb(hex);
if (rgb) {
const distance = colorDistance(removedRgb, rgb);
if (distance < minDistance) {
minDistance = distance;
closestColor = hex;
}
}
});
}
return {
...pixel,
color: closestColor,
key: closestColor
};
}
return pixel;
})
);
setMappedPixelData(newPixelData);
}, [mappedPixelData, canvasPalette, saveToHistory]);
// 去杂色:恢复颜色
const handleRestoreCanvasColor = useCallback((hexToRestore: string) => {
if (!mappedPixelData) return;
// 从已移除列表中移除
setRemovedColors(prev => prev.filter(hex => hex !== hexToRestore));
// 添加回画布色板
const newCanvasPalette = new Set(canvasPalette);
newCanvasPalette.add(hexToRestore);
setCanvasPalette(newCanvasPalette);
// 保存历史
saveToHistory();
// 重新计算整个画布的颜色映射
const allAvailableColors = Array.from(newCanvasPalette);
const newPixelData = mappedPixelData.map(row =>
row.map(pixel => {
if (!pixel.isExternal && pixel.color !== 'transparent') {
// 为每个像素找到最接近的可用颜色
let closestColor = allAvailableColors[0];
let minDistance = Infinity;
const pixelRgb = hexToRgb(pixel.color);
if (pixelRgb) {
allAvailableColors.forEach(hex => {
const rgb = hexToRgb(hex);
if (rgb) {
const distance = colorDistance(pixelRgb, rgb);
if (distance < minDistance) {
minDistance = distance;
closestColor = hex;
}
}
});
}
return {
...pixel,
color: closestColor,
key: closestColor
};
}
return pixel;
})
);
setMappedPixelData(newPixelData);
}, [mappedPixelData, canvasPalette, saveToHistory]);
// 编辑模式:执行操作
const handleEditOperation = useCallback((operation: 'fill' | 'clear' | 'invert') => {
if (!mappedPixelData) return;
@@ -1274,22 +1422,30 @@ export default function FocusMode() {
{/* 编辑模式工具栏 */}
{focusState.editMode === 'edit' && (
<EditToolbar
editTool={editTool}
hasSelection={hasSelection}
canUndo={canUndo}
canRedo={canRedo}
selectedColor={selectedColor}
availableColors={availableColors}
selectedCells={selectedCells}
onEditToolChange={setEditTool}
onEditOperation={handleEditOperation}
onUndo={handleUndo}
onRedo={handleRedo}
onRemoveNoise={handleRemoveNoise}
onManualColoring={() => {
// TODO: 实现手动上色功能
console.log('手动上色功能');
}}
onColorSelect={setSelectedColor}
onShowColorPanel={() => setFocusState(prev => ({ ...prev, showColorPanel: true }))}
/>
)}
{/* 画布颜色面板 - 去杂色模式 */}
{showCanvasColorPanel && (
<CanvasColorPanel
canvasColors={getCanvasColors()}
removedColors={removedColors}
selectedColorSystem={selectedColorSystem}
onColorRemove={handleRemoveCanvasColor}
onColorRestore={handleRestoreCanvasColor}
onClose={() => setShowCanvasColorPanel(false)}
/>
)}
{/* 底部模式切换栏 */}
<div className="bg-white border-t border-gray-200 px-4 py-3">
<div className="flex bg-gray-100 rounded-lg p-1 max-w-md mx-auto">

View File

@@ -0,0 +1,129 @@
import React from 'react';
import { getColorKeyByHex, ColorSystem } from '../utils/colorSystemUtils';
interface CanvasColorInfo {
hex: string;
key: string;
count: number;
}
interface CanvasColorPanelProps {
canvasColors: CanvasColorInfo[];
removedColors: string[];
selectedColorSystem: ColorSystem;
onColorRemove: (hex: string) => void;
onColorRestore: (hex: string) => void;
onClose: () => void;
}
const CanvasColorPanel: React.FC<CanvasColorPanelProps> = ({
canvasColors,
removedColors,
selectedColorSystem,
onColorRemove,
onColorRestore,
onClose
}) => {
// 按数量从少到多排序
const sortedColors = [...canvasColors].sort((a, b) => a.count - b.count);
return (
<div className="bg-white/95 backdrop-blur-lg border-t border-gray-200/30 px-4 py-3 max-h-64 overflow-y-auto">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-700"></h3>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded-lg transition-colors"
>
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="grid grid-cols-1 gap-2">
{/* 当前画布颜色 */}
{sortedColors.map(color => (
<button
key={color.hex}
onClick={() => onColorRemove(color.hex)}
className="flex items-center gap-3 p-2 hover:bg-red-50/50 rounded-lg transition-all duration-200 active:bg-red-100/60 group"
>
{/* 颜色块 */}
<div
className="w-6 h-6 rounded-lg border border-gray-200 flex-shrink-0"
style={{ backgroundColor: color.hex }}
/>
{/* 色号名称 */}
<span className="text-sm font-mono text-gray-700 min-w-[3rem]">
{color.key}
</span>
{/* 数量 */}
<span className="text-xs text-gray-500 ml-auto">
{color.count}
</span>
{/* 删除图标 */}
<svg className="w-4 h-4 text-gray-400 group-hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
))}
{/* 已移除颜色区域 */}
{removedColors.length > 0 && (
<>
<div className="border-t border-gray-300/50 pt-3 mt-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-gray-500"></span>
<div className="flex-1 h-px bg-gray-200"></div>
</div>
</div>
{removedColors.map(hex => {
const key = getColorKeyByHex(hex, selectedColorSystem);
return (
<button
key={hex}
onClick={() => onColorRestore(hex)}
className="flex items-center gap-3 p-2 bg-gray-50/80 hover:bg-green-50/50 rounded-lg transition-all duration-200 active:bg-green-100/60 group opacity-60 hover:opacity-100"
>
{/* 颜色块 */}
<div
className="w-6 h-6 rounded-lg border border-gray-300 flex-shrink-0 relative"
style={{ backgroundColor: hex }}
>
{/* 删除线标识 */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-full h-0.5 bg-red-400 rotate-45"></div>
</div>
</div>
{/* 色号名称 */}
<span className="text-sm font-mono text-gray-500 min-w-[3rem] line-through">
{key}
</span>
{/* 恢复图标 */}
<svg className="w-4 h-4 text-gray-400 group-hover:text-green-500 transition-colors opacity-0 group-hover:opacity-100 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
</button>
);
})}
</>
)}
</div>
{sortedColors.length === 0 && removedColors.length === 0 && (
<div className="text-center py-4 text-gray-500 text-sm">
</div>
)}
</div>
);
};
export default CanvasColorPanel;

View File

@@ -8,162 +8,46 @@ interface ColorInfo {
}
interface EditToolbarProps {
editTool: 'select' | 'wand';
hasSelection: boolean;
canUndo: boolean;
canRedo: boolean;
selectedColor: string;
availableColors: ColorInfo[];
selectedCells: Set<string>;
onEditToolChange: (tool: 'select' | 'wand') => void;
onEditOperation: (operation: 'fill' | 'clear' | 'invert') => void;
onUndo: () => void;
onRedo: () => void;
onRemoveNoise: () => void;
onManualColoring: () => void;
onColorSelect: (color: string) => void;
onShowColorPanel: () => void;
}
const EditToolbar: React.FC<EditToolbarProps> = ({
editTool,
hasSelection,
canUndo,
canRedo,
selectedColor,
availableColors,
selectedCells,
onEditToolChange,
onEditOperation,
onUndo,
onRedo,
onRemoveNoise,
onManualColoring,
onColorSelect,
onShowColorPanel
}) => {
return (
<div className="bg-white border-t border-gray-200 px-3 py-2">
<div className="flex items-center gap-3">
{/* 选择工具区域 */}
<div className="flex flex-col items-center gap-0.5">
<span className="text-[9px] text-gray-400"></span>
<div className="flex bg-gray-100 rounded p-0.5">
<button
onClick={() => onEditToolChange('select')}
className={`p-1 rounded ${editTool === 'select' ? 'bg-white shadow-sm' : ''}`}
title="矩形选择"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
</svg>
</button>
<button
onClick={() => onEditToolChange('wand')}
className={`p-1 rounded ${editTool === 'wand' ? 'bg-white shadow-sm' : ''}`}
title="魔棒选择"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
</div>
</div>
{/* 编辑操作区域 */}
<div className="flex flex-col items-center gap-0.5">
<span className="text-[9px] text-gray-400"></span>
<div className="flex gap-0.5">
<button
onClick={() => onEditOperation('fill')}
className={`p-1 rounded ${!hasSelection ? 'opacity-50' : 'active:bg-gray-100'}`}
disabled={!hasSelection}
title="填充"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21V5a2 2 0 012-2h11l-5 7h4l-5.5 8" />
</svg>
</button>
<button
onClick={() => onEditOperation('clear')}
className={`p-1 rounded ${!hasSelection ? 'opacity-50' : 'active:bg-gray-100'}`}
disabled={!hasSelection}
title="清除"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
<button
onClick={() => onEditOperation('invert')}
className="p-1 rounded active:bg-gray-100"
title="反选"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
</div>
</div>
{/* 历史记录区域 */}
<div className="flex flex-col items-center gap-0.5">
<span className="text-[9px] text-gray-400"></span>
<div className="flex gap-0.5">
<button
onClick={onUndo}
className={`p-1 rounded ${!canUndo ? 'opacity-50' : 'active:bg-gray-100'}`}
disabled={!canUndo}
title="撤销"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
</button>
<button
onClick={onRedo}
className={`p-1 rounded ${!canRedo ? 'opacity-50' : 'active:bg-gray-100'}`}
disabled={!canRedo}
title="重做"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10h-10a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" />
</svg>
</button>
</div>
</div>
<div className="bg-white/90 backdrop-blur-lg px-4 py-3">
<div className="flex items-center justify-center gap-4 max-w-sm mx-auto">
{/* 去杂色功能 */}
<button
onClick={onRemoveNoise}
className="flex items-center gap-2 px-4 py-2 rounded-xl transition-all duration-200 hover:bg-blue-50/50 active:bg-blue-100/60 active:shadow-inner active:shadow-blue-200/40"
>
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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-gray-700"></span>
</button>
<div className="h-6 w-px bg-gray-200"></div>
{/* 颜色选择区域 */}
<div className="flex flex-col gap-0.5 flex-1">
<span className="text-[9px] text-gray-400"></span>
<div className="flex gap-0.5 overflow-x-auto">
{availableColors.slice(0, 10).map(color => (
<button
key={color.color}
onClick={() => onColorSelect(color.color)}
className={`w-5 h-5 rounded border flex-shrink-0 ${
selectedColor === color.color ? 'border-blue-500 border-2' : 'border-gray-300'
}`}
style={{ backgroundColor: color.color }}
/>
))}
{availableColors.length > 10 && (
<button
onClick={onShowColorPanel}
className="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-gray-500"
style={{ fontSize: '10px' }}
>
+
</button>
)}
</div>
</div>
{/* 选择状态区域 */}
<div className="flex flex-col items-center gap-0.5">
<span className="text-[9px] text-gray-400"></span>
<div className="text-[10px] text-gray-600">
{hasSelection ? `${selectedCells.size}` : '未选'}
</div>
</div>
{/* 手动上色功能 */}
<button
onClick={onManualColoring}
className="flex items-center gap-2 px-4 py-2 rounded-xl transition-all duration-200 hover:bg-purple-50/50 active:bg-purple-100/60 active:shadow-inner active:shadow-purple-200/40"
>
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
<span className="text-sm font-medium text-gray-700"></span>
</button>
</div>
</div>
);