import React, { useRef, useEffect, useState, useCallback } from 'react'; import { Stage, Layer, Image as KonvaImage, Line } from 'react-konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Stage as StageType } from 'konva/lib/Stage'; import { useAppStore } from '../store/useAppStore'; import { Button } from './ui/Button'; import { ZoomIn, ZoomOut, RotateCcw, Download } from 'lucide-react'; import { downloadImage } from '../utils/imageUtils'; export const ImageCanvas: React.FC = () => { const { canvasImage, canvasZoom, // canvasPan, setCanvasZoom, setCanvasPan, brushStrokes, addBrushStroke, showMasks, selectedTool, isGenerating, isContinuousGenerating, retryCount, brushSize, showHistory, showPromptPanel } = useAppStore(); const stageRef = useRef(null); const [image, setImage] = useState(null); const [stageSize, setStageSize] = useState({ width: 800, height: 600 }); const [isDrawing, setIsDrawing] = useState(false); const [currentStroke, setCurrentStroke] = useState([]); const handleZoom = useCallback((delta: number) => { const stage = stageRef.current; if (stage) { const currentZoom = stage.scaleX(); const newZoom = Math.max(0.1, Math.min(3, currentZoom + delta)); // 先更新React状态以确保Konva Image组件使用正确的缩放值 setCanvasZoom(newZoom); // 使用setTimeout确保DOM已更新后再设置Stage setTimeout(() => { const stage = stageRef.current; if (stage) { // 直接通过stageRef控制Stage stage.scale({ x: newZoom, y: newZoom }); stage.batchDraw(); } }, 0); } }, [setCanvasZoom]); // 加载图像 useEffect(() => { console.log('useEffect triggered, canvasImage:', canvasImage); // 如果没有图像URL,直接返回 if (!canvasImage) { console.log('没有图像需要加载'); setImage(null); return; } let img: HTMLImageElement | null = null; let isCancelled = false; console.log('开始加载图像,URL:', canvasImage); img = new window.Image(); img.onload = () => { // 检查是否已取消 if (isCancelled) { console.log('图像加载被取消'); return; } console.log('图像加载成功,尺寸:', img.width, 'x', img.height); setImage(img); // 只在图像首次加载时自动适应画布 if (!isCancelled && img) { const isMobile = window.innerWidth < 768; const padding = isMobile ? 0.9 : 0.8; const scaleX = (stageSize.width * padding) / img.width; const scaleY = (stageSize.height * padding) / img.height; const maxZoom = isMobile ? 0.3 : 0.8; const optimalZoom = Math.min(scaleX, scaleY, maxZoom); // 立即更新React状态以确保Konva Image组件使用正确的缩放值 setCanvasZoom(optimalZoom); setCanvasPan({ x: 0, y: 0 }); // 使用setTimeout确保DOM已更新后再设置Stage setTimeout(() => { // 检查是否已取消 if (isCancelled) { return; } if (!isCancelled && img) { // 直接设置缩放,但保持Stage居中 const stage = stageRef.current; if (stage) { stage.scale({ x: optimalZoom, y: optimalZoom }); // 重置Stage位置以确保居中 stage.position({ x: 0, y: 0 }); stage.batchDraw(); } console.log('图像自动适应画布完成,缩放:', optimalZoom); } }, 0); } }; img.onerror = (error) => { // 检查是否已取消 if (isCancelled) { return; } console.error('图像加载失败:', error); console.error('图像URL:', canvasImage); // 检查是否是IndexedDB URL if (canvasImage.startsWith('indexeddb://')) { console.log('正在处理IndexedDB图像...'); // 从IndexedDB获取图像并创建Blob URL const imageId = canvasImage.replace('indexeddb://', ''); import('../services/referenceImageService').then((module) => { const referenceImageService = module; referenceImageService.getReferenceImage(imageId) .then(blob => { // 检查是否已取消 if (isCancelled) { return; } if (blob) { const newUrl = URL.createObjectURL(blob); console.log('从IndexedDB创建新的Blob URL:', newUrl); // 更新canvasImage为新的URL // 检查是否已取消 if (!isCancelled) { useAppStore.getState().setCanvasImage(newUrl); } } else { console.error('IndexedDB中未找到图像'); } }) .catch(err => { // 检查是否已取消 if (isCancelled) { return; } console.error('从IndexedDB获取图像时出错:', err); }); }).catch(err => { // 检查是否已取消 if (isCancelled) { return; } console.error('导入referenceImageService时出错:', err); }); } // 检查是否是Blob URL else if (canvasImage.startsWith('blob:')) { console.log('正在检查Blob URL是否有效...'); // 尝试从AppStore重新获取Blob并创建新的URL const blob = useAppStore.getState().getBlob(canvasImage); if (blob) { // 检查是否已取消 if (isCancelled) { return; } console.log('从AppStore找到Blob,尝试重新创建URL...'); // 重新创建Blob URL并重试加载 const newUrl = URL.createObjectURL(blob); console.log('创建新的Blob URL:', newUrl); // 更新canvasImage为新的URL useAppStore.getState().setCanvasImage(newUrl); } else { // 检查是否已取消 if (isCancelled) { return; } console.error('AppStore中未找到Blob'); // 如果AppStore中也没有,尝试通过fetch检查URL fetch(canvasImage) .then(response => { // 检查是否已取消 if (isCancelled) { return; } if (!response.ok) { console.error('Blob URL无法访问:', response.status, response.statusText); } else { console.log('Blob URL可以访问,但图像加载仍然失败'); } }) .catch(fetchErr => { // 检查是否已取消 if (isCancelled) { return; } console.error('检查Blob URL时出错:', fetchErr); }); } } }; img.src = canvasImage; // 清理函数 return () => { console.log('清理图像加载资源'); // 标记为已取消 isCancelled = true; // 取消图像加载 if (img) { img.onload = null; img.onerror = null; // 清理图像源以释放内存 img.src = ''; } }; }, [canvasImage, setCanvasZoom, setCanvasPan, stageSize.height, stageSize.width]); // 移除image依赖项 // 处理舞台大小调整 useEffect(() => { const updateSize = () => { const container = document.getElementById('canvas-container'); if (container) { setStageSize({ width: container.offsetWidth, height: container.offsetHeight }); } }; updateSize(); window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); }, [showPromptPanel, showHistory]); // 监听面板状态变化以调整画布大小 useEffect(() => { // 使用 setTimeout 确保 DOM 已更新 const timer = setTimeout(() => { const container = document.getElementById('canvas-container'); if (container) { setStageSize({ width: container.offsetWidth, height: container.offsetHeight }); } }, 100); return () => clearTimeout(timer); }, [showPromptPanel, showHistory]); // 处理鼠标滚轮缩放 useEffect(() => { const container = document.getElementById('canvas-container'); if (!container) return; const handleWheel = (e: WheelEvent) => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; handleZoom(delta); }; container.addEventListener('wheel', handleWheel, { passive: false }); return () => container.removeEventListener('wheel', handleWheel); }, [canvasZoom, handleZoom]); const handleMouseDown = (e: KonvaEventObject) => { if (selectedTool !== 'mask' || !image) return; setIsDrawing(true); const stage = e.target.getStage(); if (!stage) return; // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); if (!relativePos) return; // 计算图像在舞台上的边界 const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2; // 转换为相对于图像的坐标 const relativeX = relativePos.x - imageX; const relativeY = relativePos.y - imageY; // 检查点击是否在图像边界内 if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { setCurrentStroke([relativeX, relativeY]); } }; const handleMouseMove = (e: KonvaEventObject) => { if (!isDrawing || selectedTool !== 'mask' || !image) return; const stage = e.target.getStage(); if (!stage) return; // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); if (!relativePos) return; // 计算图像在舞台上的边界 const imageX = (stageSize.width / canvasZoom - image.width) / 2; const imageY = (stageSize.height / canvasZoom - image.height) / 2; // 转换为相对于图像的坐标 const relativeX = relativePos.x - imageX; const relativeY = relativePos.y - imageY; // 检查是否在图像边界内 if (relativeX >= 0 && relativeX <= image.width && relativeY >= 0 && relativeY <= image.height) { setCurrentStroke([...currentStroke, relativeX, relativeY]); } }; const handleMouseUp = () => { if (!isDrawing || currentStroke.length < 4) { setIsDrawing(false); setCurrentStroke([]); return; } setIsDrawing(false); addBrushStroke({ id: `stroke-${Date.now()}`, points: currentStroke, brushSize, color: '#A855F7', }); setCurrentStroke([]); }; const handleReset = () => { if (image) { const isMobile = window.innerWidth < 768; const padding = isMobile ? 0.9 : 0.8; const scaleX = (stageSize.width * padding) / image.width; const scaleY = (stageSize.height * padding) / image.height; const maxZoom = isMobile ? 0.3 : 0.8; const optimalZoom = Math.min(scaleX, scaleY, maxZoom); // 同时更新React状态以确保Konva Image组件使用正确的缩放值 setCanvasZoom(optimalZoom); setCanvasPan({ x: 0, y: 0 }); // 使用setTimeout确保DOM已更新后再设置Stage setTimeout(() => { // 直接通过stageRef控制Stage const stage = stageRef.current; if (stage) { stage.scale({ x: optimalZoom, y: optimalZoom }); stage.position({ x: 0, y: 0 }); stage.batchDraw(); } }, 0); } }; const handleDownload = async () => { // 首先尝试从当前选中的生成记录或编辑记录中获取上传后的URL const { selectedGenerationId, selectedEditId, currentProject } = useAppStore.getState(); // 获取当前选中的记录 let selectedRecord = null; if (selectedGenerationId && currentProject) { selectedRecord = currentProject.generations.find(g => g.id === selectedGenerationId); } else if (selectedEditId && currentProject) { selectedRecord = currentProject.edits.find(e => e.id === selectedEditId); } // 如果有选中的记录且有上传结果,尝试下载上传后的图像 if (selectedRecord && selectedRecord.uploadResults && selectedRecord.uploadResults.length > 0) { // 下载第一个上传结果(通常是生成的图像) const uploadResult = selectedRecord.uploadResults[0]; if (uploadResult.success && uploadResult.url) { try { await downloadImage(uploadResult.url, `nano-banana-${Date.now()}.png`); } catch (error) { console.error('下载图像失败:', error); } // 立即返回 return; } } // 如果没有上传后的URL或下载失败,回退到下载当前画布内容 const stage = stageRef.current; if (stage) { try { // 使用Konva的toDataURL方法获取画布内容 const dataURL = stage.toDataURL(); // 创建下载链接 const link = document.createElement('a'); link.href = dataURL; link.download = `nano-banana-${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log('画布内容下载成功'); } catch (error) { console.error('下载画布内容时出错:', error); // 如果Konva下载失败,回退到下载原始图像 if (canvasImage) { try { await downloadImage(canvasImage, `nano-banana-${Date.now()}.png`); } catch (error) { console.error('下载图像失败:', error); } } } } };; return (
{/* 工具栏 */} {/* 画布区域 */}
{!image && !isGenerating && (
🍌

Nano Banana AI

{selectedTool === 'generate' ? '在提示框中描述您想要创建的内容' : '上传图像开始编辑' }

)} {isGenerating && (

正在创建图像...

{/* 显示重试次数 */} {isContinuousGenerating && (

重试次数: {retryCount}

)}
)} { // 通过stageRef直接获取和设置位置 const stage = stageRef.current; if (stage) { const scale = stage.scaleX(); setCanvasPan({ x: stage.x() / scale, y: stage.y() / scale }); } }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} style={{ cursor: selectedTool === 'mask' ? 'crosshair' : 'default', zIndex: 10 }} > {image && ( { console.log('KonvaImage组件渲染完成'); }} /> )} {/* 画笔描边 */} {showMasks && brushStrokes.map((stroke) => ( ))} {/* 正在绘制的当前描边 */} {isDrawing && currentStroke.length > 2 && ( )} {/* 悬浮操作按钮 */} {image && !isGenerating && (
{Math.round(canvasZoom * 100)}%
)}
{/* 状态栏 */}
{brushStrokes.length > 0 && ( {brushStrokes.length} 个描边 )}
© 2025 Mark Fulton
); };