Release v0.0.2 #1

Merged
zhanxi merged 5 commits from wang-yuanyou-fluid-portfolio into master 2026-06-11 08:44:37 +00:00
22 changed files with 3132 additions and 6804 deletions
+6 -3
View File
@@ -1,10 +1,13 @@
# 王元有 - 前端工程师简历网站
# Wang Yuanyou Fluid Portfolio
基于 Astro 的个人简历网站,内容整理自 `王元有-前端工程师.pdf`
Independent Vite + Three.js portfolio site for 王元有, focused on a more grounded frontend-engineering resume.
## Commands
```bash
npm install
npm run dev
npm run dev -- --port 5177
npm run build
```
The page uses a restrained scroll-aware WebGL backdrop, critical first-paint styling, grounded resume summaries, responsive layout, subtle scroll reveals, project cards, skills, experience, and contact sections.
-5
View File
@@ -1,5 +0,0 @@
import { defineConfig } from "astro/config";
export default defineConfig({
output: "static",
});
@@ -0,0 +1,87 @@
# 设计文档:「与湛兮(花名)的数字思维体对话」作品集重设计
日期:2026-06-11
状态:已批准
备份:`../wang-yuanyou-fluid-portfolio-backup-20260611-110154/`
## 概念
整站是访客与「湛兮(花名)的 AI 分身」的一场对话记录。简历核心(最近一年做 SSE 流式对话、打字机渲染、思考态动画)即设计语言来源——网站本身就是其工作成果的活演示。3D 流体 =「思维体」,是画面中唯一的角色。
决策记录:
- 设计隐喻:AI 对话流(vs 职业河流 / 跨端粒子宇宙)
- 推进方式:滚动驱动的对话(vs 真·对话界面 / 滚动+问答入口)
- 流体形态:单一思维流体 + 三状态状态机(vs raymarched metaballs / GPGPU 粒子流)
- 视觉基调:深色 AI 实验室(vs 浅色编辑风 / 双主题)
- 技术路线:原生 Three.js 自写 shader + GSAP ScrollTrigger + Lenisvs 纯手写 / R3F
## 信息架构:7 轮问答
| # | 访客提问 | 回答内容 | 数据源 |
|---|---|---|---|
| 0 | —(开机自检) | 「你好,我是湛兮(花名)。」+ 身份一行 + 6 指标 token 式吐出 | metrics |
| 1 | 先介绍一下你自己? | 4 条概述逐行流式输出 | resumeSignals |
| 2 | 最近一年具体在做什么? | 4 张焦点卡依次「生成」 | focusAreas |
| 3 | 有实际项目证明吗? | 4 个项目,各为一轮子对话:项目名打字机 → 模块逐条吐出 | projects |
| 4 | 技能栈展开讲讲? | 7 组技能标签 token 流喷发归位 | skills |
| 5 | 之前的团队经历? | 3 段经历沿垂直对话流时间线生成 | experiences |
| 6 | 怎么联系你? | 联系方式 + 流体归于平静,「对话已保存」收尾 | — |
页面右侧(移动端顶部)设「会话进度轨」:7 节点对应 7 轮,可点击跳转,保证 HR 快速扫读。
## 思维流体(Three.js 自写 shader
- 几何:高细分 IcosahedronGeometry;顶点着色器 3 层 FBM simplex 噪声液态位移
- 片元:Fresnel 边缘辉光 + 双色渐变(青 #22d3ee ↔ 紫 #a78bfa+ 加性内核光晕
- 伴生粒子:约 2000 GPU 粒子,curl noise 流场
- 状态机(uniform 插值过渡):
- `idle`:低频低幅呼吸,粒子懒散环绕
- `thinking`:收缩 0.85x,噪声频率 ×3 搅动,色温升高,粒子吸入
- `answering`:回弹 1.1x 归位,粒子向内容区喷发消散,辉光脉冲与文字生成同步
- 流体位置随轮次左右缓移(lerp)与内容互让;鼠标轻微视差
## 非线性动画编排(GSAP ScrollTrigger + Lenis
每轮问答为一个 pin 区段,滚动量映射轮内时间轴:
1. 提问(0→15%):访客气泡逐字打出,steps() 离散节奏
2. 思考(15→35%):流体 thinking,内容区仅 shimmer 占位——刻意停顿即非线性核心
3. 爆发生成(35→75%):expo.out 爆发;标题打字机、卡片不等间隔 stagger(模拟 token 不均匀到达)、数字滚动跳变
4. 余韵(75→100%):流体回 idle,内容微视差上浮,解除 pin
每轮思考时长 / 爆发曲线 / stagger 间隔均不同(场景配置驱动),避免节奏雷同。向上滚动时时间轴反播,内容「被收回」。
降级:`prefers-reduced-motion` → 关 pin 与打字机、内容直出、流体仅呼吸;移动端粒子减半、DPR ≤ 2、细分降档。
## 视觉系统
- 背景 #070b14 近黑蓝 + 极淡网格点阵 + 流体环境光溢出
- 强调色:青 #22d3ee(访客/交互)、紫 #a78bfa(思维体/回答)、琥珀 #fbbf24(仅指标数字)
- 气泡语言:提问 = 右对齐细边框气泡;回答 = 无框流式文本块 + 左侧渐变「生成光标」竖线
- 字体:等宽(JetBrains Mono / 思源等宽回退)用于指标、token、技能标签;正文 Inter + 思源黑体
- 细节:回答块尾闪烁光标 ▋;项目 logo 复用 public/logos/
## 文件结构
```
src/
main.js # 入口:装配 + Lenis + 进度轨
resume-data.js # 不动(数据即简历)
dialogue.js # 7 轮问答文案与节奏配置
render.js # 由 resume-data 生成各轮 DOM
choreography.js # GSAP ScrollTrigger 时间轴编排
mind/
mind.js # 流体场景、状态机、粒子
shaders.js # GLSL
styles.css # 重写
index.html # 重写骨架
```
新依赖:gsap、lenis。重写 main.js / styles.css / index.html(旧版已整目录备份)。
## 性能与验收
- 单 canvas 固定底层;rAF 与 GSAP ticker 合并;目标桌面 60fps、移动 30fps+
- 文字全部真实 DOM(可选中/可索引),打字机仅控制 reveal;进度轨 + 锚点保证可跳转
- 验收:7 轮完整走查(含回滚反播)、375px 移动端、reduced-motion 降级、Lighthouse 不低于现版本
@@ -0,0 +1,56 @@
# 「数字思维体对话」作品集重设计 实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将现有作品集重写为「与湛兮(花名)的 AI 分身对话」式单页站点:滚动驱动的 7 轮问答 + shader 思维流体状态机。
**Architecture:** Vite 原生 JS。`resume-data.js` 不动;`dialogue.js` 定义 7 轮问答节奏配置;`render.js` 生成 DOM`mind/` 为 Three.js shader 流体(idle/thinking/answering 状态机 + GPU 粒子);`choreography.js` 用 GSAP ScrollTrigger pin+scrub 编排每轮「提问→思考→爆发生成→余韵」;Lenis 平滑滚动。
**Tech Stack:** Vite 7, Three.js 0.184, GSAP ScrollTrigger, Lenis, 自写 GLSLFBM simplex + Fresnel)。
**设计文档:** `docs/plans/2026-06-11-ai-dialogue-redesign-design.md`(已批准)
**备份:** `../wang-yuanyou-fluid-portfolio-backup-20260611-110154/`
---
### Task 1: 依赖安装
- `pnpm add gsap lenis`
- 验证: package.json 出现两依赖,`pnpm dev` 可启动。
### Task 2: `src/dialogue.js` — 轮次配置
- 7 轮:boot / intro(resumeSignals) / focus(focusAreas) / projects / skills / experience / contact。
- 每轮字段:id、label(进度轨)、question、type、side(流体停靠 -1/1)、palette(双色)、pinLength、qEnd/thinkEnd(相位)、staggerEach——逐轮不同以保证节奏差异。
- 联系信息常量:邮箱 419021733@qq.com / wmagmgema521@gmail.com、电话 19980439383、GitHub zhanBoss、男·29岁·电子科技大学 信息管理与信息系统 本科。
### Task 3: `index.html` — 骨架重写
- canvas#mind-canvasfixed 底层)、.grid-overlay、header.hud(左:姓名/DIGITAL MIND;右:● ONLINE 状态灯)、nav#session-rail、main#dialogue(空,由 render.js 填充)、Google FontsJetBrains Mono + Inter, display=swap)。
### Task 4: `src/styles.css` — 深色 AI 实验室
- 背景 #070b14、点阵网格 overlay、青 #22d3ee / 紫 #a78bfa / 琥珀 #fbbf24
- 气泡语言:.q-bubble 右对齐细边框;.answer 左侧渐变生成竖线 + 尾部 ▋ 闪烁光标;token/指标/技能用等宽字体。
- 各轮内容样式:metrics token、focus 卡、project 块、skill 标签云、timeline、contact。
- 进度轨右侧固定(移动端转顶部水平);≤768px 移动端布局;prefers-reduced-motion 关闭闪烁。
### Task 5: `src/render.js` — DOM 生成
- 每轮 section.round 结构:q-bubble(空文本,编排时打字)→ .thinking(三点 shimmer)→ .answer.gen 子项供 stagger)。
- 按 type 分发渲染器,全部读 resume-data.jsboot 轮含问候打字行 + 身份行 + metrics token。
- 文案全部真实 DOM(初始由 CSS/GSAP 隐藏,reduced-motion 时直接可见)。
### Task 6: `src/mind/shaders.js` + `src/mind/mind.js` — 思维流体
- 顶点:Ashima simplex 3D + 3 octave FBM 位移;片元:Fresnel 辉光 + uColorA/uColorB 渐变 + uHeat 色温 + AdditiveBlending。
- 粒子:~2000(移动端 900),属性 seed/radius/speed/phase,轨道半径受 uAttract(吸入)与 uBurst(喷发,setState('answering') 时置 1 后衰减)控制,全 GPU。
- 状态机 STATES{idle, thinking, answering},每帧 lerp uniformssetSide(dir) 左右停靠(移动端居中缩小);setPalette 双色渐变;指针视差;DPR≤2resize。
### Task 7: `src/choreography.js` — 滚动编排
- Lenis + ScrollTrigger 集成(gsap.ticker 驱动)。
- boot 轮:页面加载后时间驱动的开场时间轴(非滚动)。
- 轮 1-6pin + scrub 时间轴,相位 0→qEnd 打字(steps 离散)→thinkEnd 思考 shimmer→0.78 爆发 staggerexpo.out,不等间隔)→1 余韵视差;onUpdate 按相位切 mind 状态;enter/enterBack 切 side+palette。
- 进度轨高亮与 lenis.scrollTo 跳转。
- reduced-motion:跳过 pin/打字机,内容直出。
### Task 8: `src/main.js` — 装配
- 渲染 DOM → 建 rail → createMind → choreography → boot 开场。
### Task 9: 验证
- `pnpm dev` + Playwright:桌面 1440px 走查 7 轮(截图 hero/中段/尾段)、向上回滚反播、375px 移动端、console 无错误。
- `pnpm build` 成功。
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>湛兮(花名) · 数字思维体 / Digital Mind</title>
<meta
name="description"
content="湛兮(花名) — 7 年 Web 与跨端前端工程师。一场与他的数字思维体的对话:AI 对话链路、控制台业务、跨端内容流与工程实践。"
/>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<canvas id="mind-canvas" aria-hidden="true"></canvas>
<div class="grid-overlay" aria-hidden="true"></div>
<header class="hud">
<div class="hud-id">
<span class="hud-name">ZHAN XI</span>
<span class="hud-sub">DIGITAL MIND · v7.0</span>
</div>
<div class="hud-status">
<span class="status-dot" aria-hidden="true"></span>
<span class="status-text">ONLINE</span>
</div>
</header>
<nav id="session-rail" aria-label="会话进度"></nav>
<main id="dialogue"></main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+147 -3810
View File
File diff suppressed because it is too large Load Diff
+11 -9
View File
@@ -1,17 +1,19 @@
{
"name": "wang-yuanyou-resume-site",
"name": "wang-yuanyou-fluid-portfolio",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build:test": "PUBLIC_APP_ENV_LABEL=测试环境 astro build",
"build:prod": "PUBLIC_APP_ENV_LABEL=生产环境 astro build",
"preview": "astro preview"
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"build:test": "PUBLIC_APP_ENV_LABEL=测试环境 vite build",
"build:prod": "PUBLIC_APP_ENV_LABEL=生产环境 vite build",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"astro": "^6.3.1"
},
"devDependencies": {}
"gsap": "^3.15.0",
"lenis": "^1.3.23",
"three": "^0.184.0",
"vite": "^7.2.7"
}
}
+676
View File
@@ -0,0 +1,676 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
gsap:
specifier: ^3.15.0
version: 3.15.0
lenis:
specifier: ^1.3.23
version: 1.3.23
three:
specifier: ^0.184.0
version: 0.184.0
vite:
specifier: ^7.2.7
version: 7.3.5
packages:
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@rollup/rollup-android-arm-eabi@4.61.1':
resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.61.1':
resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.61.1':
resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.61.1':
resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.61.1':
resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.61.1':
resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.61.1':
resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.61.1':
resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.61.1':
resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.61.1':
resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.61.1':
resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.61.1':
resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.61.1':
resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.61.1':
resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.61.1':
resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.61.1':
resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.61.1':
resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.61.1':
resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.61.1':
resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.61.1':
resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.61.1':
resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.61.1':
resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.61.1':
resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.61.1':
resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.61.1':
resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
engines: {node: '>=18'}
hasBin: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
gsap@3.15.0:
resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==}
lenis@1.3.23:
resolution: {integrity: sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
react: '>=17.0.0'
vue: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
react:
optional: true
vue:
optional: true
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.61.1:
resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
three@0.184.0:
resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
vite@7.3.5:
resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
snapshots:
'@esbuild/aix-ppc64@0.27.7':
optional: true
'@esbuild/android-arm64@0.27.7':
optional: true
'@esbuild/android-arm@0.27.7':
optional: true
'@esbuild/android-x64@0.27.7':
optional: true
'@esbuild/darwin-arm64@0.27.7':
optional: true
'@esbuild/darwin-x64@0.27.7':
optional: true
'@esbuild/freebsd-arm64@0.27.7':
optional: true
'@esbuild/freebsd-x64@0.27.7':
optional: true
'@esbuild/linux-arm64@0.27.7':
optional: true
'@esbuild/linux-arm@0.27.7':
optional: true
'@esbuild/linux-ia32@0.27.7':
optional: true
'@esbuild/linux-loong64@0.27.7':
optional: true
'@esbuild/linux-mips64el@0.27.7':
optional: true
'@esbuild/linux-ppc64@0.27.7':
optional: true
'@esbuild/linux-riscv64@0.27.7':
optional: true
'@esbuild/linux-s390x@0.27.7':
optional: true
'@esbuild/linux-x64@0.27.7':
optional: true
'@esbuild/netbsd-arm64@0.27.7':
optional: true
'@esbuild/netbsd-x64@0.27.7':
optional: true
'@esbuild/openbsd-arm64@0.27.7':
optional: true
'@esbuild/openbsd-x64@0.27.7':
optional: true
'@esbuild/openharmony-arm64@0.27.7':
optional: true
'@esbuild/sunos-x64@0.27.7':
optional: true
'@esbuild/win32-arm64@0.27.7':
optional: true
'@esbuild/win32-ia32@0.27.7':
optional: true
'@esbuild/win32-x64@0.27.7':
optional: true
'@rollup/rollup-android-arm-eabi@4.61.1':
optional: true
'@rollup/rollup-android-arm64@4.61.1':
optional: true
'@rollup/rollup-darwin-arm64@4.61.1':
optional: true
'@rollup/rollup-darwin-x64@4.61.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.61.1':
optional: true
'@rollup/rollup-freebsd-x64@4.61.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.61.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.61.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.61.1':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-loong64-musl@4.61.1':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.61.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.61.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.61.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.61.1':
optional: true
'@rollup/rollup-openbsd-x64@4.61.1':
optional: true
'@rollup/rollup-openharmony-arm64@4.61.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.61.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.61.1':
optional: true
'@rollup/rollup-win32-x64-gnu@4.61.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.61.1':
optional: true
'@types/estree@1.0.9': {}
esbuild@0.27.7:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
'@esbuild/android-arm': 0.27.7
'@esbuild/android-arm64': 0.27.7
'@esbuild/android-x64': 0.27.7
'@esbuild/darwin-arm64': 0.27.7
'@esbuild/darwin-x64': 0.27.7
'@esbuild/freebsd-arm64': 0.27.7
'@esbuild/freebsd-x64': 0.27.7
'@esbuild/linux-arm': 0.27.7
'@esbuild/linux-arm64': 0.27.7
'@esbuild/linux-ia32': 0.27.7
'@esbuild/linux-loong64': 0.27.7
'@esbuild/linux-mips64el': 0.27.7
'@esbuild/linux-ppc64': 0.27.7
'@esbuild/linux-riscv64': 0.27.7
'@esbuild/linux-s390x': 0.27.7
'@esbuild/linux-x64': 0.27.7
'@esbuild/netbsd-arm64': 0.27.7
'@esbuild/netbsd-x64': 0.27.7
'@esbuild/openbsd-arm64': 0.27.7
'@esbuild/openbsd-x64': 0.27.7
'@esbuild/openharmony-arm64': 0.27.7
'@esbuild/sunos-x64': 0.27.7
'@esbuild/win32-arm64': 0.27.7
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
gsap@3.15.0: {}
lenis@1.3.23: {}
nanoid@3.3.12: {}
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.15:
dependencies:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.61.1:
dependencies:
'@types/estree': 1.0.9
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.61.1
'@rollup/rollup-android-arm64': 4.61.1
'@rollup/rollup-darwin-arm64': 4.61.1
'@rollup/rollup-darwin-x64': 4.61.1
'@rollup/rollup-freebsd-arm64': 4.61.1
'@rollup/rollup-freebsd-x64': 4.61.1
'@rollup/rollup-linux-arm-gnueabihf': 4.61.1
'@rollup/rollup-linux-arm-musleabihf': 4.61.1
'@rollup/rollup-linux-arm64-gnu': 4.61.1
'@rollup/rollup-linux-arm64-musl': 4.61.1
'@rollup/rollup-linux-loong64-gnu': 4.61.1
'@rollup/rollup-linux-loong64-musl': 4.61.1
'@rollup/rollup-linux-ppc64-gnu': 4.61.1
'@rollup/rollup-linux-ppc64-musl': 4.61.1
'@rollup/rollup-linux-riscv64-gnu': 4.61.1
'@rollup/rollup-linux-riscv64-musl': 4.61.1
'@rollup/rollup-linux-s390x-gnu': 4.61.1
'@rollup/rollup-linux-x64-gnu': 4.61.1
'@rollup/rollup-linux-x64-musl': 4.61.1
'@rollup/rollup-openbsd-x64': 4.61.1
'@rollup/rollup-openharmony-arm64': 4.61.1
'@rollup/rollup-win32-arm64-msvc': 4.61.1
'@rollup/rollup-win32-ia32-msvc': 4.61.1
'@rollup/rollup-win32-x64-gnu': 4.61.1
'@rollup/rollup-win32-x64-msvc': 4.61.1
fsevents: 2.3.3
source-map-js@1.2.1: {}
three@0.184.0: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
vite@7.3.5:
dependencies:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.15
rollup: 4.61.1
tinyglobby: 0.2.17
optionalDependencies:
fsevents: 2.3.3
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#07080d"/>
<path d="M12 17h8l5 25 7-25h7l7 25 5-25h7L49 50h-8l-6-22-6 22h-8L12 17Z" fill="#56f7c5"/>
<path d="M13 50h38" stroke="#ff5ea8" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

+301
View File
@@ -0,0 +1,301 @@
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
import { rounds } from "./dialogue";
gsap.registerPlugin(ScrollTrigger);
/* Lenis 平滑滚动 + ScrollTrigger 集成 */
export function initSmoothScroll() {
const lenis = new Lenis({ lerp: 0.1 });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
return lenis;
}
/* 打字机:时间轴上的离散字符 reveal(时间驱动,进入视口后完整播放) */
function addTypewriter(tl, textEl, caretEl, fullText, start, duration) {
const proxy = { p: 0 };
tl.to(
proxy,
{
p: 1,
duration,
ease: `steps(${fullText.length})`,
onUpdate() {
const n = Math.round(proxy.p * fullText.length);
textEl.textContent = fullText.slice(0, n);
if (caretEl) caretEl.style.opacity = proxy.p >= 1 ? "0" : "1";
},
},
start
);
}
/* 根据文案长度推导打字时长,保持各轮节奏差异 */
const typingDuration = (text) => Math.min(1.5, 0.34 + text.length * 0.07);
/* 开场(时间驱动,非滚动)*/
export function playBoot(mind) {
const boot = document.querySelector("#r-boot");
const typedLine = boot.querySelector(".typed-line");
const items = boot.querySelectorAll(".gen");
gsap.set(boot.querySelector(".answer"), { opacity: 1 });
gsap.set(items, { y: 26, autoAlpha: 0 });
const text = "这是我的数字思维体,向它提问即可。";
const proxy = { p: 0 };
const tl = gsap.timeline({ delay: 0.3 });
tl.call(() => mind.setState("thinking"))
.to(proxy, {
p: 1,
duration: 1.4,
ease: `steps(${text.length})`,
onUpdate: () => {
typedLine.textContent = text.slice(0, Math.round(proxy.p * text.length));
},
}, 0.5)
.call(() => mind.setState("answering"), null, 1.9)
.to(items, {
y: 0,
autoAlpha: 1,
duration: 0.7,
ease: "expo.out",
stagger: { each: 0.09, from: "start" },
}, 2.0)
.call(() => mind.setState("idle"), null, 3.4);
return tl;
}
/* 单轮问答时间轴:提问打字 → 思考 → 回答爆发生成(一次性播放) */
function buildRoundTimeline({ round, qText, qCaret, thinking, answer, items, mind }) {
const typeDur = typingDuration(round.question);
const tl = gsap.timeline({ paused: true });
// 1. 提问打字
addTypewriter(tl, qText, qCaret, round.question, 0, typeDur);
// 2. 思考 shimmer(刻意停顿 = 非线性蓄力)
tl.call(() => mind.setState("thinking"), null, typeDur)
.to(thinking, { opacity: 1, duration: 0.22 }, typeDur)
.to({}, { duration: 0.5 })
.to(thinking, { opacity: 0, duration: 0.18 })
// 3. 爆发生成:不等间隔 stagger 模拟 token 到达
.call(() => mind.setState("answering"))
.to(answer, { opacity: 1, duration: 0.2 }, "<")
.to(
items,
{
y: 0,
autoAlpha: 1,
duration: 0.7,
ease: "expo.out",
stagger: { each: Math.max(0.05, round.staggerEach), from: "start" },
},
"<0.05"
)
.call(() => mind.setState("idle"), null, "+=0.4");
return tl;
}
/* 轮次编排:进入视口即播放(不再依赖滚动 scrub,修复"不滚动则不加载" */
export function initChoreography(mind, setActive = () => {}) {
rounds
.filter((r) => r.question)
.forEach((round) => {
const section = document.getElementById(`r-${round.id}`);
const qText = section.querySelector(".q-text");
const qCaret = section.querySelector(".q-caret");
const thinking = section.querySelector(".thinking");
const answer = section.querySelector(".answer");
const items = section.querySelectorAll(".gen");
gsap.set(items, { y: 34, autoAlpha: 0 });
if (round.flow) {
buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive });
return;
}
const tl = buildRoundTimeline({ round, qText, qCaret, thinking, answer, items, mind });
ScrollTrigger.create({
trigger: section,
start: "top 60%",
end: "bottom 40%",
onToggle(self) {
if (self.isActive) setActive(round.id);
},
onEnter: () => {
mind.setSide(round.side);
mind.setPalette(round.palette);
tl.play();
},
onEnterBack: () => {
mind.setSide(round.side);
mind.setPalette(round.palette);
},
onLeave: () => mind.setState("idle"),
onLeaveBack: () => mind.setState("idle"),
});
});
}
/* 长内容轮:提问/思考时间驱动一次播放,内容块各自进入视口时「生成」 */
function buildFlowRound({ round, section, qText, qCaret, thinking, answer, items, mind, setActive }) {
gsap.set(answer, { opacity: 1 });
const typeDur = typingDuration(round.question);
const qTl = gsap.timeline({ paused: true });
addTypewriter(qTl, qText, qCaret, round.question, 0, typeDur);
qTl.to(thinking, { opacity: 1, duration: 0.2 }, typeDur)
.to(thinking, { opacity: 0, duration: 0.2 }, "+=0.5");
ScrollTrigger.create({
trigger: section,
start: "top 70%",
once: true,
onEnter: () => qTl.play(),
});
items.forEach((item, i) => {
gsap.to(item, {
y: 0,
autoAlpha: 1,
duration: 0.8,
delay: (i % 3) * 0.09, // 不等间隔,模拟 token 到达
ease: "expo.out",
scrollTrigger: { trigger: item, start: "top 88%" },
});
});
ScrollTrigger.create({
trigger: section,
start: "top 60%",
end: "bottom 55%",
onToggle(self) {
if (self.isActive) {
setActive(round.id);
mind.setSide(round.side);
mind.setPalette(round.palette);
} else {
mind.setState("idle");
}
},
onUpdate(self) {
mind.setState(self.progress < 0.1 ? "thinking" : "answering");
},
});
}
/* 进度轨:高亮由编排回调驱动,这里只负责节点与跳转 */
export function initRail(lenis) {
const setActive = (id) => {
document.querySelectorAll(".rail-node").forEach((n) => {
n.classList.toggle("active", n.dataset.round === id);
});
};
ScrollTrigger.create({
trigger: "#r-boot",
start: "top top",
end: "bottom 40%",
onToggle(self) {
if (self.isActive) setActive("boot");
},
});
const jump = (id) => {
const target = document.getElementById(`r-${id}`);
if (!target) return;
if (lenis) lenis.scrollTo(target, { offset: 2, duration: 1.2 });
else target.scrollIntoView({ behavior: "smooth" });
};
return { jump, setActive };
}
/* 模块吸附:滚动停止后,若停在两轮之间的中间态,自动滚动到目标轮顶部,
再由 onEnter 正常触发打字机动画(修复"滚不到位则动画错位" */
export function initRoundSnap(lenis) {
const sections = rounds
.map((round) => document.getElementById(`r-${round.id}`))
.filter(Boolean);
if (!sections.length) return () => {};
let timer = 0;
let lockUntil = 0;
const readY = () => (typeof lenis?.scroll === "number" ? lenis.scroll : window.scrollY);
const snapTo = (section) => {
lockUntil = performance.now() + 700;
const targetY = section.getBoundingClientRect().top + readY();
if (lenis) {
lenis.scrollTo(targetY, {
duration: 0.55,
easing: (t) => 1 - Math.pow(1 - t, 3),
});
} else {
window.scrollTo({ top: targetY, behavior: "smooth" });
}
};
const settle = () => {
if (performance.now() < lockUntil) return;
const vh = window.innerHeight || document.documentElement.clientHeight;
for (const section of sections) {
const top = section.getBoundingClientRect().top;
// 已有模块对齐视口顶部 → 状态正常,无需吸附
if (Math.abs(top) < 8) return;
// 模块露出一半(未到打字机区域)→ 吸附进入该模块
const halfEntered = top > vh * 0.12 && top < vh * 0.72;
// 模块占据主屏但未对齐 → 吸附回正
const nearlyAligned = top > -vh * 0.45 && top <= vh * 0.12;
if (halfEntered || nearlyAligned) {
snapTo(section);
return;
}
}
// 长内容轮(flow)内部深处不匹配任何区间 → 自由滚动,不干预
};
const onScroll = () => {
window.clearTimeout(timer);
// lenis 惯性收尾阶段速度趋零:立即判定吸附,不等事件完全静止
const velocity =
lenis && typeof lenis.velocity === "number" ? Math.abs(lenis.velocity) : null;
if (velocity !== null && velocity < 0.3) {
settle();
return;
}
timer = window.setTimeout(settle, 90);
};
if (lenis) lenis.on("scroll", onScroll);
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.clearTimeout(timer);
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
};
}
/* 降级:reduced motion 直接展示全部内容 */
export function applyReducedMotion() {
rounds
.filter((r) => r.question)
.forEach((round) => {
const section = document.getElementById(`r-${round.id}`);
section.querySelector(".q-text").textContent = round.question;
const answer = section.querySelector(".answer");
answer.style.opacity = "1";
});
const boot = document.querySelector("#r-boot");
boot.querySelector(".typed-line").textContent = "这是我的数字思维体。";
document.querySelectorAll(".gen").forEach((n) => (n.style.opacity = "1"));
}
-481
View File
@@ -1,481 +0,0 @@
export const resume = {
name: "王元有",
alias: "湛兮",
title: "前端开发工程师",
intent: "求职意向:前端开发工程师",
profile:
"7 年前端与跨端开发经验,长期负责 Web、App、H5、小程序与管理后台的架构设计、工程化建设和核心业务交付。熟悉 AI 产品工程、SSE 流式通信、Next.js/Vue/React Native 跨端体系、微前端和 Monorepo 协作模式。",
basics: [
{ label: "工作经验", value: "7 年" },
{ label: "技术管理", value: "2+ 年" },
{ label: "项目用户规模", value: "千万级 MAU" },
{ label: "教育背景", value: "电子科技大学 本科" },
],
contact: {
phone: "19980439383",
email: "419021733@qq.com",
emailAlt: "wmagmgema521@gmail.com",
meta: "男 | 29 岁",
},
highlights: [
"主导多款企业级产品从 0 到 1 搭建,覆盖 AI 模型服务、AI 资讯对话、供应链、门店数字化、工业互联网平台等场景。",
"掌握 Vue3、React、Next.js、React Native、Expo、UniApp 等技术栈,能独立完成 Web、移动端、App 与管理后台方案落地。",
"推动 Micro-Frontend、SSR、pnpm Monorepo、Git Flow、代码审查、CI/CD、监控体系等工程化实践。",
"有明确的性能与效率结果:构建体积优化 30%、首屏白屏时间缩短 25%、组件库工程化提升开发效率 50%、重复开发成本减少约 40%。",
],
metrics: [
{ value: "-30%", label: "构建体积优化" },
{ value: "-25%", label: "首屏白屏时间" },
{ value: "+50%", label: "组件库效率提升" },
{ value: "+40%", label: "团队人效提升" },
{ value: "40%", label: "重复开发成本降低" },
{ value: "30%", label: "消息渲染性能提升" },
],
skills: [
{
group: "前端框架",
items: [
"Next.js 16",
"React 19",
"Vue 3",
"TypeScript 5",
"Tailwind CSS 4",
"Nuxt/SSR",
],
},
{
group: "跨端开发",
items: [
"React Native 0.79",
"Expo 53",
"UniApp",
"H5",
"微信/飞书小程序",
],
},
{
group: "AI 与通信",
items: [
"Vercel AI SDK v6",
"SSE 流式通信",
"多 Agent 协作",
"Markdown/LaTeX 渲染",
"AI 对话",
"AI 生图/生视频",
],
},
{
group: "工程化",
items: [
"pnpm Monorepo",
"Git Submodules",
"Qiankun",
"Lerna",
"Git Flow",
"Playwright",
],
},
{
group: "状态与 UI",
items: [
"Zustand",
"MobX",
"Pinia",
"Radix UI",
"Ant Design",
"Element Plus",
"ProComponents",
],
},
{
group: "基础设施",
items: [
"Alova",
"next-intl",
"i18next",
"ARMS 监控",
"CDN 优化",
"Adyen 支付",
],
},
],
experiences: [
{
company: "成都海艺互娱科技有限公司",
role: "React Native 开发工程师",
period: "2025.05 - 至今",
points: [
"参与多款 AI 产品从 0 到 1 搭建与架构设计,覆盖前端性能、交互体验、国际化体系与多端组件复用。",
"在 AI 对话、生图、生视频与多 Agent 协作领域沉淀工程经验,负责流式通信与前端渲染体验优化。",
"支撑千万级海外用户产品,推动多端组件库统一建设,减少重复开发成本约 40%。",
],
},
{
company: "四川茶姬企业管理有限公司",
role: "高级 Web 前端开发",
period: "2024.03 - 2025.03",
points: [
"主导供应链管理系统前端开发,实现采购、仓储、配送等核心模块。",
"独立负责门店报损与食安管理核心功能搭建及迭代,覆盖国内、东南亚与北美业务逻辑。",
"引入阿里云 ARMS 前端监控体系,落地 CDN 静态资源优化方案,并制定代码审查与 Git 操作规范。",
],
},
{
company: "中钧科技有限公司四川分公司",
role: "前端开发组长",
period: "2022.04 - 2024.03",
points: [
"负责经营帮 PC 端微前端、门户和商管核心模块开发,完成 UI 还原、接口联调及性能优化。",
"从零搭建新 E 畅行小程序基础框架,完成技术方案制定与全流程开发。",
"主导经营帮小程序、H5、Admin 后台迭代开发与跨端技术方案设计,建立 Git Flow、代码审查和新人培训机制。",
],
},
],
projects: [
{
id: "vtrix",
name: "SeaCloud / Vtrix",
subtitle: "AI 模型服务平台(Web 端)",
period: "2025.09 - 至今",
tech: [
"Next.js 16",
"React 19",
"TypeScript",
"Tailwind CSS",
"Zustand",
"Alova",
"Vercel AI SDK",
"Radix UI",
"next-intl",
],
summary:
"面向全球用户的 AI 模型聚合服务平台,聚合 LLM、图像、视频、音频、3D 等多模态模型能力,覆盖 C 端调用与 B 端组织/分销管理。",
modules: [
"分销商客户邀请、折扣模板、额度分配、销售配置与利润率计算。",
"多币种与汇率体系,封装 useCurrency Hook 并贯穿 Pricing、Billing、API Keys 等模块。",
"组织成员、角色权限守卫、配额设置、支出限额、账单报表、交易筛选与 Excel 导出。",
"i18n Submodule 架构迁移、企业微信同步工作流、通用表格/筛选器/分页组件增强。",
],
},
{
id: "seabuzz",
name: "SeaBuzz",
subtitle: "AI 智能资讯与对话平台",
period: "2025.05 - 至今",
tech: [
"React Native",
"Expo 53",
"Expo Router",
"Zustand",
"NativeWind",
"SSE",
"i18next",
"Adyen",
"Lerna",
],
summary:
"海艺 AI 旗下 AI 新闻聚合、智能搜索与多模态对话平台,基于 Expo 实现 iOS、Android、Web 三端统一开发。",
modules: [
"AI Agent 对话完整链路:SSE 流式聊天、打字机渲染、Markdown/LaTeX、思考动画、来源引用与历史同步。",
"Discover 发现页与新闻详情,支持瀑布流、大/小卡动态布局、骨架屏与 Smart Image。",
"Google、Facebook、Discord、Email 登录与 SeaArt Auth SDK 对接。",
"Monorepo 公共包体系、API 层、数据模型、UI 组件、状态管理、OTA 热更新与代码签名机制。",
],
},
{
id: "bawang",
name: "霸王功夫",
subtitle: "门店数字化与供应链管理平台",
period: "2024.03 - 2025.03",
tech: [
"React 18",
"Vue 3",
"UniApp",
"MobX",
"Pinia",
"Element Plus",
"Ant Design",
"ProComponents",
],
summary:
"服务全球 6000+ 门店及运营伙伴的数字化管理平台,覆盖门店运营、食品安全、供应链协同等核心业务。",
modules: [
"小程序报损模块:摄像头扫码、在线报损登记,兼容微信小程序与飞书 H5。",
"食安管理模块:低频蓝牙连接 TSPL 指令集打印机,支持国内、东南亚、北美三套业务逻辑。",
"经营信息模块:多门店经营状态移动端看板,按区域、时间、指标筛选。",
"供应链模块:采购系统、供应商合同管理、供应商结算系统,对接云厉、费控等外部系统。",
],
},
{
id: "jingyingbang",
name: "经营帮平台 + 经营帮拉新",
subtitle: "工业互联网与微前端平台",
period: "2022.04 - 2024.03",
tech: [
"Vue",
"Qiankun",
"Element UI",
"华为云 OBS",
"百度地图",
"高德地图",
"IM",
"UniApp",
],
summary:
"基于信息化设计理念和区块链技术的工业互联网平台,为企业和个人提供数字化运营服务。",
modules: [
"参与单体前端到 Qiankun 微前端拆分,拆分出 12 个基础项目。",
"负责商管、门户、经营帮系列小程序/H5/Admin 后台核心业务交付。",
"引入华为云 OBS 直传减轻请求链路性能浪费。",
"开发内部 Chrome 插件 zjkj-decryption,提升数据解析效率。",
],
},
],
education: {
school: "电子科技大学",
degree: "本科",
major: "信息管理与信息系统",
period: "2023 - 2025",
},
};
export const resumeEn = {
name: "Wang Yuanyou",
alias: "mrZhan",
title: "Frontend Engineer",
intent: "Target Role: Frontend Engineer",
profile:
"Frontend and cross-platform engineer with 7 years of experience building Web, App, H5, mini-program and admin systems. Strong in AI product engineering, SSE streaming, Next.js/Vue/React Native ecosystems, micro-frontends and Monorepo collaboration.",
basics: [
{ label: "Experience", value: "7 years" },
{ label: "Tech Leadership", value: "2+ years" },
{ label: "Product Scale", value: "10M+ MAU" },
{ label: "Education", value: "UESTC Bachelor" },
],
contact: {
phone: "19980439383",
email: "419021733@qq.com",
emailAlt: "wmagmgema521@gmail.com",
meta: "Male | 29",
},
highlights: [
"Led multiple enterprise products from 0 to 1 across AI model services, AI news and chat, supply chain, store digitization and industrial internet platforms.",
"Hands-on with Vue3, React, Next.js, React Native, Expo and UniApp, capable of delivering Web, mobile, App and admin products end to end.",
"Drove engineering practices including Micro-Frontend, SSR, pnpm Monorepo, Git Flow, code review, CI/CD and frontend observability.",
"Delivered measurable outcomes: 30% smaller bundles, 25% faster first screen, 50% faster component-driven delivery and around 40% less duplicated work.",
],
metrics: [
{ value: "-30%", label: "Bundle Size" },
{ value: "-25%", label: "First Screen Blank Time" },
{ value: "+50%", label: "Component Delivery Efficiency" },
{ value: "+40%", label: "Team Productivity" },
{ value: "40%", label: "Duplicated Work Reduced" },
{ value: "30%", label: "Message Rendering Performance" },
],
skills: [
{
group: "Frontend Frameworks",
items: [
"Next.js 16",
"React 19",
"Vue 3",
"TypeScript 5",
"Tailwind CSS 4",
"Nuxt/SSR",
],
},
{
group: "Cross-platform",
items: [
"React Native 0.79",
"Expo 53",
"UniApp",
"H5",
"WeChat/Lark Mini Programs",
],
},
{
group: "AI & Streaming",
items: [
"Vercel AI SDK v6",
"SSE Streaming",
"Multi-Agent Collaboration",
"Markdown/LaTeX",
"AI Chat",
"AI Image/Video",
],
},
{
group: "Engineering",
items: [
"pnpm Monorepo",
"Git Submodules",
"Qiankun",
"Lerna",
"Git Flow",
"Playwright",
],
},
{
group: "State & UI",
items: [
"Zustand",
"MobX",
"Pinia",
"Radix UI",
"Ant Design",
"Element Plus",
"ProComponents",
],
},
{
group: "Infrastructure",
items: [
"Alova",
"next-intl",
"i18next",
"ARMS Monitoring",
"CDN Optimization",
"Adyen Payments",
],
},
],
experiences: [
{
company: "Chengdu Haiyi Interactive Entertainment Technology Co., Ltd.",
role: "React Native Engineer",
period: "2025.05 - Present",
points: [
"Contributed to architecture and 0-to-1 delivery of multiple AI products, covering performance, UX, i18n and multi-platform component reuse.",
"Built engineering experience in AI chat, image/video generation and multi-agent collaboration, with focus on streaming and frontend rendering performance.",
"Supported products serving 10M+ overseas users and promoted unified multi-platform component libraries, reducing duplicate work by around 40%.",
],
},
{
company: "Sichuan Chaji Enterprise Management Co., Ltd.",
role: "Senior Web Frontend Developer",
period: "2024.03 - 2025.03",
points: [
"Led frontend development of supply-chain management systems, including procurement, warehousing and delivery modules.",
"Owned store loss-reporting and food-safety features across China, Southeast Asia and North America business rules.",
"Introduced Alibaba Cloud ARMS frontend monitoring, delivered CDN optimization and established code review and Git workflow standards.",
],
},
{
company: "Zhongjun Technology Sichuan Branch",
role: "Frontend Team Lead",
period: "2022.04 - 2024.03",
points: [
"Delivered core modules for the Jingyingbang PC micro-frontend platform, portal and business management systems, including UI implementation, API integration and performance optimization.",
"Built the New E Travel mini-program foundation from scratch and owned technical planning through delivery.",
"Led mini-program, H5 and admin iterations, designed cross-platform solutions, and established Git Flow, code review and onboarding practices.",
],
},
],
projects: [
{
id: "vtrix",
name: "SeaCloud / Vtrix",
subtitle: "AI Model Service Platform (Web)",
period: "2025.09 - Present",
tech: [
"Next.js 16",
"React 19",
"TypeScript",
"Tailwind CSS",
"Zustand",
"Alova",
"Vercel AI SDK",
"Radix UI",
"next-intl",
],
summary:
"A global AI model aggregation platform covering LLM, image, video, audio and 3D model capabilities for developers, organizations and distributors.",
modules: [
"Built distributor invitation, discount templates, credit allocation, sales configuration and profit margin workflows.",
"Implemented global currency switching and exchange-rate conversion via a reusable useCurrency hook across Pricing, Billing and API Keys.",
"Delivered organization roles, quota controls, spending limits, billing reports, transaction filtering and Excel export flows.",
"Supported i18n submodule migration, WeCom translation sync and shared table/filter/pagination component enhancements.",
],
},
{
id: "seabuzz",
name: "SeaBuzz",
subtitle: "AI News and Conversation Platform",
period: "2025.05 - Present",
tech: [
"React Native",
"Expo 53",
"Expo Router",
"Zustand",
"NativeWind",
"SSE",
"i18next",
"Adyen",
"Lerna",
],
summary:
"An AI-powered news aggregation, smart search and multimodal conversation platform under SeaArt AI, built with Expo for iOS, Android and Web.",
modules: [
"Built the AI Agent chat flow with SSE streaming, typewriter rendering, Markdown/LaTeX, thinking animation, citations and history sync.",
"Delivered Discover feed and news detail pages with masonry layout, large/small dynamic cards, skeleton loading and Smart Image.",
"Integrated Google, Facebook, Discord and Email login with SeaArt Auth SDK.",
"Built Monorepo shared packages for API, data models, UI components, state, OTA updates and code signing.",
],
},
{
id: "bawang",
name: "Bawang Kungfu",
subtitle: "Store Digitization and Supply Chain Platform",
period: "2024.03 - 2025.03",
tech: [
"React 18",
"Vue 3",
"UniApp",
"MobX",
"Pinia",
"Element Plus",
"Ant Design",
"ProComponents",
],
summary:
"A digital operations platform serving 6,000+ stores and partners, covering store operations, food safety and supply-chain collaboration.",
modules: [
"Built mini-program loss reporting with camera scanning and online registration, compatible with WeChat mini-program and Lark H5.",
"Implemented food safety flows with Bluetooth TSPL printers and separate China, Southeast Asia and North America business rules.",
"Delivered mobile dashboards for multi-store operation metrics with region, time and KPI filtering.",
"Built procurement, supplier contract and settlement modules, integrating multiple external systems.",
],
},
{
id: "jingyingbang",
name: "Jingyingbang Platform",
subtitle: "Industrial Internet and Micro-frontend Platform",
period: "2022.04 - 2024.03",
tech: [
"Vue",
"Qiankun",
"Element UI",
"Huawei Cloud OBS",
"Baidu Map",
"Amap",
"IM",
"UniApp",
],
summary:
"An industrial internet platform based on informatization and blockchain concepts, providing digital operation services for companies and individuals.",
modules: [
"Participated in splitting a large frontend monolith into 12 Qiankun-based micro-frontend projects.",
"Delivered core business features for portals, business management, mini-program, H5 and admin systems.",
"Introduced Huawei Cloud OBS direct upload to reduce request-chain overhead.",
"Built an internal Chrome extension, zjkj-decryption, to improve data parsing efficiency.",
],
},
],
education: {
school: "University of Electronic Science and Technology of China",
degree: "Bachelor",
major: "Information Management and Information Systems",
period: "2023 - 2025",
},
};
+73
View File
@@ -0,0 +1,73 @@
// 7 轮问答的文案与节奏配置 —— 每轮节奏参数刻意不同,避免线性重复
export const contact = {
emails: ["419021733@qq.com", "wmagmgema521@gmail.com"],
phone: "19980439383",
github: "https://github.com/zhanBoss",
identity: "男 · 29 岁 · 电子科技大学 信息管理与信息系统 本科",
};
export const rounds = [
{
id: "boot",
label: "BOOT",
type: "boot",
question: null,
side: 1,
palette: ["#22d3ee", "#a78bfa"],
},
{
id: "intro",
label: "WHO",
type: "signals",
question: "先介绍一下你自己?",
side: -1,
palette: ["#2dd4bf", "#a78bfa"],
staggerEach: 0.055,
},
{
id: "focus",
label: "NOW",
type: "focus",
question: "最近一年具体在做什么?",
side: 1,
palette: ["#22d3ee", "#c084fc"],
staggerEach: 0.085,
},
{
id: "projects",
label: "PROOF",
type: "projects",
question: "有实际项目证明吗?",
side: -1,
palette: ["#38bdf8", "#a78bfa"],
flow: true, // 内容超过一屏:逐块流式生成
staggerEach: 0.045,
},
{
id: "skills",
label: "STACK",
type: "skills",
question: "技能栈展开讲讲?",
side: 1,
palette: ["#67e8f9", "#8b5cf6"],
staggerEach: 0.02,
},
{
id: "experience",
label: "PATH",
type: "experience",
question: "之前的团队经历?",
side: -1,
palette: ["#5eead4", "#a78bfa"],
staggerEach: 0.1,
},
{
id: "contact",
label: "PING",
type: "contact",
question: "怎么联系你?",
side: 0,
palette: ["#22d3ee", "#f0abfc"],
staggerEach: 0.09,
},
];
+36
View File
@@ -0,0 +1,36 @@
import gsap from "gsap";
import { renderDialogue, renderRail } from "./render";
import { createMind } from "./mind/mind";
import {
applyReducedMotion,
initChoreography,
initRail,
initRoundSnap,
initSmoothScroll,
playBoot,
} from "./choreography";
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* 1. 由简历数据生成对话 DOM */
renderDialogue(document.getElementById("dialogue"));
/* 2. 思维流体 */
const mind = createMind(document.getElementById("mind-canvas"), { reduced });
window.__mind = mind;
gsap.ticker.add(() => mind.tick());
/* 3. 进度轨 + 滚动编排 */
let lenis = null;
if (!reduced) lenis = initSmoothScroll();
const rail = initRail(lenis);
renderRail(document.getElementById("session-rail"), rail.jump);
if (!reduced) {
initChoreography(mind, rail.setActive);
initRoundSnap(lenis);
playBoot(mind);
} else {
applyReducedMotion();
}
+255
View File
@@ -0,0 +1,255 @@
import * as THREE from "three";
import { blobFragment, blobVertex, particleFragment, particleVertex } from "./shaders";
// 三状态目标值 —— 每帧向目标插值,无硬切
const STATES = {
idle: { freq: 1.15, amp: 0.24, speed: 0.22, heat: 0.0, scale: 1.0, attract: 0.12 },
thinking: { freq: 2.6, amp: 0.27, speed: 0.95, heat: 0.7, scale: 0.86, attract: 1.0 },
answering: { freq: 1.9, amp: 0.46, speed: 0.5, heat: 0.22, scale: 1.09, attract: 0.0 },
};
const lerp = (a, b, t) => a + (b - a) * t;
export function createMind(canvas, { reduced = false } = {}) {
const isMobile = window.innerWidth < 880;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 30);
camera.position.z = 6;
const group = new THREE.Group();
scene.add(group);
/* ---- 流体球 ---- */
const blobUniforms = {
uTime: { value: 0 },
uFreq: { value: STATES.idle.freq },
uAmp: { value: STATES.idle.amp },
uSpeed: { value: STATES.idle.speed },
uHeat: { value: 0 },
uOpacity: { value: 1 },
uColorA: { value: new THREE.Color("#22d3ee") },
uColorB: { value: new THREE.Color("#a78bfa") },
};
const blob = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.35, isMobile ? 48 : 96),
new THREE.ShaderMaterial({
vertexShader: blobVertex,
fragmentShader: blobFragment,
uniforms: blobUniforms,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
})
);
group.add(blob);
/* ---- 伴生粒子 ---- */
const COUNT = isMobile ? 900 : 2000;
const seeds = new Float32Array(COUNT * 3);
const radii = new Float32Array(COUNT);
const speeds = new Float32Array(COUNT);
for (let i = 0; i < COUNT; i++) {
seeds[i * 3] = Math.random();
seeds[i * 3 + 1] = Math.random();
seeds[i * 3 + 2] = Math.random();
radii[i] = 1.7 + Math.random() * 1.5;
speeds[i] = 0.25 + Math.random() * 0.6;
}
const pGeo = new THREE.BufferGeometry();
pGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3));
pGeo.setAttribute("aSeed", new THREE.BufferAttribute(seeds, 3));
pGeo.setAttribute("aRadius", new THREE.BufferAttribute(radii, 1));
pGeo.setAttribute("aSpeed", new THREE.BufferAttribute(speeds, 1));
const particleUniforms = {
uTime: { value: 0 },
uAttract: { value: STATES.idle.attract },
uBurst: { value: 0 },
uDir: { value: -1 },
uSize: { value: isMobile ? 9 : 12 },
uMouse: { value: new THREE.Vector3(999, 999, 999) },
uMouseForce: { value: 0 },
uGrab: { value: 0 },
uHeat: { value: 0 },
uOpacity: { value: 1 },
uColorA: blobUniforms.uColorA,
uColorB: blobUniforms.uColorB,
};
const particles = new THREE.Points(
pGeo,
new THREE.ShaderMaterial({
vertexShader: particleVertex,
fragmentShader: particleFragment,
uniforms: particleUniforms,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
})
);
group.add(particles);
/* ---- 状态机 ---- */
let current = { ...STATES.idle };
let target = STATES.idle;
let stateName = "idle";
let burst = 0;
const colorA = blobUniforms.uColorA.value;
const colorB = blobUniforms.uColorB.value;
const targetColorA = colorA.clone();
const targetColorB = colorB.clone();
// 停靠位置:side -1 内容在右 → 流体去左;1 → 右;0 → 居中偏后
const dock = new THREE.Vector3();
const dockTarget = new THREE.Vector3();
const pointer = { x: 0, y: 0 };
function dockFor(side) {
if (window.innerWidth < 880) {
// 移动端:居中上移、退后、缩小,让内容可读
return new THREE.Vector3(side * 0.3, 1.25, -1.6);
}
if (side === 0) return new THREE.Vector3(0, 0.95, -2.8);
return new THREE.Vector3(side * 2.05, 0.1, -0.7);
}
let side = 1;
let opacityTarget = 1;
let hasPointer = false;
let mouseSnapped = false;
// 按压牵引弹簧(欠阻尼 → 过冲回弹的阻尼感)
let grab = 0;
let grabVel = 0;
let grabTarget = 0;
const mouseRay = new THREE.Vector3();
const mouseLocal = new THREE.Vector3();
const api = {
setState(name) {
if (name === stateName || reduced) return;
stateName = name;
target = STATES[name];
if (name === "answering") burst = 1; // 喷发脉冲,随帧衰减
},
setSide(s) {
side = s;
dockTarget.copy(dockFor(s));
particleUniforms.uDir.value = s === 0 ? 0 : -s; // 粒子喷向内容区
opacityTarget = s === 0 ? 0.42 : 1; // 居中停靠时减淡,保证内容可读
},
setPalette([a, b]) {
targetColorA.set(a);
targetColorB.set(b);
},
get state() {
return stateName;
},
debug() {
return { stateName, side, dock: dock.toArray(), dockTarget: dockTarget.toArray(), group: group.position.toArray() };
},
};
api.setSide(1);
dock.copy(dockTarget);
group.position.copy(dock);
window.addEventListener("pointermove", (e) => {
pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
pointer.y = (e.clientY / window.innerHeight) * 2 - 1;
hasPointer = true;
});
window.addEventListener("pointerdown", (e) => {
pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
pointer.y = (e.clientY / window.innerHeight) * 2 - 1;
hasPointer = true;
grabTarget = 1;
});
const release = () => { grabTarget = 0; };
window.addEventListener("pointerup", release);
window.addEventListener("pointercancel", release);
window.addEventListener("blur", release);
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
api.setSide(side);
});
/* ---- 帧循环(由外部 ticker 驱动) ---- */
const clock = new THREE.Clock();
function tick() {
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
const k = 1 - Math.pow(0.0014, dt); // 帧率无关的平滑系数
current.freq = lerp(current.freq, target.freq, k);
current.amp = lerp(current.amp, target.amp, k);
current.speed = lerp(current.speed, target.speed, k);
current.heat = lerp(current.heat, target.heat, k);
current.scale = lerp(current.scale, target.scale, k);
current.attract = lerp(current.attract, target.attract, k);
burst = lerp(burst, 0, 1 - Math.pow(0.25, dt)); // 脉冲衰减
blobUniforms.uTime.value = reduced ? t * 0.25 : t;
blobUniforms.uFreq.value = current.freq;
blobUniforms.uAmp.value = current.amp;
blobUniforms.uSpeed.value = current.speed;
blobUniforms.uHeat.value = current.heat;
particleUniforms.uTime.value = reduced ? t * 0.25 : t;
particleUniforms.uAttract.value = current.attract;
particleUniforms.uBurst.value = burst;
particleUniforms.uHeat.value = current.heat;
colorA.lerp(targetColorA, k);
colorB.lerp(targetColorB, k);
const op = lerp(blobUniforms.uOpacity.value, opacityTarget, k);
blobUniforms.uOpacity.value = op;
particleUniforms.uOpacity.value = op;
const breathe = 1 + Math.sin(t * 1.4) * 0.018;
group.scale.setScalar(current.scale * breathe);
dock.lerp(dockTarget, 1 - Math.pow(0.02, dt));
group.position.set(
dock.x + pointer.x * 0.14,
dock.y - pointer.y * 0.1,
dock.z
);
group.rotation.y += dt * 0.12;
group.rotation.x = pointer.y * 0.08;
// 鼠标牵引:把指针投影到流体所在深度平面,转到 group 局部空间后平滑跟随
if (hasPointer && !reduced) {
mouseRay.set(pointer.x, -pointer.y, 0.5).unproject(camera).sub(camera.position).normalize();
const planeDist = (group.position.z - camera.position.z) / mouseRay.z;
mouseLocal.copy(camera.position).addScaledVector(mouseRay, planeDist);
group.updateMatrixWorld();
group.worldToLocal(mouseLocal);
if (!mouseSnapped) {
particleUniforms.uMouse.value.copy(mouseLocal);
mouseSnapped = true;
} else {
particleUniforms.uMouse.value.lerp(mouseLocal, 1 - Math.pow(0.01, dt));
}
particleUniforms.uMouseForce.value = lerp(particleUniforms.uMouseForce.value, 0.85, k);
// 按压牵引:欠阻尼弹簧积分 —— 按下缓冲吸入,松开过冲散开再归位
grabVel += (grabTarget - grab) * 26 * dt;
grabVel *= Math.exp(-3.2 * dt);
grab += grabVel * dt;
particleUniforms.uGrab.value = grab;
}
renderer.render(scene, camera);
}
return { ...api, tick };
}
+202
View File
@@ -0,0 +1,202 @@
// 思维流体 GLSL —— Ashima simplex noise + 3 octave FBM + Fresnel
const SIMPLEX = /* glsl */ `
vec3 mod289(vec3 x){ return x - floor(x * (1.0/289.0)) * 289.0; }
vec4 mod289(vec4 x){ return x - floor(x * (1.0/289.0)) * 289.0; }
vec4 permute(vec4 x){ return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v){
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}
float fbm(vec3 p){
float sum = 0.0;
float amp = 0.5;
for (int i = 0; i < 3; i++) {
sum += amp * snoise(p);
p *= 2.1;
amp *= 0.5;
}
return sum;
}
`;
export const blobVertex = /* glsl */ `
uniform float uTime;
uniform float uFreq;
uniform float uAmp;
uniform float uSpeed;
varying float vDisp;
varying vec3 vNormal;
varying vec3 vViewDir;
${SIMPLEX}
void main(){
float t = uTime * uSpeed;
float d = fbm(normal * uFreq + vec3(t, t * 0.7, -t * 0.4));
vDisp = d;
vec3 displaced = position + normal * d * uAmp;
vec4 mvPosition = modelViewMatrix * vec4(displaced, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewDir = normalize(-mvPosition.xyz);
gl_Position = projectionMatrix * mvPosition;
}
`;
export const blobFragment = /* glsl */ `
uniform vec3 uColorA;
uniform vec3 uColorB;
uniform float uHeat;
uniform float uOpacity;
varying float vDisp;
varying vec3 vNormal;
varying vec3 vViewDir;
void main(){
float fresnel = pow(1.0 - max(dot(vNormal, vViewDir), 0.0), 2.4);
vec3 base = mix(uColorA, uColorB, vDisp * 0.5 + 0.5);
base = mix(base, vec3(1.0, 0.62, 0.38), uHeat * 0.4);
vec3 color = base * (0.32 + fresnel * 1.6) + base * smoothstep(0.35, 0.9, vDisp) * 0.45;
float alpha = (0.22 + fresnel * 0.85) * uOpacity;
gl_FragColor = vec4(color, alpha);
}
`;
export const particleVertex = /* glsl */ `
uniform float uTime;
uniform float uAttract; // 1 = 吸入核心 (thinking)
uniform float uBurst; // 1 = 向外喷发 (answering)
uniform float uDir; // 喷发的水平方向(指向内容区)
uniform float uSize;
uniform vec3 uMouse; // 指针在粒子局部空间的位置
uniform float uMouseForce;
uniform float uGrab; // 按压牵引强度(弹簧驱动,可为负 → 回弹散开)
attribute vec3 aSeed; // 每粒子随机种子
attribute float aRadius;
attribute float aSpeed;
varying float vFade;
void main(){
float t = uTime * aSpeed;
// 进动轨道:两组相位叠加,避免规则圆轨
float theta = aSeed.x * 6.2831 + t;
float phi = aSeed.y * 3.1415 + sin(t * 0.6 + aSeed.z * 6.2831) * 0.7;
float r = aRadius * mix(1.0, 0.3, uAttract);
r += uBurst * (0.8 + aSeed.z * 2.6);
vec3 pos = vec3(
cos(theta) * sin(phi) * r,
cos(phi) * r * 0.85,
sin(theta) * sin(phi) * r
);
// 喷发时整体偏向内容区一侧
pos.x += uBurst * uDir * (0.6 + aSeed.y * 1.8);
// 鼠标牵引:靠近指针的粒子被吸过去,绕指针形成小漩涡
float md = distance(pos, uMouse);
float pull = smoothstep(2.8, 0.2, md) * uMouseForce * (0.35 + aSeed.z * 0.65);
vec3 swirl = vec3(
sin(t * 2.2 + aSeed.x * 6.2831),
cos(t * 1.7 + aSeed.y * 6.2831),
sin(t * 1.3 + aSeed.z * 6.2831)
) * (0.12 + aSeed.y * 0.3);
pos = mix(pos, uMouse + swirl, pull);
// 按住鼠标:全体粒子被牵向按压点,逐粒错相往复脉动;
// uGrab 为负(松开回弹)时 mix 外推 → 粒子向外散开再被拉回
float osc = 0.55 + 0.45 * sin(uTime * (1.2 + aSeed.x * 1.6) + aSeed.y * 6.2831);
float gpull = clamp(uGrab, -0.35, 1.25) * osc * (0.5 + aSeed.z * 0.5);
gpull = clamp(gpull, -0.4, 0.96);
vec3 gswirl = vec3(
sin(t * 1.8 + aSeed.y * 6.2831),
cos(t * 1.4 + aSeed.z * 6.2831),
sin(t * 1.1 + aSeed.x * 6.2831)
) * (0.18 + aSeed.x * 0.5);
pos = mix(pos, uMouse + gswirl, gpull);
vFade = 1.0 - uBurst * aSeed.z * 0.9;
vFade = min(vFade + pull * 0.5 + max(gpull, 0.0) * 0.35, 1.2);
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = uSize * (0.6 + aSeed.z) * (1.0 + pull * 0.5 + max(gpull, 0.0) * 0.3) * (3.4 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`;
export const particleFragment = /* glsl */ `
uniform vec3 uColorA;
uniform vec3 uColorB;
uniform float uHeat;
uniform float uOpacity;
varying float vFade;
void main(){
vec2 uv = gl_PointCoord - 0.5;
float d = length(uv);
float mask = smoothstep(0.5, 0.05, d);
vec3 color = mix(uColorA, uColorB, vFade);
color = mix(color, vec3(1.0, 0.62, 0.38), uHeat * 0.35);
gl_FragColor = vec4(color, mask * vFade * 0.75 * uOpacity);
}
`;
File diff suppressed because it is too large Load Diff
+241
View File
@@ -0,0 +1,241 @@
import {
experiences,
focusAreas,
metrics,
projects,
resumeSignals,
skills,
} from "./resume-data";
import { contact, rounds } from "./dialogue";
const el = (tag, className, html) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (html != null) node.innerHTML = html;
return node;
};
const externalLink = (className, href, text, ariaLabel = text) => {
const node = el("a", className);
node.href = href;
node.target = "_blank";
node.rel = "noreferrer";
node.textContent = text;
node.setAttribute("aria-label", ariaLabel);
return node;
};
/* ---------- 各轮回答内容 ---------- */
function bootContent(answer) {
answer.append(
el(
"h1",
"boot-greeting",
`你好,我是<span class="grad">湛兮(花名)</span>。<br /><span class="typed-line"></span>`,
),
el(
"p",
"boot-identity gen",
`7 年 Web 与跨端前端 · 最近一年在 <em>AI 产品团队</em> 做对话链路与控制台`,
),
);
const stream = el("div", "metric-stream");
metrics.forEach((m) => {
const token = el("div", "metric-token gen");
token.append(el("span", "v", m.value), el("span", "l", m.label));
stream.append(token);
});
answer.append(stream, el("div", "boot-hint gen", "SCROLL TO ASK"));
}
function signalsContent(answer) {
answer.append(el("div", "a-title gen", "Self Introduction"));
resumeSignals.forEach((line, i) => {
const row = el("div", "signal-line gen");
row.append(
el("span", "idx", String(i + 1).padStart(2, "0")),
el("p", null, line),
);
answer.append(row);
});
}
function focusContent(answer) {
answer.append(el("div", "a-title gen", "Last 12 Months"));
const grid = el("div", "focus-grid");
answer.append(grid);
focusAreas.forEach((f) => {
const card = el("div", "focus-card gen");
card.append(el("h3", null, f.title), el("p", null, f.summary));
const tags = el("div", "tag-row");
f.points.forEach((p) => tags.append(el("span", "tag", p)));
card.append(tags);
grid.append(card);
});
}
function projectsContent(answer) {
answer.append(el("div", "a-title gen", "Selected Proof"));
projects.forEach((p) => {
const block = el("article", "project-block gen");
const head = el("div", "project-head");
const meta = el("div", "project-meta");
meta.append(
p.url
? externalLink("name entity-link", p.url, p.name, `打开 ${p.name}`)
: el("span", "name", p.name),
);
if (p.links?.length > 1) {
const links = el("div", "project-links");
p.links.forEach((link) => {
links.append(
externalLink("mini-link", link.url, link.label, `打开 ${link.label}`),
);
});
meta.append(links);
}
head.append(
Object.assign(el("img"), { src: p.logo, alt: p.name, loading: "lazy" }),
meta,
el("span", "period", p.period),
);
block.append(
head,
el("div", "project-sub", p.subtitle),
el("p", "project-summary", p.summary),
);
const ul = el("ul", "project-modules");
p.modules.forEach((m) => ul.append(el("li", "gen-sub", m)));
block.append(ul);
const tags = el("div", "tag-row");
p.tech.forEach((t) => tags.append(el("span", "tag", t)));
block.append(tags);
answer.append(block);
});
}
function skillsContent(answer) {
answer.append(el("div", "a-title gen", "Token Stream / Skills"));
const HOT = new Set([
"SSE 流式通信",
"Next.js",
"React",
"React Native",
"TypeScript",
"Qiankun",
]);
skills.forEach((group) => {
const wrap = el("div", "skill-group");
wrap.append(el("div", "g-name", group.group));
const row = el("div", "tag-row");
group.items.forEach((item) => {
row.append(
el("span", `skill-token gen${HOT.has(item) ? " hot" : ""}`, item),
);
});
wrap.append(row);
answer.append(wrap);
});
}
function experienceContent(answer) {
answer.append(el("div", "a-title gen", "Career Path"));
experiences.forEach((exp) => {
const item = el("div", "exp-item gen");
const head = el("div", "exp-head");
head.append(
Object.assign(el("img"), {
src: exp.logo,
alt: exp.company,
loading: "lazy",
}),
exp.url
? externalLink(
"co entity-link",
exp.url,
exp.company,
`打开 ${exp.company}`,
)
: el("span", "co", exp.company),
el("span", "period", exp.period),
);
item.append(head, el("div", "exp-role", exp.role));
const ul = el("ul", "exp-points");
exp.points.forEach((pt) => ul.append(el("li", null, pt)));
item.append(ul);
answer.append(item);
});
}
function contactContent(answer) {
answer.append(
el("h2", "contact-lead gen", "随时可以开启下一轮对话。"),
el("p", "contact-id gen", contact.identity),
);
const actions = el("div", "contact-actions gen");
const mail = el("a", "contact-btn primary", "发送邮件");
mail.href = `mailto:${contact.emails[0]}`;
const mail2 = el("a", "contact-btn", contact.emails[1]);
mail2.href = `mailto:${contact.emails[1]}`;
const gh = el("a", "contact-btn", "GitHub / zhanBoss");
gh.href = contact.github;
gh.target = "_blank";
gh.rel = "noreferrer";
const tel = el("a", "contact-btn", contact.phone);
tel.href = `tel:${contact.phone}`;
actions.append(mail, mail2, gh, tel);
answer.append(actions, el("div", "session-end gen", "SESSION SAVED · 2026"));
}
const CONTENT = {
boot: bootContent,
signals: signalsContent,
focus: focusContent,
projects: projectsContent,
skills: skillsContent,
experience: experienceContent,
contact: contactContent,
};
/* ---------- 装配 ---------- */
export function renderDialogue(mount) {
rounds.forEach((round) => {
const section = el("section", "round");
section.id = `r-${round.id}`;
section.dataset.side = String(round.side);
const inner = el("div", "round-inner");
if (round.question) {
const qRow = el("div", "q-row");
const bubble = el("div", "q-bubble");
bubble.append(el("span", "q-text"), el("span", "q-caret"));
qRow.append(bubble);
const thinking = el("div", "thinking");
const dots = el("span", "dots");
dots.append(el("span", "dot"), el("span", "dot"), el("span", "dot"));
thinking.append(dots, el("span", null, "THINKING"));
inner.append(qRow, thinking);
}
const answer = el("div", "answer");
CONTENT[round.type](answer);
if (round.type !== "boot") answer.append(el("span", "a-caret", "▋"));
inner.append(answer);
section.append(inner);
mount.append(section);
});
}
export function renderRail(mount, onJump) {
rounds.forEach((round) => {
const node = el("button", "rail-node");
node.type = "button";
node.dataset.round = round.id;
node.setAttribute("aria-label", `跳转到 ${round.label}`);
node.append(el("span", "rail-label", round.label));
node.addEventListener("click", () => onJump(round.id));
mount.append(node);
});
}
+184
View File
@@ -0,0 +1,184 @@
export const metrics = [
{ value: "7 年", label: "Web 与跨端前端经验" },
{ value: "2 条", label: "近一年参与的产品线" },
{ value: "SSE", label: "对话流、思考态、消息重试" },
{ value: "12 个", label: "微前端拆分基础项目" },
{ value: "30%", label: "消息渲染链路优化结果" },
{ value: "6000+", label: "门店业务系统服务规模" },
];
export const focusAreas = [
{
title: "对话链路",
summary: "做过流式消息、思考态、来源引用、历史同步和失败重试,重点是让对话过程稳定、可恢复。",
points: ["SSE", "Markdown/LaTeX", "来源引用", "消息重试"],
},
{
title: "控制台业务",
summary: "参与过 API Key、账单、组织权限、额度、交易筛选和导出,知道复杂后台最怕状态不清和边界不稳。",
points: ["API Key", "Billing", "RBAC", "Excel 导出"],
},
{
title: "跨端内容流",
summary: "在 Expo / React Native 项目里做过新闻流、详情页、瀑布流、图片组件和游客转登录数据处理。",
points: ["Expo", "瀑布流", "Smart Image", "游客绑定"],
},
{
title: "工程习惯",
summary: "经历过微前端拆分、跨项目公共包、i18n 协作、监控接入和代码审查,能把模块交付和团队维护放在一起考虑。",
points: ["Qiankun", "Monorepo", "i18n", "Code Review"],
},
];
export const resumeSignals = [
"前端经验覆盖 PC 管理后台、小程序、H5、React Native 和微前端,最近一年主要在 AI 产品团队做业务前端。",
"SeaBuzz 侧更偏用户体验:对话、内容流、新闻详情、游客数据、分享和反馈链路。",
"SeaCloud / Vtrix 侧更偏控制台:Pricing、Billing、API Keys、组织权限、交易筛选和分销配置。",
"技术栈以 React / Next.js / React Native / TypeScript 为主,也有 Vue、UniApp 和 Qiankun 项目经验。",
];
export const skills = [
{
group: "对话与内容",
items: ["SSE 流式通信", "打字机响应", "Markdown/LaTeX", "来源引用", "对话历史", "新闻详情"],
},
{
group: "控制台",
items: ["Pricing", "Billing", "API Keys", "组织权限", "额度限制", "交易筛选", "Excel 导出"],
},
{
group: "跨端体验",
items: ["React Native", "Expo Router", "NativeWind", "游客转登录", "只读分享", "Smart Image"],
},
{
group: "Web 主栈",
items: ["Next.js", "React", "Vue 3", "TypeScript", "Tailwind CSS", "Zustand", "Alova"],
},
{
group: "小程序与 H5",
items: ["UniApp", "微信小程序", "飞书小程序", "H5", "摄像头扫码", "低功耗蓝牙"],
},
{
group: "工程协作",
items: ["pnpm Monorepo", "Git Submodules", "Qiankun", "Lerna", "i18n 同步", "Code Review"],
},
{
group: "工程化与质量",
items: ["ARMS 监控", "CDN 优化", "Playwright", "Git Flow", "分支规范", "新人培训"],
},
];
export const projects = [
{
id: "vtrix",
name: "SeaCloud / Vtrix",
logo: "/logos/vtrix.png",
url: "https://cloud.seaart.ai/",
links: [
{ label: "SeaCloud", url: "https://cloud.seaart.ai/" },
{ label: "Vtrix", url: "https://www.vtrix.ai/" },
],
period: "2025.09 - 至今",
subtitle: "模型服务平台控制台",
summary:
"参与模型服务平台的前端建设,主要处理 Pricing、Billing、API Keys、组织权限、额度和分销相关页面。项目复杂度不在单个页面,而在状态、权限和账务边界。",
modules: [
"建设分销控制台:客户邀请、折扣模板、额度分配、销售配置与利润率计算。",
"封装 useCurrency Hook,贯穿 Pricing、Billing、API Keys 等模型调用和费用模块。",
"落地组织角色权限、成员配额、支出限额、账单报表、交易筛选与 Excel 导出。",
"参与 i18n Submodule 架构迁移、企业微信翻译同步和通用表格/筛选器/分页组件增强。",
],
tech: ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS", "Zustand", "Alova", "Radix UI", "next-intl"],
},
{
id: "seabuzz",
name: "SeaBuzz",
logo: "/logos/seabuzz.webp",
url: "https://play.google.com/store/apps/details?id=app.seahot.ai",
period: "2025.05 - 至今",
subtitle: "资讯、搜索与对话应用",
summary:
"基于 Expo 的跨端应用,覆盖 iOS、Android、Web。我的工作集中在对话链路、内容流、登录绑定、分享和公共包沉淀。",
modules: [
"实现对话链路:SSE 流式聊天、打字机渲染、Markdown/LaTeX、思考动画、来源引用、消息重写与历史同步。",
"处理游客聊天记录本地存储、登录后自动绑定、分享链接只读模式和反馈机制。",
"交付 Discover 发现页、新闻详情、瀑布流动态布局、骨架屏、Smart Image 与富文本章节渲染。",
"集成 Google、Facebook、Discord、Email 登录与 SeaArt Auth SDK。",
"封装 Monorepo 公共包体系,覆盖 API、数据模型、UI 组件、状态、OTA 热更新与代码签名。",
],
tech: ["React Native", "Expo 53", "Expo Router", "Zustand", "NativeWind", "SSE", "i18next", "Adyen"],
},
{
id: "bawang",
name: "霸王功夫",
logo: "/logos/chagee.png",
url: "https://bwcj.com/",
period: "2024.03 - 2025.03",
subtitle: "门店、食安与供应链系统",
summary:
"服务全球 6000+ 门店及运营伙伴,覆盖门店运营、食品安全、供应链协同等核心业务。",
modules: [
"搭建小程序报损模块,调用摄像头扫码并兼容微信小程序与飞书 H5。",
"实现食安管理模块,通过低频蓝牙连接 TSPL 指令集打印机,覆盖国内、东南亚、北美三套业务逻辑。",
"交付多门店经营状态移动看板,支持区域、时间、指标筛选。",
"负责采购、供应商合同、供应商结算等供应链系统,对接云厉、费控等外部系统。",
],
tech: ["React 18", "Vue 3", "UniApp", "MobX", "Pinia", "Element Plus", "Ant Design"],
},
{
id: "jingyingbang",
name: "经营帮平台 + 经营帮拉新",
logo: "/logos/jingyingbang.png",
url: "https://jingyingbang.com/",
period: "2022.04 - 2024.03",
subtitle: "微前端平台与小程序",
summary:
"基于信息化设计理念和区块链技术的工业互联网平台,为企业和个人提供数字化运营服务。",
modules: [
"参与单体前端到 Qiankun 微前端拆分,共拆分出 12 个基础项目。",
"负责商管、门户、经营帮小程序、H5、Admin 后台核心业务交付。",
"引入华为云 OBS 直传,降低请求链路性能浪费。",
"开发内部 Chrome 插件 zjkj-decryption,提升数据解析效率。",
],
tech: ["Vue", "Qiankun", "Element UI", "华为云 OBS", "百度地图", "高德地图", "UniApp"],
},
];
export const experiences = [
{
company: "成都海艺互娱科技有限公司",
logo: "/logos/seaart.webp",
url: "https://www.seaart.ai/",
role: "React Native 开发工程师",
period: "2025.05 - 至今",
points: [
"参与 SeaBuzz、SeaCloud / Vtrix 等项目,主要负责 React Native 跨端页面和 Web 控制台模块。",
"负责对话流、内容流、分享、游客数据绑定、账单、API Keys、组织权限等业务页面开发。",
"优化消息渲染链路和跨端公共包复用,消息渲染性能提升约 30%。",
],
},
{
company: "四川茶姬企业管理有限公司",
logo: "/logos/chagee.png",
url: "https://bwcj.com/",
role: "高级 Web 前端开发",
period: "2024.03 - 2025.03",
points: [
"主导供应链管理系统前端开发,实现采购、仓储、配送等核心模块功能。",
"独立负责门店报损与食安核心功能搭建及迭代。",
"完成前端监控体系引入、CDN 静态资源优化、代码审查机制与 Git 操作规范建设。",
],
},
{
company: "中钧科技有限公司四川分公司",
logo: "/logos/zhongjun.png",
url: "https://zhongjunkeji.com/",
role: "前端开发组长",
period: "2022.04 - 2024.03",
points: [
"负责经营帮 PC 端微前端、门户和商管核心模块开发,完成 UI 还原、接口联调与性能优化。",
"从零搭建新 E 畅行小程序基础框架,制定技术方案并完成全流程开发。",
"设计 Git Flow、代码审查与分支管理机制,主导新人培训与项目分工协调。",
],
},
];
+798
View File
@@ -0,0 +1,798 @@
/* ============ 深色 AI 实验室 ============ */
:root {
color-scheme: dark;
--bg: #070b14;
--bg-soft: #0b1120;
--ink: #e6edf7;
--ink-dim: #8b97ad;
--ink-faint: #4d586e;
--cyan: #22d3ee;
--violet: #a78bfa;
--amber: #fbbf24;
--line: rgba(139, 151, 173, 0.16);
--mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, monospace;
--sans: "Inter", "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: auto;
}
body {
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 16px;
line-height: 1.7;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
::selection {
background: rgba(34, 211, 238, 0.35);
}
a {
color: var(--cyan);
text-decoration: none;
}
.entity-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--ink);
transition: color 0.18s ease;
}
.entity-link::after {
content: "↗";
font-family: var(--mono);
font-size: 10px;
color: var(--cyan);
opacity: 0.72;
transform: translateY(-1px);
}
.entity-link:hover {
color: var(--cyan);
}
/* ============ 底层画布与氛围 ============ */
#mind-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.grid-overlay {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: radial-gradient(rgba(139, 151, 173, 0.13) 1px, transparent 1px);
background-size: 34px 34px;
mask-image: radial-gradient(ellipse 90% 80% at 50% 45%, black 30%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse 90% 80% at 50% 45%, black 30%, transparent 100%);
}
/* ============ HUD ============ */
.hud {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 30;
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 28px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
background: linear-gradient(to bottom, rgba(7, 11, 20, 0.85), transparent);
pointer-events: none;
}
.hud-id {
display: flex;
flex-direction: column;
gap: 2px;
}
.hud-name {
color: var(--ink);
font-weight: 700;
}
.hud-sub {
color: var(--ink-faint);
font-size: 10px;
}
.hud-status {
display: flex;
align-items: center;
gap: 8px;
color: var(--cyan);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 10px var(--cyan);
animation: pulse 2.2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* ============ 会话进度轨 ============ */
#session-rail {
position: fixed;
right: 22px;
top: 50%;
transform: translateY(-50%);
z-index: 30;
display: flex;
flex-direction: column;
gap: 4px;
}
.rail-node {
display: flex;
align-items: center;
gap: 10px;
flex-direction: row-reverse;
background: none;
border: 0;
padding: 6px 0;
cursor: pointer;
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.18em;
color: var(--ink-faint);
transition: color 0.3s;
}
.rail-node::after {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
border: 1px solid var(--ink-faint);
transition: all 0.3s;
flex: none;
}
.rail-node .rail-label {
opacity: 0;
transform: translateX(6px);
transition: all 0.3s;
}
.rail-node:hover .rail-label {
opacity: 1;
transform: none;
}
.rail-node.active {
color: var(--cyan);
}
.rail-node.active::after {
background: var(--cyan);
border-color: var(--cyan);
box-shadow: 0 0 12px var(--cyan);
}
.rail-node.active .rail-label {
opacity: 1;
transform: none;
}
/* ============ 对话轮次 ============ */
main {
position: relative;
z-index: 10;
}
.round {
min-height: 100vh;
display: flex;
align-items: center;
padding: 72px 7vw;
}
.round-inner {
width: min(640px, 100%);
position: relative;
}
.round[data-side="-1"] .round-inner { margin-left: auto; }
.round[data-side="1"] .round-inner { margin-right: auto; }
.round[data-side="0"] .round-inner { margin: 0 auto; }
/* 提问气泡(访客) */
.q-bubble {
display: inline-flex;
align-items: baseline;
margin-left: auto;
margin-bottom: 24px;
padding: 12px 20px;
border: 1px solid rgba(34, 211, 238, 0.45);
border-radius: 18px 18px 4px 18px;
font-family: var(--mono);
font-size: 15px;
color: var(--cyan);
background: rgba(34, 211, 238, 0.05);
min-height: 46px;
min-width: 60px;
}
.q-row {
display: flex;
justify-content: flex-end;
}
.q-caret {
display: inline-block;
width: 8px;
height: 1.1em;
margin-left: 4px;
background: var(--cyan);
transform: translateY(2px);
animation: blink 0.9s steps(1) infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
/* 思考态 */
.thinking {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 22px;
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.2em;
color: var(--violet);
opacity: 0;
}
.thinking .dots {
display: inline-flex;
gap: 5px;
}
.thinking .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--violet);
animation: think 1.1s ease-in-out infinite;
}
.thinking .dot:nth-child(2) { animation-delay: 0.18s; }
.thinking .dot:nth-child(3) { animation-delay: 0.36s; }
@keyframes think {
0%, 100% { transform: translateY(0); opacity: 0.4; }
50% { transform: translateY(-5px); opacity: 1; }
}
/* 回答块 */
.answer {
position: relative;
padding-left: 26px;
opacity: 0;
}
.answer::before {
content: "";
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 2px;
border-radius: 2px;
background: linear-gradient(to bottom, var(--cyan), var(--violet));
box-shadow: 0 0 14px rgba(167, 139, 250, 0.5);
}
.a-caret {
display: inline-block;
margin-top: 14px;
color: var(--violet);
font-family: var(--mono);
animation: blink 1s steps(1) infinite;
}
.a-title {
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.22em;
color: var(--violet);
margin-bottom: 18px;
text-transform: uppercase;
}
/* ============ Round 0: BOOT ============ */
#r-boot .round-inner {
width: min(760px, 100%);
}
.boot-greeting {
font-size: clamp(34px, 5.4vw, 58px);
font-weight: 700;
line-height: 1.18;
letter-spacing: -0.01em;
min-height: 1.2em;
}
.boot-greeting .grad {
background: linear-gradient(100deg, var(--cyan), var(--violet));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.boot-identity {
margin-top: 20px;
font-family: var(--mono);
font-size: 14px;
color: var(--ink-dim);
}
.boot-identity em {
font-style: normal;
color: var(--cyan);
}
.metric-stream {
margin-top: 44px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.metric-token {
font-family: var(--mono);
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 16px;
background: rgba(11, 17, 32, 0.7);
backdrop-filter: blur(6px);
}
.metric-token .v {
color: var(--amber);
font-weight: 700;
font-size: 17px;
}
.metric-token .l {
display: block;
font-size: 11px;
color: var(--ink-dim);
margin-top: 2px;
}
.boot-hint {
margin-top: 56px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.24em;
color: var(--ink-faint);
display: flex;
align-items: center;
gap: 12px;
}
.boot-hint::after {
content: "↓";
animation: bob 1.6s ease-in-out infinite;
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(6px); }
}
/* ============ Round 1: 概述 ============ */
.signal-line {
display: flex;
gap: 14px;
margin-bottom: 18px;
font-size: 16px;
color: var(--ink);
}
.signal-line .idx {
font-family: var(--mono);
font-size: 12px;
color: var(--ink-faint);
padding-top: 5px;
flex: none;
}
/* ============ Round 2: 焦点卡 ============ */
.focus-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.focus-card {
border: 1px solid var(--line);
border-radius: 14px;
padding: 20px;
background: rgba(11, 17, 32, 0.72);
backdrop-filter: blur(8px);
transition: border-color 0.3s;
}
.focus-card:hover {
border-color: rgba(34, 211, 238, 0.4);
}
.focus-card h3 {
font-size: 16px;
margin-bottom: 8px;
color: var(--ink);
}
.focus-card p {
font-size: 13px;
color: var(--ink-dim);
margin-bottom: 12px;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
font-family: var(--mono);
font-size: 10.5px;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid rgba(167, 139, 250, 0.35);
color: var(--violet);
}
/* ============ Round 3: 项目 ============ */
.project-block {
border: 1px solid var(--line);
border-radius: 16px;
padding: 24px;
margin-bottom: 18px;
background: rgba(11, 17, 32, 0.78);
backdrop-filter: blur(8px);
}
.project-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 6px;
}
.project-head img {
width: 38px;
height: 38px;
border-radius: 10px;
object-fit: cover;
background: var(--bg-soft);
}
.project-head .name {
font-size: 18px;
font-weight: 700;
}
.project-meta {
min-width: 0;
flex: 1 1 220px;
display: flex;
flex-direction: column;
gap: 4px;
}
.project-links {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mini-link {
font-family: var(--mono);
font-size: 10px;
line-height: 1.4;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid rgba(34, 211, 238, 0.28);
color: var(--cyan);
background: rgba(34, 211, 238, 0.06);
transition: border-color 0.18s ease, background 0.18s ease;
}
.mini-link:hover {
border-color: rgba(34, 211, 238, 0.62);
background: rgba(34, 211, 238, 0.12);
}
.project-head .period {
margin-left: auto;
font-family: var(--mono);
font-size: 11px;
color: var(--ink-faint);
}
.project-sub {
font-family: var(--mono);
font-size: 12px;
color: var(--cyan);
margin-bottom: 10px;
}
.project-summary {
font-size: 13.5px;
color: var(--ink-dim);
margin-bottom: 14px;
}
.project-modules {
list-style: none;
margin-bottom: 14px;
}
.project-modules li {
position: relative;
padding-left: 18px;
font-size: 13px;
color: var(--ink);
margin-bottom: 7px;
}
.project-modules li::before {
content: "▸";
position: absolute;
left: 0;
color: var(--violet);
}
/* ============ Round 4: 技能 ============ */
.skill-group {
margin-bottom: 20px;
}
.skill-group .g-name {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.2em;
color: var(--ink-faint);
margin-bottom: 8px;
text-transform: uppercase;
}
.skill-token {
font-family: var(--mono);
font-size: 12px;
padding: 6px 12px;
border-radius: 8px;
border: 1px solid var(--line);
background: rgba(11, 17, 32, 0.7);
color: var(--ink);
display: inline-block;
}
.skill-token.hot {
border-color: rgba(34, 211, 238, 0.5);
color: var(--cyan);
}
/* ============ Round 5: 经历 ============ */
.exp-item {
position: relative;
padding: 0 0 30px 30px;
}
.exp-item::before {
content: "";
position: absolute;
left: 6px;
top: 8px;
bottom: -4px;
width: 1px;
background: var(--line);
}
.exp-item:last-child::before { bottom: auto; height: calc(100% - 8px); }
.exp-item::after {
content: "";
position: absolute;
left: 2px;
top: 8px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--bg);
border: 2px solid var(--cyan);
box-shadow: 0 0 10px rgba(34, 211, 238, 0.6);
}
.exp-head {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.exp-head img {
width: 26px;
height: 26px;
border-radius: 7px;
object-fit: cover;
}
.exp-head .co {
font-weight: 600;
font-size: 15px;
}
.exp-head .period {
font-family: var(--mono);
font-size: 11px;
color: var(--ink-faint);
}
.exp-role {
font-family: var(--mono);
font-size: 12px;
color: var(--violet);
margin: 4px 0 8px;
}
.exp-points {
list-style: none;
}
.exp-points li {
font-size: 13px;
color: var(--ink-dim);
margin-bottom: 5px;
padding-left: 16px;
position: relative;
}
.exp-points li::before {
content: "—";
position: absolute;
left: 0;
color: var(--ink-faint);
}
/* ============ Round 6: 联系 ============ */
.contact-lead {
font-size: clamp(24px, 3.4vw, 36px);
font-weight: 700;
margin-bottom: 12px;
}
.contact-id {
font-family: var(--mono);
font-size: 12.5px;
color: var(--ink-dim);
margin-bottom: 28px;
}
.contact-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 36px;
}
.contact-btn {
font-family: var(--mono);
font-size: 13px;
padding: 12px 22px;
border-radius: 10px;
border: 1px solid var(--line);
color: var(--ink);
transition: all 0.3s;
}
.contact-btn.primary {
border-color: var(--cyan);
color: #06222b;
background: var(--cyan);
box-shadow: 0 0 24px rgba(34, 211, 238, 0.35);
}
.contact-btn:not(.primary):hover {
border-color: var(--violet);
color: var(--violet);
}
.session-end {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.24em;
color: var(--ink-faint);
display: flex;
align-items: center;
gap: 12px;
}
.session-end::before,
.session-end::after {
content: "";
height: 1px;
flex: 1;
background: var(--line);
}
/* ============ 响应式 ============ */
@media (max-width: 880px) {
.round {
padding: 80px 6vw;
align-items: flex-end;
}
.round-inner {
width: 100%;
}
.focus-grid {
grid-template-columns: 1fr;
}
#session-rail {
right: auto;
top: 54px;
left: 50%;
transform: translateX(-50%);
flex-direction: row;
gap: 14px;
}
.rail-node .rail-label { display: none; }
.hud { padding: 14px 18px; }
.project-head .period {
margin-left: 0;
width: 100%;
}
}
/* ============ 降级 ============ */
@media (prefers-reduced-motion: reduce) {
.q-caret, .a-caret, .status-dot, .thinking .dot, .boot-hint::after {
animation: none;
}
.thinking { display: none; }
.answer { opacity: 1; }
}
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "astro/tsconfigs/strict"
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from "vite";
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
three: ["three"],
},
},
},
},
});