编辑、去除杂色
This commit is contained in:
@@ -31,7 +31,8 @@ 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';
|
||||
import ManualColoringPanel from '../../components/ManualColoringPanel';
|
||||
import { getColorKeyByHex, ColorSystem, getAllHexValues } from '../../utils/colorSystemUtils';
|
||||
import DownloadSettingsModal from '../../components/DownloadSettingsModal';
|
||||
import { downloadImage } from '../../utils/imageDownloader';
|
||||
import { GridDownloadOptions } from '../../types/downloadTypes';
|
||||
@@ -137,7 +138,7 @@ export default function FocusMode() {
|
||||
const clickedColorTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 编辑模式状态
|
||||
const [editTool, setEditTool] = useState<'select' | 'wand'>('select');
|
||||
const [editTool] = useState<'select' | 'wand'>('select');
|
||||
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set());
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [selectionStart, setSelectionStart] = useState<{ row: number; col: number } | null>(null);
|
||||
@@ -149,6 +150,11 @@ export default function FocusMode() {
|
||||
const [canvasPalette, setCanvasPalette] = useState<Set<string>>(new Set());
|
||||
const [removedColors, setRemovedColors] = useState<string[]>([]);
|
||||
|
||||
// 手动上色模式状态
|
||||
const [showManualColoringPanel, setShowManualColoringPanel] = useState(false);
|
||||
const [manualColoringTool, setManualColoringTool] = useState<'brush' | 'eraser'>('brush');
|
||||
const [manualColoringColor, setManualColoringColor] = useState<string>('');
|
||||
|
||||
// 下载相关状态
|
||||
const [showDownloadModal, setShowDownloadModal] = useState(false);
|
||||
const [downloadOptions, setDownloadOptions] = useState<GridDownloadOptions>({
|
||||
@@ -161,9 +167,9 @@ export default function FocusMode() {
|
||||
});
|
||||
|
||||
// 计算状态
|
||||
const hasSelection = selectedCells.size > 0;
|
||||
const canUndo = historyIndex > 0;
|
||||
const canRedo = historyIndex < history.length - 1;
|
||||
// const hasSelection = selectedCells.size > 0;
|
||||
// const canUndo = historyIndex > 0;
|
||||
// const canRedo = historyIndex < history.length - 1;
|
||||
|
||||
// 在客户端加载保存的设置
|
||||
useEffect(() => {
|
||||
@@ -447,6 +453,28 @@ export default function FocusMode() {
|
||||
}));
|
||||
}, [calculateRecommendedRegion]);
|
||||
|
||||
// 编辑模式:保存历史记录
|
||||
const saveToHistory = useCallback(() => {
|
||||
if (!mappedPixelData) return;
|
||||
|
||||
// 如果不在历史末尾,删除后面的历史
|
||||
const newHistory = history.slice(0, historyIndex + 1);
|
||||
|
||||
// 深拷贝当前状态
|
||||
const currentState = mappedPixelData.map(row => row.map(pixel => ({ ...pixel })));
|
||||
|
||||
// 添加到历史
|
||||
newHistory.push(currentState);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (newHistory.length > 50) {
|
||||
newHistory.shift();
|
||||
}
|
||||
|
||||
setHistory(newHistory);
|
||||
setHistoryIndex(newHistory.length - 1);
|
||||
}, [mappedPixelData, history, historyIndex]);
|
||||
|
||||
// 处理格子点击 - 根据不同模式执行不同操作
|
||||
const handleCellClick = useCallback((row: number, col: number) => {
|
||||
if (!mappedPixelData) return;
|
||||
@@ -567,6 +595,52 @@ export default function FocusMode() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动上色模式:直接修改颜色
|
||||
if (showManualColoringPanel) {
|
||||
// 保存历史(内联逻辑)
|
||||
if (mappedPixelData) {
|
||||
// 如果不在历史末尾,删除后面的历史
|
||||
const newHistory = history.slice(0, historyIndex + 1);
|
||||
|
||||
// 深拷贝当前状态
|
||||
const currentState = mappedPixelData.map(row => row.map(pixel => ({ ...pixel })));
|
||||
|
||||
// 添加到历史
|
||||
newHistory.push(currentState);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (newHistory.length > 50) {
|
||||
newHistory.shift();
|
||||
}
|
||||
|
||||
setHistory(newHistory);
|
||||
setHistoryIndex(newHistory.length - 1);
|
||||
}
|
||||
|
||||
const newPixelData = mappedPixelData.map(r => r.map(pixel => ({ ...pixel })));
|
||||
|
||||
if (manualColoringTool === 'brush' && manualColoringColor) {
|
||||
// 画笔模式:使用选中的颜色
|
||||
newPixelData[row][col] = {
|
||||
...newPixelData[row][col],
|
||||
color: manualColoringColor,
|
||||
key: manualColoringColor,
|
||||
isExternal: false
|
||||
};
|
||||
} else if (manualColoringTool === 'eraser') {
|
||||
// 橡皮擦模式:设为透明
|
||||
newPixelData[row][col] = {
|
||||
...newPixelData[row][col],
|
||||
color: 'transparent',
|
||||
key: 'transparent',
|
||||
isExternal: true
|
||||
};
|
||||
}
|
||||
|
||||
setMappedPixelData(newPixelData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑模式:处理选择
|
||||
if (focusState.editMode === 'edit') {
|
||||
if (editTool === 'select') {
|
||||
@@ -603,7 +677,7 @@ export default function FocusMode() {
|
||||
setSelectedCells(newSelection);
|
||||
}
|
||||
}
|
||||
}, [mappedPixelData, focusState, editTool, isSelecting, selectedColorSystem]);
|
||||
}, [mappedPixelData, focusState, editTool, isSelecting, selectedColorSystem, showManualColoringPanel, manualColoringTool, manualColoringColor, saveToHistory]);
|
||||
|
||||
// 处理颜色切换
|
||||
const handleColorChange = useCallback((color: string) => {
|
||||
@@ -993,28 +1067,6 @@ export default function FocusMode() {
|
||||
img.src = imageSrc;
|
||||
}, [gridWidth, pixelationMode, colorMergeThreshold, removeBackground, selectedColor, selectedColorSystem, customPalette]);
|
||||
|
||||
// 编辑模式:保存历史记录
|
||||
const saveToHistory = useCallback(() => {
|
||||
if (!mappedPixelData) return;
|
||||
|
||||
// 如果不在历史末尾,删除后面的历史
|
||||
const newHistory = history.slice(0, historyIndex + 1);
|
||||
|
||||
// 深拷贝当前状态
|
||||
const currentState = mappedPixelData.map(row => row.map(pixel => ({ ...pixel })));
|
||||
|
||||
// 添加到历史
|
||||
newHistory.push(currentState);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (newHistory.length > 50) {
|
||||
newHistory.shift();
|
||||
}
|
||||
|
||||
setHistory(newHistory);
|
||||
setHistoryIndex(newHistory.length - 1);
|
||||
}, [mappedPixelData, history, historyIndex]);
|
||||
|
||||
// 编辑模式:撤销
|
||||
const handleUndo = useCallback(() => {
|
||||
if (historyIndex > 0) {
|
||||
@@ -1063,6 +1115,21 @@ export default function FocusMode() {
|
||||
setShowCanvasColorPanel(true);
|
||||
}, [mappedPixelData, getCanvasColors]);
|
||||
|
||||
// 手动上色:启动模式
|
||||
const handleManualColoring = useCallback(() => {
|
||||
if (!mappedPixelData) return;
|
||||
|
||||
// 获取当前画布颜色
|
||||
const canvasColors = getCanvasColors();
|
||||
|
||||
// 设置默认选中第一个颜色(如果有的话)
|
||||
if (canvasColors.length > 0 && !manualColoringColor) {
|
||||
setManualColoringColor(canvasColors[0].hex);
|
||||
}
|
||||
|
||||
setShowManualColoringPanel(true);
|
||||
}, [mappedPixelData, getCanvasColors, manualColoringColor]);
|
||||
|
||||
// 去杂色:移除颜色
|
||||
const handleRemoveCanvasColor = useCallback((hexToRemove: string) => {
|
||||
if (!mappedPixelData) return;
|
||||
@@ -1679,16 +1746,13 @@ export default function FocusMode() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 编辑模式工具栏 - 仅在非去杂色模式时显示 */}
|
||||
{focusState.editMode === 'edit' && !showCanvasColorPanel && (
|
||||
{/* 编辑模式工具栏 - 仅在非去杂色和非手动上色模式时显示 */}
|
||||
{focusState.editMode === 'edit' && !showCanvasColorPanel && !showManualColoringPanel && (
|
||||
<EditToolbar
|
||||
selectedColor={selectedColor}
|
||||
availableColors={availableColors}
|
||||
onRemoveNoise={handleRemoveNoise}
|
||||
onManualColoring={() => {
|
||||
// TODO: 实现手动上色功能
|
||||
console.log('手动上色功能');
|
||||
}}
|
||||
onManualColoring={handleManualColoring}
|
||||
onColorSelect={setSelectedColor}
|
||||
onShowColorPanel={() => setFocusState(prev => ({ ...prev, showColorPanel: true }))}
|
||||
/>
|
||||
@@ -1706,8 +1770,21 @@ export default function FocusMode() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 底部模式切换栏 - 去杂色模式时隐藏 */}
|
||||
{!showCanvasColorPanel && (
|
||||
{/* 手动上色面板 */}
|
||||
{showManualColoringPanel && (
|
||||
<ManualColoringPanel
|
||||
canvasColors={getCanvasColors()}
|
||||
selectedColorSystem={selectedColorSystem}
|
||||
selectedTool={manualColoringTool}
|
||||
selectedColor={manualColoringColor}
|
||||
onToolChange={setManualColoringTool}
|
||||
onColorSelect={setManualColoringColor}
|
||||
onClose={() => setShowManualColoringPanel(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 底部模式切换栏 - 去杂色和手动上色模式时隐藏 */}
|
||||
{!showCanvasColorPanel && !showManualColoringPanel && (
|
||||
<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">
|
||||
<button
|
||||
|
||||
@@ -8,12 +8,12 @@ interface ColorInfo {
|
||||
}
|
||||
|
||||
interface EditToolbarProps {
|
||||
selectedColor: string;
|
||||
availableColors: ColorInfo[];
|
||||
selectedColor?: string;
|
||||
availableColors?: ColorInfo[];
|
||||
onRemoveNoise: () => void;
|
||||
onManualColoring: () => void;
|
||||
onColorSelect: (color: string) => void;
|
||||
onShowColorPanel: () => void;
|
||||
onColorSelect?: (color: string) => void;
|
||||
onShowColorPanel?: () => void;
|
||||
}
|
||||
|
||||
const EditToolbar: React.FC<EditToolbarProps> = ({
|
||||
|
||||
114
src/components/ManualColoringPanel.tsx
Normal file
114
src/components/ManualColoringPanel.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { getColorKeyByHex, ColorSystem } from '../utils/colorSystemUtils';
|
||||
|
||||
interface CanvasColorInfo {
|
||||
hex: string;
|
||||
key: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ManualColoringPanelProps {
|
||||
canvasColors: CanvasColorInfo[];
|
||||
selectedColorSystem: ColorSystem;
|
||||
selectedTool: 'brush' | 'eraser';
|
||||
selectedColor: string;
|
||||
onToolChange: (tool: 'brush' | 'eraser') => void;
|
||||
onColorSelect: (color: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ManualColoringPanel: React.FC<ManualColoringPanelProps> = ({
|
||||
canvasColors,
|
||||
selectedColorSystem,
|
||||
selectedTool,
|
||||
selectedColor,
|
||||
onToolChange,
|
||||
onColorSelect,
|
||||
onClose
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-0 bg-white border-t border-gray-200 shadow-lg z-40 flex flex-col">
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<h3 className="text-sm font-medium text-gray-800">手动上色</h3>
|
||||
</div>
|
||||
|
||||
{/* 工具选择 */}
|
||||
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => onToolChange('brush')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
|
||||
selectedTool === 'brush'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
<button
|
||||
onClick={() => onToolChange('eraser')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
|
||||
selectedTool === 'eraser'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 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>
|
||||
|
||||
{/* 颜色选择区域 - 仅在画笔模式下显示 */}
|
||||
{selectedTool === 'brush' && (
|
||||
<div className="px-3 py-2 max-h-32 overflow-y-auto">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{canvasColors.map(color => {
|
||||
const isSelected = selectedColor === color.hex;
|
||||
return (
|
||||
<button
|
||||
key={color.hex}
|
||||
onClick={() => onColorSelect(color.hex)}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs transition-all ${
|
||||
isSelected
|
||||
? 'bg-blue-100 border-blue-300 border-2'
|
||||
: 'bg-gray-50 border border-gray-200 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{/* 颜色块 */}
|
||||
<div
|
||||
className="w-3 h-3 rounded border border-gray-300"
|
||||
style={{ backgroundColor: color.hex }}
|
||||
/>
|
||||
|
||||
{/* 色号 */}
|
||||
<span className="font-mono text-gray-700">
|
||||
{getColorKeyByHex(color.hex, selectedColorSystem)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManualColoringPanel;
|
||||
@@ -13,9 +13,9 @@ interface PreviewToolbarProps {
|
||||
colorMergeThreshold: number;
|
||||
pixelationMode: PixelationMode;
|
||||
removeBackground: boolean;
|
||||
availableColors: ColorInfo[];
|
||||
mappedPixelData: MappedPixel[][] | null;
|
||||
isProcessing: boolean;
|
||||
availableColors?: ColorInfo[];
|
||||
mappedPixelData?: MappedPixel[][] | null;
|
||||
isProcessing?: boolean;
|
||||
onGridWidthChange: (width: number) => void;
|
||||
onColorMergeThresholdChange: (threshold: number) => void;
|
||||
onPixelationModeChange: (mode: PixelationMode) => void;
|
||||
|
||||
Reference in New Issue
Block a user