纯前端如何实现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-ignoredecodeImage(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-ignoreimageDecoder.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]);// ....<Selectvalue={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);<Selectvalue={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 重新进行渲染。期间也就也可以加上暂停、倍速、滤镜的功能。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MPC学习资料汇总

模型预测控制MPC学习资料汇总 需要的私信我~ 需要的私信我~ 需要的私信我~ 【01】课件内容 包含本号所有MPC课程的课件&#xff0c;以及相关MATLAB文档。 【02】课件源代码 本号所有MPC课程的源代码。 【03】MPC仿真案例 三个MPC大型仿真案例&#xff1a; 1&#xff09;…

【数据结构】09.树与二叉树

一、树的概念与结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 根结点&#xff1a;根…

俯卧撑计数器(Python)

通过 MediaPipe 检测人体姿态&#xff0c;计算俯卧撑角度和计数&#xff0c;并在图像上进行可视化展示 需要有cv2库和mediapipe库 mediapipe库&#xff1a; MediaPipe是Google开源的机器学习框架&#xff0c;用于构建实时音频、视频和多媒体处理应用程序。它提供了一组预训练的…

一文清晰了解HTML

有这样一个txt记事本文件和一张图片&#xff1a; txt文本内容是这样的&#xff1a; <html><head><title>HTML学习</title></head><body><h1>hello HTML</h1><img src"高清修复.png"/></body> </html…

LabVIEW的JKI State Machine

JKI State Machine是一种广泛使用的LabVIEW架构&#xff0c;由JKI公司开发。这种状态机架构在LabVIEW中提供了灵活、可扩展和高效的编程模式&#xff0c;适用于各种复杂的应用场景。JKI State Machine通过状态的定义和切换&#xff0c;实现了程序逻辑的清晰组织和管理&#xff…

In Search of Lost Online Test-time Adaptation: A Survey--论文笔记

论文笔记 资料 1.代码地址 https://github.com/jo-wang/otta_vit_survey 2.论文地址 https://arxiv.org/abs/2310.20199 3.数据集地址 1论文摘要的翻译 本文介绍了在线测试时间适应(online test-time adaptation,OTTA)的全面调查&#xff0c;OTTA是一种专注于使机器学习…

【软件分享】我们都需要会用的ArcGIS10.8和ArcGIS Pro

ArcGIS是地理人必备的地理制图、空间分析常用的工具&#xff0c;读地理&#xff0c;或多或少都会接触到ArcGIS的使用&#xff0c;今天小编要带来的就是ArcGIS10.8软件资源和升级版ArcGIS Pro的软件资源。 软件安装包获取 公众号回复关键词&#xff1a;“ArcGIS"&#xff…

防爆手机终端安全管理平台

防爆手机终端安全管理平台能够满足国家能源、化工企业对安全生产信息化运行需求&#xff0c;能够快速搭建起高效、快捷的移动终端管理平台&#xff0c;提高企业安全生产管理水平&#xff0c;保证企业的安全运行和可持续发展。#防爆手机 #终端安全 #移动安全 能源、化工等生产单…

公有链、私有链与联盟链:区块链技术的多元化应用与比较

引言 区块链技术自2008年比特币白皮书发布以来&#xff0c;迅速发展成为一项具有颠覆性潜力的技术。区块链通过去中心化、不可篡改和透明的方式&#xff0c;提供了一种全新的数据存储和管理方式。起初&#xff0c;区块链主要应用于加密货币&#xff0c;如比特币和以太坊。然而&…

VBA-计时器的数据进行整理

对计时器的数据进行整理 需求原始数据程序步骤VBA程序结果 需求 需要在txt文件中提取出分和秒分别在两列 原始数据 数据结构 计次7 00:01.855 计次6 00:09.028 计次5 00:08.586 计次4 00:08.865 计次3 00:07.371 计次2 00:06.192 计次1 00:05.949 程序步骤 1、利用Trim()去…

CV每日论文--2024.7.4

1、InternLM-XComposer-2.5: A Versatile Large Vision Language Model Supporting Long-Contextual Input and Output 中文标题&#xff1a;InternLM-XComposer-2.5&#xff1a;支持长上下文输入和输出的多功能大视觉语言模型 简介&#xff1a;我们推出了InternLM-XComposer-…

学生护眼台灯哪个牌子实用?值得入手的学生护眼台灯十大排名分析

在这个数码时代&#xff0c;人们对屏幕的依赖程度越来越高&#xff0c;尤其是孩子们。他们不仅在学校里需要长时间盯着教科书&#xff0c;还会在学习和娱乐中使用各种数码设备。然而&#xff0c;这也使得眼睛健康问题逐渐凸显&#xff0c;尤其是儿童近视的问题。为了保护视力&a…

Flink 提交作业的方式

参考&#xff1a; Flink运行方式及对比-腾讯云开发者社区-腾讯云

济南网站建设费用为什么差距如此之大

济南网站建设费用的差距之所以如此之大&#xff0c;主要是由于以下几个因素的影响。 首先&#xff0c;不同的网站建设公司所提供的服务内容和质量不尽相同&#xff0c;这直接导致了费用的差距。一些知名的大型网络公司会提供全方位的网站建设服务&#xff0c;包括网站设计、页面…

ELFK 8.12.2 部署 -- docker部署方式⚽

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

SpringBoot源码阅读(3)——监听器

ApplicationListener类初始化位置 在类SpringApplication的构造方法&#xff0c;第267行 在META-INFO/spring.factories中配置的实现类 spring-boot # Application Listeners org.springframework.context.ApplicationListener\ org.springframework.boot.ClearCachesApplic…

Top级“水刊”!高达10.1分,发文量大,最快1个月左右录用,几乎沾边可录!

本周投稿推荐 SCI • 能源科学类&#xff0c;1.5-2.0&#xff08;来稿即录25天&#xff09; • 计算机类&#xff0c;2.0-3.0&#xff08;纯正刊29天录用&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; 知网 • 7天录用-检索&#xff08;急录友好&…

个性化微课教学视频推荐系统-计算机毕业设计源码77648

个性化微课教学视频推荐系统 摘 要 随着信息技术的迅猛发展&#xff0c;教育领域正经历着前所未有的变革。微课作为一种新兴的教学资源形式&#xff0c;以其短小精悍、针对性强、易于传播等特点&#xff0c;逐渐受到广大师生的青睐。然而&#xff0c;在微课资源日益丰富的今天…

谷粒商城学习笔记-逆向工程错误记录

文章目录 1&#xff0c;Since Maven 3.8.1 http repositories are blocked.1.1 在maven的settings.xml文件中&#xff0c;新增如下配置&#xff1a;1.2&#xff0c;执行clean命令刷新maven配置 2&#xff0c;internal java compiler error3&#xff0c;启动逆向工程报错&#x…

【Linux】网络新手村

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 引言 今天&#xff0c;我们就开始学习Linux网络相关的内容。这篇博客作为Linux网络板块的第一篇博客看&#xff0c;我们首先要带着大家明白Linux网络的一些名词的概念&#xff0c;为之后的学习扫清障碍。然后我…