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

前言

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

<img src="xxx.gif"/>

这样就能播放一个 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">
      <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

暂停/播放

既然我们能把 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

倍速

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

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

  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

最后

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/787958.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通过rpmbuild构建Elasticsearch-7.14.2-search-guard的RPM包

系列文章目录 rpmbuild从入门到放弃 search-guard插件使用入门手册 文章目录 系列文章目录前言一、资源准备二、spec文件1.基础信息2.%prep3.%Install4.%file5.%post6.%postun 三、成果演示1.执行构建过程图示例2.执行安装RPM包示例3.进程检查4.访问esApi 总结 前言 不管是源…

Linux--深入理与解linux文件系统与日志文件分析

一、文件与存储系统的 inode 与 block 1.1 硬盘存储 最小存储单位:扇区( sector )每个扇区存储大小:512 字节1.2 文件存取--block block(块),每个 block 块大小为:4k由连续的八个扇区组成一个 block 块是文件索引最小的单位每个 block 块中包括:文件数据文件数据:就…

LeetCode(2)-反转链表、删除链表中等于val的节点、返回链表中的中间节点

一、反转链表 . - 力扣&#xff08;LeetCode&#xff09; 解法1&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ typedef struct ListNode ListNode; struct ListNode* reverseList(struct ListN…

ORA-12537: TNS:连接关闭/Io 异常: Got minus one from a read call

在另外一个数据库建立dblink的时候&#xff0c;发现执行命令报错&#xff1a; 被连接的数据库我也上去过&#xff0c;用工具尝试登陆也报错&#xff1a; IO Error: Got minus one from a read call, connect lapse 1 ms., Authentication lapse 0 ms. Got minus one from a …

Redis过期策略

过期的key集合 Redis会将每个设置了过期时间的key放入到一个独立的字典中&#xff0c;以后会定时遍历这个字典来删除到期的key。除了定时遍历之外&#xff0c;他还会使用惰性策略来删除过期的key&#xff0c;所谓惰性策略就是在客户端访问这个key的时候&#xff0c;redis对key…

生成随机密码

生成8位无重复的密码&#xff08;可以包含数字、大小写字母&#xff09; import random import string character string.digits string.ascii_letters password .join(random.sample(character, 8)) print(f"生成的随机密码为:{password}")

品牌策划秘籍:掌握这些技巧,让你的品牌一炮而红!

作为一名文案策划老司机&#xff0c;这么多年了&#xff0c;总会有一些经验的&#xff0c;这里分享出来&#xff0c;希望能够帮助后来人少走弯路吧。 想要做好品牌和文案策划&#xff0c;首先得做好“侦查”工作。 深入市场&#xff0c;了解行业动态&#xff0c;研究竞争对手…

智能遥测终端机RTU-精确监控 智能运维

智能遥测终端机RTU是物联网领域中一种重要的设备&#xff0c;它的出现无疑为远程监控和数据采集提供了强大的支持。计讯物联智能遥测终端机RTU具备数据采集、处理、通信和控制功能的设备&#xff0c;可以实现对远程设备的监控与控制。它在物联网系统中扮演着桥梁的角色&#xf…

【从零开始实现stm32无刷电机FOC】【理论】【3/6 位置、速度、电流控制】

目录 PID控制滤波单独位置控制单独速度控制单独电流控制位置-速度-电流串级控制 上一节&#xff0c;通过对SVPWM的推导&#xff0c;我们获得了控制电机转子任意受力的能力。本节&#xff0c;我们选用上节得到的转子dq轴解耦的SVPWM形式&#xff0c;对转子受力进行合理控制&…

AE-关键帧

目录 关键帧操作步骤&#xff08;以位置变化为例&#xff09; 1.确定动画起点 2.设置起点的位置属性 3.为起点打上关键帧 4.确定动画终点 5.设置终点的位置属性 改变动画速度 1.选中所有关键帧 2.拖拽 时间反向关键帧 1.选中要反向的关键帧 2.使用时间反向关键帧 …

