新增像素化预览画布组件和网格提示组件,优化图像加载错误处理逻辑,确保用户在加载失败时能获得友好的提示,同时重构画布交互逻辑以提升代码可读性和维护性。
This commit is contained in:
280
src/app/page.tsx
280
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<string | null>(null);
|
||||
const [granularity, setGranularity] = useState<number>(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<boolean>(false);
|
||||
|
||||
// ++ Add a ref for the main element ++
|
||||
const mainRef = useRef<HTMLElement>(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<HTMLCanvasElement | null>, // ++ 修改类型定义 ++
|
||||
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() {
|
||||
<p className="mt-2 text-sm sm:text-base text-gray-600">上传图片,选择色板,生成带色号的图纸和统计</p>
|
||||
</header>
|
||||
|
||||
<main className="w-full max-w-4xl flex flex-col items-center space-y-5 sm:space-y-6 relative"> {/* 添加 relative 定位 */}
|
||||
<main ref={mainRef} className="w-full max-w-4xl flex flex-col items-center space-y-5 sm:space-y-6 relative"> {/* 添加 relative 定位 */}
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
onDrop={handleDrop} onDragOver={handleDragOver} onDragEnter={handleDragOver}
|
||||
@@ -1235,17 +1241,13 @@ export default function Home() {
|
||||
<div className="bg-white p-3 sm:p-4 rounded-lg shadow">
|
||||
<div className="flex justify-center mb-3 sm:mb-4 bg-gray-100 p-2 rounded overflow-hidden"
|
||||
style={{ minHeight: '150px' }}>
|
||||
<canvas
|
||||
ref={pixelatedCanvasRef}
|
||||
onMouseMove={handleCanvasMouseMove}
|
||||
onMouseLeave={handleCanvasMouseLeave}
|
||||
onClick={handleCanvasClick}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
className={`border border-gray-300 max-w-full h-auto rounded block ${isManualColoringMode ? 'cursor-pointer' : 'cursor-crosshair'}`}
|
||||
style={{ maxHeight: '60vh', imageRendering: 'pixelated' }}
|
||||
<PixelatedPreviewCanvas
|
||||
canvasRef={pixelatedCanvasRef}
|
||||
mappedPixelData={mappedPixelData}
|
||||
gridDimensions={gridDimensions}
|
||||
isManualColoringMode={isManualColoringMode}
|
||||
selectedColor={selectedColor}
|
||||
onInteraction={handleCanvasInteraction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1368,21 +1370,7 @@ export default function Home() {
|
||||
|
||||
{/* Tooltip Display (remains the same) */}
|
||||
{tooltipData && (
|
||||
<div
|
||||
className="absolute bg-gray-800 text-white text-xs px-2 py-1 rounded shadow-lg pointer-events-none flex items-center space-x-1.5 z-50"
|
||||
style={{
|
||||
left: `${tooltipData.x + 15}px`,
|
||||
top: `${tooltipData.y + 15}px`,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-3 h-3 rounded-sm border border-gray-400 flex-shrink-0"
|
||||
style={{ backgroundColor: tooltipData.color }}
|
||||
></span>
|
||||
<span className="font-mono font-semibold">{tooltipData.key}</span>
|
||||
</div>
|
||||
<GridTooltip tooltipData={tooltipData} />
|
||||
)}
|
||||
|
||||
{/* Cleaned up the previously moved/commented out block */}
|
||||
|
||||
36
src/components/GridTooltip.tsx
Normal file
36
src/components/GridTooltip.tsx
Normal file
@@ -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<GridTooltipProps> = ({ tooltipData }) => {
|
||||
if (!tooltipData) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute bg-gray-800 text-white text-xs px-2 py-1 rounded shadow-lg pointer-events-none flex items-center space-x-1.5 z-50"
|
||||
style={{
|
||||
left: `${tooltipData.x}px`,
|
||||
top: `${tooltipData.y - 25}px`, // 向上偏移,使提示框显示在鼠标上方
|
||||
transform: 'translate(-50%, -100%)', // 水平居中,不再垂直偏移
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-3 h-3 rounded-sm border border-gray-400 flex-shrink-0"
|
||||
style={{ backgroundColor: tooltipData.color }}
|
||||
></span>
|
||||
<span className="font-mono font-semibold">{tooltipData.key}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridTooltip;
|
||||
187
src/components/PixelatedPreviewCanvas.tsx
Normal file
187
src/components/PixelatedPreviewCanvas.tsx
Normal file
@@ -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<HTMLCanvasElement | null>;
|
||||
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<PixelatedPreviewCanvasProps> = ({
|
||||
mappedPixelData,
|
||||
gridDimensions,
|
||||
isManualColoringMode,
|
||||
selectedColor,
|
||||
canvasRef,
|
||||
onInteraction,
|
||||
}) => {
|
||||
// 当数据变化时重绘画布
|
||||
useEffect(() => {
|
||||
if (mappedPixelData && gridDimensions && canvasRef.current) {
|
||||
drawPixelatedCanvas(mappedPixelData, canvasRef.current, gridDimensions);
|
||||
}
|
||||
}, [mappedPixelData, gridDimensions, canvasRef]);
|
||||
|
||||
// --- 鼠标事件处理 ---
|
||||
|
||||
// 鼠标移动时显示提示
|
||||
const handleMouseMove = (event: MouseEvent<HTMLCanvasElement>) => {
|
||||
onInteraction(event.clientX, event.clientY, event.pageX, event.pageY, false);
|
||||
};
|
||||
|
||||
// 鼠标离开时隐藏提示
|
||||
const handleMouseLeave = () => {
|
||||
onInteraction(0, 0, 0, 0, false, true);
|
||||
};
|
||||
|
||||
// 鼠标点击处理(用于手动上色模式)
|
||||
const handleClick = (event: MouseEvent<HTMLCanvasElement>) => {
|
||||
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<boolean>(false);
|
||||
|
||||
// 触摸开始时立即显示提示,无需长按
|
||||
const handleTouchStart = (event: TouchEvent<HTMLCanvasElement>) => {
|
||||
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<HTMLCanvasElement>) => {
|
||||
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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
className={`border border-gray-300 max-w-full h-auto rounded block ${
|
||||
isManualColoringMode ? 'cursor-pointer' : 'cursor-crosshair'
|
||||
}`}
|
||||
style={{
|
||||
maxHeight: '60vh',
|
||||
imageRendering: 'pixelated',
|
||||
touchAction: 'none' // 防止触摸时页面滚动
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PixelatedPreviewCanvas;
|
||||
Reference in New Issue
Block a user