金三银四,面试官连连夸赞的个人网页,如何打造?

摘要:金三银四岗位变少,但一个个人技术网站却让面试官当场夸赞。如果你也想做一个 能在面试中让人眼前一亮的个人网站,一定不要错过。
不知道大家有没有同样的感觉: 今年的 “金三银四”,似乎没有想象中那么热闹。 尤其是 前端岗位,不少公司都在收缩,机会明显少了很多。 我最近也参加了几场大厂面试(我面的是 AI 全栈开发岗)。 有一件事让我挺意外的。 某度的面试官在面试完后加微信聊天的时候说: “刚看到你简历上的个人网站时,就明显感觉到你是一个有技术追求的同学,还让 hr 赶快联系一下面试。” 然后就把这个网站发到分享到小红书上,b站上去了(之前看过类似的效果,特地复刻了一版),反响很不错,所以特此非常以下里面的技术细节。 效果如下: 点击跳转在线地址 同时,欢迎大家关注我的微信公众号:ai超级个人,会有更多的炫酷网页分享 这篇文章会稍微偏技术一些。 如果你: 对网页动效感兴趣 或者几乎没有 Three.js 基础 甚至不太懂 3D 这些思路依然很有价值。 尤其是在 你让 AI 帮你调试代码、改造项目的时候,理解这些结构会非常有帮助。 如果你想系统学习 Three.js,我只推荐一套教程: Three.js Journey(点击跳转 B 站) 这是我心中目前全球范围内,从 0 到 1 最完美的 Three.js 课程。说实话,市面上很多所谓的基础教程,且不说是否存在“割韭菜”的行为,单是乏味的教学逻辑就在浪费你的学习生命。 好了,回到正文。今天我们要深度拆解上面网站效果,以及解决如下三个核心问题: 1. 空间定位:如何在网页中调试模型位置? 在 3D 世界里,任何模型都有它的坐标。以下面这个电脑模型为例: 模型默认被放置在原点 $(0, 0, 0)$,这通常没问题。但真正的痛点在于:摄像机应该架在哪? 打个比方,这就好比现实中的人像摄影: 被拍的人 → 相当于 3D 模型 摄影师 → 相当于 Camera(相机) 模型在那不动,但摄影师的位置(Position)和对焦的方向(LookAt)决定了最终的画面。 而且我们能不能像在 Blender(知名的 3D 图形软件)里一样,在网页端也能直观地旋转、调整远近,从而找到那个最完美的视觉角度?如下图是 blender 的界面: 2. 跨次元融合:如何将真实网页嵌入 3D 场景? 请看下图: 在这台 3D 电脑模型的屏幕中央,其实嵌套了一个真实的网页(前端术语叫 iframe)。 所以问题来了,在 three.js 中如何嵌套一个别的网站的网页呢? 3. 精准对位:如何调试 iframe 的 3D 坐标? 这是上一个问题的延伸。iframe(网页) 作为一个平面,在 3D 空间中同样拥有自己的坐标和旋转参数。但问题是: 你很难凭直觉盲猜出电脑模型那块屏幕的精确数值。 因此,我们需要一套可视化的调试界面。然后配合我们第一步提到的工具,让我们可以在页面上手动微调 iframe 的位置,直到它与模型屏幕完美贴合。 最后,直接将调试好的坐标参数“写死”在代码里就能保证初始化电脑模型和网页都在合适的坐标上。 接下来,我们一步一步,从 0 到 1 实现这个过程: 网页中调整相机位置小技巧 初始的时候,我们假设有如下代码(精简后的 demo )。代码主要做的是将电脑的模型贴图加载进来。 如果你有不了解的代码块可以借助 ai 了解详细信息,因为已经是最基础的 three.js 代码,如果缺乏必要的基础,建议学习上面的教程。 注:代码使用了 react 框架,你也可以让 ai 改造为你的熟悉的技术栈,例如 vue 或者 html: import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; export interface Computer3DConfig { container: HTMLElement; modelPath: string; texturePath: string; modelScale?: number; } export class Computer3D { private scene: THREE.Scene = new THREE.Scene(); private camera: THREE.PerspectiveCamera; private renderer: THREE.WebGLRenderer; private container: HTMLElement; private config: Computer3DConfig; constructor(config: Computer3DConfig) { this.config = { ...config }; this.container = config.container; // 1. 初始化相机 const aspect = window.innerWidth / window.innerHeight; this.camera = new THREE.PerspectiveCamera(35, aspect, 10, 100000); // 2. 初始化渲染器 (WebGL) this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.domElement.style.position = "absolute"; this.renderer.domElement.style.top = "0"; this.renderer.domElement.style.zIndex = "1"; this.container.appendChild(this.renderer.domElement); this.render(); } // 加载模型与贴图 public async load(): Promise<void> { const loader = new GLTFLoader(); const textureLoader = new THREE.TextureLoader(); // 并行加载模型和贴图 const [gltf, texture] = await Promise.all([ loader.loadAsync(this.config.modelPath), textureLoader.loadAsync(this.config.texturePath), ]); // 贴图配置 texture.flipY = false; texture.colorSpace = THREE.SRGBColorSpace; const material = new THREE.MeshBasicMaterial({ map: texture }); // 遍历模型应用材质 gltf.scene.traverse((child: any) => { if (child instanceof THREE.Mesh) { child.material = material; } }); this.scene.add(gltf.scene); } // 渲染循环 private render(): void { requestAnimationFrame(this.render.bind(this)); this.renderer.render(this.scene, this.camera); } // 处理窗口大小调整(建议添加) public onWindowResize(): void { const width = window.innerWidth; const height = window.innerHeight; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); } } export default Computer3D; 避坑指南:为什么初始化你的 3D 世界是一团黑? 很多同学在加载代码后,满怀期待地打开页面,结果发现是一片漆黑。例如我们上面的代码。 别担心,并不是模型消失了,只是你正“站在模型肚子里”!(模型内部)默认情况下,相机的初始坐标在 (0, 0, 0)。而模型加载进来也通常在原点。 这种“合二为一”的状态让你什么也看不见。为了解决这个问题,我们需要像真正的摄影师一样,完成以下三个层层递进的步骤。 首先就是让相机能够完整的看到电脑模型: 第一步:开启“自动对焦”,给模型一个完美的全身照 首先模型的大小是不可控的,有的只有几厘米,有的却有几百米。 所以我们需要一个通用的“自动对焦”函数,让相机自动根据模型的大小调整距离,起码能看清楚模型的全身,然后再后续微调相机位置。 核心逻辑: 用一个隐形的方框把模型包起来(Box3),测量它的尺寸,然后把相机推到足够远的地方。 /** * ✅ 自动对焦:不仅要移相机,还要移控制器的目标点 */ private autoFitCamera(object: THREE.Object3D): void { // 1. 计算模型的包围盒 const box = new THREE.Box3().setFromObject(object); const size = box.getSize(new THREE.Vector3()); // 获取模型长宽高 const center = box.getCenter(new THREE.Vector3()); // 获取模型的中心点 // 2. 根据模型大小计算相机距离 const maxDim = Math.max(size.x, size.y, size.z); const fov = this.camera.fov * (Math.PI / 180); // 视角转弧度 // 数学公式:距离 = 对边 / tan(角度) const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5; // 3. 移动相机位置:稍微偏一点,让画面有立体感 this.camera.position.set( center.x + maxDim * 0.2, center.y + maxDim * 0.3, center.z + cameraDistance, ); // 4. 🔥 关键:让相机不仅“站得远”,还要“盯着看” // 在使用控制器时,必须更新 target 才能真正改变视线方向 this.controls.target.copy(center); this.controls.update(); } 第二步:引入“上帝视角”,让场景动起来 有了对焦还不够,如果你想 360 度观察模型,就需要 OrbitControls(轨道控制器)。它能让你的鼠标变成相机的“推进器”和“转盘”。 也就是有了 OrbitControls,我们就可以动态调整相机的位置,让相机上下左右移动,并且旋转相机视角。 代码集成方案: // 1. 引入辅助组件 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; // 2. 在类里面定义控制器属性 private controls: OrbitControls; // 3. 在 constructor (构造函数) 中初始化 // 这里的第二个参数 renderer.domElement 很重要,它决定了鼠标在哪里滑有效 this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; // 开启阻尼(手感更丝滑,不会突然停住) this.controls.dampingFactor = 0.05; // 4. 重点:在 render 循环中每一帧都要更新它 private render(): void { requestAnimationFrame(this.render.bind(this)); // 只有更新了 controls,你的拖拽和阻尼效果才会生效 this.controls.update(); this.renderer.render(this.scene, this.camera); } 第三步:搭建“导演工作站”,用键盘寻找黄金视角 上面 OrbitControls 虽然能动态调整相机位置,但是毕竟鼠标不好控制,我们还需要增加一些精细的微调方法。 我们采取的是通过键盘微调相机位置,找到感觉最好的那一刻,记录下坐标。也就是键盘一些快捷键能小范围的移动相机视角。 功能说明: 方向键和 W/S:可以在 3D 空间的前后左右上下移动。 P 键:在控制台打印当前相机的“黄金坐标”。 private setupDebugControls(): void { // 辅助线:添加网格和坐标轴,防止在 3D 空间迷失方向 const gridHelper = new THREE.GridHelper(10000, 100); const axesHelper = new THREE.AxesHelper(5000); this.scene.add(gridHelper, axesHelper); window.addEventListener("keydown", (e) => { const step = 100; // 每次按键移动的距离 const moveMap: Record<string, [number, number, number]> = { ArrowUp: [0, step, 0], // 向上 ArrowDown: [0, -step, 0], // 向下 ArrowLeft: [-step, 0, 0], // 向左 ArrowRight: [step, 0, 0], // 向右 w: [0, 0, -step], // 向前 s: [0, 0, step], // 向后 }; if (moveMap[e.key]) { const [x, y, z] = moveMap[e.key]; this.camera.position.addScaledVector(new THREE.Vector3(x, y, z), 1); } // ✅ 导出视角:当你调到满意的角度时,按 P 打印参数 if (e.key === "p") { console.log(`--- 记录当前黄金视角 ---`); console.log(`相机位置: .set(${this.camera.position.x}, ${this.camera.position.y}, ${this.camera.position.z})`); console.log(`盯着看的目标点: .set(${this.controls.target.x}, ${this.controls.target.y}, ${this.controls.target.z})`); } }); } 综上所述,大概就能模拟一个 3D 模型软件的 Camera 视角了。 将网页加入到 three.js 中 这一章节我们进入最酷的部分:让你的 3D 电脑真正“联网”(嵌入网页)。 在普通的 3D 场景中,物体通常只是死板的几何体。但 Three.js 提供了一个强大的“传送门” —— CSS3DObject。 它能把真实的 HTML 元素(如 div、iframe、video)直接塞进 3D 空间,让网页像贴纸一样贴在模型的屏幕上,且依然保持可点击、可交互。 要实现这个效果,我们需要同时运行两个渲染器: WebGLRenderer (底层):负责渲染 3D 模型(电脑外壳)。 CSS3DRenderer (顶层):负责将 HTML 元素通过 CSS3 矩阵变换,投影到 3D 空间中。 我们将这两个渲染器的画布重叠在一起,并让它们共享同一个相机(Camera)。这样当你旋转镜头时,网页和模型就会同步运动,看起来就像网页长在模型上一样。 最小实现 Demo 代码: 以下是整合了模型加载与 iframe 屏幕渲染的完整代码: import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { CSS3DRenderer, CSS3DObject, } from "three/addons/renderers/CSS3DRenderer.js"; export interface Computer3DConfig { container: HTMLElement; modelPath: string; texturePath: string; screenUrl: string; // 注入 iframe 的网址 modelScale?: number; } export class Computer3D { private scene: THREE.Scene = new THREE.Scene(); // WebGL 场景 private cssScene: THREE.Scene = new THREE.Scene(); // CSS3D 专用场景 private camera: THREE.PerspectiveCamera; private renderer: THREE.WebGLRenderer; private cssRenderer: CSS3DRenderer; private controls: OrbitControls; private container: HTMLElement; private config: Computer3DConfig; constructor(config: Computer3DConfig) { this.config = { modelScale: 4200, ...config }; this.container = config.container; const width = window.innerWidth; const height = window.innerHeight; // 1. 初始化相机 this.camera = new THREE.PerspectiveCamera(35, width / height, 10, 100000); this.camera.position.set(930, 600, 4500); // 2. 初始化 WebGL 渲染器 (渲染底层模型) this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setSize(width, height); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.domElement.style.position = "absolute"; this.renderer.domElement.style.top = "0"; this.renderer.domElement.style.zIndex = "1"; // 确保在底层 this.container.appendChild(this.renderer.domElement); // 3. 初始化 CSS3D 渲染器 (渲染顶层网页) this.cssRenderer = new CSS3DRenderer(); this.cssRenderer.setSize(width, height); this.cssRenderer.domElement.style.position = "absolute"; this.cssRenderer.domElement.style.top = "0"; this.cssRenderer.domElement.style.zIndex = "2"; // 盖在模型上面 this.container.appendChild(this.cssRenderer.domElement); // 4. 初始化轨道控制器 // 💡 注意:事件监听要绑定在最上层的 cssRenderer 元素上,否则会被遮挡 this.controls = new OrbitControls(this.camera, this.cssRenderer.domElement); this.controls.enableDamping = true; this.controls.target.set(925, 310, -300); this.render(); window.addEventListener("resize", () => this.onWindowResize()); } // 加载模型与贴图 public async load(): Promise<void> { const loader = new GLTFLoader(); const textureLoader = new THREE.TextureLoader(); const [gltf, texture] = await Promise.all([ loader.loadAsync(this.config.modelPath), textureLoader.loadAsync(this.config.texturePath), ]); texture.flipY = false; texture.colorSpace = THREE.SRGBColorSpace; const material = new THREE.MeshBasicMaterial({ map: texture }); gltf.scene.traverse((child: any) => { if (child instanceof THREE.Mesh) { child.scale.setScalar(this.config.modelScale!); child.material = material; } }); this.scene.add(gltf.scene); // 🔥 模型加载完后,开始在 3D 空间“插”一个网页 this.initIframeScreen(); } private initIframeScreen(): void { // 设定网页的逻辑分辨率(相当于显示器的分辨率) const SCREEN_W = 1480; const SCREEN_H = 1100; // 创建原生 iframe const iframe = document.createElement("iframe"); iframe.src = this.config.screenUrl; iframe.style.width = `${SCREEN_W}px`; iframe.style.height = `${SCREEN_H}px`; iframe.style.border = "none"; iframe.style.backgroundColor = "#000"; // 包装成 3D 对象 const cssObject = new CSS3DObject(iframe); /** * 🛠 坐标微调: * 这是最关键也最耗时的一步。你需要根据模型屏幕的具体位置, * 反复调整 position 和 rotation,直到 iframe 严丝合缝地贴在模型框里。 * 下一小节会有微调的方法 */ cssObject.position.set(900, 458, 765); cssObject.rotation.x = -1; // 配合模型显示器的后仰角度 this.cssScene.add(cssObject); } private onWindowResize(): void { const w = window.innerWidth; const h = window.innerHeight; this.camera.aspect = w / h; this.camera.updateProjectionMatrix(); this.renderer.setSize(w, h); this.cssRenderer.setSize(w, h); } private render(): void { requestAnimationFrame(this.render.bind(this)); this.controls.update(); // 💡 必须同时渲染 WebGL 和 CSS3D 两个场景 this.renderer.render(this.scene, this.camera); this.cssRenderer.render(this.cssScene, this.camera); } } 闭坑指南: 关于 zIndex 的博弈: 我们将 cssRenderer 的 zIndex 设为 2(放在模型上面, 模型的 zIndex 是 1,谁的 zIndex 大,谁就在上面)。所以我们需要我们的网页(用 cssRenderer 渲染)层级更高。 这样你才能直接在 3D 场景里点击网页上的按钮、滑动网页。如果 WebGL 渲染器在上面,网页就只能看不能摸了。 “穿模”与遮挡: 现在的实现方式有一个小缺陷:因为 cssRenderer 永远在 WebGL 模型之上,所以即便你把电脑转到背面,网页依然会“穿过”模型显示在最前面。 因为 cssRender 渲染的是 HTML 元素, 又因为 html 支持 css 中的 backfaceVisibility 属性可以隐藏背面不可见。 所以自然给 cssRender 元素增加 backfaceVisibility = true 即可解决穿模问题。 如何让网页严丝合缝地“贴”到模型屏幕上? 接下来就是最后一个问题了,如何把网页贴在电脑模型的屏幕上。介绍一种常见的微调方法。 我们引入调试利器:Tweakpane。 有类似功能的调试库有很多,如常见的有 dat.GUI,lil-gui 等等,我们使用的是 ai 建议的 Tweakpane 库,并且调试代码完全可以交给 ai 来写。 调试的界面如下: 这部分代码 ai 实现很容易,你可以用以下 prompt 生成对应代码即可,如下: “我正在使用 Three.js 和 CSS3DObject。请帮我引入 Tweakpane 库,为我的 cssObject 创建一个调试面板。 需求: 1. 添加 position (x,y,z) 的调试滑块,范围设置在 -2000 到 2000。 2. 添加 rotation (x,y,z) 的调试滑块,范围是 -Math.PI 到 Math.PI。 3. 添加一个 'Export' 按钮,点击后能在控制台直接打印出当前的 position.set(x,y,z) 和 rotation.set(x,y,z) 代码,方便我复制固定死。请给我完整的 TypeScript 代码片段。” 通过上面的学习,我们发现 3D 网页开发不仅仅是写代码,更是一场关于“寻找最佳视角”的艺术。 最后,欢迎大家加群一起讨论全栈 AI 的实践,讨论酷炫动画的实现。我们下期再见!