Websocket 替代方案:如何使用 Firestore 监听实时事件

大家好,我是CodeQi! 一位热衷于技术分享的码仔。 ​在现代 Web 开发中,实时更新功能对于许多应用程序(如聊天应用、协作工具和在线游戏)都是必不可少的。虽然 WebSocket 是一种常用的实时通信技术,但 Google 的 Firestore 也提供了一种强大的替代方案,使得实时监听变得…

数字信号处理教程(3)——z变换

在连续时间域中的每一种分析方法&#xff0c;在离散时间域中想必也能得到对应一种分析方法。连续傅里叶变换对应着离散傅里叶变换&#xff08;DFT&#xff09;&#xff0c;而在拉普拉斯变换则是对应着z变换。z变换能够将信号表示成离散复指数函数的线性组合。连续傅里叶变换可以…

运维系列.Nginx配置文件结构功能总结

运维系列 Nginx配置文件结构功能总结 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_285…

泛微E-Cology getFileViewUrl SSRF漏洞复现

0x01 产品简介 泛微协同管理应用平台e-cology是一套兼具企业信息门户、知识文档管理、工作流程管理、人力资源管理、客户关系管理、项目管理、财务管理、资产管理、供应链管理、数据中心功能的企业大型协同管理平台。 0x02 漏洞概述 泛微E-Cology getFileViewUrl 接口处存在…

Java如何自定义注解及在SpringBoot中的应用

注解 注解&#xff08;Annotation&#xff09;&#xff0c;也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性&#xff0c;与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面&#xff0c;用来对这些元素进行说…

灵活多变的对象创建——工厂方法模式(Python实现)

1. 引言 大家好&#xff0c;又见面了&#xff01;在上一篇文章中&#xff0c;我们聊了聊简单工厂模式&#xff0c;今天&#xff0c;我们要进一步探讨一种更加灵活的工厂设计模式——工厂方法模式。如果说简单工厂模式是“万能钥匙”&#xff0c;那工厂方法模式就是“变形金刚”…

Sorted Set 类型命令(命令语法、操作演示、命令返回值、时间复杂度、注意事项)

Sorted Set 类型 文章目录 Sorted Set 类型zadd 命令zrange 命令zcard 命令zcount 命令zrevrange 命令zrangebyscore 命令zpopmax 命令bzpopmax 命令zpopmin 命令bzpopmin 命令zrank 命令zscore 命令zrem 命令zremrangebyrank 命令zremrangebyscore 命令zincrby 命令zinterstor…

干货:高水平论文写作思路与方法

前言:Hello大家好,我是小哥谈。高水平论文的写作需要扎实的研究基础和严谨的思维方式。同时,良好的写作技巧和时间管理也是成功的关键。本篇文章转载自行业领域专家所写的一篇文章,希望大家阅读后可以能够有所收获。🌈 目录 🚀1.依托事实/证据,通过合理的逻辑,…

【MindSpore学习打卡】应用实践-热门LLM及其他AI应用-使用MindSpore实现K近邻算法对红酒数据集进行聚类分析

在机器学习领域&#xff0c;K近邻算法&#xff08;K-Nearest Neighbor, KNN&#xff09;是最基础且常用的算法之一。无论是分类任务还是回归任务&#xff0c;KNN都能通过简单直观的方式实现高效的预测。在这篇博客中&#xff0c;我们将基于MindSpore框架&#xff0c;使用KNN算法…

for nested data item, row-key is required.报错解决

今天差点被一个不起眼的bug搞到吐&#xff0c;就是在给表格设置row-key的时候&#xff0c;一直设置不成功&#xff0c;一直报错缺少row-key&#xff0c;一共就那两行代码 实在是找不到还存在什么问题... 先看下报错截图... 看下代码 我在展开行里面用到了一个表格 并且存放表格…