
CSS 能不能画出一幅会自己变化的画说真的我入门前端那会儿也觉得不太可能。动画不是应该交给 JavaScript 吗绘图不是应该上 Canvas 吗CSS 不是排排版、改改颜色就完了吗直到我认真把渐变、动画、混合模式这些属性一个个试过去才意识到 CSS 生成艺术这件事完全成立——你甚至不用写一行脚本就能在浏览器里画出流动的光晕、跳动的粒子、不断扩散的涟漪和充满诗意的动态文字。这是把代码当作画笔的创作类型本质上是在定义规则、再让浏览器渲染引擎替你跑完整个画面。这篇指南会从思路到落地一步步拆解纯 CSS 生成艺术的创作方法适合作过页面开发、又想尝试创意玩法的前端开发者也适合设计师补足一点代码手感快速产出动态视觉稿。1. 生成艺术与CSS的化学反应为什么非要用CSS画视觉诗篇1.1 生成艺术到底在“生成”什么生成艺术的核心从来不是“画了什么”而是“规则怎么定义”。传统的绘画是作者一笔一笔把结果画出来生成艺术则是作者设定一组参数、一类变化逻辑然后让系统自动演算出一系列结果。你写的不是静态图形而是图形的“生长方式”。在视觉创作里常见的规则包括颜色色相按照某个角度循环偏移、元素位置按照时间函数移动、多个图层以不同周期互相叠加。CSS 天然适合做这件事因为它的核心就是“声明式渲染”。你告诉浏览器某个元素是什么颜色、什么形状、动画怎么走剩下的计算和绘制全部交给浏览器引擎。这种特性意味着作品能自动响应时间、尺寸和用户交互画面的每一帧都不需要你手动指定。与其说 CSS 画出了什么不如说你在定义一套视觉规则然后让规则自己长出画面。1.2 为什么选择CSS而不是Canvas或WebGL很多做创意编程的朋友一开始会上手 Canvas 或 WebGL这当然没错像素级别的控制力是无可替代的。但 CSS 有几个非常实际的优势让它在“视觉诗篇”这类偏装饰、偏意象的场景里更顺手。第一代码量少得惊人。一个复杂的光晕背景用 CSS 可能二三十行就写完用 Canvas 需要自己管理画布尺寸、刷新循环、坐标计算代码体积不是一个量级。第二CSS 的合成动画跑在浏览器的合成器线程上很多属性transform、opacity不会阻塞主线程实际运行起来反而比频繁操作 Canvas 的 JavaScript 循环更流畅。第三文本天然可选中、可复制对可访问性友好。你做一个动态背景用户还能正常阅读上面的文字这对落地到官网、博客、活动页非常关键。当然CSS 生成艺术也有硬伤没有真正的随机数复杂的物理模拟很难做逐像素控制基本不可能。所以我的建议是把它用在“氛围渲染”和“动效装饰”这类场景而不是拿它去做游戏引擎或者数据可视化图表。工具选对了作品才成立。1.3 适合谁、能用到哪里去说实话CSS 生成艺术是个很“轻”的创作方式入门门槛并不高。你只需要懂一点 CSS 基础比如选择器、盒模型、颜色就能开始玩。它特别适合三类人一是前端开发者想给自己的个人主页、404 页面加一点与众不同的视觉记忆点二是设计师想快速验证动态视觉方案不用打开 After Effects 渲染半天三是对创意编程好奇但暂时不想啃 Processing 或 Three.js 的学习成本的初学者。我自己最常用的场景是把这类作品做成整页背景或者某个核心模块的动态装饰。做活动落地页的时候一个会呼吸、会流动的背景比静态 Banner 更能抓住用户注意力。还有一种玩法是把作品导出成 GIF 或录屏发到社交平台本身就是一张会动的“海报”。这些场景不需要高性能渲染不需要复杂交互纯 CSS 完全扛得住。2. 搭建创作环境与掌握三个底层能力不用框架也能开画2.1 环境准备一个编辑器加一个浏览器就够了有不少第一次接触的朋友会问写代码是不是要装一堆环境实际上做 CSS 生成艺术最简陋的配置就是系统自带的文本编辑器和 Chrome 浏览器。新建一个文本文档把后缀改成.html在里面写上一段基础的 HTML 骨架再引入style标签保存后双击用浏览器打开作品就在眼前了。这个流程我到现在做小 demo 还在用简单直接没有编译步骤所见即所得。我强烈建议把浏览器开发者工具打开尤其是 Elements 面板里对样式进行实时修改还有 Animation 面板可以逐帧查看动画时间线。调颜色、调动画速度的时候直接在开发者工具里改属性比手动改代码刷新页面高效得多。另外一个经验是尽量让页面宽度自适应因为 CSS 生成艺术在窗口大小变化时效果会完全不同响应式本身就是作品的一部分。2.2 CSS变量与calc()让画面瞬间参数化在开始写具体效果之前有三个底层能力必须先掌握第一个就是 CSS 自定义属性也就是常说的 CSS 变量。生成艺术的本质是自动化那颜色、尺寸、动画时长这些关键参数就应该被抽出来而不是散落在代码各处。可以想象这样一个场景我用:root里定义了一个--hue变量来控制整个画面的主色调然后所有颜色都通过hsl(var(--hue), 80%, 60%)来取色。想换个配色只需要改一个值整套画面的色相都会跟着偏移。这就是参数化的力量。配合calc()还能在变量基础上做运算比如calc(var(--size) * 2)让不同图层的大小形成倍数关系画面的层次感会一下子丰富起来。变量还有一个容易被忽略的用法就是配合animation在关键帧中改变。传统 CSS 动画只能改已知属性但自定义属性更像一个“容器”塞进去的值可以在动画过程中发生变化。现代浏览器还允许通过property注册带类型的变量这一点下面细讲。2.3 property与sin()/cos()生成艺术的两个“外挂”第二个底层能力是property。它来自 CSS Houdini 体系作用是把自定义属性“注册”成有明确类型的属性比如角度、长度、颜色。为什么这个对生成艺术很重要因为不带类型的变量通常不能被浏览器平滑插值你没法给一个普通变量做渐变动画。举个例子我想让一个圆锥渐变的角度自己转起来。如果直接写成background: conic-gradient(from var(--angle), ...)然后动画里把--angle从0deg改成360deg老版本的浏览器只会“瞬移”而不是“旋转”。但用property注册一下浏览器就明白了这是一个角度类型的变量应该做平滑插值。这样一来背景颜色、渐变方向、滤镜参数都能丝滑地动起来。第三个要看的能力是 CSS 三角函数也就是sin()和cos()。这两个函数能让同一个元素在不同时间点走出圆形轨迹原理和数学课上学的一致水平方向用cos垂直方向用sin就能让一个点绕着圆心旋转。纯 CSS 没有真正的随机但有了三角函数、calc()和不同的动画周期就能模拟出非常接近自然运动的轨迹这也是后面粒子效果的基础。3. 实战一流动的光晕——用多层渐变与混合模式画动态背景3.1 思路解剖光晕是怎么“长”出来的这个案例是我最常用的开场作品因为它视觉效果强烈但代码量很少。思路核心是在同一个元素上叠加多层径向渐变让每一层渐变的中心点、大小、颜色都不同再通过动画让它们的位置持续缓慢变化。多层半透明渐变叠加之后会形成类似极光、烟雾、水墨晕染的质感。关键技巧有两个。第一个是背景层要超出容器大小通常用background-size: 200% 200%这样在移动背景位置时不会露出空白边缘。第二个是使用混合模式比如mix-blend-mode: screen或者filter: blur()让不同颜色层之间产生交叠发光的视觉效果。混合模式决定了图层之间的“化学反应”screen 模式会让重叠区域变亮很像真实世界的光线叠加。3.2 核心实现与参数解读下面这段代码就是一个非常典型的流动光晕背景可以直接保存成 HTML 文件打开观看!DOCTYPE html html langzh-CN head meta charsetUTF-8 / style :root { --hue: 260; --size: 40vw; } body { margin: 0; height: 100vh; overflow: hidden; background: hsl(var(--hue), 40%, 10%); } .aura { position: fixed; inset: 0; background: radial-gradient(circle at 20% 30%, hsla(var(--hue), 90%, 70%, 0.5), transparent 40vw), radial-gradient(circle at 80% 70%, hsla(calc(var(--hue) 60), 90%, 60%, 0.4), transparent 50vw), radial-gradient(circle at 50% 50%, hsla(calc(var(--hue) - 50), 100%, 80%, 0.25), transparent 60vw); background-size: 200% 200%; filter: blur(30px) saturate(140%); animation: drift 24s ease-in-out infinite alternate; } keyframes drift { 0% { background-position: 0% 0%; } 100% { background-position: 100% 100%; } } /style /head body div classaura/div /body /html逐行拆解一下。:root里的--hue是主色相--size是光晕半径参考。body 背景用了很暗的同色系颜色这样光晕对比更强烈。.aura的position: fixed; inset: 0;让光晕层覆盖整个视口。三个radial-gradient分别放在画面的三个不同位置颜色通过hsla(var(--hue), ...)动态计算后两个颜色的色相在主轴基础上偏移了60和-50度制造出邻近色的层次感。filter: blur(30px)是让光晕变柔和的关键如果去掉这一步图层之间会有明显边缘缺乏氤氲感。动画drift控制background-position从左上角到右下角缓慢移动配合ease-in-out缓动和alternate循环画面就会像呼吸一样来回流动。这个案例改起来也很有乐趣把色相变量改成不同值整个氛围就变了。3.3 用CSS鼠标移入事件驱动变化纯 CSS 里并没有真正意义的事件系统但:hover就是最常用的“鼠标移入事件”表达方式。想让光晕随着鼠标进入而产生反应不需要 JavaScript只需要给容器加一个transition然后在:hover里改变关键变量。比如在.aura:hover中把--hue变成320同时把动画时长缩短画面就会在鼠标移入时切换情绪。配合transition: filter 1s ease, background-position 1s ease这类属性变化过程会非常顺滑。这里要注意transition只能作用于能被浏览器插值的属性比如颜色、位置、滤镜数值所以想给--hue做平滑过渡仍然需要property把它注册成number类型否则只会跳变。property --hue { syntax: number; initial-value: 260; inherits: true; } .aura { transition: --hue 1.2s ease; } .aura:hover { --hue: 320; }这个作品做到这里已经具备“生成艺术”最重要的特征画面不是死的时间和交互会让它不断变化。把它放在落地页背景用户一进入页面就被这种动态氛围包围又不影响正文阅读成本极低。4. 实战二纯CSS粒子运动场——从零到有做出动态星云4.1 粒子从哪里来box-shadow的多重采样第二个案例做粒子星云。很多人第一反应是“粒子系统肯定要 JavaScript 来管理数组”但 CSS 里有一个非常聪明的技巧用box-shadow在一个元素上复制出几十个“影子”每个影子都是一个粒子。由于 shadow 的位置偏移可以写多组值一个圆形 div 可以变成几十个圆形 div而渲染开销远低于真的创建几十个 DOM 节点。具体做法是给一个宽高为2px的圆点元素写上十几组box-shadow每组 shadow 的水平和垂直偏移都不同颜色可以用半透明白色。为了让粒子分布更像星云偏移值应该尽量不规则比如24px 18px, 60px 42px, 12px 80px这样手动分布。写多了之后你会发现粒子的数量和分布完全由 shadow 列表决定修改起来非常直观。这个技巧的另一大好处是box-shadow本身就支持动画。只要对元素做transform: translate动画所有影子会跟着整个元素一起移动。如果配合多个不同周期、不同方向的动画就能模拟出粒子各自乱舞的效果视觉上特别丰富。4.2 让粒子动起来多个关键帧错位与负延时一个元素只能有一套animation但可以通过逗号同时应用多个动画而且每个动画可以用不同的时长、不同的缓动、不同的延迟。想让粒子不至于一起同步运动核心技巧是给动画设置负的animation-delay。animation-delay是负值意味着动画从中间某一帧开始播放每个元素即使使用相同的关键帧和时间也会因为起始相位不同而呈现错落的运动状态。我通常会给三组粒子分别设置animation-delay: -0s, -7s, -15s再配上不同的时长看起来就是一群互相独立却又整体统一的粒子在漂移。如果要更进一步模拟星云旋转的感觉可以把粒子的运动轨迹设成椭圆水平位移大一些垂直位移小一些动画整体由一组translateX和一组translateY加在一起。这里就用到前面说的三角函数思想用一个关键帧模拟cos轨迹另一个关键帧模拟sin轨迹两个动画叠加粒子的路径就是一个椭圆。4.3 加入交互涟漪光圈扩散与hover触发的聚拢效果粒子层做好之后可以加一个点睛之笔涟漪光圈扩散。这个效果在很多设计稿里非常常见做法是用一个绝对定位的圆环通过::after伪元素生成初始状态透明度为1缩放比例为0.2在鼠标移入容器时触发动画让圆环逐渐放大到1.2、透明度降到0。.particle-field { position: relative; height: 100vh; background: radial-gradient(circle at center, #0b0f1a, #05070d); } .particle-field::after { content: ; position: absolute; left: 50%; top: 50%; width: 120px; height: 120px; margin: -60px 0 0 -60px; border: 2px solid rgba(180, 220, 255, 0.8); border-radius: 50%; opacity: 0; transform: scale(0.2); transition: opacity 0.8s ease, transform 0.8s ease; pointer-events: none; } .particle-field:hover::after { opacity: 0; transform: scale(2.4); animation: ripple 1.5s ease-out infinite; } keyframes ripple { 0% { opacity: 0.8; transform: scale(0.2); } 100% { opacity: 0; transform: scale(2.4); } }这里的pointer-events: none很重要它保证圆环不会挡住鼠标事件也不会影响页面的正常交互。实际项目中这个圆环可以换成任意图标或文字配合专门的鼠标移入触发逻辑整体作品会很有呼吸感。粒子层加涟漪既有“生成中”的随机感又有一个清晰可感知的交互锚点用户在页面上不会完全迷失方向。5. 视觉诗篇的文字玩法让文字本身成为生成艺术5.1 文字渐变与滚动背景background-clip的魔法光画背景还不够文字才是网页内容的灵魂。CSS 里有一个很浪漫的属性组合background-clip: text。它能把背景渐变“裁剪”成文字的形状配合透明文字颜色就能得到一个颜色不断流动的标题。这种效果经常被用来做落地页的大字 slogan视觉冲击力非常强。实现的要点是文字的颜色必须设成transparent背景用linear-gradient或与其他渐变叠加然后加上-webkit-background-clip: text和background-clip: text。想要动态效果只需要让background-position沿水平方向反复移动。具体代码如下.hero-title { font-size: clamp(2rem, 8vw, 6rem); font-weight: 900; background: linear-gradient(120deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3); background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: textFlow 6s linear infinite alternate; } keyframes textFlow { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } }注意background-size: 300% 100%一定要大于默认的100%否则背景没有多余空间可供移动动画效果就看不出来。这个渐变文字手法加上前面做的动态背景页面整体就会呈现“诗篇”般的流动感。5.2 段落分割线、删除线与装饰性文本的伪随机节奏生成艺术的趣味在于“意料之外”但文字排版要兼顾可读性。我比较常用的一种装饰手法是给段落分割线加入动态渐变色。常规的分割线是一条border-bottom但我们可以用::after加一层宽 40%、渐变色、左右边缘透明渐隐的横条再让它的宽度在一个范围内反复伸缩形成呼吸感。还有一个容易被忽略的属性是text-decoration: line-through也就是删除线。它本身是表示“不要”的文字样式但在艺术化的排版里删除线可以变成一种设计元素。比如标题上叠一个半透明删除线制造涂改感像草稿、像灵感记录和生成艺术的“过程感”非常契合。需要注意的是删除线会让文字可读性下降我一般只用于装饰性短文本或者搭配opacity: 0.4的浅色作为底层氛围字。再一个提升“随机节奏”的小技巧是给段落里的每一句、每一段设置不同的animation-duration和偏大的间隔让它们的动画相位错开。这种手法不一定需要复杂代码只要把动画时间设置成 5s、7s、11s 这样的质数视觉上就会自然产生非同步的变化这是生成艺术里很经典的做法。5.3 用选择器控制局部变化让每个字都有自己的节奏前面说过选择器是 CSS 的基础但很多人没意识到它是生成艺术的控制手柄。比如把标题拆成单个字或单词的span再用:nth-child()给第 2、第 4、第 6 个字符设置不同的动画延迟文字就会像键盘敲击般逐个出现或者像波浪一样上下起伏。这个效果通常被称为“打字机”或“逐字动画”。如果用更接近生成艺术的思路可以让每个字符的动画周期不一致比如奇数位的字符用 3 秒周期偶数位的字符用 5 秒周期然后通过animation-delay加上随机感。这样即使每帧都在动单个字符的变化却不是同步的观感上像是文字里有生命在流动。给span加display: inline-block是关键否则transform对行内元素不生效自然也不会有位移动画。.word { display: inline-block; animation: floatText 3.4s ease-in-out infinite; } .word:nth-child(2n) { animation-duration: 5.2s; animation-delay: -1.2s; } keyframes floatText { 0%, 100% { transform: translateY(0); opacity: 0.7; } 50% { transform: translateY(-14px); opacity: 1; } }这个手法在实际踩坑时有两个注意点第一animation-delay用负值能让页面加载时动画就已经处于某一帧而不是先静止再启动第二在大段正文上做逐字动画会影响阅读效率我通常只在标题、关键词或者短标语上使用。6. 常见问题速查与性能避坑做完很好跑起来也要稳6.1 动画卡顿掉帧找到开销元凶CSS 动画在大多数情况下非常流畅但只要你用了高开销属性卡顿会立刻出现。最常见的三个元凶是filter的blur()、大面积的box-shadow、以及动画background-position时背景尺寸过大。blur()本身是采样运算尤其当作用对象覆盖整个 viewport 时每一帧都要做模糊计算移动端很容易吃满 GPU。我的经验是能用半透明渐变模拟“柔光”的就不要轻易上大范围 blur如果一定要用就缩小模糊作用的元素范围或者把模糊后的元素固定住不让它持续变形。box-shadow虽然能复制粒子但粒子数量一旦超过 50 个每帧的阴影计算也很重这时候可以静态阴影 透明位移动画让浏览器走合成通道。判断卡顿来源用性能面板看帧率和 Rendering 标签页能清楚看到每个阶段的耗时。做生成艺术不是越多越好要学会给画面做减法。6.2 刷新后随机感不够如何制造伪随机纯 CSS 没有随机函数所以很多初学者做出来的生成艺术每次刷新都是同一个状态时间一久就会腻。想做出“每一次打开都不一样”的感觉有几种不需要 JavaScript 的替代思路。第一种是让动画周期足够长比如 12 秒、24 秒、61 秒这样哪怕每次刷新时动画起点相同由于周期长用户看到的实际帧也几乎都不一样。第二种是大量使用变量组合配合nth-child和不同的animation-delay在初始画面就制造差异。第三种是结合prefers-reduced-motion等媒体查询对不同设备展示不同变化强度既照顾用户偏好也让视觉效果更有层次。要注意的是伪随机始终不是真随机。如果你要的是真正“生成不同形态”的作品建议在最外层引入一小段 JavaScript比如修改body上的 CSS 变量。把逻辑留给 CSS把随机种子交给 JS这也是一种混合架构既保留生成艺术的分寸感又不破坏创作的可控性。6.3 兼容性检查与降级方案CSS 生成艺术用到的高阶特性比如property、background-clip: text、sin()/cos()在最新版的 Chrome、Edge、Safari、Firefox 中支持不尽相同。做作品前先确认目标用户用什么浏览器尤其是企业落地页用户环境五花八门。一个稳妥的降级策略是先写基础样式再写增强样式。比如渐变文字先在普通颜色下把标题设成醒目的纯色再在支持background-clip: text的浏览器里覆盖成渐变透明文字。动画同理如果不支持某个属性回退到静态效果至少不损失核心信息。做生成艺术很容易沉迷效果本身但作为博文、落地页里的内容底层的可读性和可用性永远排在第一位。我整理了一个常见问题的速查表每一条都是实操中踩过的问题现象常见原因处理方法动画在移动端明显卡顿大面积filter: blur()或动画background-position缩小模糊范围改用半透明渐变背景固定动画只动transform渐变文字显示为白色/空白忘记设置color: transparent或background-clip前缀缺失补上-webkit-background-clip: text和background-clip: text文字颜色设为transparent自定义变量动画突然跳变变量未注册类型浏览器无法插值使用property注册变量并声明syntax:hover效果一闪而过过渡属性写在伪类里而不是默认状态上把transition写在默认状态选择器中页面有横向滚动条背景图或粒子层超出视口宽度给最外层容器加overflow-x: hidden或使用position: fixed覆盖视口6.4 创作的一点个人心得做到这里我想多说一句自己的体验。刚开始做 CSS 生成艺术时我总想追求“像 JavaScript 做出来那么丰富”结果绕了很多弯路。后来我意识到CSS 的独特美感恰恰在于它的“克制”——你没法做太复杂的计算反而逼着创作者去提炼画面的本质用最少的规则表达最多的情绪。我现在的习惯是先定义一个核心的变量参数通常是色相或粒子密度然后只加一层变化再观察效果慢慢叠加。一次只动一个参数能清楚看到每个属性对画面的贡献。很多次我以为要往上堆更多的图层才能出效果实际上把一两个图层的动画时长、缓动函数调好效果反而更高级。最后再分享一个小技巧做完一个作品后把prefers-reduced-motion: reduce的情况考虑进来。它能让偏好减少动效的用户看到静态版本这不只是无障碍要求也是一个成熟创作者对用户的尊重。这些年来我无数次在一个 CSS demo 前盯着它跑了几分钟只为了等一个颜色变化的高峰。生成艺术的乐趣和写诗很像规则是笔浏览器是纸时间就是句子的停顿。希望这篇指南能让你也找到那种“用代码写诗”的手感。