fix: improve screenshot watermark visibility and robustness
parent
bdedd30f2f
commit
539a5319e4
|
|
@ -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();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue