純前端如何實現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.pswp.cn/diannao/42867.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/42867.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/42867.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MPC學習資料匯總

模型預測控制MPC學習資料匯總 需要的私信我~ 需要的私信我~ 需要的私信我~ 【01】課件內容 包含本號所有MPC課程的課件&#xff0c;以及相關MATLAB文檔。 【02】課件源代碼 本號所有MPC課程的源代碼。 【03】MPC仿真案例 三個MPC大型仿真案例&#xff1a; 1&#xff09;…

Python面試題:在 Python 中如何進行多線程編程?

在 Python 中進行多線程編程通常使用 threading 模塊。下面是一個簡單的示例&#xff0c;展示了如何創建和啟動多個線程。 示例代碼 import threading import time# 定義一個簡單的函數&#xff0c;它將在線程中運行 def print_numbers():for i in range(10):print(f"Nu…

鏈接器的工作原理,靜態鏈接與動態鏈接的區別,如何創建和使用動態鏈接庫

鏈接器在程序開發中的作用至關重要&#xff0c;它負責將多個目標文件和庫文件整合成一個可以執行的文件。在深入了解鏈接器的工作原理、靜態鏈接與動態鏈接的區別&#xff0c;以及如何創建和使用動態鏈接庫之前&#xff0c;我們先來概述一下鏈接器的基本功能。 鏈接器的工作原…

20240704每日后端------聊聊 mybatis的 where 1=1

目標 最近&#xff0c;在項目中使用MyBatis進行SQL腳本編寫時&#xff0c;我遇到了以“WHERE 11”開頭的WHERE子句的做法&#xff0c;以簡化多個條件的串聯。這里有一個例子來討論這種技術以及“WHERE 11”是否對性能有任何影響。 <select id"" parameterType&q…

【數據結構】09.樹與二叉樹

一、樹的概念與結構 1.1 樹的概念 樹是一種非線性的數據結構&#xff0c;它是由n&#xff08;n>0&#xff09;個有限結點組成一個具有層次關系的集合。把它叫做樹是因為它看起來像一棵倒掛的樹&#xff0c;也就是說它是根朝上&#xff0c;而葉朝下的。 根結點&#xff1a;根…

04采訪:數字人直播

?AI技術的迭代對數字人直播一定是有正向推動作用的。直播可持續性差,投入產出極不協調。不適合前期大量投入。直播現在這個東西有一個問題,因為直播開始帶貨了,就已經不是一個單純的娛樂性質的視頻內容,而是對帶有一種商業目的內容。 直播帶貨的痛點:對主播而言是觀眾;…

俯臥撐計數器(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…

VSCode工程中task.json的作用

在 Visual Studio Code&#xff08;VSCode&#xff09;中&#xff0c;tasks.json 文件是用來定義和配置任務&#xff08;Tasks&#xff09;的。任務指的是在開發過程中需要自動化執行的一系列操作&#xff0c;例如編譯代碼、運行測試、打包項目等。通過配置 tasks.json&#xf…

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…

*算法訓練(leetcode)第二十五天 | 134. 加油站、135. 分發糖果、860. 檸檬水找零、406. 根據身高重建隊列

刷題記錄 134. 加油站135. 分發糖果860. 檸檬水找零406. 根據身高重建隊列 134. 加油站 leetcode題目地址 記錄全局剩余油量和當前剩余油量&#xff0c;當前剩余小于0時&#xff0c;其實位置是當前位置的后一個位置。若全局剩余油量為負&#xff0c;則說明整體油量不足以走完…

防爆手機終端安全管理平臺

防爆手機終端安全管理平臺能夠滿足國家能源、化工企業對安全生產信息化運行需求&#xff0c;能夠快速搭建起高效、快捷的移動終端管理平臺&#xff0c;提高企業安全生產管理水平&#xff0c;保證企業的安全運行和可持續發展。#防爆手機 #終端安全 #移動安全 能源、化工等生產單…

公有鏈、私有鏈與聯盟鏈:區塊鏈技術的多元化應用與比較

引言 區塊鏈技術自2008年比特幣白皮書發布以來&#xff0c;迅速發展成為一項具有顛覆性潛力的技術。區塊鏈通過去中心化、不可篡改和透明的方式&#xff0c;提供了一種全新的數據存儲和管理方式。起初&#xff0c;區塊鏈主要應用于加密貨幣&#xff0c;如比特幣和以太坊。然而&…

SQL Server 設置端口詳解

前言 在數據庫管理和開發過程中&#xff0c;SQL Server是一個廣泛使用的關系型數據庫管理系統。默認情況下&#xff0c;SQL Server使用1433端口進行通信。然而&#xff0c;出于安全性、端口沖突或網絡限制等原因&#xff0c;我們有時需要更改SQL Server的默認端口。本文將詳細…

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()去…

易備數據備份軟件——低成本、高效能、全方位地守護您的數據安全

在數字化的時代&#xff0c;數據是企業和個人最寶貴的資產。然而&#xff0c;數據丟失、系統故障、惡意攻擊等威脅時刻存在。如何確保數據的安全與完整&#xff1f;易備數據備份軟件為您提供全方位無死角的解決方案&#xff0c;讓您高枕無憂&#xff01; 云備份&#xff1a;暢…

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-…

079、類的繼承

繼承是對已有的類進行擴展創建出新的類&#xff0c;這個過程就叫做繼承。其中&#xff0c;提供繼承信息的類叫做父類&#xff08;超類、基類&#xff09;&#xff0c;得到繼承信息的類稱為子類&#xff08;派生類&#xff09;。 基本語法 繼承是通過在類定義語句中使用圓括號…