From 5a4d9c973e236faa276b330de4509155789cbda7 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Thu, 23 Oct 2025 12:43:06 +0800 Subject: [PATCH] readme.md --- THINKYOU_README.md | 125 ++++++++ package.json | 8 +- pnpm-lock.yaml | 92 ++++-- src/pages/demos/base.astro | 5 +- src/pages/index.astro | 49 +-- src/styles/global.css | 1 + src/styles/thinkyou.css | 194 ++++++++++++ src/thinkyou/index.tsx | 616 +++++++++++++++++++++++++++++++++++++ 8 files changed, 1007 insertions(+), 83 deletions(-) create mode 100644 THINKYOU_README.md create mode 100644 src/styles/thinkyou.css create mode 100644 src/thinkyou/index.tsx diff --git a/THINKYOU_README.md b/THINKYOU_README.md new file mode 100644 index 0000000..b585181 --- /dev/null +++ b/THINKYOU_README.md @@ -0,0 +1,125 @@ +# 💖 ThinkYou - 浪漫的3D星空表白应用 + +一个使用 Three.js 创建的浪漫3D星空场景,"我想你了"等表白文字会从远处飞向屏幕,营造出唯美的视觉效果。 + +## ✨ 主要功能 + +### 🌟 核心效果 +- **动态文字消息**: "我想你了"、"想念你"、"好想你"、"爱你"等文字从远处飞向相机 +- **美丽的文字框**: 每条消息都有发光的粉色边框和优雅的渐变背景 +- **流畅的动画**: 文字在飞行过程中有轻微的漂浮效果 +- **智能淡出**: 文字接近相机时会逐渐透明直到消失 + +### 🌌 背景效果 +- **动态星空**: 包含数千颗不同颜色的星星,缓慢旋转营造深邃感 +- **流星效果**: 随机生成的流星从天空划过,每颗流星都有独特的颜色和轨迹 +- **相机摇摆**: 相机轻微摇摆模拟真实观感 +- **渐变背景**: 深邃的紫色到黑色渐变背景 + +### 🎮 交互控制 +- **播放/暂停**: 控制整个动画的播放状态 +- **手动生成**: 点击按钮立即生成新的表白消息 +- **速度调节**: 调整消息飞行的速度 (0.1x - 4x) +- **星空密度**: 调整星星数量 (100 - 5000颗) +- **键盘控制**: + - `空格键`: 播放/暂停 + - `回车键`: 生成新消息 + - `H键`: 显示/隐藏控制面板 + - `R键`: 重置场景 + +### 🎨 视觉优化 +- **响应式设计**: 完美适配各种屏幕尺寸 +- **优雅的UI**: 半透明面板配合毛玻璃效果 +- **发光效果**: 按钮、文字、边框的发光效果 +- **流畅动画**: 60fps流畅动画,使用requestAnimationFrame优化 +- **性能优化**: 智能的对象池管理,及时清理不需要的资源 + +## 🚀 技术栈 + +- **React 19**: 现代化的React Hooks +- **Three.js**: 3D图形渲染引擎 +- **TypeScript**: 类型安全的开发体验 +- **Tailwind CSS**: 现代化的CSS框架 +- **Astro**: 静态站点生成器 + +## 📱 使用方法 + +1. **启动项目**: + ```bash + pnpm dev + ``` + +2. **访问页面**: + - 主页: `http://localhost:4321/` + - 演示页: `http://localhost:4321/demos/base` + +3. **控制操作**: + - 使用左侧控制面板调整各种参数 + - 使用键盘快捷键进行快速操作 + - 右侧信息面板显示当前效果说明 + +## 🎯 使用场景 + +- **表白神器**: 向心爱的人表达爱意 +- **纪念日礼物**: 特殊日子的浪漫惊喜 +- **网站装饰**: 为网站添加浪漫的背景效果 +- **教学演示**: Three.js和React结合的教学案例 +- **创意展示**: 展示3D编程技能的作品集 + +## 🛠️ 自定义配置 + +### 消息内容 +在 `generateMessage` 函数中修改 `messages` 数组来自定义表白文字: + +```typescript +const messages = ['我想你了', '想念你', '好想你', '爱你', '❤️', '你的自定义文字'] +``` + +### 颜色主题 +在 `createTextGeometry` 函数中修改颜色配置: + +```typescript +// 边框颜色 +context.strokeStyle = '#ff69b4' // 粉色 +context.shadowColor = '#ff69b4' + +// 背景渐变 +gradient.addColorStop(0, 'rgba(20, 20, 40, 0.9)') +gradient.addColorStop(1, 'rgba(40, 20, 60, 0.9)') +``` + +### 星空效果 +在 `createStarField` 函数中调整星星的颜色分布和大小。 + +## 🔧 性能优化 + +- 使用 `useCallback` 优化函数重新创建 +- 智能的对象生命周期管理 +- 及时清理不需要的Three.js资源 +- 使用 `requestAnimationFrame` 实现流畅动画 +- 响应式的星空密度控制 + +## 📦 项目结构 + +``` +src/ +├── thinkyou/ +│ └── index.tsx # 主要的Three.js组件 +├── styles/ +│ ├── global.css # 全局样式 +│ └── thinkyou.css # ThinkYou应用专用样式 +└── pages/ + ├── index.astro # 主页 + └── demos/ + └── base.astro # 演示页面 +``` + +## 💝 特别说明 + +这个项目充满了对爱情的美好憧憬,每一个细节都经过精心设计,希望能为你的表白之路增添一份浪漫的色彩。无论是向心爱的人表达爱意,还是在特殊的日子里制造惊喜,这个应用都能成为你表达情感的完美工具。 + +愿所有的爱意都能被温柔以待,愿每一份真情都能得到回应。💖 + +--- + +*"我想你了" - 这简单的四个字,承载着最深的思念。* \ No newline at end of file diff --git a/package.json b/package.json index 2031701..c7d8bf8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "@kevisual/astro-simplate-template", + "name": "@kevisual/xhs-think-you", "version": "0.0.1", "description": "", "main": "index.js", - "basename": "/root/astro-simplate-template", + "basename": "/root/xhs-think-you", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "pub": "envision deploy ./dist -k astro-simplate-template -v 0.0.1 -u", + "pub": "envision deploy ./dist -k xhs-think-you -v 0.0.1 -u", "ui": "pnpm dlx shadcn@latest add " }, "keywords": [], @@ -24,6 +24,7 @@ "@kevisual/registry": "^0.0.1", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.15", + "@types/three": "^0.180.0", "astro": "^5.14.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -35,6 +36,7 @@ "react-dom": "^19.2.0", "react-toastify": "^11.0.5", "tailwind-merge": "^3.3.1", + "three": "^0.180.0", "zustand": "^5.0.8" }, "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ea5bb1..17870e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.15 version: 4.1.15(vite@6.3.6(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)) + '@types/three': + specifier: ^0.180.0 + version: 0.180.0 astro: specifier: ^5.14.8 version: 5.14.8(@types/node@24.7.2)(idb-keyval@6.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.9.3) @@ -65,6 +68,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + three: + specifier: ^0.180.0 + version: 0.180.0 zustand: specifier: ^5.0.8 version: 5.0.8(@types/react@19.2.2)(react@19.2.0) @@ -219,6 +225,9 @@ packages: resolution: {integrity: sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA==} engines: {node: '>=18'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} @@ -408,92 +417,78 @@ packages: resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.3': resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.3': resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.3': resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.3': resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.3': resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.3': resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.4': resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.4': resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.4': resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.4': resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.4': resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.4': resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.4': resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.4': resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} @@ -652,67 +647,56 @@ packages: resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.4': resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.4': resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.4': resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.4': resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.4': resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.4': resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.4': resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.4': resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.4': resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.4': resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.4': resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} @@ -801,28 +785,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.15': resolution: {integrity: sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.15': resolution: {integrity: sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.15': resolution: {integrity: sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.15': resolution: {integrity: sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==} @@ -857,6 +837,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -916,12 +899,21 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.180.0': + resolution: {integrity: sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -937,6 +929,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@webgpu/types@0.1.66': + resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1252,6 +1247,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -1473,28 +1471,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -1601,6 +1595,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + meshoptimizer@0.22.0: + resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -2053,6 +2050,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + three@0.180.0: + resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -2572,6 +2572,8 @@ snapshots: dependencies: fontkit: 2.0.4 + '@dimforge/rapier3d-compat@0.12.0': {} + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 @@ -3069,6 +3071,8 @@ snapshots: tailwindcss: 4.1.15 vite: 6.3.6(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2) + '@tweenjs/tween.js@23.1.3': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -3140,10 +3144,24 @@ snapshots: dependencies: '@types/node': 17.0.45 + '@types/stats.js@0.17.4': {} + + '@types/three@0.180.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.66 + fflate: 0.8.2 + meshoptimizer: 0.22.0 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} + '@types/webxr@0.5.24': {} + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-basic-ssl@2.1.0(vite@6.3.6(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2))': @@ -3162,6 +3180,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@webgpu/types@0.1.66': {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3541,6 +3561,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + flattie@1.1.1: {} fontace@0.3.1: @@ -4054,6 +4076,8 @@ snapshots: mdn-data@2.12.2: {} + meshoptimizer@0.22.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -4754,6 +4778,8 @@ snapshots: tapable@2.3.0: {} + three@0.180.0: {} + tiny-inflate@1.0.3: {} tinyexec@1.0.1: {} diff --git a/src/pages/demos/base.astro b/src/pages/demos/base.astro index adf099f..90e83e7 100644 --- a/src/pages/demos/base.astro +++ b/src/pages/demos/base.astro @@ -1,9 +1,8 @@ --- import Html from '@/components/html.astro'; +import { App } from '@/thinkyou'; --- -
- -
+ diff --git a/src/pages/index.astro b/src/pages/index.astro index 3e848df..63feab2 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,47 +1,8 @@ --- -// import { query } from '@/modules/query.ts'; -console.log('Hello from index.astro'); -import '../styles/global.css'; +import Html from '@/components/html.astro'; +import { App } from '@/thinkyou'; --- - - - My Homepage - - -

