ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

纯CSS绘制3D回形针:从透视到光影的完整实战

纯CSS绘制3D回形针:从透视到光影的完整实战 一枚回形针能有什么好写的说实话第一次看到 paperclip 这个练习项目标题时我心里也是这个想法。但等我真正动手尝试完全不用一张图片、不用一段 SVG path只用 CSS 把一个带金属光泽的 3D 回形针在页面上立起来的时候才发现这个小小的物件几乎能把 CSS3 里最磨人的几个知识点全部串起来3D 变换的透视关系、层叠上下文、渐变方向、多重阴影叠加、伪元素拼装、transform-origin 的坐标系……做完这一个项目比啃十篇 transform 文档都管用。如果你已经掌握了 CSS 基础但总觉得自己对 transform、perspective、box-shadow 的理解停留在会写但调不好的程度那这个项目非常适合你。它会逼着你去思考每一个像素是怎么来的而不是靠浮点数硬怼。下面我按自己的实操顺序把完整的设计思路、踩坑记录和调参心得都摊开讲。1. 先当裁缝别当程序员把回形针拆成可拼装的零件1.1 回形针的几何学看似一圈其实是六段任何复杂的图形第一步永远不是写代码而是拆结构。你拿一枚真实回形针放在桌上眯着眼睛看它的侧面轮廓会发现它根本不是一个圈而是一条金属丝按照特定路径折出来的三维曲线。把这条曲线投影到正面看到的轮廓大致可以分成六段底部长直线回形针最下面那段贴桌面的平整部分大约占整个底边的 2/3。右下圆弧从底部向上翻的过渡圆弧半径不大类似四分之一圆。右侧直线段沿着右侧向上延伸的直边。右上圆弧从右侧折向顶部的圆弧方向与右下弧相反。顶部直线段回形针最上方那根较短的横梁。左侧内圈弧与短线这才是回形针夹纸的灵魂——金属丝折回内部形成一层内圈末端通常还有一个收敛的小圆弧。注意这六段并不是都在同一个平面上。真实回形针的金属丝是绕着一个虚拟圆柱走的所以外层弧和内层弧之间隔着一条纸的厚度大约 1 到 2 毫米。这个厚度差恰恰是让回形针在立体视角下显得真实的根源。如果你只是在一个平面里画个圈那它只能叫C 形环不叫回形针。1.2 为什么我坚持用纯 CSS而不是 SVG很多朋友第一反应是画个回形针用 SVG 的 path 描一遍不就完事了三行代码边缘还光滑何必自讨苦吃。这话在真实项目里完全正确但在学习项目里我不推荐。原因有三。第一SVG path 是把别人计算好的坐标填进去你得到的是结果不是过程。而 CSS 方案要求你亲手控制每一段的长度、旋转角度和空间位置逼着你理解变换矩阵。第二SVG 描边天然是扁平的想要真实的金属立体感还得叠加 SVG 滤镜而 CSS 方案里渐变和阴影的组合方式更直观挪一个渐变角度就能看到高光变化反馈速度比调滤镜参数快得多。第三也是最重要的这个项目真正要练的是 3D 空间思维。用 CSS 让一个二维页面元素拥有三维姿态和用 SVG 画一个固定轮廓完全不是一个维度的挑战。当然我也不是鼓吹所有图形都用 CSS 画。真到了做产品 UI 图标SVG 依然是更明智的选择体积小、缩放清晰、可维护性高。CSS 画回形针的意义在于训练不在于产出。你练的是内功不是招式。1.3 定坐标系让自己的脑子先有一个三维空间拆完零件下一步是在脑海里建坐标系。我是这样设想的先画一个正面投影所有坐标都以回形针的中心为原点x 轴向右y 轴向下z 轴向屏幕外。底部横段沿 x 轴方向右侧直线段沿 y 轴方向。然后在每个转折处用 rotate 做平面折弯用 rotateY 和 rotateX 做空间翻折。这一步一定要想明白不然后面所有 transform 都会乱成一锅粥。我自己的习惯是在纸上画一张展开图把回形针沿着转折点摊平标出每一段的长度和角度再写代码。你以为是浪费时间其实调试时能省一个下午。2. 技术选型的核心透视、光影与层叠一个都不能少2.1 透视不是玄学perspective 到底改变了什么要让一个二维图片在浏览器里产生立体感关键就是透视变换。CSS 的 perspective 属性本质上是在模拟眼睛离物体有多远。数值越小透视越强近大远小越夸张数值越大透视越弱画面越接近正投影。我习惯把 perspective 放在父容器上而不是直接写在被变换的元素上这样所有子元素共享同一个透视消失点不会出现每个零件各看各的的割裂感。比如场景容器.scene { perspective: 900px; }然后给回形针整体加一个绕 y 轴的旋转让它侧面对着观众.paperclip { transform: rotateY(-30deg); }你会发现整个回形针左侧变小、右侧变大这就是透视带来的视觉深度。如果还用 translateZ 把内圈向外顶出一点立体感会立刻跳出来。很多人做 3D 效果总说看不出立体感十有八九是 perspective 没加或者加错了位置。2.2 金属光泽的本质渐变是皮阴影是骨不锈钢回形针的光泽主要由两道高光决定一道顺着金属丝的轴线方向延伸一道在截面边缘形成亮线。用 CSS 模拟时我通常这样分工用 linear-gradient 控制基础明暗让金属丝圆管状的质感先立起来。用 box-shadow 控制环境阴影和接触阴影让回形针踩在地面上。用伪元素再加一条细细的高光条进一步强化不锈钢的镜面反射。渐变方向很关键。比如一根横向的金属丝如果渐变是从上往下从白到灰再到深灰它看起来就像一根圆柱体如果渐变方向反过来或者干脆是水平的就会变成一块扁平的金属片。我每次调整完段的方向都会顺手检查一下渐变方向是否也跟着转了 90 度这是一个特别容易遗漏的细节。2.3 transform 和 z-index 的纠缠先理解层叠上下文3D 项目里最常被忽略的坑是层叠上下文。CSS 规范规定当元素设置了 transform 时它会创建一个新的层叠上下文这意味着 z-index 的参照系变了。分段拼装回形针时如果外层弧和内层弧的 z-index 突然失效多半就是这个原因。我当时的处理原则很简单内层金属丝给它一个较低的 z-index外层给较高的 z-index同时确保两者的父容器都处于同一个 transform 上下文中。如果某一层又被单独加了 transform就要重新审视层级关系。这个坑在 4.4 节我会再详细展开。3. 实操攻坚两条路线从 2.5D 到真 3D3.1 路线一2.5D 快速方案适合 30 分钟出效果如果你的目标只是快速做一个小 demo不追求每根金属丝的独立立体感那就用 2.5D 方案。核心思路是先用 div 边框画出一个平面回形针轮廓再通过整体旋转骗过眼睛。HTML 结构只需要一个容器加两个子元素div classscene div classpaperclip/div /div关键 CSS.scene { perspective: 800px; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: radial-gradient(circle, #f5f7fa 0%, #c3cfe2 100%); } .paperclip { position: relative; width: 170px; height: 200px; border: 9px solid #b0b5ba; border-radius: 60px 60px 60px 60px / 80px 80px 80px 80px; border-top-color: transparent; border-right-color: transparent; transform: rotateX(55deg) rotateZ(-45deg) rotateY(10deg); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4), 0 30px 40px -18px rgba(0, 0, 0, 0.5); }这个方案利用了一个视觉骗术只保留底部和左侧的边框形成回形针的外圈轮廓然后用 border-radius 撑出圆弧过渡。接着加一个伪元素模拟内层弧.paperclip::before { content: ; position: absolute; top: 28px; left: 20px; width: 130px; height: 150px; border: 9px solid #b0b5ba; border-radius: 50%; border-bottom-color: transparent; border-left-color: transparent; }最后把整个 .paperclip 旋转到一个带有俯视的角度原本平平的轮廓立刻有了躺在桌面上的感觉。这个方案的好处是代码量非常少缺点是回形针没有真正的厚度转到大角度时会露馅。3.2 路线二3D 分段拼装把每根金属丝都做成独立元素这才是 paperclip 这个练习项目的精华。我采用每段一个 span的结构让每一段都可以独立旋转和定位。HTML 骨架div classscene div classpaperclip i classseg seg-1/i i classseg seg-2/i i classseg seg-3/i i classseg seg-4/i i classseg seg-5/i i classseg seg-6/i i classseg seg-7/i /div /div每一段 .seg 的基本样式都一样一根细长的圆角矩形模拟一段钢丝。.seg { position: absolute; display: block; width: 70px; height: 12px; background: linear-gradient( to bottom, #f0f2f5 0%, #cfd3d8 20%, #9aa0a6 50%, #5f666c 85%, #3a3f44 100% ); border-radius: 6px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }注意我先把每段做成躺平的圆柱体然后通过 transform 把它们搬到各自的位置。第一段底部横段几乎不用动.seg-1 { bottom: 30px; left: 20px; }第二段右下圆弧。这里不能只用一条直线段硬折需要一个辅助的圆角块.seg-2 { width: 24px; height: 24px; bottom: 30px; right: 12px; background: transparent; border: 12px solid #9aa0a6; border-left-color: transparent; border-top-color: transparent; border-radius: 0 0 24px 0; transform: rotate(90deg); }第三段右侧竖直段让它竖起来.seg-3 { width: 12px; height: 60px; right: 24px; bottom: 54px; transform-origin: bottom center; transform: rotate(8deg); }第四段右上圆弧再做一个四分之一弧.seg-4 { width: 24px; height: 24px; right: 12px; top: 40px; background: transparent; border: 12px solid #9aa0a6; border-right-color: transparent; border-bottom-color: transparent; border-radius: 24px 0 0 0; transform: rotate(-20deg); }第五段顶部横段.seg-5 { width: 80px; height: 12px; top: 40px; right: 36px; transform-origin: right center; transform: rotate(-5deg); }第六段左侧内圈弧这是让回形针向内折的关键.seg-6 { width: 60px; height: 60px; top: 52px; left: 10px; background: transparent; border: 12px solid #9aa0a6; border-right-color: transparent; border-top-color: transparent; border-radius: 0 0 0 60px; transform: rotate(18deg); }第七段内部收尾短弧.seg-7 { width: 34px; height: 34px; bottom: 88px; left: 8px; background: transparent; border: 12px solid #9aa0a6; border-bottom-color: transparent; border-left-color: transparent; border-radius: 0 34px 0 0; transform: rotate(-24deg); }上面这些参数是我在开发工具里反复打磨后的一版近似值说它近似是因为你手上的屏幕分辨率、字体渲染比例都可能让回形针出现 1 到 2 像素的错位。这个不要紧重要的是理解方法先用绝对定位把每段放到大致位置再用 transform 微调角度最后用 transform-origin 调整旋转中心让段与段自然衔接。到这里回形针已经是一个由七段组成的松散模型但整体可能还是歪的。所以我会给父容器加一个统一的空间姿态.paperclip { position: relative; width: 200px; height: 220px; transform-style: preserve-3d; transform: rotateX(18deg) rotateY(-28deg) rotateZ(8deg); }这里 transform-style: preserve-3d 必须要有。没有它子元素的 3D 变换就会被父元素拍平回形针直接从三维模型退化成一幅画。这是新手最容易踩的隐形炸弹。3.3 方案对比你到底需要哪种我整理了一张对比表方便你根据场景选型维度2.5D 快速方案3D 分段拼装方案代码量约 40 行约 160 行调试难度低高立体真实感中高可旋转角度有限大角度穿帮自由旋转知识点覆盖透视、阴影透视、渐变、层叠、坐标变换推荐场景快速 demo、头像插画学习训练、交互动效时间充裕想真正搞懂 3D 变换直接上方案二只是玩票方案一足够。我自己是先做了方案一被朋友一句你这不是回形针是纸片刺激之后才痛下决心拆成七段重做。真香。4. 调参现场我替你踩过的四个坑4.1 border-radius 百分比与固定值的陷阱画圆弧段时我一开始用的是 border-radius: 50%结果圆弧变成了椭圆两个方向的曲率不一样导致回形针看起来像是被压扁过。原因是当元素的宽高不相等时50% 的圆角半径是分别按宽度和高度的 50% 计算的自然变成椭圆。而金属丝的截面是圆的转折弧也必须接近正圆。解决方法是把 border-radius 设成一个大于等于边长一半的固定值比如边长是 24px 时直接写 24px。这样无论宽高是否相等圆角都是四分之一正圆。这个小细节影响非常大。4.2 transform 的书写顺序不能乱顺序即坐标系有一次我把第七段的角度无论怎么调都差一截后来检查发现是 transform 顺序的问题。CSS 中 transform 的取值是从右往左依次应用的也就是说transform: translateX(20px) rotate(45deg);和transform: rotate(45deg) translateX(20px);结果完全不同。前者是先旋转再沿旋转后的 x 轴平移后者是先平移再旋转。我在分段摆放时规定自己统一采用先定位后旋转的顺序这样每一段的位置参数都便于理解。如果你发现自己怎么调都调不对先检查 transform 顺序而不是怀疑自己数学不好。4.3 光源方向不统一金属质感直接翻车金属丝的质感靠渐变模拟但七段零件在不同位置上如果每段都用同一个渐变方向从最终的视觉上看光源方向就会混乱。比如底部横段的高光在正上方而右侧竖段的高光也应该在左上方这样才能统一光从左上角打来的整体感受。但右侧竖段如果还沿用从上到下的渐变高光就会出现在右端看起来像光从右边打过来。所以我给每一段的渐变方向都做了手调这是一个笨办法但很有效。先确定全局光源为左上然后让每段的亮部都朝向左上方。等视觉统一之后整个回形针的金属感会立刻上一个档次。4.4 z-index 失灵与 preserve-3d 的关系分段拼装时我一度发现无论怎么设置 z-index内层弧总是不愿意跑到外层弧的后面。查了半天才发现原因是我把内层弧和外层弧分别放在两个不同的 transform 容器里而每个 transform 容器又创建了独立的层叠上下文导致彼此之间的 z-index 无法直接比较。解决办法有两种一是把所有段放在同一个 .paperclip 容器下利用 3D 空间中的 Z 轴位置决定前后关系而不是依赖 z-index二是给内外弧分别设置明确的 z-index并确保所有段都属于同一个层叠上下文。我实际采用了第一种用 translateZ 让内层弧稍微后移几像素不仅解决层级问题还顺带增加了真实的厚度感。5. 让它动起来从静态模型到可交互展示5.1 自动旋转的完整代码当回形针具备了真实的 3D 结构给它加动效就非常顺理成章了。一个自动缓慢旋转的回形针能全方位展示金属丝的光泽变化。keyframes spin { from { transform: rotateX(18deg) rotateY(-28deg) rotateZ(8deg); } to { transform: rotateX(18deg) rotateY(332deg) rotateZ(8deg); } } .paperclip { animation: spin 12s linear infinite; }这里有个细节因为父容器和子元素都用了 preserve-3d旋转时会一起转动看起来就像一枚真实回形针躺在转盘上。转速不要超过 12 秒一圈不然金属高光和阴影变化太快眼睛会花。5.2 拖拽旋转CSS 变量加一点点 JS自动旋转只是热身真正让观众觉得这模型是活的是能手动拖拽观察。我用了 CSS 自定义属性配合原生 JS 实现.paperclip { transform: rotateX(var(--rx, 18deg)) rotateY(var(--ry, -28deg)) rotateZ(8deg); }const clip document.querySelector(.paperclip); let isDragging false; let lastX, lastY; document.addEventListener(mousedown, (e) { isDragging true; lastX e.clientX; lastY e.clientY; }); document.addEventListener(mousemove, (e) { if (!isDragging) return; const dx e.clientX - lastX; const dy e.clientY - lastY; const ry parseFloat(getComputedStyle(clip).getPropertyValue(--ry) || 0); const rx parseFloat(getComputedStyle(clip).getPropertyValue(--rx) || 18); clip.style.setProperty(--ry, ry dx * 0.5); clip.style.setProperty(--rx, Math.min(60, Math.max(-60, rx - dy * 0.3))); lastX e.clientX; lastY e.clientY; }); document.addEventListener(mouseup, () { isDragging false; });这段代码的核心思路是鼠标横向移动时把增量累加到 --ry 上回形针绕 y 轴转动纵向移动时改变 --rx控制俯仰。加了边界限制避免翻转角度过大导致看着头晕。如果你还想做移动端适配把 mousedown 换成 touchstart 就行逻辑完全一样。5.3 几条脑洞回形针还能怎么玩做完标准回形针我顺手试了几个变体分享两个我觉得有意思的一个是把回形针做成上色款去掉金属渐变改成纯色扁平风格配合 CSS 动画让它在页面里甩来甩去当装饰元素另一个是纯背景玩法把回形针放大到半透明放在页面底部当水印装饰利用它独特的几何轮廓撑起视觉层次。这些玩法都不难但能让一个练习项目真正融入作品集里。6. 写在最后这个项目到底教会了我什么如果你问我做一个 paperclip 到底值不值我的答案是值而且超出预期。它逼着我在一个晚上之内把所有关于 CSS 3D 的知识点串联了起来——perspective 的数值直觉、transform 顺序的严谨、渐变光源的统一、层叠上下文的底层规则。这些东西单独看文档都很抽象但当你为了让一枚回形针不要穿帮而反复调整时它们会自动连成一张网。最后分享一个我的工作习惯不管画什么先在纸上画出展开图和坐标轴再打开浏览器。坐标不乱代码就不会乱。这个习惯后来帮我节省了大量瞎试的时间希望你也能用得上。
返回列表