feat: enhance screenshot with auth check, nickname watermark and label capture

main
mula.liu 2025-11-30 23:12:16 +08:00
parent 7037628831
commit bdedd30f2f
5 changed files with 90 additions and 34 deletions

View File

@ -15,6 +15,7 @@
"@react-three/fiber": "^9.4.0",
"antd": "^6.0.0",
"axios": "^1.13.2",
"html2canvas": "^1.4.1",
"lucide-react": "^0.555.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",

View File

@ -4,6 +4,7 @@
*/
import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { message } from 'antd';
import { useSpaceData } from './hooks/useSpaceData';
import { useHistoricalData } from './hooks/useHistoricalData';
import { useTrajectory } from './hooks/useTrajectory';
@ -103,6 +104,18 @@ function App() {
setSelectedBody(body);
};
// Screenshot handler with auth check
const handleScreenshot = useCallback(() => {
if (!user) {
message.warning('请先登录以拍摄宇宙快照');
setShowAuthModal(true);
return;
}
// Use username or full_name or fallback
const nickname = user.full_name || user.username || 'Explorer';
takeScreenshot(nickname);
}, [user, takeScreenshot]);
// Auth handlers
const handleLoginSuccess = (userData: any) => {
setUser(userData);
@ -156,7 +169,7 @@ function App() {
onToggleSound={() => setIsSoundOn(!isSoundOn)}
showDanmaku={showDanmaku}
onToggleDanmaku={() => setShowDanmaku(!showDanmaku)}
onScreenshot={takeScreenshot}
onScreenshot={handleScreenshot}
/>
{/* Auth Modal */}

View File

@ -69,7 +69,7 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
}, [selectedBody, bodies]);
return (
<div className="w-full h-full bg-black">
<div id="cosmo-scene-container" className="w-full h-full bg-black">
<Canvas
camera={{
position: [25, 20, 25], // Closer view to make solar system appear larger

View File

@ -1,57 +1,65 @@
import { useCallback } from 'react';
import * as THREE from 'three';
import html2canvas from 'html2canvas';
export function useScreenshot() {
const takeScreenshot = useCallback(() => {
// 1. Find the Three.js Canvas
const canvas = document.querySelector('canvas');
if (!canvas) {
console.error('Canvas not found');
const takeScreenshot = useCallback(async (username: string = 'Explorer') => {
// 1. Find the container that includes both the Canvas and the HTML overlays (labels)
const element = document.getElementById('cosmo-scene-container');
if (!element) {
console.error('Scene container not found');
return;
}
try {
// 2. Create a temporary 2D canvas for compositing
const tempCanvas = document.createElement('canvas');
const width = canvas.width;
const height = canvas.height;
tempCanvas.width = width;
tempCanvas.height = height;
const ctx = tempCanvas.getContext('2d');
// 2. Use html2canvas to capture the visual composite
const canvas = 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;
}
});
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 3. Draw the 3D scene onto the temp canvas
ctx.drawImage(canvas, 0, 0, width, height);
const width = canvas.width;
const height = canvas.height;
// 4. Get current camera info (approximate from scene if possible, or just current date)
// 3. 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' });
// We can try to get camera position if we had access to the store,
// but for now let's stick to generic data or we can grab it from global window if exposed
// Better: just show the Date and App Name.
// 5. Add Overlay / Watermark
// Background gradient for text
const gradient = ctx.createLinearGradient(0, height - 100, 0, height);
// Background gradient for text legibility
const gradient = ctx.createLinearGradient(0, height - 150, 0, height);
gradient.addColorStop(0, 'transparent');
gradient.addColorStop(1, 'rgba(0,0,0,0.8)');
gradient.addColorStop(1, 'rgba(0,0,0,0.9)');
ctx.fillStyle = gradient;
ctx.fillRect(0, height - 150, width, 150);
ctx.fillRect(0, height - 200, width, 200);
// App Name
// Text Settings
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 4;
// User Name & App Name (Bottom Right)
ctx.textAlign = 'right';
// Nickname@Cosmo
ctx.font = 'bold 32px sans-serif';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'right';
ctx.fillText('Cosmo', width - 40, height - 60);
ctx.fillText(`${username}@Cosmo`, width - 40, height - 60);
// Subtitle
ctx.font = '16px sans-serif';
ctx.fillStyle = '#aaaaaa';
ctx.fillText('DEEP SPACE EXPLORER', width - 40, height - 35);
// Date/Time
// Date/Time (Bottom Left)
ctx.textAlign = 'left';
ctx.font = 'bold 24px monospace';
ctx.fillStyle = '#44aaff';
@ -61,8 +69,8 @@ export function useScreenshot() {
ctx.fillStyle = '#cccccc';
ctx.fillText(timeStr, 40, height - 35);
// 6. Trigger Download
const dataUrl = tempCanvas.toDataURL('image/png');
// 4. Trigger Download
const dataUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = `Cosmo_Snapshot_${now.toISOString().slice(0,19).replace(/[:T]/g, '-')}.png`;
link.href = dataUrl;
@ -74,4 +82,4 @@ export function useScreenshot() {
}, []);
return { takeScreenshot };
}
}

View File

@ -1434,6 +1434,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -1618,6 +1623,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -2085,6 +2097,14 @@ hls.js@^1.5.17:
resolved "https://registry.npmmirror.com/hls.js/-/hls.js-1.6.15.tgz#9ce13080d143a9bc9b903fb43f081e335b8321e5"
integrity sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==
html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -2845,6 +2865,13 @@ tailwindcss@^3.4.0:
resolve "^1.22.8"
sucrase "^3.35.0"
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
@ -3000,6 +3027,13 @@ utility-types@^3.11.0:
resolved "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
vite@^7.2.4:
version "7.2.4"
resolved "https://registry.npmmirror.com/vite/-/vite-7.2.4.tgz#a3a09c7e25487612ecc1119c7d412c73da35bd4e"