色板选择系统
This commit is contained in:
@@ -1,430 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, ChangeEvent, DragEvent, useMemo, useCallback } from 'react';
|
||||
import Script from 'next/script';
|
||||
import InstallPWA from '../components/InstallPWA';
|
||||
import CanvasContainer from '../components/CanvasContainer';
|
||||
|
||||
// 导入像素化工具和类型
|
||||
import {
|
||||
PixelationMode,
|
||||
calculatePixelGrid,
|
||||
PaletteColor,
|
||||
MappedPixel,
|
||||
hexToRgb,
|
||||
} from '../utils/pixelation';
|
||||
|
||||
import {
|
||||
colorSystemOptions,
|
||||
getColorKeyByHex,
|
||||
getMardToHexMapping,
|
||||
sortColorsByHue,
|
||||
ColorSystem
|
||||
} from '../utils/colorSystemUtils';
|
||||
|
||||
// 添加自定义动画样式
|
||||
const floatAnimation = `
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-5px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
// 获取完整色板
|
||||
const mardToHexMapping = getMardToHexMapping();
|
||||
const fullBeadPalette: PaletteColor[] = Object.entries(mardToHexMapping)
|
||||
.map(([, hex]) => {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return null;
|
||||
return { key: hex, hex, rgb };
|
||||
})
|
||||
.filter((item): item is PaletteColor => item !== null);
|
||||
|
||||
export default function Home() {
|
||||
// 基础状态
|
||||
const [originalImageSrc, setOriginalImageSrc] = useState<string | null>(null);
|
||||
const [granularity, setGranularity] = useState<number>(50);
|
||||
const [pixelationMode, setPixelationMode] = useState<PixelationMode>(PixelationMode.Dominant);
|
||||
const [selectedColorSystem, setSelectedColorSystem] = useState<ColorSystem>('MARD');
|
||||
|
||||
// 色板相关
|
||||
const [activeBeadPalette] = useState<PaletteColor[]>(fullBeadPalette);
|
||||
|
||||
// 像素数据
|
||||
const [mappedPixelData, setMappedPixelData] = useState<MappedPixel[][] | null>(null);
|
||||
const [gridDimensions, setGridDimensions] = useState<{ N: number; M: number } | null>(null);
|
||||
const [colorCounts, setColorCounts] = useState<{ [key: string]: { count: number; color: string } } | null>(null);
|
||||
const [totalBeadCount, setTotalBeadCount] = useState<number>(0);
|
||||
|
||||
// UI状态
|
||||
const [isDonationModalOpen, setIsDonationModalOpen] = useState<boolean>(false);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setOriginalImageSrc(e.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拖拽
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setOriginalImageSrc(e.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成像素画
|
||||
const generatePixelArt = useCallback(() => {
|
||||
if (!originalImageSrc) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// 创建临时 canvas 获取图像数据
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 计算网格尺寸
|
||||
const N = Math.round(img.height / granularity);
|
||||
const M = Math.round(img.width / granularity);
|
||||
|
||||
// 获取备用颜色(第一个颜色)
|
||||
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
|
||||
|
||||
// 调用新的 API
|
||||
const mappedPixelData = calculatePixelGrid(
|
||||
ctx,
|
||||
img.width,
|
||||
img.height,
|
||||
N,
|
||||
M,
|
||||
activeBeadPalette,
|
||||
pixelationMode,
|
||||
fallbackColor
|
||||
);
|
||||
|
||||
// 计算颜色统计
|
||||
const colorCounts: { [key: string]: { count: number; color: string } } = {};
|
||||
let totalBeadCount = 0;
|
||||
|
||||
mappedPixelData.forEach(row => {
|
||||
row.forEach(pixel => {
|
||||
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
|
||||
if (!colorCounts[pixel.key]) {
|
||||
colorCounts[pixel.key] = { count: 0, color: pixel.color };
|
||||
}
|
||||
colorCounts[pixel.key].count++;
|
||||
totalBeadCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setMappedPixelData(mappedPixelData);
|
||||
setGridDimensions({ N, M });
|
||||
setColorCounts(colorCounts);
|
||||
setTotalBeadCount(totalBeadCount);
|
||||
setIsProcessing(false);
|
||||
};
|
||||
img.src = originalImageSrc;
|
||||
}, [originalImageSrc, granularity, activeBeadPalette, pixelationMode]);
|
||||
|
||||
// 处理像素网格更新
|
||||
const handlePixelGridUpdate = useCallback((newGrid: MappedPixel[][]) => {
|
||||
setMappedPixelData(newGrid);
|
||||
|
||||
// 重新计算颜色统计
|
||||
const counts: { [key: string]: { count: number; color: string } } = {};
|
||||
let total = 0;
|
||||
|
||||
newGrid.forEach(row => {
|
||||
row.forEach(pixel => {
|
||||
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
|
||||
if (!counts[pixel.key]) {
|
||||
counts[pixel.key] = { count: 0, color: pixel.color };
|
||||
}
|
||||
counts[pixel.key].count++;
|
||||
total++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setColorCounts(counts);
|
||||
setTotalBeadCount(total);
|
||||
}, []);
|
||||
|
||||
// 计算当前色板
|
||||
const currentColorPalette = useMemo(() => {
|
||||
if (!mappedPixelData) return [];
|
||||
|
||||
const uniqueColors = new Set<string>();
|
||||
mappedPixelData.forEach(row => {
|
||||
row.forEach(pixel => {
|
||||
if (pixel.key && pixel.key !== 'transparent' && !pixel.isExternal) {
|
||||
uniqueColors.add(pixel.key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const colorArray = Array.from(uniqueColors).map(key => {
|
||||
const colorInfo = colorCounts?.[key];
|
||||
return {
|
||||
key,
|
||||
hex: colorInfo?.color || key,
|
||||
color: colorInfo?.color || key,
|
||||
name: getColorKeyByHex(key, selectedColorSystem) || key
|
||||
};
|
||||
});
|
||||
|
||||
return sortColorsByHue(colorArray);
|
||||
}, [mappedPixelData, colorCounts, selectedColorSystem]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: floatAnimation }} />
|
||||
<InstallPWA />
|
||||
|
||||
<Script
|
||||
async
|
||||
src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
|
||||
<main ref={mainRef} className="relative min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-black to-gray-900">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-float"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-float" style={{ animationDelay: '2s' }}></div>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-green-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-float" style={{ animationDelay: '4s' }}></div>
|
||||
</div>
|
||||
|
||||
{/* 头部 */}
|
||||
<header className="relative z-10 bg-black/30 backdrop-blur-xl border-b border-white/10">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">
|
||||
拼豆底稿生成器
|
||||
</h1>
|
||||
<span className="text-sm text-gray-400">像素画图纸生成工具</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsDonationModalOpen(true)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-yellow-400 to-orange-400 text-black rounded-lg hover:from-yellow-500 hover:to-orange-500 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
打赏支持
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div className="flex-1 container mx-auto px-4 py-8">
|
||||
{!mappedPixelData ? (
|
||||
// 上传区域
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh]">
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="relative w-full max-w-2xl p-12 bg-white/5 backdrop-blur-lg rounded-2xl border-2 border-dashed border-white/20 hover:border-white/40 transition-all duration-300 cursor-pointer group"
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="text-center">
|
||||
<svg className="w-24 h-24 mx-auto mb-4 text-white/40 group-hover:text-white/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<p className="text-xl text-white/80 mb-2">点击或拖拽图片到这里</p>
|
||||
<p className="text-sm text-white/50">支持 JPG、PNG、GIF 等格式</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{originalImageSrc && (
|
||||
<div className="mt-8 space-y-6 w-full max-w-2xl">
|
||||
{/* 参数设置 */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/80 mb-2">
|
||||
精细度 ({granularity})
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="200"
|
||||
value={granularity}
|
||||
onChange={(e) => {
|
||||
setGranularity(Number(e.target.value));
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/80 mb-2">
|
||||
像素化模式
|
||||
</label>
|
||||
<select
|
||||
value={pixelationMode}
|
||||
onChange={(e) => setPixelationMode(e.target.value as unknown as PixelationMode)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
>
|
||||
<option value={PixelationMode.Average}>平均色(真实模式)</option>
|
||||
<option value={PixelationMode.Dominant}>主导色(卡通模式)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/80 mb-2">
|
||||
色号系统
|
||||
</label>
|
||||
<select
|
||||
value={selectedColorSystem}
|
||||
onChange={(e) => setSelectedColorSystem(e.target.value as ColorSystem)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
>
|
||||
{colorSystemOptions.map(option => (
|
||||
<option key={option.key} value={option.key}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={generatePixelArt}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-4 bg-gradient-to-r from-purple-500 to-blue-500 text-white rounded-xl hover:from-purple-600 hover:to-blue-600 transition-all duration-200 transform hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessing ? '生成中...' : '生成像素画'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 画布容器
|
||||
<div className="h-[calc(100vh-200px)] relative">
|
||||
<CanvasContainer
|
||||
pixelGrid={mappedPixelData}
|
||||
cellSize={10}
|
||||
onPixelGridUpdate={handlePixelGridUpdate}
|
||||
colorPalette={currentColorPalette}
|
||||
selectedColorSystem={selectedColorSystem}
|
||||
/>
|
||||
|
||||
{/* 浮动操作按钮 */}
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMappedPixelData(null);
|
||||
setColorCounts(null);
|
||||
setTotalBeadCount(0);
|
||||
}}
|
||||
className="px-4 py-2 bg-white/90 dark:bg-gray-800/90 text-gray-800 dark:text-white rounded-lg shadow-lg hover:bg-white dark:hover:bg-gray-800 transition-all duration-200"
|
||||
title="重新开始"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<select
|
||||
value={selectedColorSystem}
|
||||
onChange={(e) => setSelectedColorSystem(e.target.value as ColorSystem)}
|
||||
className="px-3 py-2 bg-white/90 dark:bg-gray-800/90 text-gray-800 dark:text-white rounded-lg shadow-lg"
|
||||
>
|
||||
{colorSystemOptions.map(option => (
|
||||
<option key={option.key} value={option.key}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="absolute bottom-4 left-4 bg-white/90 dark:bg-gray-800/90 rounded-lg shadow-lg p-4">
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="font-medium">统计信息</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
尺寸: {gridDimensions?.M} × {gridDimensions?.N}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
总拼豆数: {totalBeadCount}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
颜色数: {currentColorPalette.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 打赏模态框 */}
|
||||
{isDonationModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 max-w-md w-full">
|
||||
<h3 className="text-xl font-bold mb-4">支持作者</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
如果您觉得这个工具有帮助,欢迎打赏支持!
|
||||
</p>
|
||||
<div className="flex justify-center mb-4">
|
||||
{/* 这里可以添加二维码图片 */}
|
||||
<div className="w-48 h-48 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<span className="text-gray-500">打赏二维码</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsDonationModalOpen(false)}
|
||||
className="w-full py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,8 @@ import CelebrationAnimation from '../../components/CelebrationAnimation';
|
||||
import CompletionCard from '../../components/CompletionCard';
|
||||
import PreviewToolbar from '../../components/PreviewToolbar';
|
||||
import EditToolbar from '../../components/EditToolbar';
|
||||
import { getColorKeyByHex, ColorSystem, getMardToHexMapping } from '../../utils/colorSystemUtils';
|
||||
import ColorSystemPanel from '../../components/ColorSystemPanel';
|
||||
import { getColorKeyByHex, ColorSystem, getMardToHexMapping, getAllHexValues } from '../../utils/colorSystemUtils';
|
||||
|
||||
// 定义编辑模式类型
|
||||
type EditMode = 'focus' | 'preview' | 'edit';
|
||||
@@ -74,15 +75,6 @@ interface FocusModeState {
|
||||
editMode: EditMode;
|
||||
}
|
||||
|
||||
// 获取完整色板
|
||||
const mardToHexMapping = getMardToHexMapping();
|
||||
const fullBeadPalette: PaletteColor[] = Object.entries(mardToHexMapping)
|
||||
.map(([, hex]) => {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return null;
|
||||
return { key: hex, hex, rgb };
|
||||
})
|
||||
.filter((item): item is PaletteColor => item !== null);
|
||||
|
||||
export default function FocusMode() {
|
||||
const router = useRouter();
|
||||
@@ -133,6 +125,12 @@ export default function FocusMode() {
|
||||
const [selectedColor, setSelectedColor] = useState<string>('');
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
// 色号系统和点击格子颜色信息的状态
|
||||
const [selectedColorSystem, setSelectedColorSystem] = useState<ColorSystem>('MARD');
|
||||
const [clickedCellColor, setClickedCellColor] = useState<{ hex: string; key: string } | null>(null);
|
||||
const [showColorSystemPanel, setShowColorSystemPanel] = useState(false);
|
||||
const [customPalette, setCustomPalette] = useState<Set<string>>(new Set(getAllHexValues()));
|
||||
|
||||
// 编辑模式状态
|
||||
const [editTool, setEditTool] = useState<'select' | 'wand'>('select');
|
||||
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set());
|
||||
@@ -146,6 +144,26 @@ export default function FocusMode() {
|
||||
const canUndo = historyIndex > 0;
|
||||
const canRedo = historyIndex < history.length - 1;
|
||||
|
||||
// 在客户端加载保存的设置
|
||||
useEffect(() => {
|
||||
// 加载色号系统
|
||||
const savedColorSystem = localStorage.getItem('selectedColorSystem');
|
||||
if (savedColorSystem) {
|
||||
setSelectedColorSystem(savedColorSystem as ColorSystem);
|
||||
}
|
||||
|
||||
// 加载自定义色板
|
||||
const savedPalette = localStorage.getItem('customPalette');
|
||||
if (savedPalette) {
|
||||
try {
|
||||
const palette = new Set<string>(JSON.parse(savedPalette));
|
||||
setCustomPalette(palette);
|
||||
} catch (e) {
|
||||
console.error('Failed to load custom palette:', e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 计时器管理
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
@@ -195,8 +213,17 @@ export default function FocusMode() {
|
||||
const aspectRatio = img.height / img.width;
|
||||
const M = Math.round(N * aspectRatio); // M是纵向(高度),按比例计算
|
||||
|
||||
// 根据自定义色板构建可用颜色
|
||||
const activeBeadPalette: PaletteColor[] = Array.from(customPalette)
|
||||
.map(hex => {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return null;
|
||||
return { key: hex, hex, rgb };
|
||||
})
|
||||
.filter((color): color is PaletteColor => color !== null);
|
||||
|
||||
// 获取备用颜色
|
||||
const fallbackColor = fullBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
|
||||
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
|
||||
|
||||
// 计算像素网格
|
||||
const pixelData = calculatePixelGrid(
|
||||
@@ -205,7 +232,7 @@ export default function FocusMode() {
|
||||
img.height,
|
||||
N,
|
||||
M,
|
||||
fullBeadPalette,
|
||||
activeBeadPalette,
|
||||
PixelationMode.Dominant,
|
||||
fallbackColor
|
||||
);
|
||||
@@ -402,6 +429,15 @@ export default function FocusMode() {
|
||||
if (!mappedPixelData) return;
|
||||
|
||||
const cellColor = mappedPixelData[row][col].color;
|
||||
const pixel = mappedPixelData[row][col];
|
||||
|
||||
// 更新点击的格子颜色信息(所有模式下都执行)
|
||||
if (!pixel.isExternal && pixel.key !== 'transparent') {
|
||||
const colorKey = getColorKeyByHex(cellColor, selectedColorSystem);
|
||||
setClickedCellColor({ hex: cellColor, key: colorKey });
|
||||
} else {
|
||||
setClickedCellColor(null);
|
||||
}
|
||||
|
||||
// 专心模式:标记区域
|
||||
if (focusState.editMode === 'focus' && cellColor === focusState.currentColor) {
|
||||
@@ -529,7 +565,7 @@ export default function FocusMode() {
|
||||
setSelectedCells(newSelection);
|
||||
}
|
||||
}
|
||||
}, [mappedPixelData, focusState, editTool, isSelecting]);
|
||||
}, [mappedPixelData, focusState, editTool, isSelecting, selectedColorSystem]);
|
||||
|
||||
// 处理颜色切换
|
||||
const handleColorChange = useCallback((color: string) => {
|
||||
@@ -672,8 +708,17 @@ export default function FocusMode() {
|
||||
const aspectRatio = img.height / img.width;
|
||||
const M = Math.round(N * aspectRatio); // M是纵向(高度)
|
||||
|
||||
// 根据自定义色板构建可用颜色
|
||||
const activeBeadPalette: PaletteColor[] = Array.from(customPalette)
|
||||
.map(hex => {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return null;
|
||||
return { key: hex, hex, rgb };
|
||||
})
|
||||
.filter((color): color is PaletteColor => color !== null);
|
||||
|
||||
// 获取备用颜色
|
||||
const fallbackColor = fullBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
|
||||
const fallbackColor = activeBeadPalette[0] || { key: '#000000', hex: '#000000', rgb: { r: 0, g: 0, b: 0 } };
|
||||
|
||||
// 计算像素网格
|
||||
const pixelData = calculatePixelGrid(
|
||||
@@ -682,7 +727,7 @@ export default function FocusMode() {
|
||||
img.height,
|
||||
N,
|
||||
M,
|
||||
fullBeadPalette,
|
||||
activeBeadPalette,
|
||||
pixelationMode,
|
||||
fallbackColor
|
||||
);
|
||||
@@ -888,7 +933,7 @@ export default function FocusMode() {
|
||||
|
||||
// 更新颜色列表
|
||||
const colors = Object.entries(counts).map(([, colorData]) => {
|
||||
const displayKey = getColorKeyByHex(colorData.color, 'MARD');
|
||||
const displayKey = getColorKeyByHex(colorData.color, selectedColorSystem);
|
||||
return {
|
||||
color: colorData.color,
|
||||
name: displayKey,
|
||||
@@ -906,7 +951,7 @@ export default function FocusMode() {
|
||||
setIsProcessing(false);
|
||||
};
|
||||
img.src = imageSrc;
|
||||
}, [gridWidth, pixelationMode, colorMergeThreshold, removeBackground, selectedColor]);
|
||||
}, [gridWidth, pixelationMode, colorMergeThreshold, removeBackground, selectedColor, selectedColorSystem, customPalette]);
|
||||
|
||||
// 编辑模式:保存历史记录
|
||||
const saveToHistory = useCallback(() => {
|
||||
@@ -1036,6 +1081,16 @@ export default function FocusMode() {
|
||||
regeneratePixelArt();
|
||||
}
|
||||
}, [removeBackground]); // 只监听 removeBackground 的变化
|
||||
|
||||
// 当色号系统改变时,更新所有颜色的名称
|
||||
useEffect(() => {
|
||||
if (availableColors.length > 0 && !isProcessing) {
|
||||
setAvailableColors(prev => prev.map(color => ({
|
||||
...color,
|
||||
name: getColorKeyByHex(color.color, selectedColorSystem)
|
||||
})));
|
||||
}
|
||||
}, [selectedColorSystem]);
|
||||
|
||||
if (!mappedPixelData || !gridDimensions) {
|
||||
return (
|
||||
@@ -1070,6 +1125,14 @@ export default function FocusMode() {
|
||||
{/* TODO: 添加 Logo */}
|
||||
</div>
|
||||
|
||||
{/* 色板系统选择 */}
|
||||
<button
|
||||
onClick={() => setShowColorSystemPanel(true)}
|
||||
className="text-sm text-blue-600 hover:text-blue-700 font-medium mr-2"
|
||||
>
|
||||
{selectedColorSystem} ▼
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setFocusState(prev => ({ ...prev, showSettingsPanel: true }))}
|
||||
className="p-2 -mr-2 text-gray-600 active:bg-gray-100 rounded-lg"
|
||||
@@ -1081,12 +1144,24 @@ export default function FocusMode() {
|
||||
</header>
|
||||
|
||||
{/* 状态信息栏 - 显示颜色数量和像素尺寸 */}
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-1.5">
|
||||
<div className="bg-gray-50 border-b border-gray-200 px-4 py-1.5 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700 font-mono tracking-wide">
|
||||
<span className="font-medium">{availableColors.length}色</span>
|
||||
<span className="mx-3 text-gray-400">|</span>
|
||||
<span className="font-medium">{mappedPixelData ? `${mappedPixelData[0]?.length || 0}×${mappedPixelData.length}` : '0×0'}</span>
|
||||
</div>
|
||||
{/* 点击格子的颜色信息 */}
|
||||
{clickedCellColor && (
|
||||
<div className="flex items-center gap-2 px-3 py-1 bg-gray-100 rounded-lg">
|
||||
<div
|
||||
className="w-5 h-5 rounded border border-gray-300"
|
||||
style={{ backgroundColor: clickedCellColor.hex }}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{clickedCellColor.key}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 当前颜色状态栏 - 仅在专心模式显示 */}
|
||||
@@ -1243,6 +1318,17 @@ export default function FocusMode() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 色板系统面板 */}
|
||||
{showColorSystemPanel && (
|
||||
<ColorSystemPanel
|
||||
selectedColorSystem={selectedColorSystem}
|
||||
customPalette={customPalette}
|
||||
onColorSystemChange={setSelectedColorSystem}
|
||||
onCustomPaletteChange={setCustomPalette}
|
||||
onClose={() => setShowColorSystemPanel(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 庆祝动画 */}
|
||||
<CelebrationAnimation
|
||||
isVisible={focusState.showCelebration}
|
||||
|
||||
239
src/components/ColorSystemPanel.tsx
Normal file
239
src/components/ColorSystemPanel.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ColorSystem, colorSystemOptions, getAllHexValues, getColorKeyByHex } from '../utils/colorSystemUtils';
|
||||
|
||||
interface ColorSystemPanelProps {
|
||||
selectedColorSystem: ColorSystem;
|
||||
customPalette: Set<string>; // hex values
|
||||
onColorSystemChange: (system: ColorSystem) => void;
|
||||
onCustomPaletteChange: (palette: Set<string>) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ColorSystemPanel: React.FC<ColorSystemPanelProps> = ({
|
||||
selectedColorSystem,
|
||||
customPalette,
|
||||
onColorSystemChange,
|
||||
onCustomPaletteChange,
|
||||
onClose
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'system' | 'custom'>('system');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [tempCustomPalette, setTempCustomPalette] = useState<Set<string>>(new Set(customPalette));
|
||||
|
||||
// 获取所有可用颜色
|
||||
const allColors = getAllHexValues();
|
||||
|
||||
// 过滤颜色
|
||||
const filteredColors = allColors.filter(hex => {
|
||||
const colorKey = getColorKeyByHex(hex, selectedColorSystem);
|
||||
return colorKey.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
hex.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
});
|
||||
|
||||
// 保存自定义色板
|
||||
const handleSaveCustomPalette = () => {
|
||||
onCustomPaletteChange(tempCustomPalette);
|
||||
localStorage.setItem('customPalette', JSON.stringify(Array.from(tempCustomPalette)));
|
||||
};
|
||||
|
||||
// 加载保存的自定义色板
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('customPalette');
|
||||
if (saved) {
|
||||
try {
|
||||
const palette = new Set<string>(JSON.parse(saved));
|
||||
setTempCustomPalette(palette);
|
||||
onCustomPaletteChange(palette);
|
||||
} catch (e) {
|
||||
console.error('Failed to load custom palette:', e);
|
||||
}
|
||||
}
|
||||
}, [onCustomPaletteChange]);
|
||||
|
||||
// 切换颜色选择
|
||||
const toggleColor = (hex: string) => {
|
||||
const newPalette = new Set(tempCustomPalette);
|
||||
if (newPalette.has(hex)) {
|
||||
newPalette.delete(hex);
|
||||
} else {
|
||||
newPalette.add(hex);
|
||||
}
|
||||
setTempCustomPalette(newPalette);
|
||||
};
|
||||
|
||||
// 全选/全不选
|
||||
const handleSelectAll = () => {
|
||||
if (tempCustomPalette.size === allColors.length) {
|
||||
setTempCustomPalette(new Set());
|
||||
} else {
|
||||
setTempCustomPalette(new Set(allColors));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl bg-white rounded-2xl max-h-[90vh] flex flex-col shadow-xl">
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">色板设置</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" 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="flex border-b">
|
||||
<button
|
||||
onClick={() => setActiveTab('system')}
|
||||
className={`flex-1 py-3 px-4 font-medium transition-colors ${
|
||||
activeTab === 'system'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
色号系统
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('custom')}
|
||||
className={`flex-1 py-3 px-4 font-medium transition-colors ${
|
||||
activeTab === 'custom'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
自定义色板
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{activeTab === 'system' ? (
|
||||
/* 色号系统选择 */
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3">选择色号系统</h3>
|
||||
<div className="space-y-2">
|
||||
{colorSystemOptions.map(option => (
|
||||
<label
|
||||
key={option.key}
|
||||
className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="colorSystem"
|
||||
value={option.key}
|
||||
checked={selectedColorSystem === option.key}
|
||||
onChange={() => {
|
||||
onColorSystemChange(option.key as ColorSystem);
|
||||
localStorage.setItem('selectedColorSystem', option.key);
|
||||
}}
|
||||
className="mr-3"
|
||||
/>
|
||||
<span className="font-medium">{option.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
当前选择:<span className="font-medium">{selectedColorSystem}</span>
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
所有颜色显示将使用此色号系统
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* 自定义色板 */
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* 搜索和操作栏 */}
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索颜色编号..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-3 top-2.5 h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
已选择 {tempCustomPalette.size} / {allColors.length} 种颜色
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
{tempCustomPalette.size === allColors.length ? '取消全选' : '全选'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 颜色网格 */}
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 gap-2">
|
||||
{filteredColors.map(hex => {
|
||||
const colorKey = getColorKeyByHex(hex, selectedColorSystem);
|
||||
const isSelected = tempCustomPalette.has(hex);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={hex}
|
||||
onClick={() => toggleColor(hex)}
|
||||
className={`relative p-2 rounded-lg border-2 transition-all ${
|
||||
isSelected
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
title={`${colorKey} - ${hex}`}
|
||||
>
|
||||
<div
|
||||
className="w-full aspect-square rounded mb-1"
|
||||
style={{ backgroundColor: hex }}
|
||||
/>
|
||||
<div className="text-xs text-center font-mono">
|
||||
{colorKey}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className="p-4 border-t bg-gray-50">
|
||||
<button
|
||||
onClick={handleSaveCustomPalette}
|
||||
className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
>
|
||||
保存自定义色板
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorSystemPanel;
|
||||
Reference in New Issue
Block a user