引言
在 web 应用中,前端开发在实现动画效果时往往常用的几种方案:
- CSS3 transition / animation —— 实现过渡动画
- setInterval / setTimmeout —— 通过设置一个间隔时间来不断的改变图像的位置
- requestAnimationFrame —— 通过一个回调函数来改变图像位置,有系统来决定这个回调函数的执行时机,必定是修改的性能更好,不存在失帧现象。
在大多数需求中,css3 的 transition / animation 都能满足我们的需求,并且相对于 js 实现,可以大大提升我们的开发效率,降低开发成本。
本篇文章将着重对 animation 的使用做个总结,如果你的工作中动画需求较多,相信本篇文章能够让你有所收获:
- Animation 常用动画属性
- Animation 实现不间断播报
- Animation 实现回弹效果
- Animation 实现直播点赞效果
- Animation 与 Svg 又会擦出怎样的火花呢?- Loading 组件
- 进度条组件
 
- Animation steps() 运用- 实现打字效果
- 绘制帧动画
 
Animation 常用动画属性

介绍完 animation 常用属性,为了将这些属性更好地理解与运用,下面将手把手实现一些 DEMO 具体讲述。
Animation 实现不间断播报

通过修改内容在父元素的 y 轴位置来实现广播效果
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>不间断播报</title>
        <style>
            @keyframes scroll {
                0% {
                    transform: translate(0, 0);
                }
                100% {
                    transform: translate(0, -160px);
                }
            }
            .container {
                width: 220px;
                height: 40px;
                background: #0066ff;
                overflow: hidden;
                border-radius: 2em;
            }
            .ul {
                animation-name: scroll;
                animation-duration: 5s;
                animation-timing-function: linear;
                animation-iteration-count: infinite;
            }
            .li {
                line-height: 40px;
                vertical-align: bottom;
                color: #fff;
                text-align: center;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="ul">
                <div class="li">小刘同学加入了凹凸实验室</div>
                <div class="li">小邓同学加入了凹凸实验室</div>
                <div class="li">小李同学加入了凹凸实验室</div>
                <div class="li">小王同学加入了凹凸实验室</div>
                <!--   插入用于填充的数据数据 -->
                <div class="li">小刘同学加入了凹凸实验室</div>
            </div>
        </div>
    </body>
</html>
此处为了保存广播效果连贯性,防止滚动到最后一帧时没有内容, 需要多添加一条重复数据进行填充。
Animation 实现回弹效果
通过将过渡动画吃啊分为多个阶段,每个阶段的 top 属性停留在不同位置来实现。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>回弹效果</title>
        <style>
            @keyframes animate {
                0% {
                    top: -100%;
                    opacity: 0;
                }
                25% {
                    top: 60%;
                    opacity: 1;
                }
                50% {
                    top: 48%;
                    opacity: 1;
                }
                75% {
                    top: 52%;
                    opacity: 1;
                }
                100% {
                    top: 50%;
                    opacity: 1;
                }
            }
            button {
                padding: 6px 20px;
                font-size: 14px;
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                background: #0066ff;
                color: white;
                border: 0;
                border-radius: 2em;
                outline: none;
            }
            button:hover {
                opacity: 0.8;
                cursor: pointer;
            }
            .popup {
                position: fixed;
                top: -50%;
                width: 300px;
                line-height: 200px;
                text-align: center;
                background: #0066ff;
                color: #fff;
                border-radius: 0.25em;
                box-shadow: 0 0 10px #ccc;
                left: 50%;
                transform: translate(-50%, -50%);
            }
            .popup.active {
                animation-name: animate;
                animation-duration: 0.5s;
                animation-timing-function: cubic-bezier(0.21, 0.85, 1, 1);
                animation-iteration-count: 1;
                animation-fill-mode: forwards;
            }
            .close {
                width: 40px;
                line-height: 40px;
                height: 40px;
                border-radius: 50%;
                background: #fff;
                color: #000;
                font-size: 20px;
                top: 100%;
                box-shadow: 0 0 10px #888484;
                cursor: pointer;
                position: absolute;
                left: 50%;
                transform: translate(-50%, -50%);
            }
            .close:hover {
                filter: brightness(0.9);
            }
        </style>
    </head>
    <body>
        <button>唤起弹窗</button>
        <div class="popup">
            我是弹窗
            <div class="close">x</div>
        </div>
    </body>
    <script>
        const show = document.getElementsByTagName("button")[0];
        const close = document.getElementsByClassName("close")[0];
        const popup = document.getElementsByClassName("popup")[0];
        show.onclick = function () {
            popup.className += " active";
        };
        close.onclick = function () {
            popup.className = "popup";
        };
    </script>
</html>
为了让过渡效果更自然,这里通过 cubic-bezier() 函数定义一个被塞尔曲线来控制动画播放速度。
过渡动画执行完成后,为了让元素应用动画最后一帧的属性值,我们需要使用 animation-fill-mode: forwards 。
Animation 实现点赞效果

为了让气泡可以向上偏移,我们需要先实现一个 y 轴方向向上移动的动画。
为了让气泡向上偏移时显得不那么单调,我们再实现一个 x 轴方向上移动的动画。
这里是我的理解:
- 虽然是通过修改 margin 来改变 x 轴偏移距离,但实际上与修改 transform 没有太大的性能; 
- 因为通过 @keyframes animation-y 中的 transform 已经新建了一个渲染层; 
- animation 属性可以让该渲染层提升至合成层拥有单独的图形层,即开启了硬件加速,不会影响其他渲染层的 paint、layout; 
- 对于合成层不是很了解的同学,可以阅读一下这篇文章从浏览器渲染层面解析 css3 动效优化原理 
- 如下图所示  
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>直播点赞动画</title>
        <style>
            @keyframes animation-y {
                0% {
                    transform: translate(-50%, 100px) scale(0);
                }
                50% {
                    transform: translate(-50%, -100px) scale(1.5);
                }
                100% {
                    transform: translate(-50%, -300px) scale(1.5);
                }
            }
            @keyframes animation-x {
                0% {
                    margin-left: 0px;
                }
                25% {
                    margin-left: 25px;
                }
                75% {
                    margin-left: -25px;
                }
                100% {
                    margin-left: 0px;
                }
            }
            .btn {
                top: 80%;
                user-select: none;
                width: 50px;
                line-height: 50px;
                background: #0066ff;
                color: #fff;
                text-align: center;
                border-radius: 50%;
                box-shadow: 0 0 10px #999;
                cursor: pointer;
                position: absolute;
                left: 50%;
                transform: translate(-50%, -50%);
            }
            .btn:hover {
                opacity: 0.8;
            }
            .btn:active {
                opacity: 1;
            }
            .like {
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                width: 25px;
                height: 23px;
                pointer-events: none;
                background-size: cover;
                background-position: center;
                background-repeat: no-repeat;
                background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Coraz%C3%B3n.svg/150px-Coraz%C3%B3n.svg.png);
                animation: animation-x 3s 0s linear infinite, animation-y 4s 0s linear 1;
            }
            .like.second {
                background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Coraz%C3%B3n.svg/150px-Coraz%C3%B3n.svg.png);
                animation: animation-x 3s -2s linear infinite, animation-y 4s 0s linear
                        1;
            }
        </style>
    </head>
    <body>
        <div class="btn" onclick="like()">点赞</div>
    </body>
    <script>
        var count = 0;
        function like() {
            var dom = document.createElement("div");
            count += 1;
            dom.className = count % 2 ? "like second" : "like";
            dom.style.willChange = "margin-top";
            document.body.appendChild(dom);
            setTimeout(function () {
                document.body.removeChild(dom);
            }, 2000);
        }
    </script>
