新增放大镜工具及相关状态管理,优化手动上色模式下的用户交互体验,提升界面友好性。
This commit is contained in:
@@ -87,6 +87,8 @@ import GridTooltip from '../components/GridTooltip';
|
||||
import CustomPaletteEditor from '../components/CustomPaletteEditor';
|
||||
import FloatingColorPalette from '../components/FloatingColorPalette';
|
||||
import FloatingToolbar from '../components/FloatingToolbar';
|
||||
import MagnifierTool from '../components/MagnifierTool';
|
||||
import MagnifierSelectionOverlay from '../components/MagnifierSelectionOverlay';
|
||||
import { loadPaletteSelections, savePaletteSelections, presetToSelections, PaletteSelections } from '../utils/localStorageUtils';
|
||||
import { TRANSPARENT_KEY, transparentColorData } from '../utils/pixelEditingUtils';
|
||||
|
||||
@@ -161,6 +163,70 @@ export default function Home() {
|
||||
// 新增:悬浮调色盘状态
|
||||
const [isFloatingPaletteOpen, setIsFloatingPaletteOpen] = useState<boolean>(true);
|
||||
|
||||
// 新增:放大镜状态
|
||||
const [isMagnifierActive, setIsMagnifierActive] = useState<boolean>(false);
|
||||
const [magnifierSelectionArea, setMagnifierSelectionArea] = useState<{
|
||||
startRow: number;
|
||||
startCol: number;
|
||||
endRow: number;
|
||||
endCol: number;
|
||||
} | null>(null);
|
||||
|
||||
// 放大镜切换处理函数
|
||||
const handleToggleMagnifier = () => {
|
||||
setIsMagnifierActive(!isMagnifierActive);
|
||||
};
|
||||
|
||||
// 放大镜像素编辑处理函数
|
||||
const handleMagnifierPixelEdit = (row: number, col: number, colorData: { key: string; color: string }) => {
|
||||
if (!mappedPixelData) return;
|
||||
|
||||
// 创建新的像素数据
|
||||
const newMappedPixelData = mappedPixelData.map((rowData, r) =>
|
||||
rowData.map((pixel, c) => {
|
||||
if (r === row && c === col) {
|
||||
return {
|
||||
key: colorData.key,
|
||||
color: colorData.color
|
||||
} as MappedPixel;
|
||||
}
|
||||
return pixel;
|
||||
})
|
||||
);
|
||||
|
||||
setMappedPixelData(newMappedPixelData);
|
||||
|
||||
// 更新颜色统计
|
||||
if (colorCounts) {
|
||||
const newColorCounts = { ...colorCounts };
|
||||
|
||||
// 减少原颜色的计数
|
||||
const oldPixel = mappedPixelData[row][col];
|
||||
if (newColorCounts[oldPixel.key]) {
|
||||
newColorCounts[oldPixel.key].count--;
|
||||
if (newColorCounts[oldPixel.key].count === 0) {
|
||||
delete newColorCounts[oldPixel.key];
|
||||
}
|
||||
}
|
||||
|
||||
// 增加新颜色的计数
|
||||
if (newColorCounts[colorData.key]) {
|
||||
newColorCounts[colorData.key].count++;
|
||||
} else {
|
||||
newColorCounts[colorData.key] = {
|
||||
count: 1,
|
||||
color: colorData.color
|
||||
};
|
||||
}
|
||||
|
||||
setColorCounts(newColorCounts);
|
||||
|
||||
// 更新总计数
|
||||
const newTotal = Object.values(newColorCounts).reduce((sum, item) => sum + item.count, 0);
|
||||
setTotalBeadCount(newTotal);
|
||||
}
|
||||
};
|
||||
|
||||
const originalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const pixelatedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -2074,7 +2140,10 @@ export default function Home() {
|
||||
step: 'select-source'
|
||||
});
|
||||
setHighlightColorKey(null);
|
||||
setIsMagnifierActive(false);
|
||||
}}
|
||||
onToggleMagnifier={handleToggleMagnifier}
|
||||
isMagnifierActive={isMagnifierActive}
|
||||
/>
|
||||
|
||||
{/* 悬浮调色盘 */}
|
||||
@@ -2098,6 +2167,33 @@ export default function Home() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 放大镜工具 */}
|
||||
{isManualColoringMode && (
|
||||
<>
|
||||
<MagnifierTool
|
||||
isActive={isMagnifierActive}
|
||||
onToggle={handleToggleMagnifier}
|
||||
mappedPixelData={mappedPixelData}
|
||||
gridDimensions={gridDimensions}
|
||||
selectedColor={selectedColor}
|
||||
selectedColorSystem={selectedColorSystem}
|
||||
onPixelEdit={handleMagnifierPixelEdit}
|
||||
cellSize={gridDimensions ? Math.min(6, Math.max(4, 500 / Math.max(gridDimensions.N, gridDimensions.M))) : 6}
|
||||
selectionArea={magnifierSelectionArea}
|
||||
onClearSelection={() => setMagnifierSelectionArea(null)}
|
||||
/>
|
||||
|
||||
{/* 放大镜选择覆盖层 */}
|
||||
<MagnifierSelectionOverlay
|
||||
isActive={isMagnifierActive && !magnifierSelectionArea}
|
||||
canvasRef={pixelatedCanvasRef}
|
||||
gridDimensions={gridDimensions}
|
||||
cellSize={gridDimensions ? Math.min(6, Math.max(4, 500 / Math.max(gridDimensions.N, gridDimensions.M))) : 6}
|
||||
onSelectionComplete={setMagnifierSelectionArea}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Apply dark mode styles to the Footer */}
|
||||
<footer className="w-full md:max-w-4xl mt-10 mb-6 py-6 text-center text-xs sm:text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700 bg-gradient-to-b from-white to-gray-50 dark:from-gray-900 dark:to-gray-800/50 rounded-lg shadow-inner">
|
||||
|
||||
|
||||
@@ -7,13 +7,17 @@ interface FloatingToolbarProps {
|
||||
isPaletteOpen: boolean;
|
||||
onTogglePalette: () => void;
|
||||
onExitManualMode: () => void;
|
||||
onToggleMagnifier: () => void;
|
||||
isMagnifierActive: boolean;
|
||||
}
|
||||
|
||||
const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
isManualColoringMode,
|
||||
isPaletteOpen,
|
||||
onTogglePalette,
|
||||
onExitManualMode
|
||||
onExitManualMode,
|
||||
onToggleMagnifier,
|
||||
isMagnifierActive
|
||||
}) => {
|
||||
if (!isManualColoringMode) return null;
|
||||
|
||||
@@ -34,6 +38,21 @@ const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 放大镜按钮 */}
|
||||
<button
|
||||
onClick={onToggleMagnifier}
|
||||
className={`w-12 h-12 rounded-full shadow-lg transition-all duration-200 flex items-center justify-center ${
|
||||
isMagnifierActive
|
||||
? 'bg-green-500 text-white hover:bg-green-600'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
title={isMagnifierActive ? '关闭放大镜' : '打开放大镜'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 退出手动编辑模式按钮 */}
|
||||
<button
|
||||
onClick={onExitManualMode}
|
||||
|
||||
264
src/components/MagnifierSelectionOverlay.tsx
Normal file
264
src/components/MagnifierSelectionOverlay.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||
|
||||
interface SelectionArea {
|
||||
startRow: number;
|
||||
startCol: number;
|
||||
endRow: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
interface MagnifierSelectionOverlayProps {
|
||||
isActive: boolean;
|
||||
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||
gridDimensions: { N: number; M: number } | null;
|
||||
cellSize: number;
|
||||
onSelectionComplete: (area: SelectionArea) => void;
|
||||
}
|
||||
|
||||
const MagnifierSelectionOverlay: React.FC<MagnifierSelectionOverlayProps> = ({
|
||||
isActive,
|
||||
canvasRef,
|
||||
gridDimensions,
|
||||
onSelectionComplete
|
||||
}) => {
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
|
||||
const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 滚动禁用状态
|
||||
const scrollDisabledRef = useRef<boolean>(false);
|
||||
const savedScrollPositionRef = useRef<number>(0);
|
||||
|
||||
// 禁用/启用页面滚动
|
||||
const preventScrolling = useCallback((e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const disableScroll = useCallback(() => {
|
||||
if (scrollDisabledRef.current) return; // 避免重复禁用
|
||||
|
||||
// 保存当前滚动位置
|
||||
savedScrollPositionRef.current = window.scrollY;
|
||||
|
||||
// 设置CSS样式
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
|
||||
// 阻止滚动事件(但不使用fixed定位,避免跳转问题)
|
||||
document.addEventListener('wheel', preventScrolling, { passive: false });
|
||||
document.addEventListener('touchmove', preventScrolling, { passive: false });
|
||||
|
||||
scrollDisabledRef.current = true;
|
||||
}, [preventScrolling]);
|
||||
|
||||
const enableScroll = useCallback(() => {
|
||||
if (!scrollDisabledRef.current) return; // 避免重复启用
|
||||
|
||||
// 恢复CSS样式
|
||||
document.body.style.overflow = '';
|
||||
document.documentElement.style.overflow = '';
|
||||
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('wheel', preventScrolling);
|
||||
document.removeEventListener('touchmove', preventScrolling);
|
||||
|
||||
scrollDisabledRef.current = false;
|
||||
// 不再强制恢复滚动位置,让浏览器保持自然状态
|
||||
}, [preventScrolling]);
|
||||
|
||||
// 获取画布相对坐标
|
||||
const getCanvasCoordinates = useCallback((clientX: number, clientY: number) => {
|
||||
if (!canvasRef.current) return null;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
};
|
||||
}, [canvasRef]);
|
||||
|
||||
// 将像素坐标转换为网格坐标
|
||||
const pixelToGrid = useCallback((x: number, y: number) => {
|
||||
if (!gridDimensions || !canvasRef.current) return null;
|
||||
|
||||
const canvasWidth = canvasRef.current.width;
|
||||
const canvasHeight = canvasRef.current.height;
|
||||
const cellWidth = canvasWidth / gridDimensions.N;
|
||||
const cellHeight = canvasHeight / gridDimensions.M;
|
||||
|
||||
const col = Math.floor(x / cellWidth);
|
||||
const row = Math.floor(y / cellHeight);
|
||||
|
||||
return {
|
||||
row: Math.max(0, Math.min(gridDimensions.M - 1, row)),
|
||||
col: Math.max(0, Math.min(gridDimensions.N - 1, col))
|
||||
};
|
||||
}, [gridDimensions, canvasRef]);
|
||||
|
||||
// 处理鼠标事件
|
||||
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
||||
if (!isActive) return;
|
||||
|
||||
const coords = getCanvasCoordinates(event.clientX, event.clientY);
|
||||
if (!coords) return;
|
||||
|
||||
setIsSelecting(true);
|
||||
setSelectionStart(coords);
|
||||
setSelectionEnd(coords);
|
||||
disableScroll(); // 禁用滚动
|
||||
event.preventDefault();
|
||||
}, [isActive, getCanvasCoordinates, disableScroll]);
|
||||
|
||||
const handleMouseMove = useCallback((event: MouseEvent) => {
|
||||
if (!isSelecting || !selectionStart) return;
|
||||
|
||||
const coords = getCanvasCoordinates(event.clientX, event.clientY);
|
||||
if (!coords) return;
|
||||
|
||||
setSelectionEnd(coords);
|
||||
}, [isSelecting, selectionStart, getCanvasCoordinates]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (!isSelecting || !selectionStart || !selectionEnd) {
|
||||
enableScroll(); // 确保恢复滚动
|
||||
return;
|
||||
}
|
||||
|
||||
const startGrid = pixelToGrid(selectionStart.x, selectionStart.y);
|
||||
const endGrid = pixelToGrid(selectionEnd.x, selectionEnd.y);
|
||||
|
||||
if (startGrid && endGrid) {
|
||||
const area: SelectionArea = {
|
||||
startRow: Math.min(startGrid.row, endGrid.row),
|
||||
startCol: Math.min(startGrid.col, endGrid.col),
|
||||
endRow: Math.max(startGrid.row, endGrid.row),
|
||||
endCol: Math.max(startGrid.col, endGrid.col)
|
||||
};
|
||||
|
||||
onSelectionComplete(area);
|
||||
}
|
||||
|
||||
setIsSelecting(false);
|
||||
setSelectionStart(null);
|
||||
setSelectionEnd(null);
|
||||
enableScroll(); // 恢复滚动
|
||||
}, [isSelecting, selectionStart, selectionEnd, pixelToGrid, onSelectionComplete, enableScroll]);
|
||||
|
||||
// 处理触摸事件
|
||||
const handleTouchStart = useCallback((event: React.TouchEvent) => {
|
||||
if (!isActive) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const coords = getCanvasCoordinates(touch.clientX, touch.clientY);
|
||||
if (!coords) return;
|
||||
|
||||
setIsSelecting(true);
|
||||
setSelectionStart(coords);
|
||||
setSelectionEnd(coords);
|
||||
disableScroll(); // 禁用滚动
|
||||
event.preventDefault();
|
||||
}, [isActive, getCanvasCoordinates, disableScroll]);
|
||||
|
||||
const handleTouchMove = useCallback((event: TouchEvent) => {
|
||||
if (!isSelecting || !selectionStart) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const coords = getCanvasCoordinates(touch.clientX, touch.clientY);
|
||||
if (!coords) return;
|
||||
|
||||
setSelectionEnd(coords);
|
||||
event.preventDefault();
|
||||
}, [isSelecting, selectionStart, getCanvasCoordinates]);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
handleMouseUp();
|
||||
}, [handleMouseUp]);
|
||||
|
||||
// 事件监听器
|
||||
useEffect(() => {
|
||||
if (isSelecting) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('touchmove', handleTouchMove);
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
enableScroll(); // 清理时恢复滚动
|
||||
};
|
||||
}
|
||||
}, [isSelecting, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd, enableScroll]);
|
||||
|
||||
// 组件卸载时恢复滚动
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
enableScroll();
|
||||
};
|
||||
}, [enableScroll]);
|
||||
|
||||
// 当组件变为非活跃状态时恢复滚动
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
enableScroll();
|
||||
setIsSelecting(false);
|
||||
setSelectionStart(null);
|
||||
setSelectionEnd(null);
|
||||
}
|
||||
}, [isActive, enableScroll]);
|
||||
|
||||
// 计算选择框的样式
|
||||
const getSelectionStyle = useCallback(() => {
|
||||
if (!selectionStart || !selectionEnd || !canvasRef.current) return {};
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const minX = Math.min(selectionStart.x, selectionEnd.x);
|
||||
const minY = Math.min(selectionStart.y, selectionEnd.y);
|
||||
const maxX = Math.max(selectionStart.x, selectionEnd.x);
|
||||
const maxY = Math.max(selectionStart.y, selectionEnd.y);
|
||||
|
||||
return {
|
||||
left: rect.left + minX,
|
||||
top: rect.top + minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
position: 'fixed' as const,
|
||||
border: '2px solid #10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
pointerEvents: 'none' as const,
|
||||
zIndex: 1000
|
||||
};
|
||||
}, [selectionStart, selectionEnd, canvasRef]);
|
||||
|
||||
if (!isActive) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 覆盖层 */}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="fixed inset-0 z-50"
|
||||
style={{
|
||||
cursor: 'crosshair',
|
||||
pointerEvents: isActive ? 'auto' : 'none'
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
onTouchMove={(e) => e.preventDefault()}
|
||||
onScroll={(e) => e.preventDefault()}
|
||||
/>
|
||||
|
||||
{/* 选择框 */}
|
||||
{isSelecting && selectionStart && selectionEnd && (
|
||||
<div style={getSelectionStyle()} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MagnifierSelectionOverlay;
|
||||
281
src/components/MagnifierTool.tsx
Normal file
281
src/components/MagnifierTool.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { MappedPixel } from '../utils/pixelation';
|
||||
import { getColorKeyByHex, ColorSystem } from '../utils/colorSystemUtils';
|
||||
|
||||
interface MagnifierToolProps {
|
||||
isActive: boolean;
|
||||
onToggle: () => void;
|
||||
mappedPixelData: MappedPixel[][] | null;
|
||||
gridDimensions: { N: number; M: number } | null;
|
||||
selectedColor: MappedPixel | null;
|
||||
selectedColorSystem: ColorSystem;
|
||||
onPixelEdit: (row: number, col: number, colorData: { key: string; color: string }) => void;
|
||||
cellSize: number;
|
||||
selectionArea: SelectionArea | null;
|
||||
onClearSelection: () => void;
|
||||
}
|
||||
|
||||
interface SelectionArea {
|
||||
startRow: number;
|
||||
startCol: number;
|
||||
endRow: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
const MagnifierTool: React.FC<MagnifierToolProps> = ({
|
||||
isActive,
|
||||
onToggle,
|
||||
mappedPixelData,
|
||||
selectedColor,
|
||||
selectedColorSystem,
|
||||
onPixelEdit,
|
||||
selectionArea,
|
||||
onClearSelection
|
||||
}) => {
|
||||
const [magnifierPosition, setMagnifierPosition] = useState<{ x: number; y: number }>({ x: 50, y: 50 });
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
|
||||
const magnifierRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// 计算选择区域的尺寸
|
||||
const getSelectionDimensions = useCallback(() => {
|
||||
if (!selectionArea) return { width: 0, height: 0 };
|
||||
return {
|
||||
width: Math.abs(selectionArea.endCol - selectionArea.startCol) + 1,
|
||||
height: Math.abs(selectionArea.endRow - selectionArea.startRow) + 1
|
||||
};
|
||||
}, [selectionArea]);
|
||||
|
||||
// 渲染放大视图
|
||||
const renderMagnifiedView = useCallback(() => {
|
||||
if (!selectionArea || !mappedPixelData || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const { width, height } = getSelectionDimensions();
|
||||
const magnifiedCellSize = 20; // 放大后每个像素的大小
|
||||
|
||||
// 设置画布的实际尺寸
|
||||
canvas.width = width * magnifiedCellSize;
|
||||
canvas.height = height * magnifiedCellSize;
|
||||
|
||||
// 设置画布的显示尺寸,确保不会太大
|
||||
const maxDisplayWidth = 400;
|
||||
const maxDisplayHeight = 400;
|
||||
const displayWidth = Math.min(canvas.width, maxDisplayWidth);
|
||||
const displayHeight = Math.min(canvas.height, maxDisplayHeight);
|
||||
|
||||
canvas.style.width = `${displayWidth}px`;
|
||||
canvas.style.height = `${displayHeight}px`;
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 渲染放大的像素
|
||||
const startRow = Math.min(selectionArea.startRow, selectionArea.endRow);
|
||||
const endRow = Math.max(selectionArea.startRow, selectionArea.endRow);
|
||||
const startCol = Math.min(selectionArea.startCol, selectionArea.endCol);
|
||||
const endCol = Math.max(selectionArea.startCol, selectionArea.endCol);
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startCol; col <= endCol; col++) {
|
||||
if (row >= 0 && row < mappedPixelData.length && col >= 0 && col < mappedPixelData[0].length) {
|
||||
const pixel = mappedPixelData[row][col];
|
||||
const canvasRow = row - startRow;
|
||||
const canvasCol = col - startCol;
|
||||
|
||||
// 绘制像素
|
||||
ctx.fillStyle = pixel.color;
|
||||
ctx.fillRect(
|
||||
canvasCol * magnifiedCellSize,
|
||||
canvasRow * magnifiedCellSize,
|
||||
magnifiedCellSize,
|
||||
magnifiedCellSize
|
||||
);
|
||||
|
||||
// 绘制网格线
|
||||
ctx.strokeStyle = '#e0e0e0';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(
|
||||
canvasCol * magnifiedCellSize,
|
||||
canvasRow * magnifiedCellSize,
|
||||
magnifiedCellSize,
|
||||
magnifiedCellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectionArea, mappedPixelData, getSelectionDimensions]);
|
||||
|
||||
// 处理放大视图点击
|
||||
const handleMagnifiedClick = useCallback((event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!selectionArea || !mappedPixelData || !selectedColor || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
// 获取点击在画布上的相对位置(考虑缩放)
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
|
||||
const x = (event.clientX - rect.left) * scaleX;
|
||||
const y = (event.clientY - rect.top) * scaleY;
|
||||
|
||||
const magnifiedCellSize = 20;
|
||||
const clickedCol = Math.floor(x / magnifiedCellSize);
|
||||
const clickedRow = Math.floor(y / magnifiedCellSize);
|
||||
|
||||
const startRow = Math.min(selectionArea.startRow, selectionArea.endRow);
|
||||
const startCol = Math.min(selectionArea.startCol, selectionArea.endCol);
|
||||
|
||||
const actualRow = startRow + clickedRow;
|
||||
const actualCol = startCol + clickedCol;
|
||||
|
||||
// 确保点击在有效范围内
|
||||
if (actualRow >= 0 && actualRow < mappedPixelData.length &&
|
||||
actualCol >= 0 && actualCol < mappedPixelData[0].length) {
|
||||
onPixelEdit(actualRow, actualCol, selectedColor);
|
||||
}
|
||||
}, [selectionArea, mappedPixelData, selectedColor, onPixelEdit]);
|
||||
|
||||
// 处理拖拽移动
|
||||
const handleTitleBarMouseDown = useCallback((event: React.MouseEvent) => {
|
||||
// 只有点击在标题栏区域且不是按钮时才开始拖拽
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'BUTTON' || target.closest('button')) {
|
||||
return; // 点击按钮时不拖拽
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback((event: MouseEvent) => {
|
||||
if (isDragging && magnifierRef.current) {
|
||||
const rect = magnifierRef.current.getBoundingClientRect();
|
||||
const newX = Math.max(0, Math.min(window.innerWidth - rect.width, event.clientX - rect.width / 2));
|
||||
const newY = Math.max(0, Math.min(window.innerHeight - rect.height, event.clientY - rect.height / 2));
|
||||
setMagnifierPosition({ x: newX, y: newY });
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
// 重新渲染放大视图
|
||||
useEffect(() => {
|
||||
renderMagnifiedView();
|
||||
}, [renderMagnifiedView]);
|
||||
|
||||
if (!isActive) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 选择区域提示 */}
|
||||
{!selectionArea && (
|
||||
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg z-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span>在画布上拖拽选择要放大的区域</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 放大视图窗口 */}
|
||||
{selectionArea && (
|
||||
<div
|
||||
ref={magnifierRef}
|
||||
className="fixed bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-600 z-50 select-none"
|
||||
style={{
|
||||
left: magnifierPosition.x,
|
||||
top: magnifierPosition.y,
|
||||
maxWidth: '500px',
|
||||
maxHeight: '500px'
|
||||
}}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<div
|
||||
className="flex items-center justify-between p-3 bg-gradient-to-r from-green-500 to-teal-500 text-white rounded-t-xl cursor-move"
|
||||
onMouseDown={handleTitleBarMouseDown}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">放大镜 ({getSelectionDimensions().width}×{getSelectionDimensions().height})</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 重新选择按钮 */}
|
||||
<button
|
||||
onClick={onClearSelection}
|
||||
className="p-1 hover:bg-white/20 rounded transition-colors"
|
||||
title="重新选择区域"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-1 hover:bg-white/20 rounded transition-colors"
|
||||
title="关闭放大镜"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 放大视图内容 */}
|
||||
<div className="p-3">
|
||||
<div className="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onClick={handleMagnifiedClick}
|
||||
className="cursor-crosshair block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 当前选中颜色信息 */}
|
||||
{selectedColor && (
|
||||
<div className="mt-2 p-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className="w-4 h-4 rounded border border-gray-300 dark:border-gray-500"
|
||||
style={{ backgroundColor: selectedColor.color }}
|
||||
></div>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
当前: {getColorKeyByHex(selectedColor.color, selectedColorSystem)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MagnifierTool;
|
||||
Reference in New Issue
Block a user