乐于分享
好东西不私藏

无限拆盒(可复制源代码)

无限拆盒(可复制源代码)

效果演示

文末可一键复制完整代码

源代码

<!DOCTYPE html><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>无限拆盒</title><style>            * { margin0padding0box-sizing: border-box; } htmlbody { height100%overflow: hidden; background#eee9e2; } .scene { position: fixed; inset0perspective1200pxdisplay: flex; align-items: center; justify-content: center; } .camera { transform-style: preserve-3d; transformrotateX(-28degrotateY(32deg); } .box { position: absolute; transform-style: preserve-3d; } .hinge { position: absolute; transform-style: preserve-3d; } .face { position: absolute; } .controls { position: fixed; top16pxright16pxdisplay: flex; align-items: center; gap10pxfont11px/1 system-ui, sans-serif; colorrgba(0,0,0,0.35); } .controlsinput[type="range"] { -webkit-appearance: none; width80pxheight2pxbackgroundrgba(0,0,0,0.15); border-radius1pxoutline: none; } .controlsinput[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width12pxheight12pxbackgroundrgba(255,255,255,0.9); border1px solid rgba(0,0,0,0.2); border-radius50%cursor: pointer; } .controlsbutton { background: none; border1px solid rgba(0,0,0,0.2); border-radius3pxpadding3px8pxfont: inherit; color: inherit; cursor: pointer; transition: border-color 0.3s; } .controlsbutton:hover { border-colorrgba(0,0,0,0.4); } .credit { position: fixed; bottom10pxright14pxfont10px/1 system-ui, sans-serif; colorrgba(0,0,0,0.18); text-decoration: none; transition: color 0.3s; } .credit:hover { colorrgba(0,0,0,0.45); }</style></head><body><divclass="scene"><divclass="camera"id="cam"></div></div><divclass="controls"><span>slow</span><inputtype="range"id="speed"min="1"max="10"value="4"step="0.5"><span>fast</span><buttonid="reverse">reverse</button></div><aclass="credit"href="https://calculatequick.com"target="_blank">calculatequick.com</a><script>            (() => { const SIZE = 120const RATIO = 0.44const LID_DURATION = 0.40const LID_SWEEP = 270const WALL_DELAY = 0.38const WALL_DURATION = 0.30const FADE_IN_START = 1.5const FADE_IN_RATE = 2const FADE_OUT_START = 0.7const FADE_OUT_RATE = 1.1const cam = document.getElementById('cam'); const slider = document.getElementById('speed'); const reverseBtn = document.getElementById('reverse'); let speed = 0.0004let direction = 1let phase = 0let lastTime = 0functioncreateElement(classNameconst el = document.createElement('div'); if (className) el.className = className; return el; } functioncreateFace() const face = createElement('face'); face.style.width = `${SIZE}px`; face.style.height = `${SIZE}px`return face; } functioncreateBox(indexconst box = createElement('box'); const floor = createFace(); floor.style.left = `${-SIZE / 2}px`; floor.style.top = `${-SIZE /2}px`; floor.style.transform = 'rotateX(90deg)'; box.appendChild(floor); functioncreateWall(hingeTf, hasChildconst hinge = createElement('hinge'); hinge.style.transform = hingeTf; const wall = createFace(); wall.style.left = `${-SIZE / 2}px`; wall.style.top = `${-SIZE}px`; wall.style.transformOrigin = 'center bottom'; if (hasChild) wall.style.transformStyle = 'preserve-3d'; hinge.appendChild(wall); box.appendChild(hinge); return wall; } const half = SIZE /2const front = createWall(`translateZ(${half}px)`); const back = createWall(`translateZ(${-half}px) rotateY(180deg)`true); const left = createWall(`translateX(${-half}px) rotateY(-90deg)`); const right = createWall(`translateX(${half}px) rotateY(90deg)`); const lid = createFace(); lid.style.left = '0'; lid.style.top = `${-SIZE}px`; lid.style.transformOrigin = 'center bottom'; back.appendChild(lid); return { el: box, index, floor, lid, walls: [front, back, left, right] }; } const boxes = []; for (let i = -2; i <= 3; i++) { const box = createBox(i); cam.appendChild(box.el); boxes.push(box); } function clamp(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } function lidEase(t) { if (t <= 0) return 0; if (t >= 1) return 1; return (1 - Math.cos(t * Math.PI)) / 2; } function styleFace(face, alpha) { face.style.background = `rgba(255,255,255,${0.96 * alpha})`; face.style.border = `0.5px solid rgba(0,0,0,${0.3 * alpha})`; } slider.addEventListener('input', function () { speed = this.value * 0.0001; }); reverseBtn.addEventListener('click', function () { direction *= -1; }); function animate(now) { requestAnimationFrame(animate); phase += (now - lastTime) * speed * direction; lastTime = now; const t = ((phase % 1) + 1) % 1; for (let k = 0; k < boxes.length; k++) { const box = boxes[k]; const depth = box.index - t; const scale = Math.pow(RATIO, depth); box.el.style.transform = `scale3d(${scale},${scale},${scale})`; const progress = clamp(-depth); const lidProgress = clamp(progress / LID_DURATION); const lidAngle = 90 - LID_SWEEP * lidEase(lidProgress); box.lid.style.transform = `rotateX(${lidAngle}deg)`; const wallProgress = clamp((progress - WALL_DELAY) / WALL_DURATION); const wallAngle = -90 * wallProgress * wallProgress; for (let j = 0; j < 4; j++) { box.walls[j].style.transform = `rotateX(${wallAngle}deg)`; } let alpha = 1; if (depth > FADE_IN_START) alpha = Math.max(0, 1 - (depth - FADE_IN_START) * FADE_IN_RATE); if (depth < -FADE_OUT_START) alpha = Math.max(0, 1 - (-depth - FADE_OUT_START) * FADE_OUT_RATE); styleFace(box.floor, alpha); styleFace(box.lid, alpha); for (let j = 0; j < 4; j++) styleFace(box.walls[j], alpha); } } requestAnimationFrame(animate); })();</script></body></html>

实现思路拆分

无限拆盒是怎么画出来的?

无限拆盒(Unboxing)用 JS 动态生成 3D 盒子,大盒打开露出小盒、再打开再露出更小的盒,循环不止;CSS perspective + preserve-3d 负责空间,JS 驱动盖子与四壁旋转。

说白了就三件事

  1. HTML.scene 透视容器 + #cam 相机节点,右上角速度滑块与 reverse 按钮。
  2. CSS:固定视角 rotateX/Y.box / .hinge / .face 分层 3D 结构。
  3. JavaScript:递归 createBox() 拼六面体;requestAnimationFrame 按相位缩放、开盖、倒墙、淡入淡出。

改速度动 slider 映射的 speed,改比例动 RATIO,改节奏动 LID_DURATION 等常量。

颜色为啥长这样

  • 背景:暖灰 #eee9e2
  • 盒面:半透明白 rgba(255,255,255,0.96) + 细黑描边
  • 控件:低对比灰字与细边框,不抢主体
  • 整体极简、纸模感

动起来是啥感觉

斜 45° 俯视一串嵌套白盒,像俄罗斯套娃式开箱。

  • 最前盒盖子先掀起,四壁向外倒下
  • 露出内部缩小的下一层盒,继续同样动作
  • 远处盒淡入、近处盒淡出,形成无限循环
  • 滑块调快慢,reverse 按钮反转拆盒方向
  • 约 0.4s 开盖 + 0.3s 倒墙,节奏干脆

怎么一层层画出来

3D 结构
  1. createBox():floor + 四面墙(hinge 定位)+ back 面上的 lid。
  2. scale3d(RATIO^depth):按层级指数缩小。
  3. .camera:整体 rotateX(-28deg) rotateY(32deg) 定视角。
动画相位
  1. phase 递增,每盒 depth = index - t 算进度。
  2. lidrotateX 从 90° 扫到开盖角度,cosine ease。
  3. walls:延迟后 rotateX(-90deg) 倒下;alpha 远淡近淡。

源码获取