Welcome to my website!

-
-
- - - - + + + diff --git a/src/styles/global.css b/src/styles/global.css index 7404224..96d8da0 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,5 +1,6 @@ @import 'tailwindcss'; @import "tw-animate-css"; +@import "./thinkyou.css"; @custom-variant dark (&:is(.dark *)); diff --git a/src/styles/thinkyou.css b/src/styles/thinkyou.css new file mode 100644 index 0000000..9a3f8b0 --- /dev/null +++ b/src/styles/thinkyou.css @@ -0,0 +1,194 @@ +/* Three.js ThinkYou 应用的自定义样式 */ + +/* 滑块样式 */ +.slider::-webkit-slider-thumb { + appearance: none; + height: 20px; + width: 20px; + border-radius: 50%; + background: linear-gradient(45deg, #ff69b4, #9d4edd); + cursor: pointer; + box-shadow: 0 4px 8px rgba(255, 105, 180, 0.3); + transition: all 0.3s ease; +} + +.slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 6px 12px rgba(255, 105, 180, 0.5); +} + +.slider::-webkit-slider-track { + height: 8px; + border-radius: 4px; + background: linear-gradient(90deg, #4a5568, #2d3748); + outline: none; +} + +.slider::-moz-range-thumb { + height: 20px; + width: 20px; + border-radius: 50%; + background: linear-gradient(45deg, #ff69b4, #9d4edd); + cursor: pointer; + border: none; + box-shadow: 0 4px 8px rgba(255, 105, 180, 0.3); + transition: all 0.3s ease; +} + +.slider::-moz-range-thumb:hover { + transform: scale(1.2); + box-shadow: 0 6px 12px rgba(255, 105, 180, 0.5); +} + +.slider::-moz-range-track { + height: 8px; + border-radius: 4px; + background: linear-gradient(90deg, #4a5568, #2d3748); + outline: none; + border: none; +} + +/* 键盘按键样式 */ +kbd { + font-family: 'Courier New', monospace; + font-size: 0.75rem; + font-weight: bold; + border: 1px solid #4a5568; + border-bottom: 2px solid #2d3748; + border-radius: 3px; + padding: 2px 6px; + background: linear-gradient(180deg, #4a5568, #2d3748); + color: #e2e8f0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +/* 容器样式优化 */ +.thinkyou-container { + background: radial-gradient(ellipse at center, #1a202c 0%, #0f0f23 70%, #000000 100%); + position: relative; + overflow: hidden; +} + +/* 控制面板动画 */ +@keyframes slideInLeft { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.control-panel { + animation: slideInLeft 0.5s ease-out; +} + +/* 信息面板动画 */ +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.info-panel { + animation: slideInRight 0.5s ease-out; +} + +/* 按钮悬停效果 */ +.control-button { + position: relative; + overflow: hidden; +} + +.control-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; +} + +.control-button:hover::before { + left: 100%; +} + +/* 加载动画 */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading-text { + animation: pulse 2s infinite; +} + +/* 星空背景增强 */ +.starfield-bg { + background: + radial-gradient(2px 2px at 20px 30px, #eee, transparent), + radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.8), transparent), + radial-gradient(1px 1px at 90px 40px, #fff, transparent), + radial-gradient(1px 1px at 130px 80px, rgba(255, 255, 255, 0.6), transparent), + radial-gradient(2px 2px at 160px 30px, #fff, transparent); + background-repeat: repeat; + background-size: 200px 100px; + animation: twinkle 10s infinite linear; +} + +@keyframes twinkle { + from { transform: translateY(0); } + to { transform: translateY(-100px); } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .control-panel { + width: calc(100% - 2rem); + max-width: 350px; + } + + .info-panel { + width: calc(100% - 2rem); + max-width: 300px; + bottom: 1rem; + right: 1rem; + } + + .control-button { + font-size: 0.875rem; + padding: 0.5rem 1rem; + } +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(45deg, #ff69b4, #9d4edd); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(45deg, #ff1493, #8b5cf6); +} \ No newline at end of file diff --git a/src/thinkyou/index.tsx b/src/thinkyou/index.tsx new file mode 100644 index 0000000..7d13dec --- /dev/null +++ b/src/thinkyou/index.tsx @@ -0,0 +1,616 @@ + +import React, { useEffect, useRef, useState, useCallback } from 'react' +import * as THREE from 'three' + +interface TextMessage { + mesh: THREE.Mesh + position: THREE.Vector3 + targetPosition: THREE.Vector3 + speed: number + id: number + opacity: number +} + +interface Meteor { + mesh: THREE.Mesh + velocity: THREE.Vector3 + life: number +} + +export const App = () => { + const containerRef = useRef(null) + const sceneRef = useRef() + const rendererRef = useRef() + const cameraRef = useRef() + const textMessagesRef = useRef([]) + const starsRef = useRef() + const meteorsRef = useRef([]) + const meteorGroupRef = useRef() + const animationIdRef = useRef() + const clockRef = useRef(new THREE.Clock()) + + const [isPlaying, setIsPlaying] = useState(true) + const [messageSpeed, setMessageSpeed] = useState(1) + const [starCount, setStarCount] = useState(1500) + const [showControls, setShowControls] = useState(false) + const [backgroundMusic, setBackgroundMusic] = useState(false) + + // 创建文本几何体 + const createTextGeometry = useCallback((text: string) => { + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d')! + canvas.width = 512 + canvas.height = 256 + + // 创建渐变背景 + const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height) + gradient.addColorStop(0, 'rgba(20, 20, 40, 0.9)') + gradient.addColorStop(1, 'rgba(40, 20, 60, 0.9)') + context.fillStyle = gradient + context.fillRect(0, 0, canvas.width, canvas.height) + + // 绘制发光边框 + context.strokeStyle = '#ff69b4' + context.lineWidth = 6 + context.shadowColor = '#ff69b4' + context.shadowBlur = 15 + context.strokeRect(15, 15, canvas.width - 30, canvas.height - 30) + + // 重置阴影并绘制内边框 + context.shadowColor = 'transparent' + context.strokeStyle = '#ffffff' + context.lineWidth = 2 + context.strokeRect(20, 20, canvas.width - 40, canvas.height - 40) + + // 绘制发光文本 + context.fillStyle = '#ffffff' + context.font = 'bold 56px "Microsoft YaHei", Arial, sans-serif' + context.textAlign = 'center' + context.textBaseline = 'middle' + context.shadowColor = '#ff69b4' + context.shadowBlur = 10 + context.fillText(text, canvas.width / 2, canvas.height / 2) + + // 再次绘制文本以增强效果 + context.shadowBlur = 5 + context.fillText(text, canvas.width / 2, canvas.height / 2) + + const texture = new THREE.CanvasTexture(canvas) + texture.minFilter = THREE.LinearFilter + texture.magFilter = THREE.LinearFilter + + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + alphaTest: 0.1 + }) + + const geometry = new THREE.PlaneGeometry(12, 6) + return { geometry, material } + }, []) + + // 创建星空背景 + const createStarField = useCallback((count: number) => { + const geometry = new THREE.BufferGeometry() + const positions = new Float32Array(count * 3) + const colors = new Float32Array(count * 3) + const sizes = new Float32Array(count) + + for (let i = 0; i < count; i++) { + // 更广阔的星空分布 + positions[i * 3] = (Math.random() - 0.5) * 3000 + positions[i * 3 + 1] = (Math.random() - 0.5) * 3000 + positions[i * 3 + 2] = (Math.random() - 0.5) * 3000 + + // 更丰富的星星颜色 + const color = new THREE.Color() + const hue = Math.random() + if (hue < 0.1) { // 红色星星 + color.setHSL(0, 0.8, 0.9) + } else if (hue < 0.2) { // 蓝色星星 + color.setHSL(0.6, 0.8, 0.9) + } else if (hue < 0.3) { // 黄色星星 + color.setHSL(0.15, 0.8, 0.9) + } else { // 白色星星 + color.setHSL(0, 0, Math.random() * 0.5 + 0.5) + } + + colors[i * 3] = color.r + colors[i * 3 + 1] = color.g + colors[i * 3 + 2] = color.b + + // 随机星星大小 + sizes[i] = Math.random() * 3 + 1 + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) + geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)) + + const material = new THREE.PointsMaterial({ + size: 2, + vertexColors: true, + blending: THREE.AdditiveBlending, + transparent: true, + sizeAttenuation: true + }) + + return new THREE.Points(geometry, material) + }, []) + + // 创建流星 + const createMeteor = useCallback((): Meteor => { + const geometry = new THREE.CylinderGeometry(0.1, 0.05, 30, 8) + + // 创建发光材质 + const material = new THREE.MeshBasicMaterial({ + color: new THREE.Color().setHSL(0.1 + Math.random() * 0.2, 1, 0.8), + transparent: true, + opacity: 0.9, + emissive: new THREE.Color().setHSL(0.1 + Math.random() * 0.2, 0.5, 0.3) + }) + + const meteor = new THREE.Mesh(geometry, material) + + // 随机起始位置(从天空某处开始) + const startX = (Math.random() - 0.5) * 2000 + const startY = Math.random() * 300 + 200 + const startZ = (Math.random() - 0.5) * 2000 + + meteor.position.set(startX, startY, startZ) + + // 创建随机速度向量 + const velocity = new THREE.Vector3( + (Math.random() - 0.5) * 4, + -Math.random() * 8 - 2, // 向下的速度 + (Math.random() - 0.5) * 4 + ) + + // 根据速度方向旋转流星 + const direction = velocity.clone().normalize() + meteor.lookAt(meteor.position.clone().add(direction)) + + return { + mesh: meteor, + velocity, + life: 1.0 + } + }, []) + + // 初始化场景 + useEffect(() => { + if (!containerRef.current) return + + // 创建场景 + const scene = new THREE.Scene() + scene.background = new THREE.Color(0x000011) + sceneRef.current = scene + + // 创建相机 + const camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 2000 + ) + camera.position.z = 50 + cameraRef.current = camera + + // 创建渲染器 + const renderer = new THREE.WebGLRenderer({ antialias: true }) + renderer.setSize(window.innerWidth, window.innerHeight) + renderer.setPixelRatio(window.devicePixelRatio) + containerRef.current.appendChild(renderer.domElement) + rendererRef.current = renderer + + // 创建星空 + const stars = createStarField(starCount) + scene.add(stars) + starsRef.current = stars + + // 创建流星组 + const meteorGroup = new THREE.Group() + scene.add(meteorGroup) + meteorGroupRef.current = meteorGroup + meteorsRef.current = [] + + // 添加环境光 + const ambientLight = new THREE.AmbientLight(0x404040, 0.4) + scene.add(ambientLight) + + // 添加点光源 + const pointLight = new THREE.PointLight(0xffffff, 1, 100) + pointLight.position.set(10, 10, 10) + scene.add(pointLight) + + // 处理窗口大小变化 + const handleResize = () => { + if (!camera || !renderer) return + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) + } + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + if (containerRef.current && renderer.domElement) { + containerRef.current.removeChild(renderer.domElement) + } + renderer.dispose() + } + }, [starCount]) + + // 生成文本消息 + const generateMessage = useCallback(() => { + if (!sceneRef.current) return + + const messages = ['我想你了', '想念你', '好想你', '爱你', '❤️', '思念如潮', '心动不已', '梦见你', '牵挂你', '深爱着你', '情深如海', '永远爱你', '陪伴你', '守护你', '珍惜你'] + const randomMessage = messages[Math.floor(Math.random() * messages.length)] + + const { geometry, material } = createTextGeometry(randomMessage) + const mesh = new THREE.Mesh(geometry, material) + + // 随机起始位置(更远的距离) + const startDistance = 300 + Math.random() * 200 + const angle = Math.random() * Math.PI * 2 + const height = (Math.random() - 0.5) * 150 + + mesh.position.set( + Math.cos(angle) * startDistance, + height, + Math.sin(angle) * startDistance + ) + + // 目标位置(靠近相机但有随机偏移) + const targetPosition = new THREE.Vector3( + (Math.random() - 0.5) * 30, + (Math.random() - 0.5) * 30, + Math.random() * 15 + 8 + ) + + // 初始时让文本面向相机 + mesh.lookAt(cameraRef.current!.position) + mesh.up.set(0, 1, 0) // 确保文本保持正立 + + sceneRef.current.add(mesh) + + const message: TextMessage = { + mesh, + position: mesh.position.clone(), + targetPosition, + speed: 0.8 + Math.random() * 0.7, + id: Date.now() + Math.random(), + opacity: 1 + } + + textMessagesRef.current.push(message) + }, [createTextGeometry]) + + // 批量生成消息 + const generateBatchMessages = useCallback((count: number) => { + for (let i = 0; i < count; i++) { + // 添加小延迟避免ID冲突 + setTimeout(() => generateMessage(), i * 50) + } + }, [generateMessage]) + + // 动画循环 + const animate = useCallback(() => { + if (!sceneRef.current || !rendererRef.current || !cameraRef.current) return + + const deltaTime = clockRef.current.getDelta() + const elapsedTime = clockRef.current.getElapsedTime() + + // 更新文本消息 + textMessagesRef.current = textMessagesRef.current.filter(message => { + const direction = message.targetPosition.clone().sub(message.position).normalize() + message.position.add(direction.multiplyScalar(message.speed * messageSpeed * deltaTime * 60)) + message.mesh.position.copy(message.position) + + // 添加轻微的漂浮效果 + message.mesh.position.y += Math.sin(elapsedTime * 2 + message.id) * 0.3 + + // 让文本面向相机后面一点,创建更自然的视角 + const cameraPosition = cameraRef.current!.position + const offsetPosition = cameraPosition.clone() + // 在相机位置后面添加一个偏移 + offsetPosition.z -= 20 // 向后偏移20个单位 + message.mesh.lookAt(offsetPosition) + + // 确保文本卡片保持正立状态,避免倾斜 + message.mesh.up.set(0, 1, 0) + + // 检查距离并处理透明度 + const distance = message.position.distanceTo(message.targetPosition) + + // 当接近目标时开始淡出 + if (distance < 15) { + message.opacity = Math.max(0, distance / 15) + if (message.mesh.material instanceof THREE.MeshBasicMaterial) { + message.mesh.material.opacity = message.opacity + } + } + + // 移除过近或完全透明的消息,增加生命周期延长 + if (distance < 1 || message.opacity <= 0 || message.position.z > 100) { + sceneRef.current!.remove(message.mesh) + message.mesh.geometry.dispose() + if (message.mesh.material instanceof THREE.Material) { + message.mesh.material.dispose() + } + return false + } + + return true + }) + + // 旋转星空 + if (starsRef.current) { + starsRef.current.rotation.y += 0.0008 + starsRef.current.rotation.x += 0.0003 + } + + // 更新流星 + meteorsRef.current = meteorsRef.current.filter(meteor => { + // 更新位置 + meteor.mesh.position.add(meteor.velocity.clone().multiplyScalar(deltaTime * 60)) + + // 减少生命值 + meteor.life -= deltaTime * 0.5 + + // 更新透明度 + if (meteor.mesh.material instanceof THREE.MeshBasicMaterial) { + meteor.mesh.material.opacity = meteor.life + } + + // 移除生命值耗尽或位置过远的流星 + if (meteor.life <= 0 || meteor.mesh.position.y < -500) { + meteorGroupRef.current!.remove(meteor.mesh) + meteor.mesh.geometry.dispose() + if (meteor.mesh.material instanceof THREE.Material) { + meteor.mesh.material.dispose() + } + return false + } + + return true + }) + + // 随机添加新流星 + if (Math.random() < 0.1) { + const meteor = createMeteor() + meteorGroupRef.current!.add(meteor.mesh) + meteorsRef.current.push(meteor) + } + + // 优雅的相机移动 + if (cameraRef.current) { + cameraRef.current.position.x = Math.sin(elapsedTime * 0.3) * 3 + cameraRef.current.position.y = Math.cos(elapsedTime * 0.2) * 2 + cameraRef.current.lookAt(0, 0, 0) + } + + rendererRef.current.render(sceneRef.current, cameraRef.current) + + if (isPlaying) { + animationIdRef.current = requestAnimationFrame(animate) + } + }, [isPlaying, messageSpeed, createMeteor]) + + // 开始动画 + useEffect(() => { + if (isPlaying) { + animate() + } else { + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current) + } + } + + return () => { + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current) + } + } + }, [isPlaying, messageSpeed]) + + // 定期生成消息 + useEffect(() => { + if (!isPlaying) return + + // 初始批量生成20-40个消息 + const initialCount = Math.floor(Math.random() * 21) + 20 // 20-40个 + setTimeout(() => generateBatchMessages(initialCount), 500) + + const interval = setInterval(() => { + const currentCount = textMessagesRef.current.length + const targetCount = Math.floor(Math.random() * 81) + 20 // 20-100个目标数量 + + // 如果当前消息数量少于目标数量,生成新消息 + if (currentCount < targetCount) { + const generateCount = Math.min(20, targetCount - currentCount) // 每次最多生成10个 + for (let i = 0; i < generateCount; i++) { + setTimeout(() => generateMessage(), i * 100) + } + } + }, 2000 + Math.random() * 3000) // 2-5秒间隔检查 + + return () => clearInterval(interval) + }, [isPlaying, generateMessage, generateBatchMessages]) + + // 更新星空 + useEffect(() => { + if (starsRef.current && sceneRef.current) { + sceneRef.current.remove(starsRef.current) + const newStars = createStarField(starCount) + sceneRef.current.add(newStars) + starsRef.current = newStars + } + }, [starCount]) + + // 键盘控制 + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + switch (event.key.toLowerCase()) { + case ' ': + event.preventDefault() + setIsPlaying(!isPlaying) + break + case 'enter': + event.preventDefault() + generateMessage() + break + case 'h': + setShowControls(!showControls) + break + case 'r': + // 重置场景 + textMessagesRef.current.forEach(message => { + sceneRef.current?.remove(message.mesh) + message.mesh.geometry.dispose() + if (message.mesh.material instanceof THREE.Material) { + message.mesh.material.dispose() + } + }) + textMessagesRef.current = [] + break + } + } + + window.addEventListener('keydown', handleKeyPress) + return () => window.removeEventListener('keydown', handleKeyPress) + }, [isPlaying, showControls, generateMessage]) + + return ( +
+
+ + {/* 控制面板 */} + {showControls && ( +
+

+ ✨ 控制面板 +

+ +
+
+ + + + + + + +
+ +
+
+ + setMessageSpeed(Number(e.target.value))} + className="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider" + /> +
+ +
+ + setStarCount(Number(e.target.value))} + className="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider" + /> +
+
+ +
+
+ 当前消息数量: {textMessagesRef.current.length} +
+

+ 空格 播放/暂停 • + 回车 生成消息 • + H 隐藏面板 +

+
+
+
+ )} + + {/* 隐藏控制面板时的提示 */} + {!showControls && ( +
+

H 显示控制面板

+
+ )} + + {/* 信息提示 */} +
+
+

+ � 浪漫星空 +

+
+

💫 爱的话语从远方飞来

+

✨ 星空在你身边旋转

+

🌠 流星带着祝福划过

+

💝 每一句都是对你的思念

+
+
+
+ + {/* 加载提示 */} +
+
+
+ ✨ 正在准备浪漫的星空... ✨ +
+
+
+
+ ) +} \ No newline at end of file