readme.md

This commit is contained in:
2025-10-23 12:43:06 +08:00
parent 6648e4f5ec
commit 5a4d9c973e
8 changed files with 1007 additions and 83 deletions

125
THINKYOU_README.md Normal file
View File

@@ -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 # 演示页面
```
## 💝 特别说明
这个项目充满了对爱情的美好憧憬,每一个细节都经过精心设计,希望能为你的表白之路增添一份浪漫的色彩。无论是向心爱的人表达爱意,还是在特殊的日子里制造惊喜,这个应用都能成为你表达情感的完美工具。
愿所有的爱意都能被温柔以待,愿每一份真情都能得到回应。💖
---
*"我想你了" - 这简单的四个字,承载着最深的思念。*

View File

@@ -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": {

92
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -1,9 +1,8 @@
---
import Html from '@/components/html.astro';
import { App } from '@/thinkyou';
---
<Html>
<main>
</main>
<App client:only="react" />
</Html>

View File

@@ -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';
---
<html lang='en'>
<head>
<title>My Homepage</title>
</head>
<body>
<h1 onclick="{onClick}">Welcome to my website!</h1>
<div class='bg-amber-50 w-20 h-20 rounded-full'></div>
<div id='root'></div>
<script type='importmap' data-vite-ignore is:inline>
{
"imports": {
"react": "https://esm.sh/react@19.1.0",
"react-dom": "https://esm.sh/react-dom@19.1.0/client.js",
"react-toastify": "https://esm.sh/react-toastify@11.0.5"
}
}
</script>
<script type='module' data-vite-ignore is:inline>
import { Button, message } from 'https://esm.sh/antd?standalone';
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { createRoot } from 'react-dom';
setTimeout(() => {
toast.loading('Hello from index.astro');
window.toast = toast;
console.log('message', toast);
}, 1000);
console.log('Hello from index.astro', Button);
const root = document.getElementById('root');
const render = createRoot(root);
const App = () => {
const button = React.createElement(Button, null, 'Hello');
const messageEl = React.createElement(ToastContainer, null, 'Hello');
const wrapperMessage = React.createElement('div', null, [button, messageEl]);
return wrapperMessage;
};
// render.render(React.createElement(Button, null, 'Hello'), root);
render.render(App(), root);
</script>
</body>
</html>
<Html>
<App client:only />
</Html>

View File

@@ -1,5 +1,6 @@
@import 'tailwindcss';
@import "tw-animate-css";
@import "./thinkyou.css";
@custom-variant dark (&:is(.dark *));

194
src/styles/thinkyou.css Normal file
View File

@@ -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);
}

616
src/thinkyou/index.tsx Normal file
View File

@@ -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<HTMLDivElement>(null)
const sceneRef = useRef<THREE.Scene>()
const rendererRef = useRef<THREE.WebGLRenderer>()
const cameraRef = useRef<THREE.PerspectiveCamera>()
const textMessagesRef = useRef<TextMessage[]>([])
const starsRef = useRef<THREE.Points>()
const meteorsRef = useRef<Meteor[]>([])
const meteorGroupRef = useRef<THREE.Group>()
const animationIdRef = useRef<number>()
const clockRef = useRef<THREE.Clock>(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 (
<div className="thinkyou-container relative w-full h-screen overflow-hidden bg-linear-to-b from-indigo-950 to-purple-950">
<div ref={containerRef} className="w-full h-full" />
{/* 控制面板 */}
{showControls && (
<div className="control-panel absolute top-4 left-4 bg-black/80 text-white p-5 rounded-xl backdrop-blur-md border border-white/20 shadow-2xl">
<h3 className="text-xl font-bold mb-4 text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-purple-400">
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3 flex-wrap">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="control-button px-5 py-2 bg-linear-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 rounded-lg transition-all duration-300 shadow-lg transform hover:scale-105"
>
{isPlaying ? '⏸️ 暂停' : '▶️ 播放'}
</button>
<button
onClick={generateMessage}
className="control-button px-5 py-2 bg-linear-to-r from-green-500 to-teal-600 hover:from-green-600 hover:to-teal-700 rounded-lg transition-all duration-300 shadow-lg transform hover:scale-105"
>
💝
</button>
<button
onClick={() => generateBatchMessages(Math.floor(Math.random() * 21) + 10)}
className="control-button px-5 py-2 bg-linear-to-r from-yellow-500 to-orange-600 hover:from-yellow-600 hover:to-orange-700 rounded-lg transition-all duration-300 shadow-lg transform hover:scale-105"
>
🌟
</button>
<button
onClick={() => {
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 = []
}}
className="control-button px-5 py-2 bg-linear-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 rounded-lg transition-all duration-300 shadow-lg transform hover:scale-105"
>
🗑
</button>
</div>
<div className="space-y-3">
<div>
<label className="block text-sm mb-2 text-pink-300">
: <span className="font-mono text-white">{messageSpeed.toFixed(1)}</span>
</label>
<input
type="range"
min="0.1"
max="4"
step="0.1"
value={messageSpeed}
onChange={(e) => setMessageSpeed(Number(e.target.value))}
className="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider"
/>
</div>
<div>
<label className="block text-sm mb-2 text-purple-300">
: <span className="font-mono text-white">{starCount}</span>
</label>
<input
type="range"
min="100"
max="5000"
step="100"
value={starCount}
onChange={(e) => setStarCount(Number(e.target.value))}
className="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider"
/>
</div>
</div>
<div className="pt-2 border-t border-white/20">
<div className="text-xs text-gray-300 mb-2">
: <span className="font-mono text-pink-300">{textMessagesRef.current.length}</span>
</div>
<p className="text-xs text-gray-300">
<kbd className="px-1 bg-gray-700 rounded"></kbd> /
<kbd className="px-1 bg-gray-700 rounded"></kbd>
<kbd className="px-1 bg-gray-700 rounded">H</kbd>
</p>
</div>
</div>
</div>
)}
{/* 隐藏控制面板时的提示 */}
{!showControls && (
<div className="absolute top-4 left-4 bg-black/60 text-white px-3 py-2 rounded-lg backdrop-blur-sm">
<p className="text-sm"> <kbd className="px-1 bg-gray-700 rounded">H</kbd> </p>
</div>
)}
{/* 信息提示 */}
<div className="info-panel absolute bottom-4 right-4 bg-black/80 text-white p-4 rounded-xl backdrop-blur-md border border-white/20 shadow-2xl max-w-sm">
<div className="space-y-2">
<h4 className="font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-purple-400">
<EFBFBD>
</h4>
<div className="text-sm space-y-1 text-gray-200">
<p>💫 </p>
<p> </p>
<p>🌠 </p>
<p>💝 </p>
</div>
</div>
</div>
{/* 加载提示 */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<div className="text-white text-center">
<div className="loading-text animate-pulse text-2xl font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-purple-400">
...
</div>
</div>
</div>
</div>
)
}