import React, { useState, useRef, useCallback } from 'react'; import { MappedPixel } from '../utils/pixelation'; interface CompletionCardProps { isVisible: boolean; mappedPixelData: MappedPixel[][]; gridDimensions: { N: number; M: number }; totalElapsedTime: number; onClose: () => void; } const CompletionCard: React.FC = ({ isVisible, mappedPixelData, gridDimensions, totalElapsedTime, onClose }) => { const [userPhoto, setUserPhoto] = useState(null); const [isCapturing, setIsCapturing] = useState(false); const [cameraError, setCameraError] = useState(false); const videoRef = useRef(null); const canvasRef = useRef(null); const cardCanvasRef = useRef(null); // 计算总豆子数(排除透明区域) const totalBeads = React.useMemo(() => { if (!mappedPixelData) return 0; let count = 0; for (let row = 0; row < gridDimensions.M; row++) { for (let col = 0; col < gridDimensions.N; col++) { const pixel = mappedPixelData[row][col]; // 排除透明色和空白区域 if (pixel.color && pixel.color !== 'transparent' && pixel.color !== 'rgba(0,0,0,0)' && !pixel.color.includes('rgba(0, 0, 0, 0)')) { count++; } } } return count; }, [mappedPixelData, gridDimensions]); // 格式化时间 const formatTime = (seconds: number): string => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}小时${minutes}分钟`; } else { return `${minutes}分${secs}秒`; } }; // 生成原图缩略图 const generateThumbnail = useCallback(() => { if (!mappedPixelData) return null; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return null; const thumbnailSize = 200; canvas.width = thumbnailSize; canvas.height = thumbnailSize; const cellWidth = thumbnailSize / gridDimensions.N; const cellHeight = thumbnailSize / gridDimensions.M; // 绘制缩略图 for (let row = 0; row < gridDimensions.M; row++) { for (let col = 0; col < gridDimensions.N; col++) { const pixel = mappedPixelData[row][col]; ctx.fillStyle = pixel.color; ctx.fillRect( col * cellWidth, row * cellHeight, cellWidth, cellHeight ); } } return canvas.toDataURL(); }, [mappedPixelData, gridDimensions]); // 开启相机 const startCamera = async () => { try { setIsCapturing(true); setCameraError(false); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } // 后置摄像头 }); if (videoRef.current) { videoRef.current.srcObject = stream; } } catch (error) { console.error('无法访问相机:', error); setIsCapturing(false); setCameraError(true); } }; // 拍照 const takePhoto = () => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0); const photoDataURL = canvas.toDataURL('image/jpeg', 0.8); setUserPhoto(photoDataURL); // 停止相机 const stream = video.srcObject as MediaStream; stream?.getTracks().forEach(track => track.stop()); setIsCapturing(false); }; // 跳过拍照,使用拼豆原图 const skipPhoto = () => { const thumbnailDataURL = generateThumbnail(); if (thumbnailDataURL) { setUserPhoto(thumbnailDataURL); } }; // 生成打卡图 const generateCompletionCard = useCallback(() => { if (!userPhoto || !cardCanvasRef.current) return null; const canvas = cardCanvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return null; // 检查是否使用的是拼豆原图(通过比较是否等于generateThumbnail的结果) const thumbnailDataURL = generateThumbnail(); const isUsingPixelArt = userPhoto === thumbnailDataURL; // 设置画布尺寸 (9:16比例,适合手机分享) const cardWidth = 720; const cardHeight = 1280; canvas.width = cardWidth; canvas.height = cardHeight; return new Promise((resolve) => { // 加载用户照片/拼豆图 const userImg = new Image(); userImg.onload = () => { if (isUsingPixelArt) { // ===== 拼豆原图模式:原图占主导 ===== // 深色渐变背景,更有质感 const gradient = ctx.createLinearGradient(0, 0, 0, cardHeight); gradient.addColorStop(0, '#1a1a2e'); gradient.addColorStop(0.3, '#16213e'); gradient.addColorStop(0.7, '#0f3460'); gradient.addColorStop(1, '#533483'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, cardWidth, cardHeight); // 计算拼豆图尺寸(占据80%的高度,居中显示) const imageMaxSize = Math.min(cardWidth * 0.9, cardHeight * 0.75); const imageSize = imageMaxSize; const imageX = (cardWidth - imageSize) / 2; const imageY = (cardHeight - imageSize) / 2 - 20; // 稍微往上偏移 // 绘制主图片的装饰背景和阴影 ctx.save(); // 外层光晕效果 const glowGradient = ctx.createRadialGradient( imageX + imageSize/2, imageY + imageSize/2, imageSize/2, imageX + imageSize/2, imageY + imageSize/2, imageSize/2 + 30 ); glowGradient.addColorStop(0, 'rgba(255,255,255,0.1)'); glowGradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = glowGradient; ctx.fillRect(imageX - 30, imageY - 30, imageSize + 60, imageSize + 60); // 白色边框背景 ctx.fillStyle = '#ffffff'; ctx.shadowColor = 'rgba(0,0,0,0.3)'; ctx.shadowBlur = 25; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 15; const borderWidth = 12; ctx.fillRect(imageX - borderWidth, imageY - borderWidth, imageSize + borderWidth * 2, imageSize + borderWidth * 2); ctx.restore(); // 绘制拼豆原图 ctx.drawImage(userImg, imageX, imageY, imageSize, imageSize); // 顶部区域:简洁的完成标识 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 28px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(0,0,0,0.3)'; ctx.shadowBlur = 8; ctx.fillText('🎉 作品完成 🎉', cardWidth / 2, 80); ctx.shadowBlur = 0; // 底部信息区域:透明背景卡片 const infoY = imageY + imageSize + 50; const infoHeight = 120; const infoX = 40; const infoWidth = cardWidth - 80; // 半透明背景 ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.fillRect(infoX, infoY, infoWidth, infoHeight); // 信息文字 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`⏱️ ${formatTime(totalElapsedTime)}`, cardWidth / 2, infoY + 35); ctx.font = '18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.fillText(`🔗 完成 ${totalBeads} 颗豆子`, cardWidth / 2, infoY + 65); // 底部品牌信息 ctx.font = '14px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.fillText('七卡瓦拼豆底稿生成器', cardWidth / 2, cardHeight - 50); ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillText('perlerbeads.zippland.com', cardWidth / 2, cardHeight - 25); resolve(canvas.toDataURL('image/jpeg', 0.95)); } else { // ===== 用户照片模式:照片占主导 ===== // 温暖渐变背景 const gradient = ctx.createLinearGradient(0, 0, 0, cardHeight); gradient.addColorStop(0, '#ff9a9e'); gradient.addColorStop(0.3, '#fecfef'); gradient.addColorStop(0.7, '#fecfef'); gradient.addColorStop(1, '#ff9a9e'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, cardWidth, cardHeight); // 计算照片尺寸(占据大部分空间) const photoMaxSize = Math.min(cardWidth * 0.85, cardHeight * 0.7); const photoSize = photoMaxSize; const photoX = (cardWidth - photoSize) / 2; const photoY = (cardHeight - photoSize) / 2 - 30; // 绘制照片装饰背景和阴影 ctx.save(); // 外层装饰边框 ctx.strokeStyle = 'rgba(255,255,255,0.8)'; ctx.lineWidth = 8; ctx.strokeRect(photoX - 15, photoY - 15, photoSize + 30, photoSize + 30); // 内层白色边框背景 ctx.fillStyle = '#ffffff'; ctx.shadowColor = 'rgba(0,0,0,0.2)'; ctx.shadowBlur = 20; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 10; ctx.fillRect(photoX - 12, photoY - 12, photoSize + 24, photoSize + 24); ctx.restore(); // 绘制矩形照片 ctx.drawImage(userImg, photoX, photoY, photoSize, photoSize); // 顶部完成标识 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 32px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(0,0,0,0.3)'; ctx.shadowBlur = 8; ctx.fillText('🎉 拼豆达成', cardWidth / 2, 100); ctx.shadowBlur = 0; // 底部信息卡片 const infoCardY = photoY + photoSize + 40; const cardHeight2 = 140; const cardX = 60; const cardWidth2 = cardWidth - 120; // 信息卡片背景 ctx.fillStyle = 'rgba(255,255,255,0.95)'; ctx.shadowColor = 'rgba(0,0,0,0.1)'; ctx.shadowBlur = 15; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 8; ctx.fillRect(cardX, infoCardY, cardWidth2, cardHeight2); ctx.shadowBlur = 0; // 信息文字 ctx.fillStyle = '#333333'; ctx.font = 'bold 22px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`⏱️ 总用时 ${formatTime(totalElapsedTime)}`, cardWidth / 2, infoCardY + 40); ctx.font = '20px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = '#666666'; ctx.fillText(`🔗 共完成 ${totalBeads} 颗豆子`, cardWidth / 2, infoCardY + 75); // 添加小的拼豆原图作为装饰 if (thumbnailDataURL) { const thumbnailImg = new Image(); thumbnailImg.onload = () => { const thumbSize = 60; const thumbX = cardWidth / 2 - thumbSize / 2; const thumbY = infoCardY + 90; // 绘制小缩略图背景 ctx.fillStyle = '#ffffff'; ctx.fillRect(thumbX - 3, thumbY - 3, thumbSize + 6, thumbSize + 6); // 绘制小缩略图 ctx.drawImage(thumbnailImg, thumbX, thumbY, thumbSize, thumbSize); // 缩略图边框 ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 3; ctx.strokeRect(thumbX - 3, thumbY - 3, thumbSize + 6, thumbSize + 6); // 底部品牌信息 ctx.font = '14px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.textAlign = 'center'; ctx.fillText('七卡瓦拼豆底稿生成器', cardWidth / 2, cardHeight - 50); ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fillText('perlerbeads.zippland.com', cardWidth / 2, cardHeight - 25); resolve(canvas.toDataURL('image/jpeg', 0.95)); }; thumbnailImg.src = thumbnailDataURL; } else { // 底部品牌信息 ctx.font = '14px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.textAlign = 'center'; ctx.fillText('七卡瓦拼豆底稿生成器', cardWidth / 2, cardHeight - 50); ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fillText('perlerbeads.zippland.com', cardWidth / 2, cardHeight - 25); resolve(canvas.toDataURL('image/jpeg', 0.95)); } } }; userImg.src = userPhoto; }); }, [userPhoto, totalElapsedTime, generateThumbnail, totalBeads]); // 下载打卡图 const downloadCard = async () => { const cardDataURL = await generateCompletionCard(); if (cardDataURL) { const link = document.createElement('a'); link.download = `拼豆完成打卡-${new Date().toLocaleDateString()}.jpg`; link.href = cardDataURL; link.click(); } }; if (!isVisible) return null; return (

🎉 作品完成 🎉

总用时:{formatTime(totalElapsedTime)}

共完成:{totalBeads} 颗豆子

{!userPhoto ? (
{!isCapturing ? (

拍一张照片生成专属打卡图吧!

{cameraError && (

📱 无法访问相机,可能是权限限制或设备不支持。
你可以选择使用作品图生成打卡图。

)}
) : (
)}
) : (
{/* eslint-disable-next-line @next/next/no-img-element */} 用户照片
)}
{/* 隐藏的canvas用于生成图片 */}
); }; export default CompletionCard;