【每天一个CSS | Day19】流光文字与一笔写成的手写签名

写在前面

昨天我们做了一只拟物怀表------金属、皮革、厚度,都是"实体"的质感。

今天做两个"笔画级"的文字效果:

  • 流光文字------标题里一道光带缓慢扫过,像金属或玻璃在反光
  • 手写签名------签名像被"画"出来一样,一笔一笔出现、停笔、再被擦掉、循环

看起来是两个不相干的效果,其实本质完全一样:

都是"让一段渐变 / 线条,按进度展示"。

  • 流光:让"一段高光渐变"按进度扫过
  • 签名:让"一条SVG路径"按进度画出来

理解了这个统一视角,你就能把这两个效果都拆到最简单。


一、先看效果

深色卡片上,从上到下三件事:

  • 顶部 :衬线大字「AURUM」------金属灰字面里,一道蓝白光带每4.2秒从右往左扫过
  • 中部 :一条蓝色曲线 凭空起笔,游走转折像在签某个花体名字;收笔后一道绿色下划线补上
  • 底部 :停两秒,笔画像被橡皮擦反向擦掉,再重新写起

签名的完整循环约6.5秒------写 → 停 → 擦 → 再写。

怎么跑起来:

  1. 新建一个demo.html
  2. 把下面第二节的完整代码粘进去
  3. 双击用浏览器打开

📸 截图党提示 :录7秒GIF 抓签名完整循环最生动------静态图看不出"书写"这个过程。如果只能截图,选签名写到2/3的瞬间------笔画悬在半空,最能暗示"正在写"。


二、完整代码(复制即用)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Day 19 · 流光文字与手写签名</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { height:100%; }
  body { display:flex; align-items:center; justify-content:center; overflow:hidden;
    background:#10151c; font-family:"PingFang SC", sans-serif; }

  .card { width:min(560px, 92vw); padding:52px 48px 44px; border-radius:18px;
    background:#131a23; border:1px solid rgba(255,255,255,.07);
    box-shadow:0 30px 70px rgba(0,0,0,.45); text-align:center; }
  .label { font:400 10px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.34em; color:#5b6878; }

  /* ------ 流光文字:渐变裁进字形,高光带横扫 ------ */
  .shine { margin-top:30px;
    font-family:Georgia, "Times New Roman", serif;
    font-size:64px; font-weight:600; letter-spacing:.18em;
    background:linear-gradient(110deg, #5a6472 0%, #5a6472 38%, #f2f6fb 47%, #9fd6ff 53%, #5a6472 62%, #5a6472 100%);
    background-size:250% 100%;
    -webkit-background-clip:text; background-clip:text; color:transparent;
    -webkit-text-fill-color:transparent;
    animation:sheen 4.2s linear infinite; }
  @keyframes sheen { from { background-position:250% 0; } to { background-position:-250% 0; } }

  .divider { width:64px; height:1px; background:rgba(255,255,255,.12); margin:30px auto 6px; }

  /* ------ 手写签名:SVG stroke 描边,写完停笔、再被擦掉、循环 ------ */
  .sig { display:block; width:min(420px, 78vw); margin:0 auto; overflow:visible; }
  .stroke { fill:none; stroke-linecap:round; stroke-linejoin:round; }
  .s1 { stroke:#9fd6ff; stroke-width:3.5; stroke-dasharray:900; stroke-dashoffset:900;
    animation: draw1 6.5s ease-in-out infinite; }
  .s2 { stroke:#5cc9b4; stroke-width:3; stroke-dasharray:420; stroke-dashoffset:420;
    animation: draw2 6.5s ease-in-out infinite; }
  @keyframes draw1 { 0% { stroke-dashoffset:900; } 30% { stroke-dashoffset:0; }
                     72% { stroke-dashoffset:0; } 100% { stroke-dashoffset:-900; } }
  @keyframes draw2 { 0%, 16% { stroke-dashoffset:420; } 42% { stroke-dashoffset:0; }
                     74% { stroke-dashoffset:0; } 100% { stroke-dashoffset:-420; } }

  .note { margin-top:26px; font:400 10px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.3em; color:#39434f; }

  .tag { position:fixed; right:18px; bottom:14px; font:400 10px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.28em; color:rgba(230,237,243,.22); }
</style>
</head>
<body>
  <div class="card">
    <div class="label">SIGNATURE STUDY · DAY 19</div>
    <div class="shine">AURUM</div>
    <div class="divider"></div>
    <svg class="sig" viewBox="0 0 420 140">
      <path class="stroke s1" d="M20 96 C 60 20, 92 18, 96 62 C 99 92, 76 108, 62 100 C 50 93, 66 74, 96 72 C 150 68, 168 40, 196 44 C 226 48, 214 88, 246 84 C 282 79, 288 46, 318 52 C 344 57, 340 84, 368 78 C 386 74, 396 60, 404 48"/>
      <path class="stroke s2" d="M26 116 C 120 130, 260 126, 398 104"/>
    </svg>
    <div class="note">BACKGROUND-CLIP:TEXT + STROKE-DASHOFFSET</div>
  </div>
  <div class="tag">每天一个惊艳 CSS · DAY 19</div>
</body>
</html>

三、三个核心技法拆解

代码不长,但每一个技法都值得单独拿出来讲------它们不是"这个效果专属",而是通用工具。

技法1:background-clip: text把渐变「灌进」字形

流光文字的核心思路,你想一下------

不是"文字在发光",而是------

"一道光带扫过文字的表面**。"**

所以本质是"渐变在动",文字只是"渐变的容器"。

css 复制代码
.shine {
  /* ① 文字本身的颜色 → 透明 */
  color: transparent;
  -webkit-text-fill-color: transparent;

  /* ② 背景是一段"三段式"渐变 */
  background: linear-gradient(110deg,
    #5a6472 0%,           /* 基色 */
    #5a6472 38%,          /* 基色 */
    #f2f6fb 47%,          /* 高光带 ------ 从暗到极亮 */
    #9fd6ff 53%,          /* 高光带 ------ 从极亮到蓝 */
    #5a6472 62%,          /* 基色 */
    #5a6472 100%);        /* 基色 */

  /* ③ 背景放大到250%,光带才有空间滑动 */
  background-size: 250% 100%;

  /* ④ 关键:把背景"裁剪"到文字的笔画里 */
  -webkit-background-clip: text;
          background-clip: text;

  /* ⑤ 动画只扫background-position */
  animation: sheen 4.2s linear infinite;
}
@keyframes sheen {
  from { background-position: 250% 0; }
  to   { background-position: -250% 0; }
}

逐条理解:

① 文字颜色透明------这是"让背景透出来"的前提

css 复制代码
color: transparent;                     /* 通用 */
-webkit-text-fill-color: transparent;   /* Safari / 老Chrome需要 */

两行都要写 ------这是 Safari 的历史遗留。只写color: transparent在Safari里会"露底"(显示出默认的黑色文字)。

② background-clip: text ------ 让背景只从字形里透出来

这是整个效果的核心属性 ------它把"元素的矩形背景"裁成"文字笔画的形状"。

结果 :渐变不再覆盖整块矩形,而是只出现在文字的笔画里。

③ background-size: 250% 100% ------ 给光带留出"滑动空间"

默认background-size: 100% ------渐变正好铺满元素,没有"藏起来的部分",也就没法扫。

放大到250% ------渐变比元素宽2.5倍 ------光带可以从外面扫进来、再扫出去。

④ 动画只扫background-position

css 复制代码
@keyframes sheen {
  from { background-position: 250% 0; }   /* 光带在"很远的右边" */
  to   { background-position: -250% 0; }  /* 光带扫到"很远的左边" */
}

从250%扫到-250% ------光带完整地穿过文字,从头到尾扫一遍。

为什么首尾都停在基色#5a6472?

因为渐变的0% ~ 38%和62% ~ 100%都是基色------所以:

  • 光带在起点(250%)时------可见区域是基色
  • 光带在终点(-250%)时------可见区域还是基色
  • 循环重置时,画面上都是"纯基色" ------无缝衔接

这是"无缝循环"的通用心法 ------让循环的首尾两端,视觉上完全一致。


技法2:stroke-dashoffset是「笔尖进度条」

手写签名的核心,是把"画出来的线"变成"一段进度"。

怎么理解?

把SVG路径想象成一条"虚线":

css 复制代码
.stroke { stroke-dasharray: 900; stroke-dashoffset: 900; }

stroke-dasharray: 900 ------ 意思是"这条线由900px实线 + 900px空白交替组成"。

因为路径总长度就是900左右 ------所以实际上整条路径就是一段900px的实线。

stroke-dashoffset: 900 ------ 意思是"虚线整体向后偏移900px"。

结果 :偏移900px之后,原来那条 900px 的实线完全移出了可见范围 ------路径上什么都不显示。

动画把offset从900推到0:

css 复制代码
@keyframes draw1 {
  0%   { stroke-dashoffset: 900; }   /* 完全隐藏 */
  30%  { stroke-dashoffset: 0; }     /* 完全画出 */
  72%  { stroke-dashoffset: 0; }     /* 停笔 */
  100% { stroke-dashoffset: -900; }  /* 反向擦掉 */
}

逐段读:

分段 offset 视觉
0% → 30% 900 → 0 线条被"画"出来------笔尖从左往右走
30% → 72% 0 → 0 停笔------签名完整显示
72% → 100% 0 → -900 线条被"擦"掉------从起笔端反向消失

关键:offset推到负值(-900)------线条从起笔端开始"反向消失"。

为什么是"反向"?

因为offset是沿路径方向的偏移:

  • 正向偏移(正值) :实线段向后推 ------末端的线先消失
  • 负向偏移(负值) :实线段向前拉 ------起笔端的线先消失

"起笔端先消失"看起来就像被"橡皮擦擦掉"------从线的起点开始,一路往回擦。

"写、停、擦"三幕剧,一个属性全包。

这是stroke-dashoffset的通用价值 ------所有"沿线描边"的效果(进度条、路径动画、勾选对号、曲线绘制)都靠它。


技法3:双路径 = 双笔顺

"签名"看起来是"一笔写成",但代码里其实是"两条独立路径"。

html 复制代码
<svg viewBox="0 0 420 140">
  <path class="stroke s1" d="M20 96 C 60 20, ..."/>   <!-- 主体:花体字 -->
  <path class="stroke s2" d="M26 116 C 120 130, ..."/>  <!-- 下划线 -->
</svg>

两条路径各自持有动画:

css 复制代码
/* 主体:0% ~ 30%写完,72% ~ 100%擦掉 */
@keyframes draw1 {
  0%   { stroke-dashoffset: 900; }
  30%  { stroke-dashoffset: 0; }
  72%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -900; }
}

/* 下划线:16%才起笔,42%写完,74%起擦 */
@keyframes draw2 {
  0%, 16% { stroke-dashoffset: 420; }   /* 前16%保持隐藏 */
  42%     { stroke-dashoffset: 0; }     /* 42%写完 */
  74%     { stroke-dashoffset: 0; }     /* 停笔到74% */
  100%    { stroke-dashoffset: -420; }  /* 擦掉 */
}

时间轴对齐:

复制代码
0s        1.95s      3.0s       4.7s       6.5s
│          │          │          │          │
├──主体写下─┤          │          │          │
           ├──主体停笔──┤          │          │
        ├─下划线写下─┤  │          │          │
                      ├──下划线停笔┤          │
                                  ├──一起擦──┤

"笔顺感"的三个要点:

① 下划线的起笔比主体晚 16%------主体写了一小半,下划线才"跟上"。

为什么不是"等主体写完再画下划线"?

因为真实签名里,人是"边写边补充"的 ------主体签名写到一半,手就已经顺势拉出一道下划线。"错峰"比"严格先后"更自然。

② 两条线的"擦除"基本同步 ------主体72%起擦,下划线74%起擦------差2%。

为什么不是"主体擦完再擦下划线"?

因为**"擦掉一个签名"是一个整体动作**------不是"先擦主字再擦下划线",而是整块内容一起被"抹掉"。

③ stroke-linecap: round让起笔收笔圆润

css 复制代码
.stroke { stroke-linecap: round; stroke-linejoin: round; }

这是"手写质感"的最低成本------

  • stroke-linecap: round:线条的"两端"是圆头(不是平头)
  • stroke-linejoin: round:线条的"转折处"是圆角(不是尖角)

少了这两行------签名的笔画会显得"生硬、像打印的"。

加上这两行------笔画立刻有"手写"的温度。


四、踩坑位(我全踩过,你直接避)

① 漏掉-webkit-background-clip: text

css 复制代码
/* ❌ Safari 不认,光带扫不出来 */
background-clip: text;

/* ✅ 两行都写 */
-webkit-background-clip: text;
        background-clip: text;

Safari只认带-webkit-前缀的版本------这是Safari的历史遗留。

同时还要配-webkit-text-fill-color: transparent:

css 复制代码
/* ❌ 只写color: transparent,Safari里会"露底" */
color: transparent;

/* ✅ 两行都写 */
color: transparent;
-webkit-text-fill-color: transparent;

只写color: transparent在部分浏览器会显示出默认的黑色文字------光带效果看不到。

② background-size不放大

css 复制代码
/* ❌ 渐变默认铺满100%,光带"扫"不动 */
background: linear-gradient(110deg, ...);
animation: sheen 4.2s linear infinite;
@keyframes sheen { from { background-position: 250% 0; } to { background-position: -250% 0; } }

/* ✅ 先放大到250%,position才有可动空间 */
background-size: 250% 100%;

background-position是"在背景图内部移动" ------如果背景图正好铺满元素 ,position怎么动,画面上都不会变化。

先放大background-size ------让背景图大于元素 ------光带才有"藏起来的空间" ------position才有"可动区间"。

这条规律通用 ------所有"扫光 / 滑动 / 移动"的background-position动画,都要先把background-size放大。

③ dasharray给小了

css 复制代码
/* ❌ 路径真实长度超过dasharray,写完一半就断 */
.stroke { stroke-dasharray: 400; }   /* 实际路径长度900 */

/* ✅ 给个足够大的数字,宁可浪费不可截断 */
.stroke { stroke-dasharray: 900; }   /* 或1200,都行 */

stroke-dasharray必须 ≥ 路径的实际长度。

给小了 ------整条线被切成"实线 + 空白 + 实线 + 空白"------动画只能画出一部分。

如何量路径的真实长度?

浏览器控制台一行:

js 复制代码
document.querySelector('.s1').getTotalLength();

返回一个数字 ------比如874.23------取整往上加一点(900) ------安全值。

拿不准就给天文数字 ------900 / 1200 / 2000------宁可浪费不可截断。

④ SVG path忘写fill: none

css 复制代码
/* ❌ 曲线内部被默认黑色填充实心块,签名变墨渍 */
.stroke { /* 没有fill */ }

/* ✅ 明确fill: none */
.stroke { fill: none; }

SVG path默认fill 是黑色 ------如果不显式设为none,路径围出来的所有区域都会被填成黑色。

结果:你想要的"一条线",变成了"一块黑的墨渍"。

这是第一次写SVG路径最容易踩的坑 ------fill: none是必写项。

⑤ 擦除阶段没设计

css 复制代码
/* ❌ 只有"写"没有"擦",循环时笔画闪断 */
@keyframes draw1 {
  0%   { stroke-dashoffset: 900; }
  100% { stroke-dashoffset: 0; }
}
/* 循环重置时,笔画从0瞬间跳回900 ------ 闪一下 */

/* ✅ 让 offset 冲过 0 到负值,笔画反向消失 */
@keyframes draw1 {
  0%   { stroke-dashoffset: 900; }
  30%  { stroke-dashoffset: 0; }     /* 写完 */
  72%  { stroke-dashoffset: 0; }     /* 停笔 */
  100% { stroke-dashoffset: -900; }  /* 反向擦掉 */
}

只写"0 → 写完"的动画 ------循环重置时,笔画从"写完"瞬间跳回"隐藏" ------观感上就是闪一下。

加上"擦除段" ------offset从0推到-900------笔画从起笔端反向消失 ------循环天衣无缝。

这是"无缝循环"的又一条通用心法 ------让"结束状态"和"起始状态"视觉上一致。

  • 流光:首尾都是基色
  • 签名:首尾都是"隐藏"

五、变式挑战(10--30分钟,动手才有收获)

任务:签你自己的名字。

有两条路线:

路线A:手动画路径(推荐)

  1. 打开任意矢量软件(Figma / Illustrator / Inkscape)
  2. 用铅笔工具(Pencil)随手涂一笔------像签名一样
  3. 导出为SVG ------拿到path的d属性
  4. 替换到.s1的d="..."里

路线B:找现成的字(更工程化)

  1. 在网上搜任意手写风格字母的SVG path(很多免费字体站提供SVG)
  2. 复制d属性替换进.s1
  3. dasharray改成新路径的长度 ------浏览器控制台document.querySelector('.s1').getTotalLength()一行就能量出来
  4. 调整动画时长------路径长的,动画时长要长一点,否则"笔"走太快

进阶方向(有余力再玩):

① 让签名的笔画有"粗细变化"

css 复制代码
.s1 { stroke-width: 3.5; }
.s1:hover { stroke-width: 5; }   /* hover时变粗 */

真实签名里,笔画的粗细是变化的 ------CSS做不到"沿路径变化线宽",但可以用两层路径叠出效果 (外层细、内层粗,用stroke-dashoffset错开)------高级玩法。

② 加"落笔的墨点"

css 复制代码
.sig::before {
  content: ""; position: absolute; ...;   /* 一个圆点 */
  animation: drop 6.5s ease-in-out infinite;
}
@keyframes drop {
  0%   { opacity: 0; }
  5%   { opacity: 1; }   /* 起笔时墨点出现 */
  10%  { opacity: 0; }   /* 墨水渗开,消失 */
}

"落笔的墨点"是手写感的加分项------签名开始的瞬间,一个小墨点出现又消失。

③ 换成中文名签名

中文名的难度在于笔画多 ------但stroke-dashoffset的原理完全一样。关键是把"多笔画"拆成多条path,各自按笔顺错峰动画。

这是最有意思的挑战 ------你可以在评论区贴出你的中文名签名,我会挑几个做特写拆解。

④ 让流光文字"循环更慢一点"

现在4.2s一次扫过 ------可以试试8s或10s ------"优雅"和"频率"成反比。

⑤ 让流光和签名"错峰"

现在两个动画独立跑(流光4.2s、签名6.5s)------可以让它们"同步" ------比如签名写完的瞬间,流光正好扫过 ------"笔落墨干,纸面反光"。


六、小结

今天这一篇里藏着 5个通用技能点:

技能 适用场景
background-clip: text + 透明文字 所有"渐变文字 / 金属字 / 流光字"
background-size放大 + position滑动 所有"扫光 / 高光扫过"效果
stroke-dashoffset做"描边进度" 所有"沿线书写 / 进度条 / 对勾动画"
stroke-linecap: round手写质感 所有"手绘 / 手写 / 曲线"效果
首尾状态一致做无缝循环 所有循环动画的"重置隐蔽化"

最值得记住的三条:

① 文字流光和手写签名,本质是同一件事。

"让一段渐变 / 线条,按进度展示。"

  • 流光 :渐变 + background-position动
  • 签名 :SVG线 + stroke-dashoffset动

**一个"填充"的进度,一个"描边"的进度。**理解了这个统一视角,你就能把两个效果都拆到最简单。

② background-size不放大,background-position就动不了。

这是"扫光"效果的唯一前提------

  • 背景图 = 元素大小 :position怎么动,画面不变
  • 背景图 > 元素大小 :position动起来,光带才"扫过"

"先放大,再滑动"------所有扫光效果的通用公式。

③ 无缝循环的关键,是"结束状态 = 起始状态"。

  • 流光:首尾都是基色(光带藏在外面)
  • 签名 :首尾都是隐藏(offset冲过0到负值)

"让重置发生在看不见的时候"。


如果这一篇对你有帮助,三连支持一下这个专栏 🙏

点赞 = 更新加速,收藏 = 随时复制代码,评论 = 你想看什么效果,我排进日程。

本专栏所有demo,复制即用,随便商用。

目标更新:不断更。

相关推荐
逐光者9331 小时前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32
To_OC1 小时前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
CDwenhuohuo1 小时前
electron pc项目打包成桌面端
前端·javascript·electron
前端大斗师1 小时前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
Jmyd01232 小时前
用 Web VR 引擎搭建 3D 虚拟展厅:从素材上云到多端实时渲染的完整数据链路
前端·3d·vr·三维数字化
刘天远3 小时前
Agent成本核算实现:事件表、状态分布与Python归集
前端·数据库·人工智能·python
欣欣之王来了3 小时前
开发环境准备:Node.js、npm、VS Code安装配置
前端·学习·架构·项目·vue3教程
xiaolinudao1233 小时前
将多个 Excel 表格中的数据合并到单个表中|6 种实现方案全解析
java·前端·excel
LRL_4 小时前
跨平台 Node 模块安装指南:如何利用 pnpm/npm 配置`supportedArchitectures` 锁定平台依赖
前端·npm·node.js