【视觉高级篇】18 # 如何生成简单动画让图形动起来?

简介: 【视觉高级篇】18 # 如何生成简单动画让图形动起来?

说明

【跟月影学可视化】学习笔记。



动画的三种形式


   固定帧动画:预先准备好要播放的静态图像,然后将这些图依次播放,实现起来最简单,只需要为每一帧准备一张图片,然后循环播放即可。


   增量动画:就是在每帧给元素的相关属性增加一定的量,但也很好操作,就是不好精确控制动画细节。


   时序动画:使用时间和动画函数来计算每一帧中的关键属性值,然后更新这些属性,这种方法能够非常精确地控制动画的细节,所以它能实现的动画效果更丰富,应用最广泛。


实现固定帧动画


3个静态图像如下:

b5e61a79946f42569cb7db594388db32.png


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实现固定帧动画</title>
    <style>
        .bird {
            position: absolute;
            left: 100px;
            top: 100px;
            width:86px;
            height:60px;
            zoom: 0.5;
            background-repeat: no-repeat;
            background-image: url(./assets/img/bird.png);
            background-position: -178px -2px;
            animation: flappy .5s step-end infinite;
        }
        @keyframes flappy {
            0% {background-position: -178px -2px;}
            33% {background-position: -90px -2px;}
            66% {background-position: -2px -2px;}
        }
    </style>
</head>
<body>
    <div class="bird"></div>
</body>
</html>


ecc1766363ae423e8dee187588a4091f.gif



实现增量动画

实现橙红色方块旋转的动画:给这个方块的每一帧增加一个 rotate 角度。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>实现增量动画</title>
        <style>
            .block {
                width: 100px;
                height: 100px;
                top: 100px;
                left: 100px;
                transform-origin: 50% 50%;
                position: absolute;
                background: salmon;
            }
        </style>
    </head>
    <body>
        <div class="block"></div>
        <script>
            const block = document.querySelector(".block");
            let rotation = 0;
            function update() {
                block.style.transform = `rotate(${rotation++}deg)`;
                requestAnimationFrame(update);
            }
            update();
        </script>
    </body>
</html>

080a87008098445796dfac3dd6d26d3d.gif



实现时序动画

以上面的方块旋转为例,首先定义初始时间和周期,然后在 update 中计算当前经过时间和进度 p,最后通过 p 来更新动画元素的属性。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>实现时序动画</title>
        <style>
            .block {
                width: 100px;
                height: 100px;
                top: 100px;
                left: 100px;
                transform-origin: 50% 50%;
                position: absolute;
                background: salmon;
            }
        </style>
    </head>
    <body>
        <div class="block"></div>
        <script>
            const block = document.querySelector(".block");
            const startAngle = 0; // 起始旋转角度
            const T = 2000; // 旋转周期
            let startTime = null; // 初始旋转的时间
            function update() {
                startTime = startTime == null ? Date.now() : startTime;
                const p = (Date.now() - startTime) / T; // 旋转进度 = 当前经过的时间 / 旋转周期
                const angle = startAngle + p * 360; // 当前角度
                block.style.transform = `rotate(${angle}deg)`;
                requestAnimationFrame(update);
            }
            update();
        </script>
    </body>
</html>

3c9f215d39c040bd82630a5d0b9932d0.gif


虽然时序动画实现起来比增量动画写法更复杂,但我们可以更直观、精确地控制旋转动画的周期(速度)、起始角度等参数。



定义标准动画模型

定义一个类 Timing 用来处理时间:

// 类 Timing 用来处理时间
export class Timing {
    constructor({ duration, iterations = 1 } = {}) {
        this.startTime = Date.now();
        this.duration = duration;
        this.iterations = iterations;
    }
    get time() {
        return Date.now() - this.startTime;
    }
    get p() {
        const progress = Math.min(this.time / this.duration, this.iterations);
        return this.isFinished ? 1 : progress % 1;
    }
    get isFinished() {
        return this.time / this.duration >= this.iterations;
    }
}


实现一个 Animator 类,用来真正控制动画过程:

import { Timing } from './timing.js';
// Animator 类,用来真正控制动画过程
export class Animator {
    constructor({ duration, iterations }) {
        this.timing = { duration, iterations };
    }
    animate(target, update) {
        let frameIndex = 0;
        const timing = new Timing(this.timing);
        return new Promise((resolve) => {
            function next() {
                if (update({ target, frameIndex, timing }) !== false && !timing.isFinished) {
                    requestAnimationFrame(next);
                } else {
                    resolve(timing);
                }
                frameIndex++;
            }
            next();
        });
    }
}


