【html】帧动画

268 阅读4分钟

关于实现帧动画的一次记录

原理

通过背景图片实现:
1:将动画帧组合成一张雪碧图
2:以背景图的方式引入
3:通过控制background-position改变位置以达到连贯的动画效果(setTimeout/setInterval/requestAnimationFrame)

setTimeout 与 requestAnimationFrame 的区别:
引擎层面:

  • setTimeout 属于 JS 引擎,存在事件轮询,存在事件队列。
  • requestAnimationFrame 属于 GUI 引擎,发生在渲 染过程的中重绘重排部分,与电脑分辨路保持一致。
    性能层面:
  • 当页面被隐藏或最小化时,定时器 setTimeout 仍在后台执行动画任 务。
  • 当页面处于未激活的状态下,该页面的屏幕刷新任 务会被系统暂停,requestAnimationFrame 也会停止。
    应用层面:
  • 利用 setTimeout,这种定时机制去做动画,模拟固定时间刷新页面。
  • requestAnimationFrame 由浏览器专门为动画提供 的 API,在运行时浏览器会自动优化方法的调用,在特定性环境下可以有效节省了 CPU 开销。

效果

静态图!!!
1642993786(1)

工具

雪碧图生成:css Sprites Generator

ps:随便找的一个雪碧图生成网站,优点使用方便快捷,可以批量上传图片;

1642995965(1)

注意

  • 需要考虑图片的加载问题,图片可能比较大,加载完成时间不确定;
    • 需要图片预加载,加载完成后开始执行动画(看个人需要,看后续更新)[已更新]
  • 动画效果的实现方式
    • 本文用了setTimeout实现简单的效果,当然最好的方式是用requestAnimationFrame实现

代码

话不多说,说多无谓

