news 2026/9/15 23:48:04

纯CSS3绘制蒙娜丽莎:原理、技巧与性能边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯CSS3绘制蒙娜丽莎:原理、技巧与性能边界

简介:这是一份用纯CSS3绘制世界名画《蒙娜丽莎》的源码示例,面向前端开发者、网页设计师以及对CSS图形创意感兴趣的技术爱好者,用于直观理解CSS3在复杂图形绘制与视觉表现上的实际能力。压缩包共2个文件,包含一个CSS样式文件和一个HTML页面文件,整体仅50KB,可轻松在浏览器中打开查看与调试。已有90人学习下载。代码通过精湛运用border-radius、clip-path、渐变、box-shadow、filter等CSS3属性,以像素级控制模拟画作的脸部轮廓、色彩层次、光影变化以及神秘微笑,展示了无需任何图片或Canvas即可由CSS独立完成艺术绘制的思路。对于希望深挖CSS3新特性、拓展网页视觉创意边界的人而言,这份代码兼具观赏性与学习价值,可作为研究CSS绘图技术的小型范例,从中获得实现技巧与设计启发。

1. 纯CSS3绘制蒙娜丽莎:一次把浏览器图形能力逼到极限的实践

第一次看到“纯CSS3绘制的蒙娜丽莎画像源码.zip”这个压缩包时,多数人的第一反应是“噱头”。毕竟CSS3的定位是样式描述语言,和绘画八竿子打不着。但当你解压并打开里面的HTML文件,看到几十个div节点在无任何图片、无canvas、无SVG的情况下拼出那幅模糊却可辨认的《蒙娜丽莎》轮廓时,会意识到这件事的本质不是“画画”,而是把CSS3的盒模型、边框、渐变、阴影、变形和动画做了一次高密度的组合应用。这类源码适合前端工程师、可视化方向开发者,以及所有想测试“浏览器渲染极限”的人。它能回答一个很实际的问题:在不使用位图和矢量图的前提下,纯CSS能把复杂图像还原到什么程度,以及这种做法的代价和边界在哪里。

2. 从像素到盒子:纯CSS3绘图的核心建模逻辑

2.1 CSS3能绘制哪些“基本图形”

CSS3绘图不是魔法,而是把一切视觉元素拆解为几何原语。标准盒模型只能画矩形,但通过border-radius可以变出圆形和椭圆;通过border的宽度和颜色可以模拟三角形;通过linear-gradientradial-gradient可以产生任意角度的色彩过渡;通过box-shadow的多重叠加可以复制出远处的轮廓;通过clip-path可以把元素裁剪成任意多边形。这五种能力的组合,理论上可以逼近任意二维形状。

蒙娜丽莎这幅画的特点是人物半身像、背景有山脉和道路、面部有微妙的明暗过渡。用纯CSS绘制时,最常见的策略是用大量小尺寸的div作为“像素块”,每块填充近似颜色,然后通过transform: rotate()处理倾斜的边缘,用box-shadow柔化硬边界。更进阶的做法是用多个大尺寸的渐变图层模拟光影,然后用clip-path裁出轮廓。

2.2 为什么选择CSS3而不是Canvas或SVG

选择CSS3方案并非因为它更高效,而是因为它满足一个特殊约束:完全脱离脚本和图片资源。纯CSS绘图的HTML文件可以直接双击打开,不需要服务器,不依赖本地图片,所有内容都在CSS规则里。它的可移植性和“源码感”很强——任何人拿到这个zip解压后都能看到每一笔是怎么画出来的。相比之下,Canvas需要JavaScript驱动,SVG本身就是矢量标记,都不算“纯CSS”。

另外,CSS3绘图天然支持响应式缩放,因为所有尺寸如果使用百分比或rem,可以随视口变化。配合transform: scale()还能整体无损缩放,这是固定像素的Canvas做不到的。

2.3 从位图到CSS代码的转换流程

实际制作这样一个源码包,不会有人手动去画几百个div。常见做法是先写好一个转换脚本,把位图转成CSS布局。下面是一个简化版的Python转换脚本思路,用于把一张低分辨率图片转换为CSS网格:

from PIL import Image image = Image.open('mona_lisa.png').convert('RGB') # 缩放到目标网格尺寸,比如400x600 image = image.resize((400, 600)) pixels = image.load() css_lines = [] for y in range(600): for x in range(400): r, g, b = pixels[x, y] # 每个像素用一个绝对定位的div表示 css_lines.append( f'"x{x}y{y}":{{"width":"1px","height":"1px","left":"{x}px",' f'"top":"{y}px","background":"rgb({r},{g},{b})"}}' ) # 输出JSON样式块,后续再转换成CSS类 print(','.join(css_lines))

这段脚本的核心逻辑是遍历每个像素点,生成一个绝对定位的1像素方块,用背景色模拟像素颜色。输出后的JSON可以再通过模板引擎生成CSS或内联样式。400x600的尺寸会产生24万个节点,浏览器会崩溃,所以实际缩放要控制在100x150左右,约1.5万个节点,这是纯CSS能渲染的上限附近。

从这段脚本可以看到,纯CSS绘图的本质是“用DOM节点替代像素”。这也是为什么大部分源码包里的蒙娜丽莎看起来有一种马赛克感——节点数量有限,细节必然丢失。想要进一步提升还原度,就得引入渐变和阴影来弥补节点数量的不足,这属于优化阶段的内容。

3. 动手拆解:纯CSS3绘制蒙娜丽莎的层级结构与绘制步骤

3.1 整体架构:分层绘制而不是逐像素堆叠

如果你把解压后的源码包当成一个完整的HTML/CSS项目,会发现它通常不是扁平的一堆div,而是有清晰层级。一般按画面内容分为背景层、人物轮廓层、五官层和光影层。背景层包含山脉、道路和天空,使用大面积的radial-gradientlinear-gradient来模拟;人物轮廓层用深色背景块裁剪出头部和身体的形状;五官层是最精细的部分,由数十个中小型div配合border-radiusbox-shadow构成;光影层则是覆盖在人物上的半透明渐变层,用来统一色调。

这种分层方式有两个好处:一是减少节点数量,二是让代码更可读。逐像素堆叠虽然简单,但无法体现绘制意图。分层绘制意味着每一层都能独立调整颜色、位置和透明度,方便后续修改。比如调整背景色调,只需要改动背景层的渐变值,而不需要重新生成所有像素块。

3.2 用border-radius和box-shadow绘制面部轮廓

面部是蒙娜丽莎最核心的部分,也是最难用CSS还原的。一个可行的做法是用一个宽约120px、高约160px的圆形div作为脸部基底,然后通过box-shadow的多重叠加来模拟下颌和颧骨的明暗过度。box-shadow的优势在于它可以生成不占据布局位置的柔和色块,适合描绘阴影。

下面是一段典型的面部轮廓CSS代码,展示了如何用阴影堆出立体感:

