这篇文章来自孟智强同学的投稿,他介绍了如何用CSS画一个冰墩墩,展示了很多CSS知识和动画效果如transform的巧妙使用
成品展示
冰墩墩本墩 | 本案冰墩墩 |
---|---|
不足之处:
- 整体感觉不够“墩”
- “冰晶外壳”未实现
- “冰丝带”不理想
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>
要点:
- 尽量精简 HTML 结构,减少不必要的标签
- 利用标签的前后顺序来控制堆叠层级
用到的主要CSS技术
- 相对单位
em
、%
- 圆角
border-radius
- 渐变
radial-gradient
、linear-gradient
- 轮廓线
outline
、outline-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个圆角的半径。
所以利用完全体形式的 border-radius
就可以轻松调教出冰墩墩的身体形状:
.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 实现这样的形状想想都头疼。
如果我们转变思路,把耳朵做成圆形,再改变冰墩墩身体与耳朵的层叠顺序,让身体的形状挡住一部分耳朵形状,问题将迎刃而解:
<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
的值呢?画个图来分析下:
其中 24em
是“身体”的宽度,不难看出投影的 x-offset
值为 24 - 4 - 4 - 3,我们直接用 CSS 中的 calc
来写,无需人工计算,代码如下:
.bdd-ears {
...
filter: drop-shadow(calc(24em - 4em * 2 - 3em) 0 0);
}
四肢
“四肢”也用到了堆叠层级原理,利用“身体”来遮挡四肢形状的一部分,从而让“四肢”在视觉上与“身体”连成整体。
“四肢”的做法比较简单,具体就不展开讨论了,只说下思路:
- 用矩形做出主体(胳膊、腿)
- 用
border-radius
控制末端的圆角 - 用
transform
变换位置和角度 - 用伪元素修饰细节(手、脚趾)
- 通过叠加组合出整体(相同颜色的不同形状叠加成整体)
右侧手掌的“爱心”是怎么做的呢?
- 用伪元素做两个顶部为圆角的矩形
- 左边的逆时针旋转,一般为45度
transform: rotate(-45deg)
- 右边的顺时针旋转
- 调整距离,使两个图形叠加成心型
面部(冰丝带)
“面部”是一个栗子形状的图形,有多条不同颜色的边框(冰丝带)。
利用前面 border-radius
的知识,我们可以轻松做出栗子形状的面部,但“冰丝带”怎么做呢?我们的第一直觉可能是用多层 HTML 标签嵌套,再为每个标签设置边框样式,但这样做有2个缺点:
-
HTML 结构臃肿,包含许多无意义的标签
-
需要为每层标签设置圆角样式,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盒阴影图形生成技术
眼睛
眼睛是心灵的窗户,“眼睛”实现效果的好与坏直接关系到冰墩墩整体形象生动与否。
眼睛的实现细节还是蛮多的,前面讲过的知识几乎都出场了。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;}
这里可能有小伙伴要问:“黑眼圈”的黑色椭圆为什么要用伪元素来生成?直接用 div.bdd-eye
标签的背景色不就可以了吗?
的确可以,不过 div.bdd-eye
标签作为父级,它的 transform 旋转效果会影响到所有后代元素,使后代元素的平面参考点发生偏移,后代元素在定位、变换时将变得非常麻烦。所以这里采用脱离文档流的伪元素来实现,这样就不会影响到其他元素。
口鼻
“嘴巴”和“鼻子”的实现用前面的知识就可以做出来了,这里不再详细展开,只说下思路。
鼻子:
- 用
border-radius
调整出合适形状(倒栗子形 ~ 误) - 用伪元素生成高亮椭圆,修饰高光
- 用盒阴影生成下方阴影,修饰立体效果
.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
冰墩墩身上贴的冬奥会会旗 Logo,从网上下载现成的素材直接用就好了,这里用的是 SVG 矢量格式的素材。
素材引入的话,使用 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 距离,就不得不通过繁琐的手动计算来实现,现今有了位置关键字和数值连用的支持,就可以很方便的实现背景图的定位。
总结
- 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>