案例背景:会“互动”的表情球动画
如下图所示,一个网页中有两个可爱的球形小人,它们能眨眼、微笑、甚至“亲吻”彼此。
这种动态效果仅通过CSS实现,无需JavaScript。核心在于布局设计(display
和position
)与动画控制(@keyframes
和animation
)的结合。
本文将结合代码代码,在一步步教你如何“亲吻”的同时,深入了解这些技术的底层逻辑和实际应用。
第一步:双球居中与并排布局
目标:通过绝对定位实现容器居中,结合inline-block
与float
控制双球并排。
代码实现:
<div class="container">
<div id="l-ball" class="ball"></div>
<div id="r-ball" class="ball"></div>
</div>
CSS布局解析:
.container {
width: 238px; /* 固定容器宽度 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.ball {
display: inline-block;
vertical-align: top;
width: 100px;
height: 100px;
border: 8px solid #000;
border-radius: 50%;
}
#r-ball {
float: right; /* 强制右球靠右 */
}
解析:
1. 混合布局方案:inline-block
+ float
- 问题:如何让两个球体严格左右贴边排列?
- 代码实现:
.ball { display: inline-block; } #r-ball { float: right; }
- 解析:
inline-block
使左球默认左对齐,右球通过float: right
强制右对齐。- 容器设置固定宽度
238px
(计算逻辑:100px球宽×2 + 8px边框×4 + 20px间隙)。
- 对比:
- 纯
inline-block
:需处理默认间距(如父元素设置font-size:0
)。 - 纯
float
:需清除浮动避免高度塌陷。 - flex布局:更简洁(
display: flex; justify-content: space-between
),但此处展示传统方案。
- 纯
2. 绝对定位居中与容器宽度
- 问题:如何确保容器居中且双球间距精准?
- 代码实现:
.container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 238px; }
- 解析:
width: 238px
精确计算双球总占位,避免依赖margin
或gap
控制间距。- 绝对定位居中方案兼容性好,不受父元素布局影响。
- 对比传统方法:
margin: 0 auto
:需父元素有宽度且自身为block
,无法垂直居中。flex/grid
居中:需设置body
为height: 100vh
,适应性略差。
第二步:边框构建与层级控制
目标:通过边框增强立体感,控制双球层级实现互动效果。
代码实现:
.ball {
border: 8px solid #000;
position: relative;
}
#l-ball { z-index: 50; }
#r-ball { z-index: 40; }
解析:
1. 边框替代背景色方案
- 问题:如何让球体呈现描边卡通风格?
- 代码实现:
.ball { border: 8px solid #000; }
- 解析:
- 边框宽度8px与背景色形成对比,替代纯色填充方案。
border-radius: 50%
确保边框呈现完美圆形。
2. 层级控制(z-index)
- 问题:如何实现左球始终覆盖右球?
- 代码实现:
#l-ball { z-index: 50; } #r-ball { z-index: 40; }
- 解析:
- 左球设置更高层级,确保动画过程中始终覆盖右球。
position: relative
激活z-index属性,避免层级失效。
第三步:面部元素定位与伪元素应用
目标:通过绝对定位和伪元素构建眼睛、嘴巴、腮红等面部特征,减少DOM层级。
3.1 眼睛的定位与状态切换
代码实现:
.eye {
width: 15px;
height: 14px;
border-radius: 50%;
border-bottom: 5px solid #000; /* 默认下半圆 */
position: absolute;
top: 30px;
}
.eye-r-p {
border-top: 5px solid #000; /* 反转边框方向 */
border-bottom: 0;
}
解析:
- 问题:如何用最简代码实现睁眼/闭眼效果?
- 实现方案:
- 通过
border-bottom
绘制眼睛下半圆弧(默认睁眼)。 - 添加
.eye-r-p
类反转边框方向(border-top
),实现闭眼效果。
- 通过
- 定位逻辑:
top: 30px
让眼睛垂直居中于球体(球高100px,眼睛顶部距离球顶30px)。- 左眼
left: 10px
,右眼right: 5px
,适配不同球体面部偏移。
- 实现方案:
3.2 嘴巴的边框魔法
代码实现:
.mouth {
width: 30px;
height: 14px;
border-bottom: 5px solid #000; /* 仅保留下边框 */
position: absolute;
bottom: 20px;
left: 35px;
}
解析:
- 问题:如何用单一边框属性实现微笑曲线?
- 实现方案:
- 设置
border-bottom
模拟嘴角上扬的弧形。 width:30px
与height:14px
控制弧形的曲率。
- 设置
- 对比方案:
clip-path
:需复杂路径计算,兼容性差。svg
:需额外资源加载,增加HTTP请求。
- 实现方案:
3.3 伪元素实现腮红与亲吻符号
代码实现:
/* 左球腮红 */
.face::after {
content: "";
width: 18px;
height: 8px;
background-color: #badc58;
border-radius: 50%;
position: absolute;
top: 60px;
left: 50px;
}
/* 右球腮红 */
.face-r::after {
width: 10px;
height: 10px;
left: 5px;
}
/* 亲吻符号 */
.kiss {
border-left: 5px solid #000; /* 左侧边框模拟唇印 */
}
解析:
- 问题:如何在不增加HTML节点的情况下添加装饰元素?
- 实现方案:
- 左球腮红通过
.face::after
生成,右球调整尺寸和位置。 - 亲吻符号利用
border-left
绘制半圆,通过旋转形成"X"形。
- 左球腮红通过
- 优势:
- 减少DOM节点(原始代码减少4个
<div>
)。 - 伪元素默认脱离文档流,避免布局干扰。
- 减少DOM节点(原始代码减少4个
- 实现方案:
第四步:复合动画与精准时序控制
目标:通过多动画协同与关键帧同步,实现自然互动效果。
4.1 左球动画(眨眼+位移)
代码实现:
@keyframes close {
0% { transform: translate(0); }
20% { transform: translate(20px); } /* 右移 */
35% { transform: translate(20px); } /* 保持 */
55% { transform: translate(0); } /* 复位 */
}
@keyframes face {
20% { transform: translate(5px) rotate(-2deg); } /* 头部微倾 */
}
#l-ball {
animation: close 4s ease infinite;
}
.face-l {
animation: face 4s ease infinite;
}
解析:
- 时序同步:
close
动画控制整体移动,face
动画实现面部微调。- 20%-35%阶段双动画叠加,呈现"侧身眨眼"的自然效果。
- 性能优化:
- 仅使用
transform
属性(GPU加速),避免left/top
触发布局重排。
- 仅使用
4.2 右球动画(亲吻动作)
代码实现:
@keyframes kiss {
50% { transform: translate(30px) rotate(20deg); } /* 前冲+旋转 */
60% { transform: translate(-33px); } /* 回弹 */
}
@keyframes kiss-m {
55.1% { opacity: 1; } /* 精准匹配亲吻接触点 */
}
#r-ball {
animation: kiss 4s ease infinite;
}
.kiss-m {
animation: kiss-m 4s ease infinite;
}
解析:
- 物理模拟:
- 50%阶段前冲+旋转模拟"靠近亲吻"。
- 60%阶段负向位移实现弹性回弹效果。
- 视觉同步:
- 唇印(
.kiss-m
)在55.1%突然显示,匹配碰撞瞬间。 - 层级控制(左球
z-index:50
)确保回弹时覆盖右球。
- 唇印(
4.3 动画时序全景图
时间轴 | 左球动作 | 右球动作 | 交互事件 |
---|---|---|---|
0-20% | 静止 | 静止 | - |
20-35% | 右移20px + 眨眼 | 静止 | 左球主动靠近 |
50-55% | 复位 | 前冲30px + 旋转20° | 右球发起亲吻 |
55.1% | - | 显示唇印 | 亲吻接触 |
60-77% | - | 回弹-33px | 分离 |
完整代码示例
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div class='container'>
<div id='l-ball' class='ball'>
<div class='face face-l'>
<div class='eye eye-l'></div>
<div class='eye eye-r'></div>
<div class='mouth'></div>
</div>
</div>
<div id='r-ball' class='ball'>
<div class='face face-r'>
<div class='eye eye-l eye-r-p'></div>
<div class='eye eye-r eye-r-p'></div>
<div class='mouth mouth-r'></div>
<div class='kiss-m'>
<div class='kiss'></div>
<div class='kiss'></div>
</div>
</div>
</div>
</div>
</body>
</html>
Css:
<style>
body {
background-color: #78e08f;
margin: 0;
}
.container {
margin: auto;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 238px;
}
.face {
width: 70px;
height: 30px;
position: absolute;
right: 0;
top: 30px;
border-top-right-radius: 15px;
}
#r-ball {
animation: kiss 4s ease infinite;
background-color: white;
}
@keyframes kiss {
40% {
transform: translate(0px);
}
50% {
transform: translate(30px) rotate(20deg);
}
60% {
transform: translate(-33px);
}
67% {
transform: translate(-33px);
}
77% {
transform: translate(0px);
}
}
.kiss {
width: 13px;
height: 10px;
background-color: white;
border-radius: 50%;
border-left: 5px solid;
}
.kiss-m {
position: absolute;
left: 20px;
top: 22px;
opacity: 0;
animation: kiss-m 4s ease infinite;
}
@keyframes kiss-m {
0% {
opacity: 0;
}
55% {
opacity: 0;
}
55.1% {
opacity: 1;
}
66% {
opacity: 1;
}
66.1% {
opacity: 0;
}
}
.mouth-r {
animation: mouth-m 4s ease infinite;
}
@keyframes mouth-m {
0% {
opacity: 1;
}
54.9% {
opacity: 1;
}
55% {
opacity: 0;
}
66% {
opacity: 0;
}
66.1% {
opacity: 1;
}
}
.face:after {
position: absolute;
content: "";
width: 18px;
height: 8px;
background-color: #badc58;
left: -5px;
top: 20px;
border-radius: 50%;
}
.face:before {
position: absolute;
content: "";
width: 18px;
height: 8px;
background-color: #badc58;
right: -8px;
top: 20px;
border-radius: 50%;
z-index: -1;
}
.face-r {
left: 0;
top: 37px;
}
.face-r:after {
width: 10px;
height: 10px;
left: 5px;
}
.face-r:before {
width: 10px;
height: 10px;
right: -4px;
}
.eye {
width: 15px;
height: 14px;
border-radius: 50%;
border-bottom: 5px solid;
position: absolute;
}
.eye-r-p {
border-top: 5px solid;
border-bottom: 0px solid;
}
.eye-l {
left: 10px;
}
.eye-r {
right: 5px;
}
.mouth {
width: 30px;
height: 14px;
border-radius: 50%;
border-bottom: 5px solid;
position: absolute;
bottom: -5px;
transform: translate(3px);
left: 0;
right: 0;
margin: auto;
}
.ball {
border: 8px solid;
width: 100px;
height: 100px;
border-radius: 50%;
display: inline-block;
vertical-align: top;
position: relative;
}
#r-ball {
position: relative;
z-index: 40;
float: right;
}
#l-ball {
animation: close 4s ease infinite;
position: relative;
z-index: 50;
background-color: #fff;
}
.face-l {
animation: face 4s ease infinite;
}
@keyframes close {
0% {
transform: translate(0)
}
20% {
transform: translate(20px)
}
35% {
transform: translate(20px)
}
55% {
transform: translate(0px)
}
100% {
transform: translate(0px)
}
}
@keyframes face {
0% {
transform: translate(0) rotate(0);
}
10% {
transform: translate(0) rotate(0);
}
20% {
transform: translate(5px) rotate(-2deg);
}
28% {
transform: translate(0) rotate(0);
}
35% {
transform: translate(5px) rotate(-2deg);
}
50% {
transform: translate(0) rotate(0);
}
100% {
transform: translate(0) rotate(0);
}
}
</style>
总结:从静态到动态的完整流程
- 布局设计:通过
position: absolute
和transform
实现居中布局。 - 元素构建:使用伪元素和绝对定位添加面部细节。
- 动画控制:通过
@keyframes
定义关键帧,用animation
绑定动画属性。 - 性能优化:优先使用
transform
和opacity
触发硬件加速。
通过以上步骤,你不仅能复现“表情球”的互动效果,还能掌握CSS动画的核心技术,为复杂动态效果的设计打下基础。