.face { width: 120px; height: 150px; border-radius: 50% 50% 45% 45%; background: radial-gradient(circle at 35% 30%, #e8c39e 0%, #d4a373 50%, #b87d4b 100%); box-shadow: 12px 20px 0 -5px #a06b3c, -8px 15px 0 -8px #c69a6d, 0 28px 0 -12px #8a5a33; }

box-shadow的四个参数依次是水平偏移、垂直偏移、模糊半径和扩展半径。这里用了负的扩展半径,让阴影比元素本身小一圈,从而形成类似下颌骨的轮廓。第一个阴影往右下偏移,带少量模糊,模拟颧骨下方的凹陷;第二个阴影往左下偏移,模拟另一侧的受光面;第三个阴影向下延伸,模拟脖颈与下巴的连接区域。这类参数的组合需要反复微调,也是源码包里最值得研究的部分。

从代码执行的顺序看,background中的多个渐变是叠加关系,box-shadow列表也是逐个绘制,先写的在底层。因此,阴影的顺序不能随意调换,否则会遮挡主色块。

3.3 用clip-path裁出身体和头发的异形轮廓

面部可以用圆角矩形近似,但身体部分——那条著名的披肩和交叉的双手——就不规则了。手动细分多个div会非常繁琐,此时clip-path: polygon()可以一次裁出任意多边形。假设我们要画一个从肩到手的轮廓,可以定义多个顶点:

.body { position: absolute; left: 50px; top: 210px; width: 200px; height: 220px; background: linear-gradient(135deg, #2d1f1a, #5a3a2a); clip-path: polygon( 20% 0%, 80% 0%, 95% 20%, 90% 50%, 100% 80%, 85% 100%, 15% 100%, 0% 80%, 10% 50%, 5% 20% ); transition: clip-path 0.3s ease; }

这里的clip-path: polygon()按百分比依次定义十个顶点,形成一个类似肩部和双臂交叉的剪影。注意clip-path的百分比是相对于元素自身的宽高,所以调整外层的widthheight时,裁剪形状会等比缩放。这个特性非常适合用于响应式布局。

在源码包中,你可能会看到多个clip-path互相叠加的例子,比如先用一个大矩形裁剪出背景画布,再在内部叠加多个带独立clip-path的元素。每一层裁剪的结果会直接显示为最终像素。调试这种代码时,建议先在浏览器开发者工具中打开“字体/形状”可视化辅助,逐层隐藏其他元素来检查每一层的裁剪边界。

3.4 用CSS3动画完成“绘制过程”展示

很多这类源码包不只是静态画像,还会附带一段动画,模拟画笔从无到有画出蒙娜丽莎的效果。实现这个效果的核心是CSS3动画延迟动画执行次数。给每个绘制元素绑定一个opacity动画,从0到1,并用animation-delay控制出现顺序。

.paint-layer { opacity: 0; animation: reveal 0.6s ease-out forwards; animation-delay: 0.2s; } .paint-layer:nth-child(2) { animation-delay: 0.5s; } .paint-layer:nth-child(3) { animation-delay: 0.9s; }

这里的animation-fill-mode: forwards很关键。它的作用是让动画结束后保持最后一帧的状态,也就是opacity: 1。如果你不写forwards,动画结束后元素会跳回初始的opacity: 0,画像就消失了。这一点对应“CSS3动画完成后状态的保持”这个高频搜索词。同时,动画执行次数默认是1次,如果你想反复播放绘制过程,需要写animation-iteration-count: infinite,或者设成指定的次数。

如果你想让动画更具手绘感,可以尝试animation-direction: alternate配合infinite,让图像在透明度上做往返变化。但要注意,alternate会在每次循环时反向播放,如果动画是从0到1,反向播放就是1到0,正好产生闪烁消失的效果,用在不透明度上时要谨慎。

4. 优化与边界:纯CSS3绘图在真实项目中的性能瓶颈和校验方法

4.1 节点数量对渲染性能的影响

当你打开源码包里的HTML文件时,浏览器需要为每个div创建独立的渲染层。一个超过50000个节点的页面,在性能低的机器上滚动或调整窗口大小都会卡顿。实际做这类项目时,建议控制节点数量在10000以内。这里有一个简单的性能测试方法:在开发者工具的性能面板录制10秒交互过程,观察FPS是否稳定在50以上。如果掉帧严重,就需要合并节点或减少阴影复杂度。

另一个常见瓶颈是box-shadow。每个阴影都会触发一次额外的绘制操作,一个元素上挂十个阴影,绘制成本远超十个普通元素。在蒙娜丽莎这种明暗过渡丰富的图像上,阴影很容易堆到几十个。优化方案是尽量使用background: linear-gradient替代阴影来模拟柔光,因为渐变是浏览器内置的高效绘制路径,而多重阴影的绘制是多趟叠加。

4.2 不同浏览器的CSS3支持差异

clip-pathmaskbackdrop-filter这些高阶CSS属性在不同浏览器中的实现差异明显。clip-path: polygon()在Chrome和Firefox中支持良好,但在旧版Safari中可能需要-webkit-clip-path前缀。纯CSS绘图源码如果不加前缀,在iPhone上打开可能会直接显示成矩形色块。解决方案有两种:一是使用Autoprefixer在构建时自动添加前缀,二是干脆放弃旧浏览器支持,在页面顶部提示使用现代浏览器。对于这类偏实验性质的源码,第二种做法更常见。

conic-gradient也是近年才被广泛支持的属性,适合绘制类似光盘或扇形渐变。如果在蒙娜丽莎的手部位置使用conic-gradient做袖口褶皱,需要留意浏览器兼容性。可以用一个简单的特性检测来兜底:

@supports (background: conic-gradient(red 0deg, blue 360deg)) { .sleeve { background: conic-gradient(#3a2a1a 0deg, #5a3a2a 90deg, #2d1f1a 180deg); } }

@supports规则会在浏览器不支持conic-gradient时跳过内部的样式声明,从而避免出现完全空白的内容。在源码包的CSS文件里,你会看到大量这种分支写法,这是保证画像在多种浏览器中至少“能看”的关键。

4.3 从zip源码中提取有效内容的策略

拿到纯CSS3绘制的蒙娜丽莎画像源码.zip之后,不要急着双击index.html,先看目录结构。常见的zip包内会包含css/js/img/目录,但既然是纯CSS3,基本可以排除img。查看css/下的主样式文件,搜索linear-gradientbox-shadow的次数,就能初步判断这个源码是“真绘制”还是“用图片背景冒充”。另外一个值是animation属性的数量,如果动画数量明显多于图像分层数量,说明它加了展示效果,可以作为学习重点。

校验图像还原度的方法很简单:用截图工具截取渲染出的页面,与原始蒙娜丽莎图片放在一起,用图像差分算法计算像素误差。如果手边没有图像处理库,可以用浏览器的“截图到文件”功能保存PNG,然后在Python中用Pillow读取两张图片,计算RMSE值:

from PIL import Image import math original = Image.open('mona_original.png').convert('RGB') rendered = Image.open('mona_css.png').convert('RGB') # 调整尺寸一致 rendered = rendered.resize(original.size) diff = 0 pixels_orig = list(original.getdata()) pixels_rend = list(rendered.getdata()) for p1, p2 in zip(pixels_orig, pixels_rend): diff += sum((c1 - c2) ** 2 for c1, c2 in zip(p1, p2)) rmse = math.sqrt(diff / (len(pixels_orig) * 3)) print(f"RMSE: {rmse}")

RMSE值越小,说明两张图越接近。通常纯CSS绘制的画像RMSE会在25到40之间,肉眼看上去是“能认出人物但线条有锯齿”的效果。如果RMSE低于10,就要怀疑是否用了图片作底图,或者使用了非常庞大的节点数。

4.4 常见错误:为什么你的镜像“画不完”或“白屏”

很多人试跑这类源码时遇到两类问题。第一类是CSS动画画到一半停止,画像只显示上半身。原因多半是某些元素的animation-delay设置较长,而且没有递增到最后一个元素,导致后面的元素一直在等,动画序列看起来没有执行完。检查方法是在开发者工具的”动画“面板里查看所有活动的动画时间轴,看是否有一个动画的时间线悬在末端。第二类是页面完全白屏,打开控制台发现大量Invalid property value报错,这通常是因为clip-path的多边形顶点数量超过了浏览器限制,或某个渐变的角度值不合法。把对应的CSS规则用注释逐条屏蔽,能快速定位到出错的属性。

遇到zip压缩包解压后文件名乱码的情况,尤其是中文文件名在Mac或Linux上解压后变乱码,可以尝试用The Unarchiver7-Zip的UTF-8模式解压。纯CSS源码的文件应当按照index.htmlstyle.css这样的ASCII文件名命名,否则浏览器无法正确加载子资源。

5. 进阶技巧:用CSS变量和混合模式让CSS画像可交互

5.1 用CSS变量控制整幅画像的色调

静态的纯CSS蒙娜丽莎只能是一个固定结果。如果我们想让用户切换不同的色调,比如从暖色调切换到冷色调,不需要修改几十处颜色值。正确做法是把所有阴影和渐变的颜色值抽出来定义为CSS变量:

:root { --skin-highlight: #e8c39e; --skin-mid: #d4a373; --skin-shadow: #b87d4b; --bg-sky: #7aa3b8; --bg-mountain: #5c7a6a; } .face { background: radial-gradient(circle at 35% 30%, var(--skin-highlight) 0%, var(--skin-mid) 50%, var(--skin-shadow) 100%); } .body { background: linear-gradient( 135deg, color-mix(in srgb, var(--skin-shadow) 30%, black), var(--skin-shadow) ); }

这里用到了color-mix()函数,它可以把一个颜色按照百分比与另一个颜色混合,产生新的颜色值。在旧版浏览器中如果不支持color-mix(),则会使用第二个参数var(--skin-shadow)作为兜底背景色。通过改变:root中的两个颜色变量,整幅画像的脸色、背景、服饰会同步变化,这在调试阶段非常好用,可以用来检查哪一层颜色的边界是否越界。

5.2 利用混合模式叠加纹理增强细节

纯色渐变画出的图像表面太“平”,缺少油画那种粗糙感。CSS的mix-blend-mode属性可以让元素与背景发生乘法、叠加等混合,产生笔触效果。操作方式是在画像的最上层放置一个半透明的噪点元素,这个元素可以是一个很细的重复渐变:

.texture { position: absolute; inset: 0; background-image: repeating-linear-gradient(45deg, rgba(0,0,0,0.03) 0px, rgba(0,0,0,0.03) 1px, transparent 1px, transparent 3px), repeating-linear-gradient(-45deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 2px, transparent 2px, transparent 4px); mix-blend-mode: multiply; pointer-events: none; }

mix-blend-mode: multiply会让黑色像素变暗,白色像素不变色,从而在明暗交界处产生类似油画的脏色感。由于repeating-linear-gradient按非常小的单位重复,视觉上就会形成细碎的噪点纹理。记得给这个纹理层加pointer-events: none,否则它会拦截鼠标事件,导致用户无法点击画像下方的其他交互元素。

5.3 让画像动起来:基于鼠标位置的视角偏移

纯CSS也可以感知用户交互,依靠的是CSS用户动作伪类以及配合CSS变量在JavaScript中赋值。不过更严格的“纯CSS”方案,用的是@media (hover: hover)配合多个鼠标不动作时的默认值来实现有限的交互。如果想要真正跟随鼠标偏移,还是需要几行JavaScript,但可以把绘制逻辑留在CSS中。例如,在HTML上加一个监听鼠标移动的事件,只更新CSS变量:

const portrait = document.getElementById('portrait'); portrait.addEventListener('mousemove', (e) => { const rect = portrait.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width - 0.5; const y = (e.clientY - rect.top) / rect.height - 0.5; portrait.style.setProperty('--shift-x', `${x * 20}px`); portrait.style.setProperty('--shift-y', `${y * 20}px`); });

然后在CSS里用这两个变量给画像的多个层级设置轻微的反向位移:

.face-background { transform: translate(var(--shift-x, 0), var(--shift-y, 0)) scale(1.02); } .foreground { transform: translate(calc(var(--shift-x, 0) * -1.5), calc(var(--shift-y, 0) * -1.2)); }

这样鼠标移动时,背景层和主体层产生相对运动,形成类似视差的效果。注意var()函数中使用了自定义默认值,当JavaScript尚未赋值时,元素会停留在原始位置。这个技巧可以和CSS3动画延迟结合,让不同图层以不同的速度响应鼠标,产生立体感。对于蒙娜丽莎这种带有神秘微笑的画像,轻微的动态偏移会让人物有一种“活着”的错觉,这也是你在其他纯CSS作品集中经常看到的视觉效果。

5.4 验证你的实现是否“纯CSS3”

最后教一个判断方法:在浏览器开发者工具中屏蔽JavaScript。按下Ctrl+Shift+P,输入“Disable JavaScript”并回车。接着刷新页面,如果画像依然完整显示,且只有鼠标交互类的效果失效,说明静态绘制部分确实是纯CSS3。如果刷新后画像消失或颜色错乱,说明源码中依赖JavaScript做了动态样式注入,并不是真正意义的“纯CSS3”画像。这个小小的校验动作,能帮你区分那些披着“纯CSS3”外衣但实际用脚本生成样式的源码包,也是你审视这幅蒙娜丽莎源码包时最直接的验证标准。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 23:43:55

从零构建引擎:核心原理与最小实现指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:42:00

JWT安全漏洞解析与靶场实战技巧

1. JWT靶场解题实战指南最近在安全圈里流行一句话:"不会打JWT靶场的安全工程师,就像不会用筷子的厨师"。作为Web安全领域的经典题型,JWT相关漏洞在各种CTF比赛和渗透测试靶场中频繁出现。今天我就以"好靶场"平台为例&…

作者头像 李华
网站建设 2026/9/15 23:41:17

C语言联合体与枚举:内存复用、类型安全与标签联合体实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:39:58

Vue3路由核心:useRoute与useRouter的职责、用法与避坑实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:39:19

内容创作两年实操复盘:从写作方法论到数据思维的成长之路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:39:08

大数据场景下数据清洗的质量控制策略与实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华