实现序列帧图片动画的3种方式

10,510 阅读2分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

做可视化项目或者其他项目时,我们都可能会用到一些动画,当有些动画前端实现起来比较复杂时,UI组那边就会甩过来好几十张图片过来。在这些图片中,每一张图片代表一帧,我们可以利用类似走马灯的方式,把一张张图片按照一定顺序循环呈现到页面上实现动画效果。

为什么不用gif格式来播放动画呢?主要考虑的问题有两个:
1、UI组那边说gif格式会导致图像失真,边缘有锯齿;
2、使用序列帧图片的方式,通过代码方便控制,可以轻松控制其暂停或者播放;

1、通过css3中的animation属性

通过animation属性不断变换元素的背景图来达到动画效果。

<div class="animation"></div>

//sass写法
$imgCount: 34;
.animation {
  position: absolute;
  width: 800px;
  height: 800px;
  animation: animation 2s infinite;
}
@keyframes animation {
  @for $i from 1 through $imgCount {
    #{($i * 100%/$imgCount)} {
      background-image: url(./img/#{$i}.png);
    }
  }
}

执行上面的代码时会看到当动画第一次执行时,会一直闪烁。那是因为浏览器加载图片、渲染图片需要时间,有个空白过渡期,这时候可以利用浏览器缓存的策略,先让图片预加载。

let count = 0
const animationDiv = document.getElementsByClassName("animation")[0]
const imgCount = 34
const images = []
animationDiv.style.display = "none"
for (let i = 0; i <= imgCount; i += 1) {
    images[i] = new Image()
    images[i].src = `./img/${i}.png`
    images[i].onload = () => {
        count += 1
        if (count === imgCount) {
            animationDiv.style.display = "block"
        }
    }
}

上面的做法其实不太好,因为图片过多导致请求过多,可能会影响页面请求; 通常的做法是把全部的图片合成一张,通过改变background-postion的值进行动画。

.animation {
    position: absolute;
    width: 800px;
    height: 800px;
    background-image: url(./img/img.png);
    animation: animation 2s steps(34) infinite;
}
@keyframes animation {
    0% {
        background-position: 0;
    }
    100% {
        background-position: 100%;
    }
}

通过在animation元素上设置animation-play-statepaused或者running可以控制动画停止或者播放;

2、通过canvas实现序列帧图片动画

<canvas width="800" height="800" id="canvas"></canvas>
<script>
let count = 0
let ctx = null
let timeout = null
const imgSrcs = []
const images = []
const imgCount = 34
const delay = 100
const canvas = document.getElementById('canvas')
for (let i = 0; i <= imgCount; i += 1) {
    imgSrcs.push(`./img/${i}.png`)
}
const animation = (i) => {
    ctx = canvas.getContext('2d')
    if (!ctx) return
    ctx.clearRect(0, 0, images[i].width, images[i].height)
    ctx.drawImage(images[i], 0, 0, images[i].width, images[i].height)
    if (i === imgSrcs.length - 1) {
        timeout = setTimeout(() => {
        animation(0)
        }, delay)
    } else {
        timeout = setTimeout(() => {
        animation(i + 1)
        }, delay)
    }
}
for (let i = 0; i < imgSrcs.length; i += 1) {
    images[i] = new Image()
    images[i].src = imgSrcs[i]
    images[i].onload = () => {
        count += 1
        if (count === imgSrcs.length) {
        animation(0)
        }
    }
}
</script>

当然,也可以将图片合成一张,通过drawImage方法中的sx,sy,swidth,sheight四个参数不断的对图片进行裁剪实现动画,这里要注意的是使用canvas实现系列帧动画时,如果要把图片合成一张时,低版本的火狐和谷歌浏览器会空白显示不出来,也许是因为合成的图片像素太高了。

let ctx = null
let timeout = null
const delay = 100
const imgCount = 34
const canvas = document.getElementById('canvas')
const images = new Image()
images.src = "./img/img.png"
images.onload = () => {
    animation(0)
}
const animation = (i) => {
    ctx = canvas.getContext('2d')
    if (!ctx) return
    ctx.clearRect(0, 0, images.width, images.height)
    ctx.drawImage(images, i * images.width, 0,images.width,images.height,0,0, images.width,images.height)
    if (i === imgCount) {
        timeout = setTimeout(() => {
            animation(0)
        }, delay)
    } else {
        timeout = setTimeout(() => {
            animation(i + 1)
        }, delay)
    }
}

3、通过动态改变img标签路径实现系列帧图片动画

该方法也经常见,就是通过不断改变img标签中的路径,实现动画效果;

<img id="img">
<script>
const img = document.getElementById("img")
const delay = 100
const imgCount = 34
const animation = (i) => {
if (i === imgCount) {
    animation(0)
        return
    }
    img.src = `./img/1_000${i}.png`
    img.onload = () => {
        setTimeout(() => {
            animation(i + 1)
        }, delay);
    }
}

animation(0)
</script>

以上是实现序列帧图片动画的3种方式,第一种使用animation属性的方式应该是最常用、最简单的了,不需要写js。当然,当动画有其他需求时,可以采用第二种方式,通过canvas可以变换出各种各样的动画出来。