Files
home/christmas.html
hehh b5974a4407 feat(christmas): 添加圣诞主题3D互动页面
- 创建了基于Three.js的3D圣诞树场景
- 实现了粒子系统,包括装饰球、礼盒和彩带
- 添加了照片上传功能,支持用户自定义装饰
- 集成了手势识别,可通过手势控制视角和模式
- 设计了多种显示模式:散落、树形和聚焦视图
- 添加了后期处理效果,增强视觉体验
- 实现了响应式设计,适配不同屏幕尺寸
- 添加了节日加载动画和UI界面
- 配置了SEO和社交平台分享元数据
2025-12-12 16:47:58 +08:00

673 lines
25 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;
}
</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: 1500,
dustCount: 2500,
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();
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" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 2.2;
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.5, 0.4, 0.85);
bloomPass.threshold = 0.7;
bloomPass.strength = 0.45;
bloomPass.radius = 0.4;
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();
}
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, lerpSpeed * dt);
// 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; // Added Z for more natural tumble
} else if (mode === 'TREE') {
// Reset rotations slowly
this.mesh.rotation.x = THREE.MathUtils.lerp(this.mesh.rotation.x, 0, dt);
this.mesh.rotation.z = THREE.MathUtils.lerp(this.mesh.rotation.z, 0, dt);
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), 4*dt);
}
}
// --- CREATION ---
function createParticles() {
const sphereGeo = new THREE.SphereGeometry(0.5, 32, 32);
const boxGeo = new THREE.BoxGeometry(0.55, 0.55, 0.55);
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, 16, 0.08, 8, false);
const goldMat = new THREE.MeshStandardMaterial({
color: CONFIG.colors.champagneGold,
metalness: 1.0, roughness: 0.1,
envMapIntensity: 2.0,
emissive: 0x443300,
emissiveIntensity: 0.3
});
const greenMat = new THREE.MeshStandardMaterial({
color: CONFIG.colors.deepGreen,
metalness: 0.2, roughness: 0.8,
emissive: 0x002200,
emissiveIntensity: 0.2
});
const redMat = new THREE.MeshPhysicalMaterial({
color: CONFIG.colors.accentRed,
metalness: 0.3, roughness: 0.2, clearcoat: 1.0,
emissive: 0x330000
});
const candyMat = new THREE.MeshStandardMaterial({ map: caneTexture, roughness: 0.4 });
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("NOEL", 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;
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;
}
} else {
STATE.hand.detected = false;
}
}
function setupEvents() {
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
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 = clock.getDelta();
// 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) * 3.0 * dt;
STATE.rotation.x += (targetRotX - STATE.rotation.x) * 3.0 * dt;
} else {
if(STATE.mode === 'TREE') {
STATE.rotation.y += 0.3 * dt;
STATE.rotation.x += (0 - STATE.rotation.x) * 2.0 * dt;
} 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>
</body>
</html>