diff --git a/frontend/src/hooks/useScreenshot.ts b/frontend/src/hooks/useScreenshot.ts index 59c7e11..fbce3d3 100644 --- a/frontend/src/hooks/useScreenshot.ts +++ b/frontend/src/hooks/useScreenshot.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import html2canvas from 'html2canvas'; +import { message } from 'antd'; export function useScreenshot() { const takeScreenshot = useCallback(async (username: string = 'Explorer') => { @@ -7,77 +8,112 @@ export function useScreenshot() { const element = document.getElementById('cosmo-scene-container'); if (!element) { console.error('Scene container not found'); + message.error('无法找到截图区域'); return; } + const hideMessage = message.loading('正在生成宇宙快照...', 0); + try { // 2. Use html2canvas to capture the visual composite - const canvas = await html2canvas(element, { + // We use a slightly lower scale if DPR is too high to save memory/performance, + // but usually window.devicePixelRatio is fine (2 or 3). + const capturedCanvas = await html2canvas(element, { backgroundColor: '#000000', useCORS: true, // Allow loading cross-origin images (textures) logging: false, - scale: window.devicePixelRatio, // Capture at high resolution - allowTaint: true, // Allow tainted canvas (might limit editing but okay for saving) - ignoreElements: (_el) => { - // Ignore elements that we don't want in the screenshot (if any end up inside) - return false; - } + scale: window.devicePixelRatio, + allowTaint: true, // Needed if some textures are tainted (may block download if CORS fails) + ignoreElements: (_el) => false, }); - const ctx = canvas.getContext('2d'); - if (!ctx) return; + // 3. Create a fresh canvas for composition to ensure clean state + const width = capturedCanvas.width; + const height = capturedCanvas.height; + + const finalCanvas = document.createElement('canvas'); + finalCanvas.width = width; + finalCanvas.height = height; + const ctx = finalCanvas.getContext('2d'); - const width = canvas.width; - const height = canvas.height; + if (!ctx) { + throw new Error('无法创建绘图上下文'); + } - // 3. Add Overlay / Watermark + // Draw the captured scene + ctx.drawImage(capturedCanvas, 0, 0); + + // 4. Add Overlay / Watermark const now = new Date(); const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); const timeStr = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + // Calculate dynamic font sizes based on image width (e.g., for 4k screens) + // Base logic: width 1920 -> size 32. Ratio ~ 0.016 + const baseScale = width / 1920; + const titleSize = Math.max(24, Math.floor(32 * baseScale)); + const subTitleSize = Math.max(12, Math.floor(16 * baseScale)); + const dateSize = Math.max(18, Math.floor(24 * baseScale)); + const timeSize = Math.max(14, Math.floor(18 * baseScale)); + + // Margins + const marginX = Math.max(20, Math.floor(40 * baseScale)); + const marginY = Math.max(20, Math.floor(35 * baseScale)); + const gradientHeight = Math.floor(200 * baseScale); + // Background gradient for text legibility - const gradient = ctx.createLinearGradient(0, height - 150, 0, height); + const gradient = ctx.createLinearGradient(0, height - gradientHeight, 0, height); gradient.addColorStop(0, 'transparent'); gradient.addColorStop(1, 'rgba(0,0,0,0.9)'); ctx.fillStyle = gradient; - ctx.fillRect(0, height - 200, width, 200); + ctx.fillRect(0, height - gradientHeight, width, gradientHeight); - // Text Settings - ctx.shadowColor = 'rgba(0,0,0,0.5)'; + // Text Settings common + ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 4; + ctx.textBaseline = 'bottom'; - // User Name & App Name (Bottom Right) + // --- Right Side: User & App --- ctx.textAlign = 'right'; - // Nickname@Cosmo - ctx.font = 'bold 32px sans-serif'; - ctx.fillStyle = '#ffffff'; - ctx.fillText(`${username}@Cosmo`, width - 40, height - 60); - - // Subtitle - ctx.font = '16px sans-serif'; + // Subtitle (Bottom) + ctx.font = `${subTitleSize}px sans-serif`; ctx.fillStyle = '#aaaaaa'; - ctx.fillText('DEEP SPACE EXPLORER', width - 40, height - 35); - - // Date/Time (Bottom Left) - ctx.textAlign = 'left'; - ctx.font = 'bold 24px monospace'; - ctx.fillStyle = '#44aaff'; - ctx.fillText(dateStr, 40, height - 60); + ctx.fillText('DEEP SPACE EXPLORER', width - marginX, height - marginY); - ctx.font = '18px monospace'; - ctx.fillStyle = '#cccccc'; - ctx.fillText(timeStr, 40, height - 35); + // Nickname@Cosmo (Above Subtitle) + ctx.font = `bold ${titleSize}px sans-serif`; + ctx.fillStyle = '#ffffff'; + // Move up by subTitle height + padding + ctx.fillText(`${username}@Cosmo`, width - marginX, height - marginY - subTitleSize - 10); - // 4. Trigger Download - const dataUrl = canvas.toDataURL('image/png'); + // --- Left Side: Date & Time --- + ctx.textAlign = 'left'; + + // Time (Bottom) + ctx.font = `${timeSize}px monospace`; + ctx.fillStyle = '#cccccc'; + ctx.fillText(timeStr, marginX, height - marginY); + + // Date (Above Time) + ctx.font = `bold ${dateSize}px monospace`; + ctx.fillStyle = '#44aaff'; + ctx.fillText(dateStr, marginX, height - marginY - timeSize - 10); + + // 5. Trigger Download + const dataUrl = finalCanvas.toDataURL('image/png'); const link = document.createElement('a'); link.download = `Cosmo_Snapshot_${now.toISOString().slice(0,19).replace(/[:T]/g, '-')}.png`; link.href = dataUrl; link.click(); + + message.success('宇宙快照已保存'); } catch (err) { console.error('Screenshot failed:', err); + message.error('截图失败,请稍后重试'); + } finally { + hideMessage(); } }, []);