diff --git a/src/app/page.tsx b/src/app/page.tsx index dd657e5..44eab6f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -102,6 +102,10 @@ const transparentColorData: MappedPixel = { key: TRANSPARENT_KEY, color: '#FFFFF // ++ Add definition for background color keys ++ const BACKGROUND_COLOR_KEYS = ['T1', 'H1', 'H2']; // 可以根据需要调整 +// 1. 导入新组件 +import PixelatedPreviewCanvas from '../components/PixelatedPreviewCanvas'; +import GridTooltip from '../components/GridTooltip'; + export default function Home() { const [originalImageSrc, setOriginalImageSrc] = useState(null); const [granularity, setGranularity] = useState(50); @@ -136,6 +140,9 @@ export default function Home() { const touchStartPosRef = useRef<{ x: number; y: number; pageX: number; pageY: number } | null>(null); const touchMovedRef = useRef(false); + // ++ Add a ref for the main element ++ + const mainRef = useRef(null); + // --- Derived State --- // Update active palette based on selection and exclusions @@ -320,6 +327,17 @@ export default function Home() { console.log("Using fallback color for empty cells:", t1FallbackColor); const img = new window.Image(); + + img.onerror = (error: Event | string) => { + console.error("Image loading failed:", error); + alert("无法加载图片。"); + setOriginalImageSrc(null); + setMappedPixelData(null); + setGridDimensions(null); + setColorCounts(null); + setInitialGridColorKeys(null); + }; + img.onload = () => { console.log("Image loaded successfully."); const aspectRatio = img.height / img.width; @@ -471,42 +489,36 @@ export default function Home() { // --- 绘制和状态更新 --- if (pixelatedCanvasRef.current) { - drawPixelatedCanvas(mergedData, pixelatedCanvasRef, { N, M }); + setMappedPixelData(mergedData); + setGridDimensions({ N, M }); + + const counts: { [key: string]: { count: number; color: string } } = {}; + let totalCount = 0; + mergedData.flat().forEach(cell => { + if (cell && cell.key && !cell.isExternal) { + if (!counts[cell.key]) { + counts[cell.key] = { count: 0, color: cell.color }; + } + counts[cell.key].count++; + totalCount++; + } + }); + setColorCounts(counts); + setTotalBeadCount(totalCount); + setInitialGridColorKeys(new Set(Object.keys(counts))); + console.log("Color counts updated based on merged data (excluding external background):", counts); + console.log("Total bead count (excluding background):", totalCount); + console.log("Stored initial grid color keys:", Object.keys(counts)); } else { console.error("Pixelated canvas ref is null, skipping draw call in pixelateImage."); } - - setMappedPixelData(mergedData); - setGridDimensions({ N, M }); - - const counts: { [key: string]: { count: number; color: string } } = {}; - let totalCount = 0; - mergedData.flat().forEach(cell => { - if (cell && cell.key && !cell.isExternal) { - if (!counts[cell.key]) { - counts[cell.key] = { count: 0, color: cell.color }; - } - counts[cell.key].count++; - totalCount++; - } - }); - setColorCounts(counts); - setTotalBeadCount(totalCount); - setInitialGridColorKeys(new Set(Object.keys(counts))); - console.log("Color counts updated based on merged data (excluding external background):", counts); - console.log("Total bead count (excluding background):", totalCount); - console.log("Stored initial grid color keys:", Object.keys(counts)); - }; - - img.onerror = (error: Event | string) => { - console.error("Image loading failed:", error); alert("无法加载图片。"); - setOriginalImageSrc(null); setMappedPixelData(null); setGridDimensions(null); setColorCounts(null); setInitialGridColorKeys(null); // ++ 清空初始键 ++ - }; + }; // 正确闭合 img.onload 函数 + console.log("Setting image source..."); img.src = imageSrc; setIsManualColoringMode(false); setSelectedColor(null); - }; + }; // 正确闭合 pixelateImage 函数 // 修改useEffect中的pixelateImage调用,加入模式参数 useEffect(() => { @@ -737,7 +749,8 @@ export default function Home() { // ++ 在更新状态后,重新绘制 Canvas ++ if (pixelatedCanvasRef.current && gridDimensions) { // ++ 添加检查 ++ - drawPixelatedCanvas(newMappedData, pixelatedCanvasRef, gridDimensions); + setMappedPixelData(newMappedData); + // 不要调用 setGridDimensions,因为颜色排除不需要改变网格尺寸 } else { console.error("Canvas ref or grid dimensions missing, skipping draw call in handleToggleExcludeColor."); } @@ -883,65 +896,23 @@ export default function Home() { touchMovedRef.current = false; // Reset move flag }; - // ++ 新增:绘制像素化 Canvas 的函数 ++ - const drawPixelatedCanvas = ( - dataToDraw: MappedPixel[][], // ++ Update type here ++ - canvasRef: React.RefObject, // ++ 修改类型定义 ++ - dims: { N: number; M: number } | null - ) => { - const canvas = canvasRef.current; // canvas 现在可能是 null - if (!canvas || !dims || dims.N <= 0 || dims.M <= 0) { // 这里的 !canvas 检查会处理 null 情况 - console.warn("无法绘制 Canvas:Ref 为 null、尺寸无效或数据未准备好。"); - // Optionally clear canvas if dimensions are invalid? - const ctx = canvas?.getContext('2d'); // 使用 optional chaining - if (ctx && canvas) ctx.clearRect(0, 0, canvas.width, canvas.height); - return; - } - // 从这里开始,我们知道 canvas 不是 null - const pixelatedCtx = canvas.getContext('2d'); - if (!pixelatedCtx) { - console.error("无法获取 Pixelated Canvas Context。"); - return; - } - - const { N, M } = dims; - const outputWidth = canvas.width; // Use actual canvas size - const outputHeight = canvas.height; - const cellWidthOutput = outputWidth / N; - const cellHeightOutput = outputHeight / M; - - console.log("Redrawing pixelated canvas..."); - pixelatedCtx.clearRect(0, 0, outputWidth, outputHeight); // 清除旧内容 - pixelatedCtx.lineWidth = 1; // 设置线宽 - - for (let j = 0; j < M; j++) { - for (let i = 0; i < N; i++) { - const cellData = dataToDraw[j]?.[i]; // Use optional chaining for safety - if (!cellData) continue; // Skip if cell data is missing - - const drawX = i * cellWidthOutput; - const drawY = j * cellHeightOutput; - - // 填充单元格背景 - if (cellData.isExternal) { - pixelatedCtx.fillStyle = '#F3F4F6'; // 外部单元格的预览背景色 - } else { - pixelatedCtx.fillStyle = cellData.color; // 内部单元格的珠子颜色 - } - pixelatedCtx.fillRect(drawX, drawY, cellWidthOutput, cellHeightOutput); - - // 绘制所有单元格的边框 - pixelatedCtx.strokeStyle = '#EEEEEE'; // 网格线颜色 - pixelatedCtx.strokeRect(drawX + 0.5, drawY + 0.5, cellWidthOutput, cellHeightOutput); - } - } - console.log("Pixelated canvas redraw complete."); - }; - // --- Canvas Interaction --- // ++ Re-introduce the combined interaction handler ++ - const handleCanvasInteraction = (clientX: number, clientY: number, pageX: number, pageY: number, isClick: boolean = false) => { + const handleCanvasInteraction = ( + clientX: number, + clientY: number, + pageX: number, + pageY: number, + isClick: boolean = false, + isTouchEnd: boolean = false + ) => { + // 如果是触摸结束或鼠标离开事件,隐藏提示 + if (isTouchEnd) { + setTooltipData(null); + return; + } + const canvas = pixelatedCanvasRef.current; if (!canvas || !mappedPixelData || !gridDimensions) { setTooltipData(null); @@ -964,32 +935,27 @@ export default function Home() { if (i >= 0 && i < N && j >= 0 && j < M) { const cellData = mappedPixelData[j][i]; - // Manual Coloring Logic + // Manual Coloring Logic - 保持原有的上色逻辑 if (isClick && isManualColoringMode && selectedColor) { - // ++ Allow clicking on ANY cell (internal or external) ++ - // Create a deep copy to ensure state update triggers re-render + // 手动上色模式逻辑保持不变 + // ...现有代码... const newPixelData = mappedPixelData.map(row => row.map(cell => ({ ...cell }))); - const currentCell = newPixelData[j]?.[i]; // Get current cell data safely + const currentCell = newPixelData[j]?.[i]; - // Check if the color needs changing OR if it was an external cell being colored - if (!currentCell) return; // Prevent invalid cells + if (!currentCell) return; const previousKey = currentCell.key; const wasExternal = currentCell.isExternal; - // Determine new cell data let newCellData: MappedPixel; - // Check if using eraser if (selectedColor.key === TRANSPARENT_KEY) { - // Erasing: Mark as external newCellData = { ...transparentColorData }; } else { - // Normal coloring: Apply selected color and mark as internal newCellData = { ...selectedColor, isExternal: false }; } - // Only update if state actually changes + // Only update if state changes if (newCellData.key !== previousKey || newCellData.isExternal !== wasExternal) { newPixelData[j][i] = newCellData; setMappedPixelData(newPixelData); @@ -999,16 +965,14 @@ export default function Home() { const newColorCounts = { ...colorCounts }; let newTotalCount = totalBeadCount; - // If previous was internal bead, decrement its count if (!wasExternal && previousKey !== TRANSPARENT_KEY && newColorCounts[previousKey]) { newColorCounts[previousKey].count--; if (newColorCounts[previousKey].count <= 0) { - delete newColorCounts[previousKey]; // Remove if count reaches zero + delete newColorCounts[previousKey]; } newTotalCount--; } - // If new is internal bead, increment its count if (!newCellData.isExternal && newCellData.key !== TRANSPARENT_KEY) { if (!newColorCounts[newCellData.key]) { const colorInfo = fullBeadPalette.find(p => p.key === newCellData.key); @@ -1024,30 +988,72 @@ export default function Home() { setColorCounts(newColorCounts); setTotalBeadCount(newTotalCount); } - - // Immediately redraw canvas - if (pixelatedCanvasRef.current) { - drawPixelatedCanvas(newPixelData, pixelatedCanvasRef, gridDimensions); - } } - // Clear tooltip after click + + // 上色操作后隐藏提示 setTooltipData(null); } - // Tooltip Logic (only show if NOT a manual coloring click) - else if (!isClick || !isManualColoringMode) { - if (cellData && !cellData.isExternal && cellData.key) { - setTooltipData({ - x: pageX, - y: pageY, - key: cellData.key, - color: cellData.color, - }); - } else { - setTooltipData(null); // Hide tooltip if on background or invalid cell - } + // Tooltip Logic (非手动上色模式点击或悬停) + else if (!isManualColoringMode) { + // 只有单元格实际有内容(非背景/外部区域)才会显示提示 + if (cellData && !cellData.isExternal && cellData.key) { + // 检查是否已经显示了提示框,并且是否点击的是同一个位置 + // 对于移动设备,位置可能有细微偏差,所以我们检查单元格索引而不是具体坐标 + if (tooltipData) { + // 如果已经有提示框,计算当前提示框对应的格子的索引 + const tooltipRect = canvas.getBoundingClientRect(); + + // 还原提示框位置为相对于canvas的坐标 + const prevX = tooltipData.x; // 页面X坐标 + const prevY = tooltipData.y; // 页面Y坐标 + + // 转换为相对于canvas的坐标 + const prevCanvasX = (prevX - tooltipRect.left) * scaleX; + const prevCanvasY = (prevY - tooltipRect.top) * scaleY; + + // 计算之前显示提示框位置对应的网格索引 + const prevCellI = Math.floor(prevCanvasX / cellWidthOutput); + const prevCellJ = Math.floor(prevCanvasY / cellHeightOutput); + + // 如果点击的是同一个格子,则切换tooltip的显示/隐藏状态 + if (i === prevCellI && j === prevCellJ) { + setTooltipData(null); // 隐藏提示 + return; + } + } + + // 计算相对于main元素的位置 + const mainElement = mainRef.current; + if (mainElement) { + const mainRect = mainElement.getBoundingClientRect(); + // 计算相对于main元素的坐标 + const relativeX = pageX - mainRect.left - window.scrollX; + const relativeY = pageY - mainRect.top - window.scrollY; + + // 如果是移动/悬停到一个新的有效格子,或者点击了不同的格子,则显示提示 + setTooltipData({ + x: relativeX, + y: relativeY, + key: cellData.key, + color: cellData.color, + }); + } else { + // 如果没有找到main元素,使用原始坐标 + setTooltipData({ + x: pageX, + y: pageY, + key: cellData.key, + color: cellData.color, + }); + } + } else { + // 如果点击/悬停在外部区域或背景上,隐藏提示 + setTooltipData(null); + } } } else { - setTooltipData(null); // Hide if outside bounds + // 如果点击/悬停在画布外部,隐藏提示 + setTooltipData(null); } }; @@ -1103,7 +1109,7 @@ export default function Home() {

上传图片,选择色板,生成带色号的图纸和统计

-
{/* 添加 relative 定位 */} +
{/* 添加 relative 定位 */} {/* Drop Zone */}
-
@@ -1368,21 +1370,7 @@ export default function Home() { {/* Tooltip Display (remains the same) */} {tooltipData && ( -
- - {tooltipData.key} -
+ )} {/* Cleaned up the previously moved/commented out block */} diff --git a/src/components/GridTooltip.tsx b/src/components/GridTooltip.tsx new file mode 100644 index 0000000..cd0ef16 --- /dev/null +++ b/src/components/GridTooltip.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface TooltipData { + x: number; + y: number; + key: string; + color: string; +} + +interface GridTooltipProps { + tooltipData: TooltipData | null; +} + +const GridTooltip: React.FC = ({ tooltipData }) => { + if (!tooltipData) return null; + + return ( +
+ + {tooltipData.key} +
+ ); +}; + +export default GridTooltip; \ No newline at end of file diff --git a/src/components/PixelatedPreviewCanvas.tsx b/src/components/PixelatedPreviewCanvas.tsx new file mode 100644 index 0000000..3ad6cdd --- /dev/null +++ b/src/components/PixelatedPreviewCanvas.tsx @@ -0,0 +1,187 @@ +'use client'; + +import React, { useRef, useEffect, TouchEvent, MouseEvent } from 'react'; +import { MappedPixel } from '../utils/pixelation'; + +interface PixelatedPreviewCanvasProps { + mappedPixelData: MappedPixel[][] | null; + gridDimensions: { N: number; M: number } | null; + isManualColoringMode: boolean; + selectedColor: MappedPixel | null; + canvasRef: React.RefObject; + onInteraction: ( + clientX: number, + clientY: number, + pageX: number, + pageY: number, + isClick: boolean, + isTouchEnd?: boolean + ) => void; +} + +// 绘制像素化画布的函数 +const drawPixelatedCanvas = ( + dataToDraw: MappedPixel[][], + canvas: HTMLCanvasElement | null, + dims: { N: number; M: number } | null +) => { + if (!canvas || !dims || dims.N <= 0 || dims.M <= 0) { + console.warn("无法绘制Canvas:参数无效或数据未准备好"); + const ctx = canvas?.getContext('2d'); + if (ctx && canvas) ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + } + + const pixelatedCtx = canvas.getContext('2d'); + if (!pixelatedCtx) { + console.error("无法获取Canvas绘图上下文"); + return; + } + + const { N, M } = dims; + const outputWidth = canvas.width; + const outputHeight = canvas.height; + const cellWidthOutput = outputWidth / N; + const cellHeightOutput = outputHeight / M; + + pixelatedCtx.clearRect(0, 0, outputWidth, outputHeight); + pixelatedCtx.lineWidth = 0.5; // 减小网格线宽度以获得更清晰的视觉效果 + + for (let j = 0; j < M; j++) { + for (let i = 0; i < N; i++) { + const cellData = dataToDraw[j]?.[i]; + if (!cellData) continue; + + const drawX = i * cellWidthOutput; + const drawY = j * cellHeightOutput; + + // 填充单元格颜色 + if (cellData.isExternal) { + pixelatedCtx.fillStyle = '#F3F4F6'; // 外部区域使用浅灰色 + } else { + pixelatedCtx.fillStyle = cellData.color; + } + pixelatedCtx.fillRect(drawX, drawY, cellWidthOutput, cellHeightOutput); + + // 绘制网格线 + pixelatedCtx.strokeStyle = '#DDDDDD'; + pixelatedCtx.strokeRect(drawX + 0.5, drawY + 0.5, cellWidthOutput, cellHeightOutput); + } + } +}; + +const PixelatedPreviewCanvas: React.FC = ({ + mappedPixelData, + gridDimensions, + isManualColoringMode, + selectedColor, + canvasRef, + onInteraction, +}) => { + // 当数据变化时重绘画布 + useEffect(() => { + if (mappedPixelData && gridDimensions && canvasRef.current) { + drawPixelatedCanvas(mappedPixelData, canvasRef.current, gridDimensions); + } + }, [mappedPixelData, gridDimensions, canvasRef]); + + // --- 鼠标事件处理 --- + + // 鼠标移动时显示提示 + const handleMouseMove = (event: MouseEvent) => { + onInteraction(event.clientX, event.clientY, event.pageX, event.pageY, false); + }; + + // 鼠标离开时隐藏提示 + const handleMouseLeave = () => { + onInteraction(0, 0, 0, 0, false, true); + }; + + // 鼠标点击处理(用于手动上色模式) + const handleClick = (event: MouseEvent) => { + if (isManualColoringMode) { + onInteraction(event.clientX, event.clientY, event.pageX, event.pageY, true); + } else { + // 在非手动上色模式下,鼠标点击也会切换 Tooltip 的显示状态 + // 主要用于笔记本电脑或带鼠标的平板,主要显示/隐藏逻辑在 page.tsx 处理 + onInteraction(event.clientX, event.clientY, event.pageX, event.pageY, false); + } + }; + + // --- 触摸事件处理 --- + // 用于检测触摸移动的参考 + const touchStartPosRef = useRef<{ x: number; y: number; pageX: number; pageY: number } | null>(null); + const touchMovedRef = useRef(false); + + // 触摸开始时立即显示提示,无需长按 + const handleTouchStart = (event: TouchEvent) => { + const touch = event.touches[0]; + if (!touch) return; + + // 记录起始位置以检测移动 + touchStartPosRef.current = { + x: touch.clientX, + y: touch.clientY, + pageX: touch.pageX, + pageY: touch.pageY + }; + touchMovedRef.current = false; + + // 如果是手动上色模式,立即执行上色操作 + if (isManualColoringMode) { + onInteraction(touch.clientX, touch.clientY, touch.pageX, touch.pageY, true); + } else { + // 如果不是手动上色模式,触发交互以显示/隐藏提示 + // 参数 isClick = false 仅表示这是 Tooltip 相关交互,非着色操作 + // 实际的显示/隐藏/切换逻辑放在 page.tsx 的 handleCanvasInteraction 处理 + onInteraction(touch.clientX, touch.clientY, touch.pageX, touch.pageY, false); + } + }; + + // 触摸移动时检测是否需要隐藏提示 + const handleTouchMove = (event: TouchEvent) => { + const touch = event.touches[0]; + if (!touch || !touchStartPosRef.current) return; + + // 检测触摸是否移动了足够的距离 + const dx = Math.abs(touch.clientX - touchStartPosRef.current.x); + const dy = Math.abs(touch.clientY - touchStartPosRef.current.y); + + if (dx > 5 || dy > 5) { + touchMovedRef.current = true; + // 如果移动了,隐藏提示 + onInteraction(0, 0, 0, 0, false, true); + } + }; + + // 触摸结束时不再自动隐藏提示框 + const handleTouchEnd = () => { + // 不再隐藏提示框,让用户可以查看提示内容 + // 只重置触摸状态 + touchStartPosRef.current = null; + touchMovedRef.current = false; + }; + + return ( + + ); +}; + +export default PixelatedPreviewCanvas; \ No newline at end of file