关于实现帧动画的一次记录
原理
通过背景图片实现:
1:将动画帧组合成一张雪碧图
2:以背景图的方式引入
3:通过控制background-position
改变位置以达到连贯的动画效果(setTimeout/setInterval/requestAnimationFrame)
setTimeout 与 requestAnimationFrame 的区别:
引擎层面:
- setTimeout 属于 JS 引擎,存在事件轮询,存在事件队列。
- requestAnimationFrame 属于 GUI 引擎,发生在渲 染过程的中重绘重排部分,与电脑分辨路保持一致。
性能层面: - 当页面被隐藏或最小化时,定时器 setTimeout 仍在后台执行动画任 务。
- 当页面处于未激活的状态下,该页面的屏幕刷新任 务会被系统暂停,requestAnimationFrame 也会停止。
应用层面: - 利用 setTimeout,这种定时机制去做动画,模拟固定时间刷新页面。
- requestAnimationFrame 由浏览器专门为动画提供 的 API,在运行时浏览器会自动优化方法的调用,在特定性环境下可以有效节省了 CPU 开销。
效果
静态图!!!
工具
雪碧图生成:css Sprites Generator
ps:随便找的一个雪碧图生成网站,优点使用方便快捷,可以批量上传图片;
注意
- 需要考虑图片的加载问题,图片可能比较大,加载完成时间不确定;
- 需要图片预加载,加载完成后开始执行动画(看个人需要,看后续更新)[已更新]
- 动画效果的实现方式
- 本文用了
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>
后续更新(图片加载部分)
完善: 加载图片部分
封装一个通用的图片加载模块
实现功能:
- 加载图片(单个图片、多个图片)
- 全部加载完成回调通知
加载图片思路
- 通过
new Image()
加载图片 - 通过
Image
的onload、onerror
事件监听图片资源的加载 - 回调通知图片加载情况
关于 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 中的加载了
封装实现
- 定义加载函数
loadImg
,接受两个参数:需要加载的图片url,加载完成的回调- 需要加载的图片:考虑参数兼容性:单个图片、多个图片(数组、对象形式)
- 回调需要返回么?需要返回什么?(本demo没有实现返回)
- 遍历图片加载
- 参数的判断:
- 字符串参数处理
- 数组参数处理(元素是对象、字符串)、
- 对象参数处理
- 参数的判断:
- 单个图片加载的实际方法
- 单个图片加载完成后的处理
- 加载成功 or 加载失败
- 判断是否全部加载完成
- 调用加载完成回调
以下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>