3D 作品集
构建一个只属于这个人的一页式作品集。产生这个技能的网站是围绕一位 DJ 的唱片箱搭建的:每个项目都是一个可以翻阅的唱片封套,当唱片在面前时它会旋转。示例:https://alessio-simoya.vercel.app
不要复制那个概念。遵循流程,找到属于他们的概念。
1. 首先收集所有输入
在设计之前先询问这些信息。从一开始就用真实的内容构建。
- 项目: 每个项目提供一份简短的事实摘要:它是什么、你的角色、日期、它实现了什么、技术栈、在线链接,以及一份绝对不能声称的清单。
- 经历: 公司、职位、日期、地点,以及每份工作的一个成果。
- 技能: 分组列出,并注明每项技能在何处得到验证。
- 工作之外的生活: 爱好、团队、他们擅长烹饪什么、去过哪里。概念就来源于此,所以要问得比他们主动提供的更多。
- 素材: 标志、产品截图、视频、Rive 或 Lottie 文件。询问项目文件夹的位置并搜索它们。
- 链接: LinkedIn、GitHub 等。除非对方明确要求,否则永远不要公开他们的电子邮件地址。
2. 找到核心概念
选择对方世界中最具特色的事物,将其做成英雄区,作为一个用户可以与之互动的真实 3D 对象。DJ 会有一个唱片箱。攀岩者可能会有一面岩点墙;面包师会有一盘面包。每个项目都成为该对象中的一个物品。
把大胆放在那里。页面的其余部分保持简洁:纯色色块、一种有个性的展示字体、一种普通的正文字体、四到五种命名颜色。
3. 技术栈
- Vite、React 和 TypeScript(严格模式)
- three.js 用于 3D,直接编写(不需要场景框架)
- 仅当对方已有
.riv文件时才使用 Rive;延迟加载其 wasm - tRPC 和 zod 用于 API,作为 Vercel 函数位于
api/trpc/[trpc].ts - Resend 用于电子邮件,Cloudflare Turnstile 用于反垃圾,Neon Postgres 用于记录
- 通过
@fontsource-variable/*自托管可变字体
4. 3D 搭建方式
使用一个固定的、全屏的 WebGL 画布,设置 pointer-events: none,并将每个 3D 视图绘制到一个 DOM 占位元素的矩形区域内:
const r = el.getBoundingClientRect();
renderer.setViewport(r.left, innerHeight - r.bottom, r.width, r.height);
renderer.setScissor(r.left, innerHeight - r.bottom, r.width, r.height);
renderer.render(scene, camera);
每帧清除一次,开启裁剪测试,并跳过任何屏幕外的元素。这样只需一个上下文就能在页面下方提供多个 3D 对象,并且它们会随布局滚动。
为每个小对象创建一个“骨架”:一个组、一个相机位置、一个注视点和一个由共享时钟驱动的 tick(t) 函数。延迟构建骨架,一次只显示一个。在 2D 画布上绘制纹理(标志、标签、图案),这样就不需要加载模型文件。
曾出现问题及避免方法:
- 比元素大的地面阴影平面会显示为一个硬边矩形。将其大小调整到视图内。
- 当你需要“绕自身轴旋转”时,先旋转组,再旋转子元素。
- 精灵和透明平面在实体网格之后绘制;对象下方的发光效果应是其下面的一个平面,而不是面向相机的精灵。
5. 已证明有效的区域
- 英雄区: 名字设置得很大,一个循环显示其角色的贴纸,一句话介绍,以及交互式对象。其背后的彩色圆盘会随着焦点项目变化。
- 工作: 每个案例研究以该品牌颜色的条带打开,带有其真实标志,然后是横向滚动的图片、视频和动画卷轴,三个简短亮点,几个大数字,以及技术栈标签。长文本放在“阅读完整故事”后面。
- 经历: 每份工作一行,每行附带一个大数字。
- 旅程: 学校或里程碑,每个都有自己的动画对象。
- 技能: 行进的文字行,而不是表格。
- 下班时间: 每个兴趣一个动画对象。
- 旅行: 地点作为冰箱磁铁展示在钢门上,可以拖动。
- 打个招呼: 联系表单。
一个浮动的小标题栏通过 IntersectionObserver 标记当前区域。
6. 规则
- 仅限事实。 使用对方提供的内容。如果某个区域需要对方没有提供的信息,就省略它。在每个项目的“不得声称”列表前添加注释,放在内容文件中。
- 不要虚荣计数。 提交次数、代码行数、页面数或模型数显得不成熟。展示产品和它实现的功能。
- 减少文字。 如果屏幕大部分是段落,就将其转化为视觉效果。
- 尊重他人工作。 不要从预览中重建付费模板,也不要复制品牌的产品设计(例如,知名比赛用球);要用同样的精神创作原创内容。仅使用自由许可的图片,并在页面上注明出处。
- 截图 不得显示真实客户的姓名、金额或面部。
- 动效要有关闭开关。 尊重
prefers-reduced-motion,添加一个“暂停动效”按钮,并在 WebGL 不可用时提供静态回退。 - 将所有内容放在一个类型文件中 (
src/content/portfolio.ts),以便对方无需接触组件即可编辑文字。
7. 联系表单
一个 tRPC 过程,contact.send,输入由 zod 校验。按顺序,它:
- 如果隐藏的蜜罐字段被填写,则静默返回成功;
- 对每个地址应用一个小的速率限制;
- 用 Cloudflare 验证 Turnstile 令牌,失败则拒绝;
- 通过 Resend 给所有者发送邮件,发件人设为
reply_to; - 将每条消息保存到
messages表; - 仅当访客勾选了默认未勾选的“让我了解最新动态”复选框时,才将其保存到
subscribers表(同时记录同意措辞和时间)并添加到 Resend 受众列表。
没有那个勾选,永远不要将任何人添加到邮件列表。
环境变量:RESEND_API_KEY、ENQUIRY_TO_EMAIL、ENQUIRY_FROM_EMAIL、RESEND_AUDIENCE_ID、DATABASE_URL、TURNSTILE_SECRET,以及浏览器使用的公开站点密钥。
曾出现问题:
from值可能已经是姓名 <地址>格式。再次包装会导致 Resend 返回 422。当它包含<时,直接使用。- Vite 只暴露以
envPrefix中某个前缀开头的变量。如果站点密钥以其他名称到达,请添加该前缀。 - Turnstile 小部件仅在其在 Cloudflare 中配置的主机名上有效。
- 在生产环境中,如果没有 Turnstile 密钥,则拒绝发送。仅在本地开发中使用 Cloudflare 发布的测试密钥。
- 添加一个
CONTACT_DRY_RUN=1开关,在生产环境中忽略,以便可以在不发送电子邮件的情况下测试整个路径。 - 要在
vite dev下运行相同的处理程序,添加一个小插件,使用server.ssrLoadModule加载函数并传递一个Request。 - 使用表格和内联样式构建电子邮件 HTML,并对每个字段进行转义。
8. 边工作边检查
每轮更改后:
- 运行构建;
- 在 1440 像素和 390 像素宽度下,为每个区域截取 Playwright 屏幕截图(启动 Chromium 时使用
--use-gl=swiftshader以便 WebGL 在无头模式下渲染); - 确认
document.documentElement.scrollWidth - innerWidth为 0; - 在说明完成之前,自己查看屏幕截图。
明确说明未检查的内容:真实手机、Safari、动效的感觉、真实邮件投递。
9. 发布
git init,并忽略.env*和.vercel。在首次提交前扫描暂存文件中的密钥。vercel link,添加环境变量,vercel --prod。- 检查在线页面是否加载成功且没有失败请求,以及 API 是否拒绝没有 Turnstile 令牌的请求。
- 请所有者通过在线表单发送一条真实消息。
初始提示
给用户填写以下内容:
使用 3d-portfolio 技能构建我的 3D 作品集。
我的姓名和一行描述:
在英雄区循环显示的角色(4 到 6 个):
项目(每个项目提供:事实、我的角色、日期、链接、不得声称的内容):
经历(公司、职位、日期、每个一个成果):
技能分组:
学校或里程碑:
工作之外的兴趣(请具体说明):
我去过的地方:
我的标志、截图和动画在此电脑上的位置:
要展示的链接(LinkedIn、GitHub 等):
我永远不希望出现在网站上的东西: