用CSS画一个冰墩墩

1,785 阅读8分钟

这篇文章来自孟智强同学的投稿,他介绍了如何用CSS画一个冰墩墩,展示了很多CSS知识和动画效果如transform的巧妙使用

成品展示

冰墩墩本墩本案冰墩墩
1_7fe449cac885b6d818f2219726a20986_378x379.png@900-0-90-f.png2_e2cf8a367973444ed9e76492125c3b61_310x327.png@900-0-90-f.png

不足之处:

  • 整体感觉不够“墩”
  • “冰晶外壳”未实现
  • “冰丝带”不理想

HTML 结构

	<div class="bdd">
	  <div class="bdd-ears"></div>  <!-- 耳朵 -->
	  <div class="bdd-arm left"></div> <!-- 左臂 -->
	  <div class="bdd-arm right"> <!-- 右臂(爱心掌印) -->
	    <span class="heart"></span>
	  </div>
	  <div class="bdd-leg left"></div> <!-- 左腿 -->
	  <div class="bdd-leg right"></div> <!-- 右腿 -->
	  <div class="bdd-body"> <!-- 身体 -->
	    <div class="bdd-face"> <!-- 面部(冰丝带) -->
	      <div class="bdd-eye left"> <!-- 左眼 -->
	        <span class="bdd-eye-inner"></span>
	      </div>
	      <div class="bdd-eye right"> <!-- 右眼 -->
	        <span class="bdd-eye-inner"></span>
	      </div>
	      <div class="bdd-oronasal">
	        <span class="bdd-norse"></span> <!-- 鼻子 -->
	        <span class="bdd-mouth"></span> <!-- 嘴巴 -->
	      </div>
	    </div>
	    <div class="bdd-logo"></div> <!-- 会旗logo -->
	  </div>
	</div>

要点:

  1. 尽量精简 HTML 结构,减少不必要的标签
  2. 利用标签的前后顺序来控制堆叠层级

用到的主要CSS技术

  • 相对单位 em%
  • 圆角 border-radius
  • 渐变 radial-gradientlinear-gradient
  • 轮廓线 outlineoutline-offset
  • 盒阴影 box-shadow
  • 投影滤镜 drop-shadow
  • 变换 transform
  • 堆叠层级
  • 背景图相关
  • 伪元素

实现细节

身体

冰墩墩的身体是一个椭圆形,自然而然会想到用 border-radius 来实现。这货最常见的使用姿势形如 border-radius: 10%; ,10% 是一个简写形式,实际上它表示的是 border-radius: 10% 10% 10% 10%;,分别为图形的左上、右上、右下和左下的圆角半径。

冰墩墩的身体呈左右对称、上下不对称的椭圆,这种非常规的椭圆是无法仅用4个值的 border-radius 做出来的,事实上 border-radius 还有一种8个值的完全体写法:

	border-radius: 10% 10% 10% 10% / 10% 10% 10% 10%;

上述写法可以分别控制每一个圆角在水平方向和垂直方向上的半径,其值以斜杠为界,左侧的4个值表示水平方向4个圆角(左上、右上、右下、左下)的半径,右侧的4个值则表示垂直方向4个圆角的半径。

3_c6ed606eff44ee25c1201fee40d756e1_625x708.gif@900-0-90-f.gif

所以利用完全体形式的 border-radius 就可以轻松调教出冰墩墩的身体形状:

4_ef8e82956b120ff3de8fea50186edea6_278x303.png@900-0-90-f.png

	.bdd-body {
	  position: relative;
	  width: 24em;  /* 宽高比:3:4 */
	  height: 32em;
	  background: radial-gradient(#fff 46%, #c2cacd); /* 径向渐变 */
	  border-radius: 82% 82% 75% 75% / 72% 72% 88% 88%;
	}

耳朵

外观上看,冰墩墩的耳朵是一个半圆形,而且内凹贴合在弧形的身体上,想要用 CSS 实现这样的形状想想都头疼。

6_ca7f3bf6b8785827588795e6bce1dc3c_282x300.png@900-0-90-f.png

如果我们转变思路,把耳朵做成圆形,再改变冰墩墩身体与耳朵的层叠顺序,让身体的形状挡住一部分耳朵形状,问题将迎刃而解:

7_5ec8fbff3feeabe520b43dbde5a36aa9_269x239.gif@900-0-90-f.gif

	<div class="bdd">
	  <div class="bdd-ears"></div>  <!-- 耳朵 -->
	  <div class="bdd-body">...</div> <!-- 身体 -->
	</div>
	.bdd-ears {
	  position: absolute;
	  top: 1em;
	  left: 4em;
	  width: 3em;
	  height: 3em;
	  background: currentColor;
	  border-radius: 50%;
	  outline: .1em solid;  /* 轮廓描边 */
	  outline-offset: .5em;  /* 轮廓与主体的间距 */
	}

注:outline 在以前版本的浏览器上有个 bug,表现为忽略主体元素的圆角,只渲染为矩形。

接下来我们就可以复制一份“耳朵”的 HTML 标签,然后再控制 right 定位,就能做出右耳朵。这里我们有更好的方案——使用投影滤镜——从而无需额外的 HTML 标签和 CSS 代码就能生成右耳朵。

投影滤镜顾名思义就是输出元素的投影效果,可以想象为光源照射物体产生的投影。来看下投影滤镜的语法:

filter: drop-shadow(x-offset y-offset blur spread color);

x-offset 和 y-offset 用来控制投影的位置;blur 控制阴影的模糊半径;spread 控制阴影的扩展半径,即阴影的扩大或缩小;color 控制阴影的颜色。

在本案中,我们需要将左耳的投影(右耳)按水平方向平移到“身体”右侧,距右 4em 与左耳对称,那如何计算水平方向偏移即 x-offset 的值呢?画个图来分析下:

8_5723714af612464ea19ee88939cc5bc3_386x191.png@900-0-90-f.png

其中 24em 是“身体”的宽度,不难看出投影的 x-offset 值为 24 - 4 - 4 - 3,我们直接用 CSS 中的 calc 来写,无需人工计算,代码如下:

	.bdd-ears {
	  ...
	  filter: drop-shadow(calc(24em - 4em * 2 - 3em) 0 0);
	}

四肢

“四肢”也用到了堆叠层级原理,利用“身体”来遮挡四肢形状的一部分,从而让“四肢”在视觉上与“身体”连成整体。

9_75ee9940a4daabf5a50899b6a049c8c7_283x310.png@900-0-90-f.png

“四肢”的做法比较简单,具体就不展开讨论了,只说下思路:

  1. 用矩形做出主体(胳膊、腿)
  2. 用 border-radius 控制末端的圆角
  3. 用 transform 变换位置和角度
  4. 用伪元素修饰细节(手、脚趾)
  5. 通过叠加组合出整体(相同颜色的不同形状叠加成整体)

右侧手掌的“爱心”是怎么做的呢?

  1. 用伪元素做两个顶部为圆角的矩形
  2. 左边的逆时针旋转,一般为45度 transform: rotate(-45deg)
  3. 右边的顺时针旋转
  4. 调整距离,使两个图形叠加成心型

10_cd3de9e47c9a8be16d56cb30b90872c7_350x280.gif@900-0-90-f.gif

面部(冰丝带)

“面部”是一个栗子形状的图形,有多条不同颜色的边框(冰丝带)。

11_6eda3e8767fa5a22d15846b57f15a8e8_283x297.png@900-0-90-f.png

利用前面 border-radius 的知识,我们可以轻松做出栗子形状的面部,但“冰丝带”怎么做呢?我们的第一直觉可能是用多层 HTML 标签嵌套,再为每个标签设置边框样式,但这样做有2个缺点:

  1. HTML 结构臃肿,包含许多无意义的标签

  2. 需要为每层标签设置圆角样式,CSS 代码非常冗余

更好的方案是利用盒阴影(box-shadow)来实现,盒阴影支持多值,通过批量叠加盒阴影,我们就可以用1层 HTML 标签做出多重边框的效果。

	.bdd-face {
	  position: absolute;
	  top: 2.5em;
	  left: .6em;
	  right: .6em;
	  height: 60%;
	  background: #fff;
	  border-radius: 60% 60% 55% 55% / 72% 72% 50% 50%;  /* 栗子形状 */
	 
	  /* 冰丝带(从外到里) */
	  box-shadow: inset 0 0 0 .2em #86dc94,
	              inset 0 0 0 .4em #fff,
	              inset 0 0 0 .6em #8a9ab7,
	              inset 0 0 0 .8em #fff,
	              inset 0 0 0 1em #f8fd47,
	              inset 0 0 0 1.3em #f42cf4,
	              inset 0 0 0 1.4em #fff,
	              inset 0 0 0 1.7em #82e970,
	              inset 0 0 0 2em #6e88df,
	              inset 0 -1em 2em 2em rgba(0, 0, 0, .25);  /* 面部下方阴影 */
	}

box-shadow 是一个非常有用的 CSS 属性,更多有趣的玩法请参阅 张鑫旭:CSS3 box-shadow盒阴影图形生成技术

眼睛

眼睛是心灵的窗户,“眼睛”实现效果的好与坏直接关系到冰墩墩整体形象生动与否。

12_adf11e35977611b5483efec9d4b0bfe3_277x308.png@900-0-90-f.png

眼睛的实现细节还是蛮多的,前面讲过的知识几乎都出场了。HTML 结构上,还是一如既往地保持精简:

	<div class="bdd-eye right">
	  <span class="bdd-eye-inner"></span>
	</div>

然后善加利用伪元素来装饰细节:

	.bdd-eye {
	  position: absolute;
	  top: 4em;
	}
	.bdd-eye::before {
	  content: '';
	  position: absolute;
	  width: 5em;
	  height: 8em;
	  background: currentColor;
	  border-radius: 50%;
	}
	 
	.bdd-eye.left {left: 4em;}
	.bdd-eye.left::before {
	  transform: rotate(45deg);
	}
	.bdd-eye.right {right: 4em;}
	.bdd-eye.right::before {
	  margin-left: -5em;  /* 自身宽度 */
	  transform: rotate(-45deg);
	}
	 
	.bdd-eye-inner {
	  position: absolute;
	  top: 3em;
	  width: 1.3em;
	  height: 1.5em;
	  background: linear-gradient(#232520, #06070c);
	  border-radius: 50%;
	  box-shadow: 0 0 .35em .35em #949176,
	              0 0 0 .6em,
	              0 0 0 .8em #fff;
	}
	.bdd-eye-inner::before {
	  content: '';
	  position: absolute;
	  left: 0;
	  width: 70%;
	  height: 40%;
	  transform: translate(25%, -80%);
	  background: #fff;
	  border-radius: 50%;
	}
	.bdd-eye-inner::after {
	  content: '';
	  position: absolute;
	  left: 0;
	  bottom: 0;
	  width: .7em;
	  height: .7em;
	  transform: translate(-25%, 20%);
	  background: #00f6ff;
	  border-radius: 50%;
	  opacity: .35;
	}
	.left > .bdd-eye-inner {left: 2.5em;}
	.right > .bdd-eye-inner {right: 2.5em;}

13_fe538221f219a815f1e6730d65192429_748x329.png@900-0-90-f.png

这里可能有小伙伴要问:“黑眼圈”的黑色椭圆为什么要用伪元素来生成?直接用 div.bdd-eye 标签的背景色不就可以了吗?

的确可以,不过 div.bdd-eye 标签作为父级,它的 transform 旋转效果会影响到所有后代元素,使后代元素的平面参考点发生偏移,后代元素在定位、变换时将变得非常麻烦。所以这里采用脱离文档流的伪元素来实现,这样就不会影响到其他元素。

口鼻

“嘴巴”和“鼻子”的实现用前面的知识就可以做出来了,这里不再详细展开,只说下思路。

14_63bf7b7c0e2347b425742ed8f064ea33_283x304.png@900-0-90-f.png

鼻子:

  1. 用 border-radius 调整出合适形状(倒栗子形 ~ 误)
  2. 用伪元素生成高亮椭圆,修饰高光
  3. 用盒阴影生成下方阴影,修饰立体效果
	.bdd-norse {
	  width: 2.4em;
	  height: 1.6em;
	  background: currentColor;
	  border-radius: 42% 42% 60% 60% / 50% 50% 70% 70%;
	  box-shadow: 0 .3em .2em rgba(0, 0, 0, .3);
	}
	.bdd-norse::before {
	  content: '';
	  display: block;
	  width: 60%;
	  height: 45%;
	  margin: .15em auto;
	  border-radius: 50%;
	  background: rgba(255, 255, 255, .5);
	}

嘴巴:

  1. 生成一个只有下边框的椭圆(嘴巴弧度)
  2. 用投影滤镜加阴影,修饰细节
	.bdd-mouth {
	  width: 3.5em;
	  height: 1.2em;
	  border-bottom: .2em solid #b21e25;
	  border-radius: 50%;
	  filter: drop-shadow(0 0 .1em currentColor);
	}

会旗 Logo

冰墩墩身上贴的冬奥会会旗 Logo,从网上下载现成的素材直接用就好了,这里用的是 SVG 矢量格式的素材。

15_020e0f03891f4826114b6e404f39244c_284x296.png@900-0-90-f.png

素材引入的话,使用 CSS 中喜闻乐见的 background 将 SVG 素材当成背景图片引入就可以了:

	.bdd-logo {
	  position: absolute;
	  top: 0;
	  right: 0;
	  bottom: 0;
	  left: 0;
	  background: url(./images/logo.svg) bottom 2em center no-repeat;
	  background-size: 6em;
	}

此处有个知识点:CSS3 中的 background-position 支持位置关键字和数值连用,像上面 background 属性值中的 bottom 2em center 就表示背景图垂直靠底,距底部2em,水平居中。

在以前 CSS2 的时代,背景图定位的数值类型值,只能基于左上角来定位,想要实现靠右 20px 距离或靠下 20px 距离,就不得不通过繁琐的手动计算来实现,现今有了位置关键字和数值连用的支持,就可以很方便的实现背景图的定位。

16_0b35150ddd0b89547f56050aea11d5d4_643x650.png@900-0-90-f.png

总结

  • border-radius 的完全体写法可调教出各种非常规的圆角效果
  • drop-shadow 可生成元素形状的真实投影
  • outline 可生成可控间距的描边效果
  • box-shadow 批量堆叠,可实现多重边框效果
  • 善用元素堆叠和层级关系,在 transform 的加持下可生成丰富的图形
  • 伪元素是修饰细节的好帮手
  • CSS3 的 background-position 支持更加方便、精确的定位写法

附代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>冰墩墩</title>
  <style>
    .bdd {
      position: relative;
      display: inline-block;
      margin: .5em 5em;
      font-size: 10px;
      color: #000;
    }

    /******************** 身体 ********************/
    .bdd-body {
      position: relative;
      width: 24em;
      height: 32em;
      background: radial-gradient(#fff 46%, #c2cacd);
      border-radius: 82% 82% 75% 75% / 72% 72% 88% 88%;
    }

    /******************** 耳朵 ********************/
    .bdd-ears {
      position: absolute;
      top: 1em;
      left: 4em;
      width: 3em;
      height: 3em;
      background: currentColor;
      border-radius: 50%;
      outline: .1em solid;
      outline-offset: .5em;
      filter: drop-shadow(calc(24em - 4em * 2 - 3em) 0 0);
    }

    /******************** 面部 ********************/
    .bdd-face {
      position: absolute;
      top: 2.5em;
      left: .6em;
      right: .6em;
      height: 60%;
      background: #fff;
      border-radius: 60% 60% 55% 55% / 72% 72% 50% 50%;

      /* 从外到里 */
      box-shadow: inset 0 0 0 .2em #86dc94,
                  inset 0 0 0 .4em #fff,
                  inset 0 0 0 .6em #8a9ab7,
                  inset 0 0 0 .8em #fff,
                  inset 0 0 0 1em #f8fd47,
                  inset 0 0 0 1.3em #f42cf4,
                  inset 0 0 0 1.4em #fff,
                  inset 0 0 0 1.7em #82e970,
                  inset 0 0 0 2em #6e88df,
                  inset 0 -1em 2em 2em rgba(0, 0, 0, .25);
    }

    /******************** 眼睛 ********************/
    .bdd-eye {
      position: absolute;
      top: 4em;
    }
    .bdd-eye::before {
      content: '';
      position: absolute;
      width: 5em;
      height: 8em;
      background: currentColor;
      border-radius: 50%;
    }

    .bdd-eye.left {left: 4em;}
    .bdd-eye.left::before {
      transform: rotate(45deg);
    }
    .bdd-eye.right {right: 4em;}
    .bdd-eye.right::before {
      margin-left: -5em;  /* 自身宽度 */
      transform: rotate(-45deg);
    }

    .bdd-eye-inner {
      position: absolute;
      top: 3em;
      width: 1.3em;
      height: 1.5em;
      background: linear-gradient(#232520, #06070c);
      border-radius: 50%;
      box-shadow: 0 0 .35em .35em #949176,
                  0 0 0 .6em,
                  0 0 0 .8em #fff;
    }
    .bdd-eye-inner::before {
      content: '';
      position: absolute;
      left: 0;
      width: 70%;
      height: 40%;
      transform: translate(25%, -80%);
      background: #fff;
      border-radius: 50%;
    }
    .bdd-eye-inner::after {
      content: '';
      position: absolute;
      left: 0;
      bottom: 0;
      width: .7em;
      height: .7em;
      transform: translate(-25%, 20%);
      background: #00f6ff;
      border-radius: 50%;
      opacity: .35;
    }
    .left > .bdd-eye-inner {left: 2.5em;}
    .right > .bdd-eye-inner {right: 2.5em;}

    /******************** 鼻子 ********************/
    .bdd-oronasal {
      position: absolute;
      top: 42%;
      left: 50%;
      display: flex;
      flex-direction: column;
      align-items: center;
      transform: translateX(-50%);
    }

    .bdd-norse {
      width: 2.4em;
      height: 1.6em;
      background: currentColor;
      border-radius: 42% 42% 60% 60% / 50% 50% 70% 70%;
      box-shadow: 0 .3em .2em rgba(0, 0, 0, .3);
    }
    .bdd-norse::before {
      content: '';
      display: block;
      width: 60%;
      height: 45%;
      margin: .15em auto;
      border-radius: 50%;
      background: rgba(255, 255, 255, .5);
    }

    /******************** 嘴巴 ********************/
    .bdd-mouth {
      width: 3.5em;
      height: 1.2em;
      border-bottom: .2em solid #b21e25;
      border-radius: 50%;
      filter: drop-shadow(0 0 .1em currentColor);
    }

    /******************** 会旗 logo ********************/
    .bdd-logo {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background: url(./images/logo.svg) bottom 2em center no-repeat; // 图片为冬奥会logo
      background-size: 6em;
    }

    /******************** 胳膊 ********************/
    .bdd-arm {
      position: absolute;
      width: 3.5em;
      height: 10.5em;
      transform: rotate(30deg);
      background: currentColor;
    }
    .bdd-arm::before {
      content: '';
      position: absolute;
      width: 4em;
      height: 4em;
      background: currentColor;
      border-radius: 50%;
    }

    .bdd-arm.left {
      top: 42%;
      transform-origin: 100% 0;
      border-radius: 0 0 5em 5em;
    }
    .bdd-arm.left::before {bottom: -1em;}

    .bdd-arm.right {
      top: 25%;
      right: -5.5em;
      transform-origin: 0 0;
      border-radius: 5em 5em 0 0;
    }
    .bdd-arm.right::before {
      top: -1em;
      right: 0;
    }

    /******************** 爱心(掌印) ********************/
    .heart {
      position: relative;
      transform: rotate(-30deg);
    }
    .heart::before, .heart::after {
      content: '';
      position: absolute;
      top: -.2em;
      left: 1.3em;
      width: 1em;
      height: 1.5em;
      transform-origin: 50% 100%;
      transform: rotate(-60deg);
      border-radius: 1em 1em 0 0;
      background: #fb001f;
    }
    .heart::after {
      transform: rotate(40deg) translate(-.4em, .3em);
    }

    /******************** 腿部 ********************/
    .bdd-leg {
      position: absolute;
      bottom: -3em;
      width: 5em;
      height: 6em;
      background: currentColor;
      border-radius: 0 0 1em 1.5em;
    }
    .bdd-leg::before {
      content: '';
      position: absolute;
      bottom: 0;
      right: 0;
      width: 2em;
      height: 2em;
      transform: translateX(10%);
      background: currentColor;
      border-radius: 50%;
    }
    
    .bdd-leg.left {left: 5em;}
    .bdd-leg.right {
      right: 5em;
      transform: scaleX(-1);
    }
  </style>
</head>
<body>
  <div class="bdd">
    <div class="bdd-ears"></div>
    <div class="bdd-arm left"></div>
    <div class="bdd-arm right">
      <span class="heart"></span>
    </div>
    <div class="bdd-leg left"></div>
    <div class="bdd-leg right"></div>
    <div class="bdd-body">
      <div class="bdd-face">
        <div class="bdd-eye left">
          <span class="bdd-eye-inner"></span>
        </div>
        <div class="bdd-eye right">
          <span class="bdd-eye-inner"></span>
        </div>
        <div class="bdd-oronasal">
          <span class="bdd-norse"></span>
          <span class="bdd-mouth"></span>
        </div>
      </div>
      <div class="bdd-logo"></div>
    </div>
  </div>
</body>
</html>