【每天一个CSS | Day21】落地页Hero动画的分镜表

写在前面

动画系周,最后一天。

这一周我们分别攻下了六种动画:

效果 核心技法
打字机 ch + steps() + delay排时间轴
六种加载器 border / conic / scaleY / background-position
烟花 box-shadow粒子群 + 核心scale
拟物怀表 conic-gradient拉丝金属 + 体外支点
流光签名 background-clip: text + stroke-dashoffset
滚动叙事 animation-timeline: scroll(root) / view()

今天把它们缝成一个完整的落地页Hero区。

一篇"分镜表"驱动的产品首屏动画。

这一篇交付的不是"一个新效果",而是一种编排方法:

  • 分镜表怎么画------delay从0累加,动笔之前先定表
  • 双动画怎么交棒 ------to必须等于下一棒的from
  • 节奏密度怎么定------相邻分镜间隔0.15 ~ 0.35s

Hero的质感,20%来自编排,80%来自光照纪律。


一、先看效果

深色发布会现场:远处一团极光缓慢呼吸,脚下网格地板向灭点收缩。

页面打开的瞬间像有人按了播放键------2秒内依次发生:

  • 0.10s :绿字kicker(AURORA AUDIO · NEW)先亮起
  • 0.35s:大标题「SOUND, FROZEN IN LIGHT.」从容升起
  • 0.70s:中央的翡翠球体带着两圈反向旋转的轨道环放大入场
  • 1.60s:球体进入"永续悬浮"------轻轻上下呼吸
  • 0.95s ~ 1.25s :三枚参数24H / 38G / LDAC依次错峰浮起
  • 1.40s :最后一枚「立即预约」按钮带一点过冲弹出来

全程2秒,之后页面只留下低速呼吸------球体悬浮、轨道环自转、极光漂移。

刷新页面可重看入场。

怎么跑起来:

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

📸 截图党提示 :录3秒 GIF 抓从页面打开到CTA弹出的完整入场最生动。静态图选两张拼图------入场中段 (球体正在放大、参数浮了一半)+ 入场完毕(全部到位)。


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

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 21 · 周挑战:落地页 Hero 动画</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html { overflow-x:hidden; }
  body { min-height:100vh; overflow-x:hidden; background:#070a12;
    font-family:"PingFang SC", sans-serif; color:#e6edf3; }

  /* ------ 氛围:极光带 + 网格地板 ------ */
  .aur { position:fixed; inset:0; overflow:hidden; }
  .aur i { position:absolute; right:-6%; top:-14%; width:560px; height:300px; border-radius:50%;
    background:radial-gradient(ellipse, rgba(13,133,116,.4), transparent 65%);
    filter:blur(70px); animation: drift 16s ease-in-out infinite alternate; }
  @keyframes drift { to { transform:translate(-70px,40px) scale(1.15); } }
  .floor { position:fixed; left:-20vw; right:-20vw; bottom:-6vh; height:36vh;
    background:
      linear-gradient(rgba(125,211,252,.07) 1.5px, transparent 1.5px),
      linear-gradient(90deg, rgba(125,211,252,.07) 1.5px, transparent 1.5px);
    background-size:54px 34px;
    transform:perspective(600px) rotateX(62deg); transform-origin:top center;
    -webkit-mask:linear-gradient(#000 30%, transparent 92%);
            mask:linear-gradient(#000 30%, transparent 92%);
    pointer-events:none; }

  main { position:relative; z-index:2; min-height:100vh;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    padding:48px 20px; }

  /* ------ 分镜 1 · 0.10s:kicker 亮起 ------ */
  .kicker { font:400 11px/1 "SF Mono", Consolas, monospace; letter-spacing:.34em;
    color:#5cc9b4; opacity:0; transform:translateY(12px);
    animation:in .5s ease .1s both; }

  /* ------ 分镜 2 · 0.35s:主标题升起 ------ */
  h1 { margin-top:16px; text-align:center;
    font-size:clamp(38px, 6vw, 56px); font-weight:600; line-height:1.14; letter-spacing:.02em;
    opacity:0; transform:translateY(28px);
    animation:in .7s cubic-bezier(.2,.8,.2,1) .35s both; }

  /* ------ 分镜 3 · 0.70s:产品球体入场,1.60s 起持续悬浮 ------ */
  .stage { position:relative; width:320px; height:240px; margin-top:10px;
    perspective:900px; transform-style:preserve-3d; display:grid; place-items:center; }
  .orb { width:180px; height:180px; border-radius:50%;
    background:radial-gradient(circle at 33% 28%, #d8f7ec, #0d8574 48%, #073f37 82%);
    box-shadow:0 0 60px 10px rgba(13,133,116,.35), inset -14px -18px 40px rgba(0,0,0,.45);
    opacity:0; transform:scale(.55) translateY(36px);
    animation:in .8s cubic-bezier(.2,.8,.2,1) .7s both, bob 5s ease-in-out 1.6s infinite alternate; }
  @keyframes bob { from { transform:translateY(0); } to { transform:translateY(-14px); } }

  /* 轨道环:常驻自转(0s 起转,0.95s 才淡入) */
  .ring { position:absolute; left:50%; top:50%; border-radius:50%;
    border:1.5px solid rgba(125,211,252,.45); opacity:0;
    animation:fade .7s ease .95s both; }
  .r1 { width:310px; height:100px; margin:-50px 0 0 -155px;
    animation:fade .7s ease .95s both, spinz1 12s linear infinite; }
  .r2 { width:250px; height:82px; margin:-41px 0 0 -125px;
    border-color:rgba(255,194,207,.35);
    animation:fade .7s ease 1.1s both, spinz2 9s linear infinite; }
  @keyframes fade { from { opacity:0; } to { opacity:1; } }
  @keyframes spinz1 { from { transform:rotateX(74deg) rotateZ(0deg); }
                      to   { transform:rotateX(74deg) rotateZ(360deg); } }
  @keyframes spinz2 { from { transform:rotateX(70deg) rotateZ(0deg); }
                      to   { transform:rotateX(70deg) rotateZ(-360deg); } }

  /* ------ 分镜 4 · 0.95s 起:三枚参数错峰浮起 ------ */
  .specs { display:flex; gap:36px; margin-top:14px; }
  .s { text-align:center; opacity:0; transform:translateY(14px); animation:in .5s ease both; }
  .s:nth-child(1) { animation-delay:.95s; }
  .s:nth-child(2) { animation-delay:1.1s; }
  .s:nth-child(3) { animation-delay:1.25s; }
  .s b { display:block; font:600 24px/1.2 "SF Mono", Consolas, monospace; color:#e6edf3; }
  .s span { font-size:11px; letter-spacing:.22em; color:#5b6878; }

  /* ------ 分镜 5 · 1.40s:CTA 过冲弹出 ------ */
  .cta { margin-top:30px; padding:14px 40px; border-radius:99px; cursor:pointer;
    border:1px solid rgba(92,201,180,.5); background:rgba(13,133,116,.18);
    color:#bfeedf; font-size:14px; letter-spacing:.2em;
    opacity:0; transform:scale(.9);
    animation:pop .6s cubic-bezier(.34,1.56,.64,1) 1.4s both;
    transition:background .25s, box-shadow .25s; }
  .cta:hover { background:rgba(13,133,116,.34); box-shadow:0 0 30px rgba(13,133,116,.35); }
  @keyframes pop { to { opacity:1; transform:scale(1); } }

  /* ------ 统一入场帧:初始态写在元素上,这里只管「到位」------ */
  @keyframes in { to { opacity:1; transform:none; } }

  .tag { position:fixed; right:18px; bottom:14px; font:400 10px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.28em; color:rgba(230,237,243,.2); }
</style>
</head>
<body>
  <div class="aur"><i></i></div>
  <div class="floor"></div>
  <main>
    <div class="kicker">AURORA AUDIO · NEW</div>
    <h1>SOUND, FROZEN<br>IN LIGHT.</h1>
    <div class="stage">
      <i class="ring r1"></i>
      <i class="ring r2"></i>
      <div class="orb"></div>
    </div>
    <div class="specs">
      <div class="s"><b>24H</b><span>续航</span></div>
      <div class="s"><b>38G</b><span>重量</span></div>
      <div class="s"><b>LDAC</b><span>编码</span></div>
    </div>
    <button class="cta">立即预约</button>
  </main>
  <div class="tag">每天一个惊艳 CSS · DAY 21 · WEEK 3 FINALE</div>
</body>
</html>

三、三个核心技法拆解

今天和前几篇不一样------它不讲新效果,讲的是"编排方法"。

三个技法,从"心法"到"技巧"到"审美",层层递进。

技法1:先画分镜表,再写第一行CSS

这是本篇最重要的工程心法。

很多同学做入场动画的方式 ------想到哪写到哪:给kicker加个delay、给标题加个delay、给按钮加个delay......最后发现时间点乱成一锅粥。

正确做法 :先在注释里立一张分镜表。

分镜表 ------ 入场编排

# 元素 入点 时长 缓动 之后
1 kicker 0.10s 0.5s ease -
2 主标题 0.35s 0.7s 收尾减速 -
3 球体 0.70s 0.8s 收尾减速 悬浮 ∞
3+ 轨道环 0.95s 0.7s ease 自转 ∞
4 参数 ×3 0.95s 0.5s ease -
5 CTA 1.40s 0.6s 过冲曲线 -

然后逐行翻译成animation-delay------代码一行不多、一行不少。

这张表就是整篇文章的"设计稿"。

表格里最值钱的一列,是"入点"------相邻分镜的间隔:

相邻间隔 观感
< 0.1s 像事故------元素"挤"在一起出现
0.15 ~ 0.35s 甜点位------前一个刚站稳、后一个接上
> 0.5s 像卡顿------用户以为页面加载失败了

本页的间隔:

  • kicker → 标题 :0.35 - 0.10 = 0.25s(甜点位)
  • 标题 → 球体 :0.70 - 0.35 = 0.35s(稍长------因为标题是核心,多给点呼吸)
  • 球体 → 参数 :0.95 - 0.70 = 0.25s(球体入场和参数入场重叠------这是刻意的)
  • 参数 → CTA :1.40 - 1.25 = 0.15s(CTA是最后出场,节奏加快)

注意"重叠"这件事 ------球体还在放大(0.70s ~ 1.50s),参数已经在浮起(0.95s起)。

完全不重叠 ------观感像"报菜名",一样一样来。

适度重叠------观感像"编舞",有节奏感。

**"重叠"是分镜表里最优雅的部分------**它让整个入场不是"串行",而是"有节奏的并行"。


技法2:双动画交棒------to必须等于下一棒的from

球体挂了两条动画:

css 复制代码
.orb {
  opacity: 0;
  transform: scale(.55) translateY(36px);   /* 初始态 */
  animation:
    in  .8s cubic-bezier(.2,.8,.2,1) .7s  both,   /* 入场:0.7s起,0.8s长 */
    bob 5s ease-in-out 1.6s infinite alternate;    /* 悬浮:1.6s起,无限 */
}

@keyframes in  { to { opacity: 1; transform: none; } }        /* 终点:正常态 */
@keyframes bob { from { transform: translateY(0); }           /* 起点:正常态 */
                 to   { transform: translateY(-14px); } }

两条动画的关系:

  • in :0.7s ~ 1.5s播放一次,把球体从"缩小 + 下移"拉到"正常"
  • bob :1.6s起,无限循环,在"正常"和"上移14px"之间往复

为什么能无缝衔接?

因为两条动画的"交棒点"严格对齐:

时刻 in的状态 bob的状态 球体实际状态
1.5s 播放结束:translateY(0) 尚未开始 正常态
1.6s 已结束(forwards保持终态) 开始:translateY(0) 正常态
1.6s+ --- 从0 → -14px 悬浮

bob的from: translateY(0)精确等于in的to: transform: none ------这就是"交棒"。

交棒不齐会怎样?

css 复制代码
/* ❌ 交棒不齐:in结束时还在translateY(20px),bob从0起跳 */
@keyframes in  { to { transform: translateY(20px); } }
@keyframes bob { from { transform: translateY(0); } }

结果 :在1.5s → 1.6s那个瞬间,球体会**"跳"一下**------从+20px瞬间跳到0。

这一跳非常明显------观众说不清哪里不对,但会感到"不专业"。

通用心法:

两条动画交棒时,前一棒的to必须精确等于后一棒的from。

数值要一个像素一个像素地对齐 ------包括translateY、scale、opacity、rotate 全部。

另一种做法:让两条动画"分工"。

css 复制代码
/* ✅ 一条管opacity,一条管transform------永不冲突 */
.orb {
  animation:
    fade-in .8s ease .7s both,          /* 只改opacity */
    bob     5s ease-in-out 1.6s infinite alternate;  /* 只改transform */
}

"opacity归A、transform归B" ------两条动画各管各的属性 ------从根本上避免交棒冲突。

本页球体用的是"严格对齐"法 ------因为入场动画本身也要改transform(scale + translateY),只能靠"对齐"解决。

能分工就分工,不能分工就严格对齐------这是双动画的通用原则。


技法3:一条光的故事

整页只有一个光源。

这是Hero区质感的核心。

所有发光 / 反光 / 阴影,全部指向同一个方向(左上)+ 同一个色系(青绿):

① 球体高光在左上

css 复制代码
background: radial-gradient(circle at 33% 28%, #d8f7ec, #0d8574 48%, #073f37 82%);
/*                                 ↑ 33% 28% = 左上方偏内 */

高光点在33% 28% ------左上偏内 ------光从"左上"打来。

② 球体投影在右下

css 复制代码
box-shadow:
  0 0 60px 10px rgba(13,133,116,.35),              /* 外发光:同色系青绿 */
  inset -14px -18px 40px rgba(0,0,0,.45);          /* 内阴影:右下 */

外发光用青绿(同色系) ------"球在发光" 。

内阴影在右下(-14px -18px) ------"球被左上光照射"。

③ 极光带用同色系青绿

css 复制代码
background: radial-gradient(ellipse, rgba(13,133,116,.4), transparent 65%);

和球体用的是同一个色号 ------"光源来自同一处"。

④ 地板网格也是青蓝

css 复制代码
background: linear-gradient(rgba(125,211,252,.07) 1.5px, transparent 1.5px), ...

淡青蓝的网格线 ------和球体、极光、CTA都呼应。

⑤ CTA发光也是青绿

css 复制代码
.cta { border: 1px solid rgba(92,201,180,.5); background: rgba(13,133,116,.18); }
.cta:hover { box-shadow: 0 0 30px rgba(13,133,116,.35); }

边框、背景、hover光晕 ------全是青绿同色系。

"观众不会总结这些细节,但会感到'这页很贵'。"

这就是"光照纪律"的含义 ------不是"每个元素都做得好看",而是"所有元素都遵守同一个光的规则"。

对比一下"光源打架"的页面:

  • 球体光从左上,按钮光从右上
  • 极光是紫色,CTA是红色
  • 地板是冷青,标题是暖橙

结果:每个元素单独看都挺好看,合在一起**"像四个设计师做的"**。

这就是为什么"光照纪律"是Hero的质感命脉。

"Hero的质感 20%来自编排,80%来自光照纪律。"


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

① 有delay没写both

css 复制代码
/* ❌ delay期间元素以"裸奔态"(opacity 1)站在台上,开场全穿帮 */
.kicker { animation: in .5s ease .1s; }

/* ✅ both:delay期间保持"第一帧"状态 */
.kicker { animation: in .5s ease .1s both; }

animation-fill-mode: both是入场动画的必备:

  • backwards :动画开始之前 (delay期间),保持第一帧的状态
  • forwards :动画结束之后 ,保持最后一帧的状态

只写forwards ------delay期间元素显示"初始CSS状态" ------如果初始态是opacity: 1,开场所有元素都亮着 ------"裸奔"。

只写backwards ------动画结束后元素回到初始态 ------"闪回"。

写both ------两头都管 ------开场安全、结束稳妥。

这是"分镜表"动画的通用必备 ------打字机强调过forwards,今天多了backwards的场景。

② 两条动画都在动transform且交棒不齐

css 复制代码
/* ❌ 入场结束时元素在translateY(20px),悬浮动画的from是0------1.6s 那刻会"跳" */
@keyframes in  { to { transform: translateY(20px); } }
@keyframes bob { from { transform: translateY(0); } }

两条动画都在改transform ------后一条动画一旦运行,整个transform属性被接管------前一条动画的"终态"不再生效。

所以两端的值必须"逐值相等":

  • in的to:transform: none (等价于translateY(0) scale(1))
  • bob的from:translateY(0)

两者对齐 ------交棒无缝。

更稳妥的做法是"属性分工":

css 复制代码
/* ✅ 一条管opacity,一条管transform------永不冲突 */
.orb {
  animation:
    fade-in .8s ease .7s both,             /* 只改opacity */
    bob     5s ease-in-out 1.6s infinite alternate;  /* 只改transform */
}

"能分工就分工,不能分工就对齐" ------双动画的通用原则。

③ 轨道环组合变换漏写rotateX

css 复制代码
/* ❌ 环会从"倾斜轨道"瞬间弹回平面再转 */
@keyframes spinz1 {
  from { transform: rotateZ(0deg); }
  to   { transform: rotateZ(360deg); }
}

/* ✅ 每一帧都写全组合变换 */
@keyframes spinz1 {
  from { transform: rotateX(74deg) rotateZ(0deg); }
  to   { transform: rotateX(74deg) rotateZ(360deg); }
}

组合变换里,每个关键帧都要写全所有transform函数 ------不能只写"变化的那一个"。

为什么?

因为 transform每个关键帧是"整体替换" ------不是"增量叠加"。漏写rotateX ------这一帧的 transform里就没有rotateX ------环会瞬间"弹回平面" ------再转。

这是之前就强调过的"顺序 + 完整"原则------今天它换了个场景又出现一次。

④ 地板层挡点击

css 复制代码
/* ❌ fixed装饰层挡住CTA,鼠标点不到按钮 */
.floor { position: fixed; /* ... */ }

/* ✅ 装饰层记得pointer-events: none */
.floor { position: fixed; pointer-events: none; }

position: fixed的装饰层 (极光带、地板网格、暗角)------它们覆盖整个视口 ------默认会拦截所有点击。

CTA按钮明明在"上面" ------却点不到------因为装饰层在事件上"截胡"了。

所有"纯视觉装饰层"都加pointer-events: none ------"可看不可摸"。

⑤ 入场做完就没事了

css 复制代码
/* ❌ 入场2秒后,页面彻底"死"了 */
.kicker { animation: in .5s ease .1s both; }   /* 只在0.1s ~ 0.6s动一下 */
h1      { animation: in .7s ease .35s both; }  /* 只在0.35s ~ 1.05s动一下 */
/* ... 2 秒后全部静止 ... */

"入场是预告片,正片是页面常态。"

如果入场做完就静止 ------2秒后页面"死"了 ------像一张静态海报。

至少留一条低速常驻动画 ------让页面"活着"。

本页的常驻动画:

  • 球体悬浮 (bob 5s)------中速,气息感
  • 轨道环自转 (spinz1 12s / spinz2 9s)------中速,机械感
  • 极光带漂移 (drift 16s)------低速,氛围感

三条动画,三个档位,各司其职 ------这是讲过的"动画分档"心法。

注意错拍 :轨道环用12s / 9s(互质)------永不同步 ------像有生命。

"错拍才高级"


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

任务:换皮成你自己的Hero。

基础版(10分钟) ------替换三样东西:

位置 替换成
文案 你的博客slogan / 产品一句话
球体 你的头像圆形裁切 + 同款光晕
参数 你的三个数据(粉丝数 / 项目数 / 年份......)
CTA 你的行动号召(订阅 / 联系 / 查看作品)

挑战点:只准改内容与颜色token,不准动任何delay / 时长。

为什么这个限制很重要?

因为**"编排独立于内容"**------分镜表是"骨架",内容是"血肉"。

  • 换内容 :骨架不动,血肉换新------省时省力
  • 改骨架 :所有元素的时间要重新对齐------牵一发动全身

如果你能"不动delay就换出一个新Hero" ------说明你真正掌握了"编排"这件事。

进阶版(20--30分钟):

① 换色系

保留分镜表,把青绿换成你喜欢的色系:

  • 琥珀金 (#e8a95c / #b06a2a)------暖调,像烛光
  • 赛博紫 (#b9a7ff / #7a3bd0)------冷调,像霓虹
  • 珊瑚红 (#ff8a8a / #c94a4a)------活力,像海报

改色要"成套" ------球体高光、外发光、极光、网格、CTA一起换 ------不能只换一个。

② 加"hover反应"

CTA的hover效果可以升级:

css 复制代码
.cta:hover {
  background: rgba(13,133,116,.34);
  box-shadow: 0 0 30px rgba(13,133,116,.35);
  transform: translateY(-2px) scale(1.02);   /* 微微浮起 + 放大 */
}

"浮起 + 放大"是按钮的通用交互语言 ------和磁吸导航一个思路。

③ 加"滚动后的状态"

复习滚动驱动 ------让Hero在滚动时淡出:

css 复制代码
main {
  animation: fade-out linear both;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}
@keyframes fade-out { to { opacity: 0; transform: scale(.95); } }

滚动离开首屏时,Hero整体淡出 + 微微缩小 ------为下方内容让位。

④ 加"环境声"

省略------这会破坏"零JS"人设。


六、小结

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

技能 适用场景
先画分镜表,再写代码 所有"多元素入场"场景
相邻分镜间隔0.15 ~ 0.35s 所有"编排节奏"的参考值
both保证delay 期间不"裸奔" 所有带delay的入场动画
双动画"交棒对齐"或"属性分工" 所有"入场 → 常驻"的动画
光照纪律(一条光的故事) 所有Hero区 / 品牌页
入场后留常驻动画 所有"演示页 / 落地页"

最值得记住的三条:

① 先画分镜表,再写第一行CSS。

分镜表是"设计稿" ------delay、时长、缓动、之后的状态 ------全部先想清楚。

然后写代码就是"翻译" ------一行不多、一行不少。

"想到哪写到哪" ------必然导致时间点混乱 ------这是编排型动画的头号反面教材。

② 双动画交棒:to必须等于下一棒的from。

"入场 → 常驻" ------所有Hero区都会遇到的两段式动画。

能分工就分工 ------opacity归A、transform归B。

不能分工就严格对齐 ------逐值逐像素核对。

③ "Hero的质感20% 来自编排,80%来自光照纪律。"

观众不会总结这些细节,但会感到"这页很贵"。

"这页很贵"的感觉,不来自"每个元素都做得好看" ------来自"所有元素遵守同一个光的规则"。

一条光的故事 ------这是Hero设计的核心审美纪律。


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

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

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

目标更新:不断更。

相关推荐
neoooo1 小时前
🚀Claude Code 小技巧(一):一键进入危险模式
前端·人工智能·后端
bmxy小明同学3 小时前
2026-10-11-幻觉治理
服务器·前端·javascript
Coder_张三3 小时前
iOS 26 适配
前端·ios·swift
m0_587383003 小时前
无人自助拼豆系统落地开发实战指南 核心逻辑实现与部署经验分享
前端·javascript·vue.js·人工智能·经验分享·需求分析
zhaiyy3 小时前
HTML Popover API 实践
前端·javascript·html
牧艺4 小时前
SPA 也能像原生 App:View Transitions API 从原理到可上线实战
前端·javascript·css
Dovis(誓平步青云)4 小时前
现场问题先关闭还是先验收,建筑任务怎样处理依赖
前端·javascript·数据库·mysql·音视频
用户84298142418104 小时前
压缩 ≠ 加密:前端代码保护的三层实践与踩坑记录
前端·javascript
前端一课4 小时前
别把人生过成一场等待退休的倒计时
前端