<!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>
        .ani-box {
            width: 1229px;
            height: 775px;
            background-image: url(https://tvax1.sinaimg.cn/large/005YCUMZgy1gyolthipy8j34th46ghe1.jpg);
            margin: 0 auto;
            transform: scale(.6); // 太大了
        }
    </style>
</head>

<body>
    <div class="main-box">
        <div class="ani-box" id="aniBox"></div>
    </div>
    <script>
        // 帧动画定位
        // 记录每帧的位置
        var positions =
            [
                '-10px -10px', '-1259px -10px', '-10px -687px', '-1259px -687px', '-10px -1364px', '-1259px -1364px', '-2508px -10px', '-2508px -687px',
                '-2508px -1364px', '-10px -2041px', '-1259px -2041px', '-2508px -2041px', '-10px -2718px', '-1259px -2718px', '-2508px -2718px', '-3757px -10px',
                '-3757px -687px', '-3757px -1364px', '-3757px -2041px', '-3757px -2718px', '-10px -3395px', '-1259px -3395px', '-2508px -3395px', '-3757px -3395px',
                '-10px -4072px', '-1259px -4072px', '-2508px -4072px', '-3757px -4072px', '-5006px -10px', '-5006px -687px', '-5006px -1364px', '-5006px -2041px',
                '-5006px -2718px', '-5006px -3395px', '-5006px -4072px', '-10px -4749px', '-1259px -4749px', '-2508px -4749px', '-3757px -4749px', '-5006px -4749px'
            ]

        // 计时器实例
        var trigerAniTimer = null;

        // 帧动画执行方法
        function frameAnimation(ele, positions) {
            var index = 0;
            function run() {
                var pos = positions[index];
                // 改变位置
                ele.style.backgroundPosition = pos;
                index++;
                if (index >= positions.length) {
                    index = 0;
                }
                trigerAniTimer = setTimeout(run, 60);
            }
            run();
        }

        // 开始动画
        function startTrigerAni() {
            var trigerAniEl = document.getElementById('aniBox');
            frameAnimation(trigerAniEl, positions);
        }
        
        startTrigerAni()
    </script>
</body>

</html>

后续更新(图片加载部分)

完善: 加载图片部分

封装一个通用的图片加载模块
实现功能:

  • 加载图片(单个图片、多个图片)
  • 全部加载完成回调通知

加载图片思路

  1. 通过 new Image() 加载图片
  2. 通过 Imageonload、onerror事件监听图片资源的加载
  3. 回调通知图片加载情况

关于 Image() 的那些事,引用 MDN :

Image()函数将会创建一个新的HTMLImageElement实例。
它的功能等价于 document.createElement('img')
Image(width, height)

提示:
在 FF 中,img对象的加载包含在body的加载过程中,既是 img加载完之后,body才算是加载完毕,触发 window.onload 事件。
在 IE 中,img对象的加载是不包含在 body的加载过程之中的,body加载完毕,window.onload事件触发时,img对象可能还未加载结束,img.onload事件会在 window.onload之后触发。
所以window.onload之后执行就不会影响 FF 中的加载了

封装实现

  1. 定义加载函数loadImg,接受两个参数:需要加载的图片url,加载完成的回调
    1. 需要加载的图片:考虑参数兼容性:单个图片、多个图片(数组、对象形式)
    2. 回调需要返回么?需要返回什么?(本demo没有实现返回)
  2. 遍历图片加载
    1. 参数的判断:
      1. 字符串参数处理
      2. 数组参数处理(元素是对象、字符串)、
      3. 对象参数处理
  3. 单个图片加载的实际方法
  4. 单个图片加载完成后的处理
    1. 加载成功 or 加载失败
    2. 判断是否全部加载完成
  5. 调用加载完成回调

以下loadImg.js:

/**
 * 
 * @param { Array | Object | String } images 数组形式:['xxx','xxx',{src:'xx}] ; 对象形式:{a:'xxx',b:'xxxx'};字符串形式加载单个链接:'xxx'
 * @param { Function } resolve 加载完成回调
 * @returns void
 */
function loadImg(images, resolve) {
    // 统计
    var count = 0
    var errs = []
    var success = []
    if (typeof images === 'string') {
        images = [images]
    }
    for (var key in images) {
        // 非自身属性跳过
        if (!images.hasOwnProperty(key)) {
            continue
        }
        var item = images[key]
        // 转换成需要的对象形式 
        // 接受的对象形式 { src: 'xxxx' }
        if (typeof item === 'string') {
            item = {
                src: item
            }
        }
        item.imgKey = key
        if (!item || !item['src']) {
            console.log('参数不符合格式,已跳过该项加载 =====> key,images[key] :', key, images[key]);
            continue
        }
        console.log('  =====> item:', item);
        // 数量+1
        count++
        // 加载
        toLoad(item)
    }

    // 数量为0 直接走成功回调
    if (count === 0) return resolve()
    function toLoad(item) {
        item.img = new Image()
        // 加载成功
        item.img.onload = function () {
            saveResult(item, 'success')
        }
        // 加载失败
        item.img.onerror = function () {
            saveResult(item, 'error')
        }
        // src 属性一定要写到 onload 的后面,否则程序在 IE 中会出错。
        item.img.src = item.src
    }

    // 保存加载结果
    function saveResult(item, type) {
        item.img.onload = item.img.onerror = null
        item.loadStatus = type
        count--
        if (type === 'success') {
            success.push({
                key: item.imgKey,
                src: item.src,
                loadStatus: 'success'
            })
        } else {
            errs.push({
                key: item.imgKey,
                src: item.src,
                loadStatus: 'error'
            })
        }
        checkLoadStatus()
    }

    // 判断是否全部加载完成
    function checkLoadStatus() {
        if (count > 0) return
        console.log(' 全部加载完毕');
        // 输出加载失败的
        if (errs.length) {
            console.log(' 加载失败 =====> ', errs);
        }
        resolve()
    }

}

升级版本的完整代码index.html:

<!--
 * @Author: your name
 * @Date: 2022-01-24 10:36:35
 * @LastEditTime: 2022-01-24 16:36:47
 * @LastEditors: Please set LastEditors
 * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 * @FilePath: \动画帧\src\index.html
-->
<!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>
        .ani-box {
            width: 1229px;
            height: 775px;
            /* background-image: url(https://tvax1.sinaimg.cn/large/005YCUMZgy1gyolthipy8j34th46ghe1.jpg); */
            margin: 0 auto;
            transform: scale(.6);
        }
    </style>
</head>

<body>
    <div class="main-box">
        <div class="ani-box" id="aniBox"></div>
    </div>
    <script src="./js/loadImg.js"></script>
    <script>
        var trigerAniEl = document.getElementById('aniBox');
        var bgImg = 'https://tvax1.sinaimg.cn/large/005YCUMZgy1gyolthipy8j34th46ghe1.jpg'
        loadImg(bgImg, function () {
            // 加载完成后设置背景
            trigerAniEl.style.backgroundImage = "url(" + bgImg + ")"
            // 开始动画
            startTrigerAni()
        })

        // 帧动画定位
        var positions =
            [
                '-10px -10px', '-1259px -10px', '-10px -687px', '-1259px -687px', '-10px -1364px', '-1259px -1364px', '-2508px -10px', '-2508px -687px',
                '-2508px -1364px', '-10px -2041px', '-1259px -2041px', '-2508px -2041px', '-10px -2718px', '-1259px -2718px', '-2508px -2718px', '-3757px -10px',
                '-3757px -687px', '-3757px -1364px', '-3757px -2041px', '-3757px -2718px', '-10px -3395px', '-1259px -3395px', '-2508px -3395px', '-3757px -3395px',
                '-10px -4072px', '-1259px -4072px', '-2508px -4072px', '-3757px -4072px', '-5006px -10px', '-5006px -687px', '-5006px -1364px', '-5006px -2041px',
                '-5006px -2718px', '-5006px -3395px', '-5006px -4072px', '-10px -4749px', '-1259px -4749px', '-2508px -4749px', '-3757px -4749px', '-5006px -4749px'
            ]
        var trigerAniTimer = null;
        // 帧动画执行方法
        function frameAnimation(ele, positions) {
            var index = 0;
            function run() {
                var pos = positions[index];
                ele.style.backgroundPosition = pos;
                index++;
                if (index >= positions.length) {
                    index = 0;
                }
                trigerAniTimer = setTimeout(run, 60);
            }
            run();
        }

        // 开始动画
        function startTrigerAni() {
            var trigerAniEl = document.getElementById('aniBox');
            frameAnimation(trigerAniEl, positions);
        }

    </script>
</body>

</html>