0


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

前言

GIF

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

<imgsrc="xxx.gif"/>

这样就能播放一个

GIF

,不知道大家有没有思考过一个问题?在播放

GIF

的时候,可以把这个

GIF

暂停/停止播放吗?可以把这个

GIF

倍速播放吗?听起来是很离谱的需求,你为啥不直接给我一个视频呢?

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

GIF

的实现。

ImageDecoder

首先先来了解一下 WebCodecs API ,它旨在浏览器提供原生的音视频处理能力。

WebCodecs API

的核心包含两大部分:编码器(

Encoder

)和解码器(

Decoder

)。编码器把原始的媒体数据(如音频或视频)进行编码,转换成特定的文件格式(如

mp3

mp4

等)。解码器则是进行逆向操作,把特定格式的文件解码为原始的媒体数据。

使用

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

结构,十分简单:

<divclassName="container"><div>原始gif</div>
      {init && <imgsrc={Flower}/>}
      <div>canvas渲染的gif</div><canvasref={canvas}/></div>

首先通过

fetch

获取到

GIF

图的元数据,这里有一个

getDimensions

方法,它是获取

GIF

图的原始宽高信息的:

const getDimensions =(blob):any=>{returnnewPromise((resolve)=>{const img = document.createElement("img");
      img.addEventListener("load",(e)=>{URL.revokeObjectURL(blob);returnresolve({ width: img.naturalWidth, height: img.naturalHeight });});
      img.src =URL.createObjectURL(blob);});};

获取到宽高信息后,对

canvas

元素赋值宽高,并且定义一个离屏

canvas

对象,后续用它来操作像素,同时也对他赋值宽高。

然后就可以调用

decodeImage

来解码

GIF

constdecodeImage=async(imageByteStream)=>{//@ts-ignore
    imageDecoder.current =newImageDecoder({
      data: imageByteStream,
      type:"image/gif",});const imageFrame =await imageDecoder.current.decode({
      frameIndex: imageIndex.current,// imageIndex从0开始});const track = imageDecoder.current.tracks.selectedTrack;awaitrenderImage(imageFrame, track);};

这里的

imageIndex

0

开始,

imageFrame

表示第

imageIndex

帧的图像信息,拿到图像信息和轨道之后,就可以把图像渲染出来。

constrenderImage=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

暂停/播放

既然我们能把

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

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

constrenderImage=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

倍速

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

    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

滤镜

既然我们是拿到每一帧图像的信息到

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种图像滤镜是否让你想起一位故人

constdoFilter=(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

最后

以上就是本文的全部内容,主要介绍了

ImageDecoder

解码

GIF

图像之后,再利用

canvas

重新进行渲染。期间也就也可以加上暂停、倍速、滤镜的功能。

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

标签: 前端

本文转载自: https://blog.csdn.net/weixin_42684490/article/details/140257874
版权归原作者 可乐鸡翅- 所有, 如有侵权,请联系我们删除。

“纯前端如何实现Gif暂停、倍速播放”的评论:

还没有评论