</html>
Animation 与 Svg 绘制 loading / 进度条 组件
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Loading / 进度条</title>
        <style>
            :root {
                --color: #0079f5;
            }
            div {
                color: var(--color);
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
            }
            div label {
                display: flex;
                align-items: center;
            }
            @keyframes loading-active {
                0% {
                    stroke-dashoffset: 0;
                }
                100% {
                    stroke-dashoffset: -207;
                }
            }
            .loading svg {
                transform: rotate(-150deg);
            }
            .loading circle {
                animation: loading-active 1s 0s ease-out infinite;
            }
            .progress circle {
                stroke-dasharray: 157 157;
                stroke-dashoffset: 0;
                stroke-linecap: round;
                transition: stroke-dashoffset 0.8s cubic-bezier(0.29, 0.6, 0.42, 0.99);
            }
            .progress .trail {
                stroke-dashoffset: 0;
            }
            .progress span {
                left: 118px;
                top: 150px;
                position: absolute;
                transform: translate(-50%, -50%);
            }
            .progress button {
                margin-right: 5px;
                border: 0;
                color: #fff;
                padding: 4px 10px;
                background: var(--color);
                border-radius: 0.25em;
                outline: none;
            }
            .progress button:hover {
                opacity: 0.8;
            }
        </style>
    </head>
    <body>
        <div>
            <label class="loading">
                Loading:
                <svg with="100" height="100" viewBox="0 0 60 60">
                    <circle
                        cx="30"
                        cy="30"
                        r="25"
                        fill="transparent"
                        stroke-width="4"
                        stroke="#0079f5"
                        stroke-dasharray="50 157"
                        stroke-linecap="round"
                    ></circle>
                </svg>
            </label>
            <label class="progress">
                进度条:
                <svg with="100" height="100" viewBox="0 0 60 60">
                    <defs>
                        <linearGradient id="gradient" x1="100%" y1="0%" x2="0%" y2="0%">
                            <stop offset="0%" stop-color="#0079f5"></stop>
                            <stop offset="100%" stop-color="#6149f6"></stop>
                        </linearGradient>
                    </defs>
                    <circle
                        class="trail"
                        cx="30"
                        cy="30"
                        r="25"
                        fill="transparent"
                        stroke-width="4"
                        stroke="#eee"
                    ></circle>
                    <circle
                        id="progress-bar"
                        class="path"
                        cx="30"
                        cy="30"
                        r="25"
                        fill="transparent"
                        stroke-width="4"
                        stroke="url(#gradient)"
                        style="stroke-dashoffset: 141.3"
                    ></circle>
                </svg>
                <span id="progress-detail">20%</span>
                <button onclick="reduce()">减少</button>
                <button onclick="add()">增加</button>
            </label>
        </div>
    </body>
    <script>
        const bar = document.getElementById("progress-bar");
        const detail = document.getElementById("progress-detail");
        const total = 157; // 圆周长
        const per = total / 100; //一个百分比进度代表的周长
        let progress = 20; // 当前百分比进度
        function add() {
            if (progress >= 100) {
                return;
            }
            progress += 20;
            update();
        }
        function reduce() {
            if (progress <= 0) {
                return;
            }
            progress -= 20;
            update();
        }
        function update() {
            bar.style.strokeDashoffset = total - per * progress;
            detail.innerHTML = ``;
        }
    </script>
</html>