【每天一个CSS | Day18】会摆动的拟物怀表:黄铜、皮革与催眠摆

写在前面

昨天我们做了烟花------爆发、散开、熄灭,是"事件型"的动画。

今天换一种完全不同的气质:拟物(Skeuomorphism)。

在扁平化设计大行其道的今天,"拟物"听起来有点复古。但它恰恰是训练 CSS 功底的最佳题目------因为拟物有一个极其残酷的评判标准:

观众看一眼就知道"像不像"。

不像极光可以模糊、不像像素可以简化------怀表要是一眼假,就是假。

今天做一只黄铜怀表:

  • 拉丝金属表壳------conic-gradient 转出高光
  • 奶白表盘------罗马数字 + 双层刻度
  • 黄铜指针 + 红色秒针------秒针每秒跳一格
  • 玻璃高光------一条斜向白色渐变压在表盘上
  • 整只表轻轻摆动------像挂在一只看不见的钩子上

拟物不是堆细节,是三件套的纪律。


一、先看效果

暗棕色皮革质感的空间里,一只黄铜怀表被吊在看不见的钩子上,缓缓左右摆动:

  • 表壳:拉丝金属质感,随角度变换高光
  • 表盘:奶白色,四角是12 / 3 / 6 / 9四个罗马数字,外圈一圈细密刻度
  • 指针 :黄铜时针和分针指向 7点45分,红色秒针每秒跳一格
  • 玻璃高光:从左上到右下,一条斜向白色渐变压在表盘上
  • 表壳底部:一圈厚度阴影------表盘"有厚度"
  • 地板:拖着一团模糊的影子

摆动周期4.5s,摆幅±2.4°------像真的怀表被风轻轻吹动。

怎么跑起来:

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

📸 截图党提示 :录5秒GIF 抓"摆动 + 秒针跳格"最生动。静态截图选表身向左倾斜的时刻------那个角度的高光最亮、拉丝质感最明显。


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

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 18 · 会摆动的拟物怀表</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { height:100%; }
  body { display:flex; flex-direction:column; align-items:center; justify-content:center;
    overflow:hidden; background:radial-gradient(ellipse at 50% 28%, #2a2119, #17110c 78%);
    font-family:"SF Mono", Consolas, monospace; }

  .wrap { display:flex; flex-direction:column; align-items:center; }

  /* ------ 吊挂摆动:支点在体外(transform-origin 的 y 是负值)------ */
  .swing { transform-origin:50% -34px; animation: swing 4.5s ease-in-out infinite alternate; }
  @keyframes swing { from { transform:rotate(-2.4deg); } to { transform:rotate(2.4deg); } }

  .ring { width:30px; height:36px; margin:0 auto -8px; border-radius:50%;
    border:8px solid; border-color:#a8853f #6e5427 #a8853f #6e5427; }
  .crown { width:22px; height:20px; margin:0 auto -6px; position:relative; z-index:2;
    background:linear-gradient(90deg, #6e5427, #e8cf9a 45%, #6e5427);
    border-radius:6px 6px 3px 3px; box-shadow:0 3px 6px rgba(0,0,0,.4); }

  /* ------ 黄铜表壳:conic 拉丝 + 双向 inset ------ */
  .case { width:250px; height:250px; border-radius:50%; padding:13px;
    background:conic-gradient(from 210deg, #6e5427, #e8cf9a 12%, #8a6d3b 26%, #caa96a 42%,
      #6e5427 55%, #e8cf9a 68%, #8a6d3b 80%, #caa96a 92%, #6e5427);
    box-shadow:0 26px 46px rgba(0,0,0,.55),
      inset 0 2px 6px rgba(255,236,190,.55), inset 0 -4px 10px rgba(0,0,0,.5); }

  .dial { position:relative; width:100%; height:100%; border-radius:50%;
    background:radial-gradient(circle at 50% 36%, #f8f0dd, #ead8b6 68%, #d3c09a);
    box-shadow:inset 0 5px 16px rgba(90,70,35,.4), inset 0 -3px 8px rgba(255,255,255,.55); }
  .dial::before { content:""; position:absolute; inset:12px; border-radius:50%;
    background:
      repeating-conic-gradient(from -1.4deg, rgba(96,74,38,.55) 0deg 2.6deg, transparent 2.6deg 90deg),
      repeating-conic-gradient(from -0.5deg, rgba(96,74,38,.3) 0deg 1deg, transparent 1deg 30deg);
    -webkit-mask:radial-gradient(closest-side, transparent 84%, #000 85%, #000 96%, transparent 96%);
            mask:radial-gradient(closest-side, transparent 84%, #000 85%, #000 96%, transparent 96%); }
  .dial::after { content:""; position:absolute; inset:0; border-radius:50%;
    background:linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
    pointer-events:none; }

  .n { position:absolute; font-family:Georgia, "Times New Roman", serif;
    font-size:20px; color:#6b5427; }
  .n12 { top:13px; left:50%; transform:translateX(-50%); }
  .n3  { right:17px; top:50%; transform:translateY(-50%); }
  .n6  { bottom:11px; left:50%; transform:translateX(-50%); }
  .n9  { left:17px; top:50%; transform:translateY(-50%); }
  .brand { position:absolute; top:50px; width:100%; text-align:center;
    font-size:8px; letter-spacing:.34em; color:#96784a; }

  /* ------ 指针:金属渐变 + 负 delay 对时到 7:45:18 ------ */
  .hand { position:absolute; left:50%; bottom:50%; transform-origin:50% 100%;
    border-radius:5px; animation:rot linear infinite; }
  .hour { width:8px; height:50px; margin-left:-4px;
    background:linear-gradient(90deg, #8a6d3b, #4a3717 60%, #2e2210);
    box-shadow:1px 2px 4px rgba(60,40,10,.35);
    animation-duration:43200s; animation-delay:-27900s; }
  .min  { width:5px; height:74px; margin-left:-2.5px;
    background:linear-gradient(90deg, #a8853f, #4a3717 60%, #2e2210);
    box-shadow:1px 2px 4px rgba(60,40,10,.35);
    animation-duration:3600s; animation-delay:-2700s; }
  .sec  { width:2px; height:86px; margin-left:-1px; background:#b3402e;
    animation-duration:60s; animation-delay:-18s; animation-timing-function:steps(60); }
  .sec::after { content:""; position:absolute; bottom:-11px; left:50%; width:8px; height:11px;
    margin-left:-4px; background:#b3402e; border-radius:4px; }
  .cap { position:absolute; left:50%; bottom:50%; width:15px; height:15px; margin:-7.5px;
    border-radius:50%; background:radial-gradient(circle at 35% 30%, #f0d9a0, #8a6d3b 70%);
    box-shadow:0 1px 3px rgba(0,0,0,.4); }
  @keyframes rot { to { transform:rotate(360deg); } }

  .floor { width:210px; height:18px; margin-top:36px; border-radius:50%;
    background:radial-gradient(ellipse, rgba(0,0,0,.5), transparent 70%); }

  .tag { position:fixed; right:18px; bottom:14px; font-size:10px;
    letter-spacing:.28em; color:rgba(240,217,160,.3); }
</style>
</head>
<body>
  <div class="wrap">
    <div class="swing">
      <div class="ring"></div>
      <div class="crown"></div>
      <div class="case">
        <div class="dial">
          <span class="n n12">12</span><span class="n n3">3</span>
          <span class="n n6">6</span><span class="n n9">9</span>
          <span class="brand">CHRONO · 18</span>
          <div class="hand hour"></div>
          <div class="hand min"></div>
          <div class="hand sec"></div>
          <div class="cap"></div>
        </div>
      </div>
    </div>
    <div class="floor"></div>
  </div>
  <div class="tag">每天一个惊艳 CSS · DAY 18</div>
</body>
</html>

三、三个核心技法拆解

代码看着长,其实核心只有三个点------每一个拆开都能用在别的地方。

技法1:conic-gradient拉「拉丝金属」

金属感的来源,你想一下------

不是"颜色多",而是------

高光角度连续变化。

真实的金属表面反射环境光:光从不同角度打过来,亮度一路渐变 。所以金属的关键不是"用金色",而是把高光做出"角度连续性"。

css 复制代码
.case {
  background: conic-gradient(from 210deg,
    #6e5427,           /* 暗铜 */
    #e8cf9a 12%,       /* 亮金 ------ 高光 */
    #8a6d3b 26%,       /* 中间棕 */
    #caa96a 42%,       /* 中间金 */
    #6e5427 55%,       /* 暗铜 ------ 阴影 */
    #e8cf9a 68%,       /* 亮金 ------ 又一圈高光 */
    #8a6d3b 80%,       /* ... */
    #caa96a 92%,
    #6e5427);
}

这就是"拉丝金属"的核心。

conic-gradient从210°起手 ------在四档色之间反复过渡。

  • #6e5427:暗铜(阴影面)
  • #8a6d3b:中间棕
  • #caa96a:中间金
  • #e8cf9a:亮金(高光面)

四档色循环两轮 ------就像金属表面反射出一圈环境光。

为什么"循环两轮"?

只循环一轮:金属只有"一明一暗"------太干净、太假。

循环两轮 :金属有两组高光 + 两组阴影------更像真的黄铜表面(有凹凸、有反射)。

"从210°起手"是什么意思?

from 210deg让渐变的起点 在210°方向(左上角)------和"光源在左上"的设定一致。

**这还不是全部。**还要加两条inset阴影------给壳体厚度:

css 复制代码
box-shadow:
  0 26px 46px rgba(0,0,0,.55),                    /* 外投影:表壳悬空的落影 */
  inset 0 2px 6px rgba(255,236,190,.55),          /* 内高光:上边缘亮 */
  inset 0 -4px 10px rgba(0,0,0,.5);               /* 内阴影:下边缘暗 */

逐条读:

阴影 作用
0 26px 46px ... 表壳悬浮的落影------底部深、远处淡
inset 0 2px 6px ... 内嵌高光------上边缘亮
inset 0 -4px 10px ... 内嵌阴影------下边缘暗

这三条阴影给"厚度"。

"上亮下暗"是拟物最重要的光向------光源在左上,所以:

  • 上边缘:受光,亮
  • 下边缘:背光,暗

材质立刻从"色块"变成"物件"。


技法2:催眠摆的支点在体外

摆动动画本身极其简单:

css 复制代码
@keyframes swing { from { transform:rotate(-2.4deg); } to { transform:rotate(2.4deg); } }
.swing { animation: swing 4.5s ease-in-out infinite alternate; }

但真正让"摆动像真的"的,是transform-origin:

css 复制代码
.swing { transform-origin: 50% -34px; }

transform-origin: 50% -34px是什么?

  • 50%:水平居中
  • -34px :垂直方向在元素上方34px处 ------一个在元素外部的点

这个点在DOM里不存在 ------但浏览器以它为支点旋转元素。

想象一下真实的怀表吊挂:

  • 表挂在钩子上
  • 表会在钩子下方绕钩子摆动
  • 表本身不会"原地左右扭"

所以支点必须在"表体外部的上方" ------就是表链悬挂的那个点。

如果transform-origin写在元素中心会怎样?

css 复制代码
/* ❌ 默认值center center:表原地左右扭,不像被吊着 */
.swing { transform-origin: center; }

结果 :整块表原地左右扭------像被一根钉子钉在墙上还在摇。

这是"吊挂"和"摇头"的本质区别。

摆幅压到±2.4°、周期4.5s------这是"悬挂物"而非"钟摆"的可信范围:

  • ±2.4°:小幅度轻晃,像微风吹动
  • 4.5s一个来回:慢节奏,符合真实吊挂物的"催眠感"
  • ease-in-out:两头慢中间快,像物理摆动的自然加减速
  • alternate:往复循环,不用写两个关键帧

这是一条通用心法:

所有"吊挂 / 飘动 / 摆动"的动画,transform-origin必须放在身体外部的"支点"处。

它适用于:吊灯、挂钟、铃铛、风吹的树叶、天平的托盘。


技法3:拟物三件套纪律

拟物最容易翻车的地方------不是"细节不够",而是"光源打架"。

本页所有拟物元素,共用同一套"光":

① 双向阴影------左上亮、右下暗

css 复制代码
/* 表壳 */
box-shadow: 0 26px 46px rgba(0,0,0,.55),          /* 外投影 */
            inset 0 2px 6px rgba(255,236,190,.55), /* 上亮 */
            inset 0 -4px 10px rgba(0,0,0,.5);      /* 下暗 */

/* 表盘 */
box-shadow: inset 0 5px 16px rgba(90,70,35,.4),   /* 上暗(凹陷) */
            inset 0 -3px 8px rgba(255,255,255,.55); /* 下亮(凹陷反光) */

/* 表冠 */
box-shadow: 0 3px 6px rgba(0,0,0,.4);             /* 下方投影 */

/* 秒针配重 */
/* 指针 */
box-shadow: 1px 2px 4px rgba(60,40,10,.35);       /* 右下投影 */

所有阴影都是"左上亮、右下暗" ------光源唯一。

② 左上135°定向高光

css 复制代码
/* 表壳高光:conic-gradient from 210deg,起点在左上 */
/* 表盘玻璃高光:135deg斜向渐变 */
.dial::after {
  background: linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
}

/* 表冠圆柱高光:90deg横向渐变,中间亮 */
.crown {
  background: linear-gradient(90deg, #6e5427, #e8cf9a 45%, #6e5427);
}

/* 指针金属高光:左侧1/3处亮 */
.hour {
  background: linear-gradient(90deg, #8a6d3b, #4a3717 60%, #2e2210);
}

所有高光都从"左上"来 ------或横向、或斜向、或放射 ,但方向一致。

③ 材质色不超过三档

css 复制代码
/* ✅ 只有四档色:深铜、中棕、中金、亮金 */
/* 所有金属元素共用这四档色 */

为什么是三档(本例四档)?

因为金属反光的色阶是有限的 ------暗、中、亮三档就够。

如果贪心加更多中间色(比如再加五种不同的棕色)------画面立刻"出土文物"。

拟物翻车的第二大原因:色阶贪多。

"光源唯一,材质才统一"------拟物翻车九成是光源打架。

如果你发现自己做的拟物"看起来不像"------第一件事不是加细节,而是检查"是不是所有阴影、高光都来自同一个光源"。


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

① 想用border-image: conic-gradient(...)做金属边框

css 复制代码
/* ❌ border-image与border-radius不同圆,圆角处直接断裂 */
.case {
  border: 13px solid;
  border-image: conic-gradient(...) 1;
  border-radius: 50%;
}

border-image和border-radius是"不兼容"的 ------border-image会被切成四段方形的边框图案,遇到圆角时会断裂。

正确做法:嵌套法。

html 复制代码
<div class="case">           <!-- 外层conic圆(金属壳) -->
  <div class="dial">...</div> <!-- 内层表盘 -->
</div>
css 复制代码
/* ✅ 外层conic圆 + padding + 内层dial */
.case {
  width: 250px; height: 250px; border-radius: 50%;
  padding: 13px;                          /* 让出13px的"边框" */
  background: conic-gradient(...);         /* 金属壳 */
}
.dial {
  width: 100%; height: 100%; border-radius: 50%;
  background: radial-gradient(...);        /* 奶白表盘 */
}

外层圆和内层圆各自是独立的圆 ------圆角不会断裂。

"圆环金属壳 = 外层conic圆 + padding + 内层dial"------这是所有"复杂圆环"的通用做法。

② 摆动的origin写在元素中心

css 复制代码
/* ❌ 默认center center:整块表原地左右扭,不像被吊着 */
.swing { transform-origin: center; }

/* ✅ 支点悬在体外:50% -34px */
.swing { transform-origin: 50% -34px; }

这是"吊挂"和"摇头"的分界线。

判断法 :想象有一个钩子 ------你的transform-origin应该正好在钩子的位置 。如果钩子在表体外部,y就是负值。

摆幅小一点(±2.4°),周期长一点(4.5s)------"轻轻摆动"比"大幅摇摆"更像真的吊挂。

③ 拟物色超过三档

css 复制代码
/* ❌ 金银铜再加一堆中间色,画面立刻"出土文物" */
background: linear-gradient(90deg,
  #4a3717, #6e5427, #8a6d3b, #a8853f, #caa96a, #e8cf9a, #f0d9a0, #fff0c0);

拟物最容易"贪多"------总觉得多一个色阶就更真实。

实际上相反:

  • 色阶少(3~4档):干净、有质感
  • 色阶多(7档以上):糊成一团、显得"脏"

锁定"一深、一浅、一亮"三档 (深铜、中金、亮金),其余全靠透明度和白黑云雾。

④ 秒针没配重

css 复制代码
/* ❌ 细长秒针绕针尖转,视觉重心不稳 */
.sec { width: 2px; height: 86px; background: #b3402e; }

/* ✅ 针尾加一小段同色矩形当配重 */
.sec::after {
  content:""; position: absolute; bottom: -11px; left: 50%; width: 8px; height: 11px;
  margin-left: -4px; background: #b3402e; border-radius: 4px;
}

真实的机械表,秒针有两段:

  • 长段:从中心指到表盘外圈
  • 短段 :从中心往后伸出一小截,当配重

少了这一小截 ------秒针看起来"头重脚轻",像要飞出去。

加这一小截 ------机械表的味道就对了。

这条细节不只适用于秒针 ------所有"指针 / 指针状图形 / 风向标 / 指南针",尾部都该加配重。

⑤ 玻璃高光盖过指针

css 复制代码
/* ❌ 高光层放在指针前面,指针被盖住看不清 */
.dial::before { /* 高光 */ }

/* ✅ 高光层放最后(DOM 顺序在指针之后),透明度压在0.4以下 */
.dial::after {
  background: linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,0) 42%);
  /*                                          ↑ 42%透明度,别再高了 */
  pointer-events: none;
}

"玻璃存在感要有,指针必须看得清。"

顺序 :指针在DOM里放前面,玻璃高光在指针后面 (::after)------玻璃"压"在指针上,但不遮住。

透明度控制在0.4以下 ------高光太强,指针就"雾化"了。

这是"拟物"的通用心法 ------所有"装饰层压在主图层上"的场景,透明度都是控制在0.3 ~ 0.4之间。


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

任务:改成倒计时怀表。

核心改动:

css 复制代码
/* 秒针动画周期从60s改600s(10分钟一圈) */
.sec { animation-duration: 600s; }

/* delay调到表盘读数就是你想要的倒计时 */
.sec { animation-delay: -XXXs; }   /* XXX = 已流逝秒数 */

"读数即倒计时"的心算方法:

  • 负delay = 已流逝时间
  • 想让它显示"还剩3分钟" :让指针指向"3分钟"位置------delay = -(10 - 3)× 60 = -420s
  • 想让倒计时从"还剩5分钟"开始:delay = -300s

再加上一条conic进度环:

css 复制代码
.case::before {
  content:""; position: absolute; inset: -8px;    /* 外扩8px,套在表壳外 */
  border-radius: 50%;
  background: conic-gradient(#b3402e 0 30%, rgba(255,255,255,.08) 30% 100%);
  mask: radial-gradient(closest-side, transparent 92%, #000 93%);
  animation: spin 600s linear infinite;           /* 和秒针同步 */
}

效果 :表壳外围有一圈红色的"进度",随着时间推移慢慢填满。

难点:

  • "读数即倒计时"的心算------delay负值对应"已流逝",想清楚"还剩多少 → 已流逝多少"
  • 进度环和秒针同步------两者动画时长必须一致(都是600s),否则会不同步
  • 颜色随进度变化------用两层conic叠出来(红色表示"已过去",深灰表示"还剩")

其他变式方向(选做):

① 加一条秒针的"走路声"

省略------这需要JS或音频文件,会破坏"零JS"人设。

② 让表盘有"指针移动时的阴影"

给指针加一个"双层阴影"------指针转到某些角度时,阴影方向跟着变。这是拟物的高级玩法,但CSS做不到"方向随角度变"------需要JS实时计算。

③ 换怀表为正计时

秒针、分针、时针从00:00:00开始------去掉负delay即可。

④ 加"表盘反光"的动态效果

让玻璃高光的透明度缓慢呼吸------模拟光线在玻璃上的微微变化。


六、小结

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

技能 适用场景
conic-gradient做"拉丝金属" 所有金属质感(表壳、按钮、边框、奖章)
transform-origin放在体外做吊挂 所有"吊挂 / 摆动 / 飘动"动画
拟物三件套纪律(光、向、色) 所有拟物设计
inset阴影做厚度 所有"有厚度的圆环 / 按钮 / 卡片"
嵌套法做复杂圆环 所有"外圈 + 内圈"的圆环结构

最值得记住的三条:

① 金属感不来自"颜色多",来自"高光角度连续变化"。

conic-gradient是最好的工具------它让高光沿着"角度"连续变化,而不是"沿着直线渐变"。

四档色循环两轮------就是"拉丝金属"的全部秘密。

② 吊挂物的支点,必须在身体外部。

transform-origin: 50% -34px------这个负值,就是"看不见的钩子"的位置。

"吊挂"和"原地摇头"的区别,就在这一个数字。

③ 拟物翻车九成是"光源打架"。

光源唯一------所有阴影都来自同一个方向。

光向一致------所有高光都从"左上"来。

色阶少而精------三档就够,多了就"出土文物"。

这三条纪律,是拟物和"堆细节"的分界线。


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

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

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

目标更新:100 天,不断更。

相关推荐
Lvan的前端笔记3 小时前
docker:基于docker的前端部署方案
前端·docker·状态模式
程序人生8883 小时前
从“把隐私文件上传到别人云端“到本地可控:DocConverter Web 立项初心与架构选型
前端·图像处理·人工智能·opencv·架构·ocr·文心一言
开开心心就好3 小时前
视频里的图片怎么提取?双击一下就导出
java·前端·人工智能·spring·智能手机·intellij-idea·excel
爱喝水的小周3 小时前
实验十 Vue‑Router 王者荣耀战绩查询 APP 开发实践
前端·javascript·vue.js
yexianglunbai3 小时前
Vue 3 全面详解:从核心特性到工程化实践
前端·javascript·vue.js
传人once3 小时前
选择文件按钮样式如何美化
前端·html·css3·jquery·html5
parade岁月4 小时前
告别 Deno 时代?运行时进入一年倒计时,Deno 团队并入 Cloudflare
前端
Lucky_Turtle4 小时前
【HTML】前端页面禁止缩放
前端·html
恋猫de小郭4 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter