纯前端如何实现Gif暂停、倍速播放

可乐鸡翅- 2024-08-03 08:03:02 阅读 59

前言

<code>GIF 我相信大家都不会陌生,由于它被广泛的支持,所以我们一般用它来做一些简单的动画效果。一般就是设计师弄好了之后,把文件发给我们。然后我们就直接这样使用:

<img src="xxx.gif"/>code>

这样就能播放一个 GIF ,不知道大家有没有思考过一个问题?在播放 GIF 的时候,可以把这个 GIF 暂停/停止播放吗?可以把这个 GIF 倍速播放吗?听起来是很离谱的需求,你为啥不直接给我一个视频呢?

anyway,那我们今天就一起来尝试实现一下上述的一些功能在 GIF 的实现。

ImageDecoder

首先先来了解一下 WebCodecs API ,它旨在浏览器提供原生的音视频处理能力。 WebCodecs API 的核心包含两大部分:编码器( Encoder )和解码器( Decoder )。编码器把原始的媒体数据(如音频或视频)进行编码,转换成特定的文件格式(如 mp3mp4 等)。解码器则是进行逆向操作,把特定格式的文件解码为原始的媒体数据。

使用 WebCodecs API ,我们可以对原始媒体数据进行更细粒度的操作,如进行合成、剪辑等,然后把操作后的数据进行编码,保存成新的媒体文件。

不过需要注意的是 WebCodecs API 还属于实验性阶段,并未在所有浏览器中支持。

ImageDecoder 是 WebCodecs API 的一部分,它可以让我们解码图片,获取到图片的元数据。

假设我们这样导入一个 GIF

import Flower from "./flower.gif";

导入之后,通过 ImageDecoder 解码 GIF 获取到每一帧的关键信息:如图像信息、每一帧的持续时长等。获取到这些信息之后,再通过 canvas+定时器 把这个 GIF 在画图中绘制出来,下面一起来看看具体操作:

useEffect(() => {

const run = async () => {

const res = await fetch(Flower);

const clone = res.clone();

const blob = await res.blob();

const { width, height } = await getDimensions(blob);

canvas.current.width = width;

canvas.current.height = height;

offscreenCanvas.current = new OffscreenCanvas(width, height);

//@ts-ignore

decodeImage(clone.body);

};

run();

}, []);

顺带说一下 html 结构,十分简单:

<div className="container">code>

<div>原始gif</div>

{init && <img src={Flower} />}

<div>canvas渲染的gif</div>

<canvas ref={canvas} />

</div>

首先通过 fetch 获取到 GIF 图的元数据,这里有一个 getDimensions 方法,它是获取 GIF 图的原始宽高信息的:

const getDimensions = (blob): any => {

return new Promise((resolve) => {

const img = document.createElement("img");

img.addEventListener("load", (e) => {

URL.revokeObjectURL(blob);

return resolve({ width: img.naturalWidth, height: img.naturalHeight });

});

img.src = URL.createObjectURL(blob);

});

};

获取到宽高信息后,对 canvas 元素赋值宽高,并且定义一个离屏 canvas 对象,后续用它来操作像素,同时也对他赋值宽高。

然后就可以调用 decodeImage 来解码 GIF

const decodeImage = async (imageByteStream) => {

//@ts-ignore

imageDecoder.current = new ImageDecoder({

data: imageByteStream,

type: "image/gif",

});

const imageFrame = await imageDecoder.current.decode({

frameIndex: imageIndex.current, // imageIndex从0开始

});

const track = imageDecoder.current.tracks.selectedTrack;

await renderImage(imageFrame, track);

};

这里的 imageIndex0 开始, imageFrame 表示第 imageIndex 帧的图像信息,拿到图像信息和轨道之后,就可以把图像渲染出来。

const renderImage = async (imageFrame, track) => {

const offscreenCtx = offscreenCanvas.current.getContext("2d");

offscreenCtx.drawImage(imageFrame.image, 0, 0);

const temp = offscreenCtx.getImageData(

0,

0,

offscreenCanvas.current.width,

offscreenCanvas.current.height

);

const ctx = canvas.current.getContext("2d");

ctx.putImageData(temp, 0, 0);

setInit(true);

if (track.frameCount === 1) {

return;

}

if (imageIndex.current + 1 >= track.frameCount) {

imageIndex.current = 0;

}

const nextImageFrame = await imageDecoder.current.decode({

frameIndex: ++imageIndex.current,

});

window.setTimeout(() => {

renderImage(nextImageFrame, track);

}, (imageFrame.image.duration / 1000) * factor.current);

};

imageFrame.image 中就可以获取到当前帧的图像信息,然后就可以把它绘制到画布中。其中 track.frameCount 表示当前 GIF 有多少帧,当到达最后一帧时,将 imageIndex 归零,实现循环播放。

其中 factor.current 表示倍速,后续会提到,这里先默认看作 1

一起来看看效果:

Kapture 2024-05-06 at 22.26.56.gif

暂停/播放

既然我们能把 <code>GIF 的图像信息每一帧都提取出来放到 canvas 中重新绘制成一个动图,那么实现暂停/播放功能也不是什么难事了。

下面的展示我会把原 GIF 去掉,只留下我们用 canvas 绘制的动图。

用一个按钮表示暂停开始状态:

const [playing, setPlaying] = useState(true);

const playingRef = useRef(true);

useEffect(() => {

playingRef.current = playing;

}, [playing]);

// ....

<div>

<Button onClick={() => setPlaying((prev) => !prev)}>

{playing ? "暂停" : "开始"}

</Button>

</div>

然后在 renderImage 方法中,如果当前状态是暂停,则停止渲染。

const renderImage = async (imageFrame, track) => {

const offscreenCtx = offscreenCanvas.current.getContext("2d");

offscreenCtx.drawImage(imageFrame.image, 0, 0);

const temp = offscreenCtx.getImageData(

0,

0,

offscreenCanvas.current.width,

offscreenCanvas.current.height

);

const ctx = canvas.current.getContext("2d");

// 根据状态判断是否渲染

if (playingRef.current) {

ctx.putImageData(temp, 0, 0);

}

setInit(true);

if (track.frameCount === 1) {

return;

}

if (imageIndex.current + 1 >= track.frameCount) {

imageIndex.current = 0;

}

const nextImageFrame = await imageDecoder.current.decode({

frameIndex: playingRef.current

? ++imageIndex.current

: imageIndex.current, // 根据状态判断是否要渲染下一帧

});

window.setTimeout(() => {

renderImage(nextImageFrame, track);

}, (imageFrame.image.duration / 1000) * factor.current);

};

一起来看看效果:

Kapture 2024-05-06 at 22.36.33.gif

倍速

再来回顾一下渲染下一帧的逻辑:

<code> window.setTimeout(() => {

renderImage(nextImageFrame, track);

}, (imageFrame.image.duration / 1000) * factor.current);

这里获取到每一帧原本的持续时长之后,乘以一个 factor ,我们只要改变这个 factor ,就可以实现各种倍速。

这里用一个下拉框,实现 0.5/1/2 倍速:

const [speed, setSpeed] = useState(1);

const factor = useRef(1);

useEffect(() => {

factor.current = speed;

}, [speed]);

// ....

<Select

value={ speed}

onChange={ (e) => setSpeed(e)}

options={ [

{

label: "0.5X",

value: 2,

},

{

label: "1X",

value: 1,

},

{

label: "2X",

value: 0.5,

},

]}

></Select>

一起来看看效果:

Kapture 2024-05-06 at 22.42.13.gif

滤镜

既然我们是拿到每一帧图像的信息到 <code>canvas 中进行渲染的,那么我们也就可以对 canvas 做一些滤镜操作。以常见的灰度滤镜、黑白滤镜为例:

const [filter, setFilter] = useState(0);

const filterRef = useRef(0);

<Select

value={ filter}

onChange={ (e) => setFilter(e)}

options={ [

{

label: "无滤镜",

value: 0,

},

{

label: "灰度",

value: 1,

},

{

label: "黑白",

value: 2,

},

]}

></Select>

同样的,用一个下拉框来表示所选择的滤镜,然后我们实现一个函数,对 temp 进行像素变换

image.png

像素变换如下,更多的像素变换可以参考我的这篇文章——这10种图像滤镜是否让你想起一位故人

<code> const doFilter = (imageData) => {

if (filterRef.current === 1) {

const data = imageData.data;

const threshold = 128;

for (let i = 0; i < data.length; i += 4) {

const gray = (data[i] + data[i + 1] + data[i + 2]) / 3;

const binaryValue = gray < threshold ? 0 : 255;

data[i] = binaryValue;

data[i + 1] = binaryValue;

data[i + 2] = binaryValue;

}

}

if (filterRef.current === 2) {

const data = imageData.data;

for (let i = 0; i < data.length; i += 4) {

const red = data[i];

const green = data[i + 1];

const blue = data[i + 2];

const gray = 0.299 * red + 0.587 * green + 0.114 * blue;

data[i] = gray;

data[i + 1] = gray;

data[i + 2] = gray;

}

}

return imageData;

};

一起来看看效果:

Kapture 2024-05-06 at 23.02.04.gif

最后

以上就是本文的全部内容,主要介绍了 <code>ImageDecoder 解码 GIF 图像之后,再利用 canvas 重新进行渲染。期间也就也可以加上暂停、倍速、滤镜的功能。

如果你觉得有意思的话,点点关注点点赞吧~



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。