“我想只用 HTML 和 CSS 来重现这块新鲜、热气腾腾的面包。”这是一位前端开发者在一次 CSS 艺术挑战中的开场白。他选择的“慰藉食物”不是汉堡也不是披萨,而是格鲁吉亚西部伊梅列季地区的传统奶酪面包——哈恰普里。这块面包在他的代码里缓缓升起热气,然后被整齐地切成 13 片,整个过程没有用到一行 JavaScript,也没有插入任何图片或 SVG。

作品本身就是一个纯 CSS 的视觉实验。面包的圆形饼体、内部融化的芝士、表面的烘焙焦斑和刷过黄油的油亮高光,全都由多层径向渐变与裁剪路径堆叠而成。更让前端社区觉得有趣的是,他把切面包的过程做成了一段无限循环的 CSS 动画,每片面包都沿着精确计算的多边形区域逐步“消失”,直到整块面包被一片片分走,而剩下的空盘上还留着一点碎屑和热气。

打开网易新闻 查看精彩图片

这位创作者选择伊梅列季哈恰普里并非偶然。他写道,这种奶酪面包的芝士完全包在面团内部,烤到金黄色后刷一层黄油,和披萨把馅料摊在表面的做法不同。对他来说,这道食物意味着温暖、家,以及童年记忆。正因为如此,他不满足于只画一个静态的圆饼,而是试图让用户在屏幕上感受到面包刚出炉时的那股蒸汽和黄油光泽。

用纯 CSS 完成这种效果,意味着一切视觉元素都必须从最基础的属性里“抠”出来。面包的主体是一组复杂的径向渐变叠加:用半透明的深褐色模拟烘焙产生的焦斑,金黄色的区域对应烤到恰到好处的表皮,最上层则是一层淡黄色高光,用来模仿热面包表面刚刷过黄油后反光的状态。整个圆形被分成了 13 个切片,每个切片都使用相同的渐变层次,让人在动画启动前看到的仍然是一块完整无缝的面包。

这 13 个切片的划分,靠的是 clip-path: polygon() 以及一场小小的三角函数计算。作者没有选择更简单的方式——比如画一个整圆然后用覆盖层遮住切片——而是从一开始就把面包精确分割成 13 个扇形,每个扇形的裁剪路径都由 50% 50% 的中心点与两个外沿顶点构成。13 这个数字也是权衡后的结果:数量太少,切片的消失过程会显得生硬;数量太多,HTML 元素就会膨胀。对他来说,13 片刚好能在动画流畅度和代码复杂度之间取得平衡。

切片的动画也完全来自 CSS 自定义动画。每个切片被赋予一个名为 slice-cut 的 keyframe 动画,时长为 20 秒,循环往复。在动画过程中,切片从完整展示逐渐变为透明或位移,模拟出面包被一片片拿走的效果。因为所有切片共用同一套渐变背景,只要计算精度足够,它们在动画前后都能保持视觉上的连续感,不会露出拼接的缝隙。

布局上,作者还加入了一个充满“食物博客”味道的细节:他用 shape-outside: circle(48%) 让文字环绕在面包的周围飘浮。整块面包被放置在一个浮动容器里,文字像杂志排版一样沿着圆形的边缘自然流动。容器尺寸则用了 clamp(180px, 24vw, 320px) 来保证不同屏幕下都有合理的比例,既不会过度挤压阅读空间,也不会让面包小到看不出纹理。

对于关注无障碍的前端开发者来说,这个作品还藏着两个容易忽视的细节。首先,所有切片动画都放在 @media (prefers-reduced-motion: reduce) 的查询里做了降级处理:当用户的系统开启了“减少动态效果”,面包就不会执行切片动画,而是保持完整状态,避免对前庭敏感或偏好静态内容的用户造成干扰。其次,作品结尾那句格鲁吉亚语“გემრიელად მიირთვით!”被用 lang="ka" 标注了语言属性,这意味着屏幕阅读器能够识别并尽量用正确的发音读出这句“用餐愉快”,而不会把它当作乱码或跳过。

整个作品的代码完全托管在线上,所有视觉元素都在一张 HTML 页面和一份 CSS 文件里完成。从面包的发酵气孔到表面微焦的色块,再到切割后的残留蒸汽,没有使用任何 canvas、SVG 或图片资源。开发者甚至略带调侃地提到,他只是想看看用 CSS 能做到什么程度,结果发现用来做一道家乡菜好像正合适。

这种看似苛刻的“纯 CSS 约束”实际上推动了不少有趣的技术尝试。clip-path 配合自定义动画,让几何分割不再是静态图表;多层渐变彼此叠加,使二维平面产生了烘焙食品的立体感和质感;shape-outside 则把文字绕排从简单的矩形解放出来,让排版能服务于叙事。更重要的是,这些练习往往源于开发者对某个日常事物的私人情感——比如此处的伊梅列季奶酪面包——而正是这种情感让技术有了温度,让一段布满三角函数和多边形的代码最终指向了“家”和“童年”。

或许下次再打开一个 CSS 文件的时刻,也可以想一想:有没有哪道菜、哪件老物件,值得用最纯粹的前端语言去重现它的光泽与故事。毕竟,就像作者在作品末尾留下的那句格鲁吉亚语祝语一样,代码之后,还是可以好好吃一顿。