用 Animator 实现四个方块的轮换转动:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>定义标准动画模型</title>
        <style>
            .container {
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
                width: 300px;
            }
            .block {
                width: 100px;
                height: 100px;
                margin: 20px;
                flex-shrink: 0;
                transform-origin: 50% 50%;
            }
            .block:nth-child(1) {
                background: salmon;
            }
            .block:nth-child(2) {
                background: slateblue;
            }
            .block:nth-child(3) {
                background: seagreen;
            }
            .block:nth-child(4) {
                background: sandybrown;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
        </div>
        <script type="module">
            import { Animator } from "./common/lib/animator/index.js";
            const blocks = document.querySelectorAll(".block");
            const animator = new Animator({
                duration: 1000,
                iterations: 1.5
            });
            (async function () {
                let i = 0;
                while (true) {
                    await animator.animate(
                        blocks[i++ % 4],
                        ({ target, timing }) => {
                            target.style.transform = `rotate(${timing.p * 360}deg)`;
                        }
                    );
                }
            })();
        </script>
    </body>
</html>

403bb2670f55443883d5e78ada2f5c47.gif



插值与缓动函数

用 Animator 实现一个方块,让它从 0px 处匀速运动到 400px 处。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>插值与缓动函数</title>
        <style>
            .block {
                position: relative;
                width: 100px;
                height: 100px;
                background: salmon;
            }
        </style>
    </head>
    <body>
        <div class="block"></div>
        <script type="module">
            import { Animator } from "./common/lib/animator/index.js";
            const block = document.querySelector(".block");
            const animator = new Animator({ duration: 3000 });
            document.addEventListener('click', () => {
                animator.animate({
                    el: block,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
            });
        </script>
    </body>
</html>


26a1a093959c47a19724017cd41aa678.gif

下面加入缓动函数,抽象出一个映射函数专门处理 p 的映射,这个函数叫做缓动函数(Easing Function)

// 类 Timing 用来处理时间
export class Timing {
    constructor({ duration, iterations = 1, easing = p => p } = {}) {
        this.startTime = Date.now();
        this.duration = duration;
        this.iterations = iterations;
        this.easing = easing;
    }
    get time() {
        return Date.now() - this.startTime;
    }
    get p() {
        const progress = Math.min(this.time / this.duration, this.iterations);
        return this.isFinished ? 1 : this.easing(progress % 1);
    }
    get isFinished() {
        return this.time / this.duration >= this.iterations;
    }
}


export class Animator {
    constructor({ duration, iterations, easing }) {
        this.timing = { duration, iterations, easing };
    }
}


下面对比一下加了缓动函数 easing: p => p ** 2

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>插值与缓动函数2</title>
        <style>
            .block {
                position: relative;
                width: 100px;
                height: 100px;
                background: salmon;
            }
            .block2 {
                position: relative;
                width: 100px;
                height: 100px;
                background: seagreen;
            }
        </style>
    </head>
    <body>
        <h2>匀速</h2>
        <div class="block"></div>
        <h2>加了缓动函数,匀加速</h2>
        <div class="block2"></div>
        <script type="module">
            import { Animator } from "./common/lib/animator/index.js";
            const block = document.querySelector(".block");
            const animator = new Animator({
                duration: 3000
            });
            const block2 = document.querySelector(".block2");
            const animator2 = new Animator({
                duration: 3000,
                easing: p => p ** 2
            });
            document.addEventListener('click', () => {
                animator.animate({
                    el: block,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    console.log("animator--->p", p)
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
                animator2.animate({
                    el: block2,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    console.log("animator2--->p", p)
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
            });
        </script>
    </body>
</html>


8b431eb2a6934e61a5564901217b23e2.gif



贝塞尔曲线缓动

三次贝塞尔曲线的参数方程:

d0f7bed8a47a4c86a12617a932ad407d.png



贝塞尔缓动函数:

b0a256196b4043dda9971190b4af12e7.png


   可以使用 牛顿迭代法(Newton’s method) 把三次贝塞尔曲线参数方程变换成贝塞尔曲线缓动函数。


我们可以使用现成的 JavaScript 库 bezier-easing 来生成贝塞尔缓动函数。


更多的贝塞尔缓动函数可以查看https://easings.net/#

下面我使用这个效果对比一下https://easings.net/#easeInOutExpo


c391ce92163c472ea94434d0f5ef5e73.png


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>贝塞尔曲线缓动</title>
        <style>
            .block {
                position: relative;
                width: 100px;
                height: 100px;
                background: salmon;
            }
            .block2 {
                position: relative;
                width: 100px;
                height: 100px;
                background: seagreen;
            }
            .block3 {
                position: relative;
                width: 100px;
                height: 100px;
                background: slateblue;
            }
        </style>
    </head>
    <body>
        <h2>匀速</h2>
        <div class="block"></div>
        <h2>加了缓动函数,匀加速</h2>
        <div class="block2"></div>
        <h2>贝塞尔曲线缓动</h2>
        <div class="block3"></div>
        <script src="./common/lib/animator/bezier-easing.js"></script>
        <script type="module">
            import { Animator } from "./common/lib/animator/index.js";
            const block = document.querySelector(".block");
            const animator = new Animator({
                duration: 3000
            });
            const block2 = document.querySelector(".block2");
            const animator2 = new Animator({
                duration: 3000,
                easing: p => p ** 2
            });
            const block3 = document.querySelector(".block3");
            // 使用 easeInOutExpo (0.87, 0, 0.13, 1) 效果 https://easings.net/#easeInOutExpo
            const animator3 = new Animator({
                duration: 3000,
                easing: BezierEasing(0.87, 0, 0.13, 1)
            });
            document.addEventListener('click', () => {
                animator.animate({
                    el: block,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    console.log("animator--->p", p)
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
                animator2.animate({
                    el: block2,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    console.log("animator2--->p", p)
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
                animator3.animate({
                    el: block3,
                    start: 0,
                    end: 400
                }, ({
                    target: { el, start, end },
                    timing: { p }
                }) => {
                    // 线性插值方法
                    console.log("animator3--->p", p)
                    const left = start * (1 - p) + end * p;
                    el.style.left = `${left}px`;
                });
            });
        </script>
    </body>
</html>


dd6a130ff3324abea128f14f9953076f.gif



目录
相关文章
|
6月前
|
XML 搜索推荐 Java
Android App开发之自定义图形中位图与图形互转、剪裁图形内部区域、给图形添加部件的讲解及实战(附源码 简单易懂)
Android App开发之自定义图形中位图与图形互转、剪裁图形内部区域、给图形添加部件的讲解及实战(附源码 简单易懂)
67 0
|
6月前
|
定位技术
Pyglet综合应用|推箱子游戏地图编辑器之图片跟随鼠标
Pyglet综合应用|推箱子游戏地图编辑器之图片跟随鼠标
61 0
【Unity3D--自由观察模型】模型自动旋转+触屏旋转和缩放
展示3D模型,同时实现模型自动旋转和触屏旋转和缩放
382 0
【视觉高级篇】19 # 如何用着色器实现像素动画?2
【视觉高级篇】19 # 如何用着色器实现像素动画?
87 0
【视觉高级篇】19 # 如何用着色器实现像素动画?2
|
数据可视化 异构计算
【视觉高级篇】19 # 如何用着色器实现像素动画?
【视觉高级篇】19 # 如何用着色器实现像素动画?
100 0
【视觉高级篇】19 # 如何用着色器实现像素动画?
|
数据可视化
【视觉高级篇】20 # 如何用WebGL绘制3D物体?
【视觉高级篇】20 # 如何用WebGL绘制3D物体?
179 0
【视觉高级篇】20 # 如何用WebGL绘制3D物体?
An动画基础之散件动画原理与形状提示点
An动画基础之散件动画原理与形状提示点
892 0
An动画基础之散件动画原理与形状提示点
|
前端开发 JavaScript 开发者
让交互更加生动!有意思的鼠标跟随 3D 旋转动效
让交互更加生动!有意思的鼠标跟随 3D 旋转动效
405 0
让交互更加生动!有意思的鼠标跟随 3D 旋转动效
|
前端开发
巧用滤镜实现高级感拉满的文字快闪切换效果
巧用滤镜实现高级感拉满的文字快闪切换效果
247 0
巧用滤镜实现高级感拉满的文字快闪切换效果
Qt 隐藏标题栏可拖拽,自由缩放
Qt在隐藏标题栏的情况下,实现拖拽很简单,可以看这里https://blog.csdn.net/z609932088/article/details/80865742 或者这里:https://blog.csdn.net/z609932088/article/details/50898022
511 0
Qt 隐藏标题栏可拖拽,自由缩放