Files
home/christmas.html
hehh 785bf0fe61 perf(christmas): 优化圣诞树页面性能
- 减少CSS代码行数,合并重复样式声明
- 降低粒子系统中几何体的分段数以减少内存占用
- 减少粒子和尘埃粒子的数量以提升渲染性能
- 添加硬件加速样式以提高动画流畅度
- 优化WebGL渲染器配置,限制像素比率
- 调整后期处理效果参数以平衡视觉效果与性能
- 优化动画插值计算,防止过度计算
- 在窗口大小调整事件中添加防抖动处理
- 限制动画帧时间差最大值以稳定动画表现
- 添加手势状态变化检测以减少不必要的状态切换
2025-12-12 17:05:25 +08:00

795 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于我 - Honesty</title>
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--SEO信息 -->
<meta name="description"
content="关于Honesty,关于HeHouHui,关于HeHui,关于明厚, About Me Honesty, About Me HeHouHui, About Me HeHui">
<meta name="keywords" content="Honesty,HeHouHui,HeHui,明厚">
<link rel="canonical" href="https://www.hehouhui.cn/about.html">
<meta name="author" content="Honesty">
<!-- 社交平台分享优化 -->
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://www.hehouhui.cn/christmas.html">
<meta property="og:title" content="🎄圣诞快乐🎉 - Honesty的个人主页">
<meta property="og:description"
content="我是一名充满热情的Java后端开发工程师专注于AI技术的探索与应用。来自湖南现在上海工作享受在这座充满活力的城市中追求技术梦想。">
<meta property="og:image" content="https://www.hehouhui.cn/images/avatar.jpeg">
<meta property="og:site_name" content="Honesty的个人主页">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://www.hehouhui.cn/christmas.html">
<meta property="twitter:title" content="🎄圣诞快乐🎉 - Honesty的个人主页">
<meta property="twitter:description"
content="我是一名充满热情的Java后端开发工程师专注于AI技术的探索与应用。来自湖南现在上海工作享受在这座充满活力的城市中追求技术梦想。">
<meta property="twitter:image" content="https://www.hehouhui.cn/images/avatar.jpeg">
<meta property="twitter:site" content="@Honesty861024">
<link rel="alternate" hreflang="zh-cn" href="https://www.hehouhui.cn/about.html">
<link rel="alternate" hreflang="en" href="https://www.hehouhui.cn/about.html?lang=en">
<link rel="alternate" hreflang="x-default" href="https://www.hehouhui.cn/about.html">
<!-- 微信小程序/朋友圈分享 -->
<meta property="wechat:image" content="https://www.hehouhui.cn/images/avatar.jpeg">
<meta property="wechat:title" content="关于我 - Honesty的个人主页">
<meta property="wechat:description" content="我是一名充满热情的Java后端开发工程师专注于AI技术的探索与应用。">
<title>Grand Luxury Tree Final v2</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000000; font-family: 'Times New Roman', serif; }
#canvas-container { width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; }
/* UI Overlay - Minimalist */
#ui-layer {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
z-index: 10; pointer-events: none;
display: flex; flex-direction: column;
align-items: center;
padding-top: 40px;
box-sizing: border-box;
/* Remove transition here as we don't hide the whole layer anymore */
}
/* When hidden class is applied to specific elements */
.ui-hidden {
opacity: 0;
pointer-events: none !important;
}
/* Loading */
#loader {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #000; z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.8s ease-out;
}
.loader-text {
color: #d4af37; font-size: 14px; letter-spacing: 4px; margin-top: 20px;
text-transform: uppercase; font-weight: 100;
}
.spinner {
width: 40px; height: 40px; border: 1px solid rgba(212, 175, 55, 0.2);
border-top: 1px solid #d4af37; border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Typography - Centerpiece */
h1 {
color: #fceea7; font-size: 56px; margin: 0; font-weight: 400;
letter-spacing: 6px;
text-shadow: 0 0 50px rgba(252, 238, 167, 0.6);
background: linear-gradient(to bottom, #fff, #eebb66);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
font-family: 'Cinzel', 'Times New Roman', serif;
opacity: 0.9;
transition: opacity 0.5s ease; /* Ensure smooth transitions if needed */
}
/* Upload Button - Restored & Elegant */
.upload-wrapper {
margin-top: 20px;
pointer-events: auto;
text-align: center;
transition: opacity 0.5s ease; /* Add transition for smooth hiding */
}
.upload-btn {
background: rgba(20, 20, 20, 0.6);
border: 1px solid rgba(212, 175, 55, 0.4);
color: #d4af37;
padding: 10px 25px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 3px;
font-size: 10px;
transition: all 0.4s;
display: inline-block;
backdrop-filter: blur(5px);
}
.upload-btn:hover {
background: #d4af37;
color: #000;
box-shadow: 0 0 20px rgba(212, 175, 55, 0.5);
}
.hint-text {
color: rgba(212, 175, 55, 0.5);
font-size: 9px;
margin-top: 8px;
letter-spacing: 1px;
text-transform: uppercase;
}
#file-input { display: none; }
/* Webcam feedback */
#webcam-wrapper {
position: absolute; bottom: 40px; right: 40px;
width: 120px; height: 90px;
border: 1px solid rgba(255,255,255,0.1);
overflow: hidden; opacity: 0; /* Hidden by default but functional */
pointer-events: none;
}
/* 添加硬件加速样式以提高性能 */
#canvas-container, #ui-layer, #loader {
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
}
.particle {
will-change: transform;
}
</style>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap');
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"@mediapipe/tasks-vision": "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/+esm"
}
}
</script>
</head>
<body>
<div id="loader">
<div class="spinner"></div>
<div class="loader-text">Loading Holiday Magic</div>
</div>
<div id="canvas-container"></div>
<div id="ui-layer">
<h1>Merry Christmas - Honesty</h1>
<div class="upload-wrapper">
<label class="upload-btn">
Add Memories
<input type="file" id="file-input" multiple accept="image/*">
</label>
<div class="hint-text">Press 'H' to Hide Controls</div>
</div>
</div>
<!-- Webcam hidden structure -->
<div id="webcam-wrapper">
<video id="webcam" autoplay playsinline style="display:none;"></video>
<canvas id="webcam-preview"></canvas>
</div>
<script type="module">
import * as THREE from 'three';
import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js';
import {RoomEnvironment} from 'three/addons/environments/RoomEnvironment.js';
import {FilesetResolver, HandLandmarker} from '@mediapipe/tasks-vision';
// --- CONFIGURATION ---
const CONFIG = {
colors: {
bg: 0x000000,
champagneGold: 0xffd966,
deepGreen: 0x03180a,
accentRed: 0x990000,
},
particles: {
count: 1000, // 减少粒子数量以提高性能
dustCount: 2000, // 减少尘埃粒子数量
treeHeight: 24,
treeRadius: 8
},
camera: {
z: 50
}
};
const STATE = {
mode: 'SCATTER',
focusIndex: -1,
focusTarget: null,
hand: {detected: false, x: 0, y: 0},
rotation: {x: 0, y: 0}
};
let scene, camera, renderer, composer;
let mainGroup;
let clock = new THREE.Clock();
let particleSystem = [];
let photoMeshGroup = new THREE.Group();
let handLandmarker, video, webcamCanvas, webcamCtx;
let caneTexture;
async function init() {
initThree();
setupEnvironment();
setupLights();
createTextures();
createParticles();
createDust();
createDefaultPhotos();
setupPostProcessing();
setupEvents();
await initMediaPipe();
// Play background music
const audio = document.getElementById('bg-music');
if (audio) {
// 尝试自动播放音频
try {
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
console.log("Audio autoplay failed:", error);
});
}
} catch (error) {
console.error("Error attempting to play audio:", error);
}
}
const loader = document.getElementById('loader');
loader.style.opacity = 0;
setTimeout(() => loader.remove(), 800);
animate();
}
function initThree() {
const container = document.getElementById('canvas-container');
scene = new THREE.Scene();
scene.background = new THREE.Color(CONFIG.colors.bg);
scene.fog = new THREE.FogExp2(CONFIG.colors.bg, 0.01);
camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, CONFIG.camera.z);
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
stencil: false,
depth: true,
logarithmicDepthBuffer: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 2.2;
renderer.setClearColor(0x000000, 1);
container.appendChild(renderer.domElement);
mainGroup = new THREE.Group();
scene.add(mainGroup);
}
function setupEnvironment() {
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
}
function setupLights() {
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const innerLight = new THREE.PointLight(0xffaa00, 2, 20);
innerLight.position.set(0, 5, 0);
mainGroup.add(innerLight);
const spotGold = new THREE.SpotLight(0xffcc66, 1200);
spotGold.position.set(30, 40, 40);
spotGold.angle = 0.5;
spotGold.penumbra = 0.5;
scene.add(spotGold);
const spotBlue = new THREE.SpotLight(0x6688ff, 600);
spotBlue.position.set(-30, 20, -30);
scene.add(spotBlue);
const fill = new THREE.DirectionalLight(0xffeebb, 0.8);
fill.position.set(0, 0, 50);
scene.add(fill);
}
function setupPostProcessing() {
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.4, 0.85);
bloomPass.threshold = 0.8;
bloomPass.strength = 0.35;
bloomPass.radius = 0.35;
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
}
function createTextures() {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 128, 128);
ctx.fillStyle = '#880000';
ctx.beginPath();
for (let i = -128; i < 256; i += 32) {
ctx.moveTo(i, 0);
ctx.lineTo(i + 32, 128);
ctx.lineTo(i + 16, 128);
ctx.lineTo(i - 16, 0);
}
ctx.fill();
caneTexture = new THREE.CanvasTexture(canvas);
caneTexture.wrapS = THREE.RepeatWrapping;
caneTexture.wrapT = THREE.RepeatWrapping;
caneTexture.repeat.set(3, 3);
}
class Particle {
constructor(mesh, type, isDust = false) {
this.mesh = mesh;
this.type = type;
this.isDust = isDust;
this.posTree = new THREE.Vector3();
this.posScatter = new THREE.Vector3();
this.baseScale = mesh.scale.x;
// Individual Spin Speed
// Photos spin slower to be readable
const speedMult = (type === 'PHOTO') ? 0.3 : 2.0;
this.spinSpeed = new THREE.Vector3(
(Math.random() - 0.5) * speedMult,
(Math.random() - 0.5) * speedMult,
(Math.random() - 0.5) * speedMult
);
this.calculatePositions();
// 添加类标识符以提高性能
mesh.userData.particleType = type;
}
calculatePositions() {
// TREE: Tight Spiral
const h = CONFIG.particles.treeHeight;
const halfH = h / 2;
let t = Math.random();
t = Math.pow(t, 0.8);
const y = (t * h) - halfH;
let rMax = CONFIG.particles.treeRadius * (1.0 - t);
if (rMax < 0.5) rMax = 0.5;
const angle = t * 50 * Math.PI + Math.random() * Math.PI;
const r = rMax * (0.8 + Math.random() * 0.4);
this.posTree.set(Math.cos(angle) * r, y, Math.sin(angle) * r);
// SCATTER: 3D Sphere
let rScatter = this.isDust ? (12 + Math.random() * 20) : (8 + Math.random() * 12);
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
this.posScatter.set(
rScatter * Math.sin(phi) * Math.cos(theta),
rScatter * Math.sin(phi) * Math.sin(theta),
rScatter * Math.cos(phi)
);
}
update(dt, mode, focusTargetMesh) {
let target = this.posScatter;
if (mode === 'TREE') target = this.posTree;
else if (mode === 'FOCUS') {
if (this.mesh === focusTargetMesh) {
const desiredWorldPos = new THREE.Vector3(0, 2, 35);
const invMatrix = new THREE.Matrix4().copy(mainGroup.matrixWorld).invert();
target = desiredWorldPos.applyMatrix4(invMatrix);
} else {
target = this.posScatter;
}
}
// Movement Easing - 优化动画插值计算
const lerpSpeed = (mode === 'FOCUS' && this.mesh === focusTargetMesh) ? 5.0 : 2.0;
this.mesh.position.lerp(target, Math.min(lerpSpeed * dt, 1.0));
// Rotation Logic - CRITICAL: Ensure spin happens in Scatter
if (mode === 'SCATTER') {
this.mesh.rotation.x += this.spinSpeed.x * dt;
this.mesh.rotation.y += this.spinSpeed.y * dt;
this.mesh.rotation.z += this.spinSpeed.z * dt;
} else if (mode === 'TREE') {
// Reset rotations slowly
this.mesh.rotation.x = THREE.MathUtils.lerp(this.mesh.rotation.x, 0, Math.min(dt * 2, 1.0));
this.mesh.rotation.z = THREE.MathUtils.lerp(this.mesh.rotation.z, 0, Math.min(dt * 2, 1.0));
this.mesh.rotation.y += 0.5 * dt;
}
if (mode === 'FOCUS' && this.mesh === focusTargetMesh) {
this.mesh.lookAt(camera.position);
}
// Scale Logic
let s = this.baseScale;
if (this.isDust) {
s = this.baseScale * (0.8 + 0.4 * Math.sin(clock.elapsedTime * 4 + this.mesh.id));
if (mode === 'TREE') s = 0;
} else if (mode === 'SCATTER' && this.type === 'PHOTO') {
// Large preview size in scatter
s = this.baseScale * 2.5;
} else if (mode === 'FOCUS') {
if (this.mesh === focusTargetMesh) s = 4.5;
else s = this.baseScale * 0.8;
}
this.mesh.scale.lerp(new THREE.Vector3(s, s, s), Math.min(4 * dt, 1.0));
}
}
// --- CREATION ---
function createParticles() {
// 优化几何体和材质的创建,减少内存占用
const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); // 减少分段数以提高性能
const boxGeo = new THREE.BoxGeometry(0.55, 0.55, 0.55, 1, 1, 1); // 减少分段数
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, -0.5, 0), new THREE.Vector3(0, 0.3, 0),
new THREE.Vector3(0.1, 0.5, 0), new THREE.Vector3(0.3, 0.4, 0)
]);
const candyGeo = new THREE.TubeGeometry(curve, 8, 0.08, 4, false); // 减少分段数
const goldMat = new THREE.MeshStandardMaterial({
color: CONFIG.colors.champagneGold,
metalness: 1.0, roughness: 0.1,
envMapIntensity: 2.0,
emissive: 0x443300,
emissiveIntensity: 0.3,
flatShading: false
});
const greenMat = new THREE.MeshStandardMaterial({
color: CONFIG.colors.deepGreen,
metalness: 0.2, roughness: 0.8,
emissive: 0x002200,
emissiveIntensity: 0.2,
flatShading: false
});
const redMat = new THREE.MeshPhysicalMaterial({
color: CONFIG.colors.accentRed,
metalness: 0.3, roughness: 0.2, clearcoat: 1.0,
emissive: 0x330000,
flatShading: false
});
const candyMat = new THREE.MeshStandardMaterial({
map: caneTexture,
roughness: 0.4,
flatShading: false
});
for (let i = 0; i < CONFIG.particles.count; i++) {
const rand = Math.random();
let mesh, type;
if (rand < 0.40) {
mesh = new THREE.Mesh(boxGeo, greenMat);
type = 'BOX';
} else if (rand < 0.70) {
mesh = new THREE.Mesh(boxGeo, goldMat);
type = 'GOLD_BOX';
} else if (rand < 0.92) {
mesh = new THREE.Mesh(sphereGeo, goldMat);
type = 'GOLD_SPHERE';
} else if (rand < 0.97) {
mesh = new THREE.Mesh(sphereGeo, redMat);
type = 'RED';
} else {
mesh = new THREE.Mesh(candyGeo, candyMat);
type = 'CANE';
}
const s = 0.4 + Math.random() * 0.5;
mesh.scale.set(s, s, s);
mesh.rotation.set(Math.random() * 6, Math.random() * 6, Math.random() * 6);
mainGroup.add(mesh);
particleSystem.push(new Particle(mesh, type, false));
}
const starGeo = new THREE.OctahedronGeometry(1.2, 0);
const starMat = new THREE.MeshStandardMaterial({
color: 0xffdd88, emissive: 0xffaa00, emissiveIntensity: 1.0,
metalness: 1.0, roughness: 0
});
const star = new THREE.Mesh(starGeo, starMat);
star.position.set(0, CONFIG.particles.treeHeight / 2 + 1.2, 0);
mainGroup.add(star);
mainGroup.add(photoMeshGroup);
}
function createDust() {
// 优化灰尘粒子的几何体以提高性能
const geo = new THREE.TetrahedronGeometry(0.08, 0);
const mat = new THREE.MeshBasicMaterial({ color: 0xffeebb, transparent: true, opacity: 0.8 });
for (let i = 0; i < CONFIG.particles.dustCount; i++) {
const mesh = new THREE.Mesh(geo, mat);
mesh.scale.setScalar(0.5 + Math.random());
mainGroup.add(mesh);
particleSystem.push(new Particle(mesh, 'DUST', true));
}
}
function createDefaultPhotos() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, 512, 512);
ctx.strokeStyle = '#eebb66';
ctx.lineWidth = 15;
ctx.strokeRect(20, 20, 472, 472);
ctx.font = '500 60px Times New Roman';
ctx.fillStyle = '#eebb66';
ctx.textAlign = 'center';
ctx.fillText("JOYEUX", 256, 230);
ctx.fillText("W.F", 256, 300);
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
addPhotoToScene(tex);
}
function addPhotoToScene(texture) {
const frameGeo = new THREE.BoxGeometry(1.4, 1.4, 0.05);
const frameMat = new THREE.MeshStandardMaterial({
color: CONFIG.colors.champagneGold,
metalness: 1.0,
roughness: 0.1
});
const frame = new THREE.Mesh(frameGeo, frameMat);
const photoGeo = new THREE.PlaneGeometry(1.2, 1.2);
const photoMat = new THREE.MeshBasicMaterial({map: texture});
const photo = new THREE.Mesh(photoGeo, photoMat);
photo.position.z = 0.04;
const group = new THREE.Group();
group.add(frame);
group.add(photo);
const s = 0.8;
group.scale.set(s, s, s);
photoMeshGroup.add(group);
particleSystem.push(new Particle(group, 'PHOTO', false));
}
function handleImageUpload(e) {
const files = e.target.files;
if (!files.length) return;
Array.from(files).forEach(f => {
const reader = new FileReader();
reader.onload = (ev) => {
new THREE.TextureLoader().load(ev.target.result, (t) => {
t.colorSpace = THREE.SRGBColorSpace;
addPhotoToScene(t);
});
}
reader.readAsDataURL(f);
});
}
// --- MEDIAPIPE ---
async function initMediaPipe() {
video = document.getElementById('webcam');
webcamCanvas = document.getElementById('webcam-preview');
webcamCtx = webcamCanvas.getContext('2d');
webcamCanvas.width = 160;
webcamCanvas.height = 120;
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm"
);
handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
delegate: "GPU"
},
runningMode: "VIDEO",
numHands: 1
});
if (navigator.mediaDevices?.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({video: true});
video.srcObject = stream;
video.addEventListener("loadeddata", predictWebcam);
}
}
let lastVideoTime = -1;
async function predictWebcam() {
if (video.currentTime !== lastVideoTime) {
lastVideoTime = video.currentTime;
if (handLandmarker) {
const result = handLandmarker.detectForVideo(video, performance.now());
processGestures(result);
}
}
requestAnimationFrame(predictWebcam);
}
function processGestures(result) {
if (result.landmarks && result.landmarks.length > 0) {
STATE.hand.detected = true;
const lm = result.landmarks[0];
STATE.hand.x = (lm[9].x - 0.5) * 2;
STATE.hand.y = (lm[9].y - 0.5) * 2;
const thumb = lm[4]; const index = lm[8]; const wrist = lm[0];
const pinchDist = Math.hypot(thumb.x - index.x, thumb.y - index.y);
const tips = [lm[8], lm[12], lm[16], lm[20]];
let avgDist = 0;
tips.forEach(t => avgDist += Math.hypot(t.x - wrist.x, t.y - wrist.y));
avgDist /= 4;
// 添加手势状态变化检测以减少不必要的状态切换
const prevMode = STATE.mode;
if (pinchDist < 0.05) {
if (STATE.mode !== 'FOCUS') {
STATE.mode = 'FOCUS';
const photos = particleSystem.filter(p => p.type === 'PHOTO');
if (photos.length) STATE.focusTarget = photos[Math.floor(Math.random()*photos.length)].mesh;
}
} else if (avgDist < 0.25) {
STATE.mode = 'TREE';
STATE.focusTarget = null;
} else {
STATE.mode = 'SCATTER';
STATE.focusTarget = null;
}
// 只有在状态发生变化时才进行相关处理
if (prevMode !== STATE.mode) {
console.log(`Mode changed to: ${STATE.mode}`);
}
} else {
STATE.hand.detected = false;
}
}
function setupEvents() {
// 优化窗口大小调整事件处理,使用防抖动技术
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}, 100); // 100ms 防抖动延迟
});
document.getElementById('file-input').addEventListener('change', handleImageUpload);
// Toggle UI logic - ONLY hide controls, keep title
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'h') {
const controls = document.querySelector('.upload-wrapper');
if (controls) controls.classList.toggle('ui-hidden');
}
});
}
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
// Rotation Logic
if (STATE.mode === 'SCATTER' && STATE.hand.detected) {
const targetRotY = STATE.hand.x * Math.PI * 0.9;
const targetRotX = STATE.hand.y * Math.PI * 0.25;
STATE.rotation.y += (targetRotY - STATE.rotation.y) * Math.min(3.0 * dt, 1.0);
STATE.rotation.x += (targetRotX - STATE.rotation.x) * Math.min(3.0 * dt, 1.0);
} else {
if (STATE.mode === 'TREE') {
STATE.rotation.y += 0.3 * dt;
STATE.rotation.x += (0 - STATE.rotation.x) * Math.min(2.0 * dt, 1.0);
} else {
STATE.rotation.y += 0.1 * dt;
}
}
mainGroup.rotation.y = STATE.rotation.y;
mainGroup.rotation.x = STATE.rotation.x;
particleSystem.forEach(p => p.update(dt, STATE.mode, STATE.focusTarget));
composer.render();
}
init();
</script>
<!-- Background Music -->
<audio id="bg-music" loop preload="auto">
<source src="data/christmas.mp3" type="audio/mpeg">
</audio>
<script>
// Auto play background music
document.addEventListener('DOMContentLoaded', function () {
const audio = document.getElementById('bg-music');
try {
// 尝试自动播放
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.then(_ => {
// 自动播放成功
console.log("Background music is playing");
}).catch(error => {
// 自动播放失败(浏览器限制),需要用户交互
console.log("Auto-play prevented by browser policy:", error);
// 添加用户交互事件来播放音频
document.body.addEventListener('click', function () {
if (audio.paused) {
audio.play();
}
}, {once: true});
});
}
} catch (error) {
console.error("Error attempting to play audio:", error);
}
});
</script>
<!-- 不蒜子统计 -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// 动态加载不蒜子统计脚本
const script = document.createElement('script');
script.src = "//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.abbr.min.js";
script.async = true;
document.head.appendChild(script);
});
</script>
</body>
</html>