generated from template/astro-simple-template
readme.md
This commit is contained in:
125
THINKYOU_README.md
Normal file
125
THINKYOU_README.md
Normal 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 # 演示页面
|
||||
```
|
||||
|
||||
## 💝 特别说明
|
||||
|
||||
这个项目充满了对爱情的美好憧憬,每一个细节都经过精心设计,希望能为你的表白之路增添一份浪漫的色彩。无论是向心爱的人表达爱意,还是在特殊的日子里制造惊喜,这个应用都能成为你表达情感的完美工具。
|
||||
|
||||
愿所有的爱意都能被温柔以待,愿每一份真情都能得到回应。💖
|
||||
|
||||
---
|
||||
|
||||
*"我想你了" - 这简单的四个字,承载着最深的思念。*
|
||||
@@ -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
92
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
import Html from '@/components/html.astro';
|
||||
import { App } from '@/thinkyou';
|
||||
---
|
||||
|
||||
<Html>
|
||||
<main>
|
||||
|
||||
</main>
|
||||
<App client:only="react" />
|
||||
</Html>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
194
src/styles/thinkyou.css
Normal 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
616
src/thinkyou/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user