包阅导读总结
1. 关键词:纯前端、GIF、ImageDecoder、暂停播放、倍速滤镜
2. 总结:本文介绍纯前端实现 GIF 暂停、倍速播放等功能。先讲解 WebCodecs API 及其中的 ImageDecoder,通过获取 GIF 关键信息,利用 canvas 和定时器绘制。然后分别阐述实现暂停/播放、倍速和滤镜效果的方法。
3. 主要内容:
– 前言
– 提出 GIF 能否暂停、倍速播放的问题
– ImageDecoder
– 介绍 WebCodecs API 包括编码器和解码器
– 重点讲解 ImageDecoder 可解码图片获取元数据
– 展示获取 GIF 元数据及绘制的代码
– 暂停/播放
– 通过按钮控制状态实现暂停/播放
– 倍速
– 用下拉框改变 factor 实现不同倍速
– 滤镜
– 下拉框选择滤镜,对像素进行变换
– 最后
– 总结本文主要内容,呼吁点赞关注
思维导图:
文章地址:https://juejin.cn/post/7388488504055775268
文章来源:juejin.cn
作者:可乐鸡翅kele
发布时间:2024/7/8 0:17
语言:中文
总字数:1895字
预计阅读时间:8分钟
评分:86分
标签:前端开发,GIF处理,WebCodecs API,ImageDecoder,Canvas
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
GIF
我相信大家都不会陌生,由于它被广泛的支持,所以我们一般用它来做一些简单的动画效果。一般就是设计师弄好了之后,把文件发给我们。然后我们就直接这样使用:
<img src="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
结构,十分简单:
<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) => { imageDecoder.current = new ImageDecoder({ data: imageByteStream, type: "image/gif", }); const imageFrame = await imageDecoder.current.decode({ frameIndex: imageIndex.current, }); const track = imageDecoder.current.tracks.selectedTrack; await renderImage(imageFrame, track); };
这里的 imageIndex
从 0
开始, 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
。
一起来看看效果:
暂停/播放
既然我们能把 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); };
一起来看看效果:
倍速
再来回顾一下渲染下一帧的逻辑:
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>
一起来看看效果:
滤镜
既然我们是拿到每一帧图像的信息到 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
进行像素变换
像素变换如下,更多的像素变换可以参考我的这篇文章——这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; };
一起来看看效果:
最后
以上就是本文的全部内容,主要介绍了 ImageDecoder
解码 GIF
图像之后,再利用 canvas
重新进行渲染。期间也就也可以加上暂停、倍速、滤镜的功能。
如果你觉得有意思的话,点点关注点点赞吧~