效果大概这样

(图中用来播放的gif图来源于贴吧。如果你觉得侵权请私信我,我立刻下架)

     canvas这个东西只能渲染静态图片,不能渲染动图。即便我们用常规的加载图片的办法去绘制gif。我们也只能绘制他的第一帧。不能动。所以想在 canvas 上渲染 gif,我们必须拿到 gif 的每一帧和每一帧播放的时间。于是我找到一个 gif 的控制的库 --  libgif.js (点击跳转)

观察他的element后发现他就是用 canvas 来进行播放 gif 的。不过他的用法是先得有一个 img元素 然后创建 SuperGif对象 来进行控制。这不是很符合我的需求。我是想要直接读取路径直接把 gif 在 canvas渲染出来。于是花了半天把源码看了下。然后把核心代码整了出来稍加修改。改为就读路径就渲染gif

大概思路就是 xhr 请求文件 ---- 解析gif ---- 把每一帧的图像和播放时间存在 FRAME_LIST 里面。最后用 setInterval 来进行播放。 因为我们每一帧和播放时间都有了。所以 无论是播放、倍速、暂停、切换 上/下一帧都能轻松实现。我这里只给到播放,暂停之类的大家可以自己扩展。

直接用 loadGIF 方法就会自己加载且自动播放。

 loadGIF("./example_gifs/fff.gif");

播放方法是 playGif。调用  playGif 方法地方就是加载结束的地方。FRAME_LIST 这个全局变量就是存放当前gif所有帧的数组。扩展请在这些地方扩展。其他的地方你要动的话,请三思。毕竟我把源码拿过来后我自己也改了(欸嘿)。

下面是源码:(运行的时候注意,因为读本地文件肯定会存在跨域问题,直接跑铁不行。如要运行,请整为同源 ps:可以看上面效果图的url地址。例如你用vscode 跑的话,可以装一个 Live Server 这样的插件来运行总之方法很多。)

<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><canvas id="canvas" width="800" height="600" style="background-color: antiquewhite;"></canvas><script>var CANVAS = document.getElementById("canvas");var CTX = CANVAS.getContext('2d');var FRAME_LIST = []; // 存放每一帧以及对应的延时var TEMP_CANVAS = document.createElement("canvas");//用来拿 imagedata 的工具人var TEMP_CANVAS_CTX = null// 工具人的 getContext('2d')var GIF_INFO = {}; // GIF 的一些信息var STREAM = null;var LAST_DISPOSA_METHOD = null;var CURRENT_FRAME_INDEX = -1; //当前帧的下标var TRANSPARENCY = null;var DELAY = 0; // 当前帧的时间class Stream {constructor(data) {this.data = data;this.len = data.length;this.pos = 0;}readByte() {if (this.pos >= this.data.length) {throw new Error('Attempted to read past end of stream.');}if (this.data instanceof Uint8Array)return this.data[this.pos++];elsereturn this.data.charCodeAt(this.pos++) & 0xFF;};readBytes(n) {let bytes = [];for (let i = 0; i < n; i++) {bytes.push(this.readByte());}return bytes;};read(n) {let chars = '';for (let i = 0; i < n; i++) {chars += String.fromCharCode(this.readByte());}return chars;};readUnsigned() { // Little-endian.let unsigned = this.readBytes(2);return (unsigned[1] << 8) + unsigned[0];};};// 转流数组function byteToBitArr(bite) {let byteArr = [];for (let i = 7; i >= 0; i--) {byteArr.push(!!(bite & (1 << i)));}return byteArr;};// Generic functionsfunction bitsToNum(ba) {return ba.reduce(function (s, n) {return s * 2 + n;}, 0);};function lzwDecode(minCodeSize, data) {// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?let pos = 0; // Maybe this streaming thing should be merged with the Stream?function readCode(size) {let code = 0;for (let i = 0; i < size; i++) {if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {code |= 1 << i;}pos++;}return code;};let output = [],clearCode = 1 << minCodeSize,eoiCode = clearCode + 1,codeSize = minCodeSize + 1,dict = [];function clear() {dict = [];codeSize = minCodeSize + 1;for (let i = 0; i < clearCode; i++) {dict[i] = [i];}dict[clearCode] = [];dict[eoiCode] = null;};let code = null, last = null;while (true) {last = code;code = readCode(codeSize);if (code === clearCode) {clear();continue;}if (code === eoiCode) {break};if (code < dict.length) {if (last !== clearCode) {dict.push(dict[last].concat(dict[code][0]));}}else {if (code !== dict.length) {throw new Error('Invalid LZW code.');}dict.push(dict[last].concat(dict[last][0]));}output.push.apply(output, dict[code]);if (dict.length === (1 << codeSize) && codeSize < 12) {// If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.codeSize++;}}return output;};function readSubBlocks() {let size = null, data = '';do {size = STREAM.readByte();data += STREAM.read(size);} while (size !== 0);return data;};function doImg(img) {if (!TEMP_CANVAS_CTX) {// 没有就创建TEMP_CANVAS_CTX = TEMP_CANVAS.getContext('2d');}const currIdx = FRAME_LIST.length,ct = img.lctFlag ? img.lct : GIF_INFO.gct;/*LAST_DISPOSA_METHOD indicates the way in which the graphic is tobe treated after being displayed.Values :    0 - No disposal specified. The decoder isnot required to take any action.1 - Do not dispose. The graphic is to be leftin place.2 - Restore to background color. The area used by thegraphic must be restored to the background color.3 - Restore to previous. The decoder is required torestore the area overwritten by the graphic withwhat was there prior to rendering the graphic.Importantly, "previous" means the frame stateafter the last disposal of method 0, 1, or 2.*/if (currIdx > 0) {// 这块不要动if (LAST_DISPOSA_METHOD === 3) {// Restore to previous// If we disposed every TEMP_CANVAS_CTX including first TEMP_CANVAS_CTX up to this point, then we have// no composited TEMP_CANVAS_CTX to restore to. In this case, restore to background instead.if (CURRENT_FRAME_INDEX !== null && CURRENT_FRAME_INDEX > -1) {TEMP_CANVAS_CTX.putImageData(FRAME_LIST[CURRENT_FRAME_INDEX].data, 0, 0);} else {TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);}} else {CURRENT_FRAME_INDEX = currIdx - 1;}if (LAST_DISPOSA_METHOD === 2) {// Restore to background color// Browser implementations historically restore to transparent; we do the same.// http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);}}let imgData = TEMP_CANVAS_CTX.getImageData(img.leftPos, img.topPos, img.width, img.height);img.pixels.forEach(function (pixel, i) {if (pixel !== TRANSPARENCY) {imgData.data[i * 4 + 0] = ct[pixel][0];imgData.data[i * 4 + 1] = ct[pixel][1];imgData.data[i * 4 + 2] = ct[pixel][2];imgData.data[i * 4 + 3] = 255; // Opaque.}});TEMP_CANVAS_CTX.putImageData(imgData, img.leftPos, img.topPos);// 补充第1帧// if(currIdx == 0){//     pushFrame(DELAY);// }};function pushFrame(delay) {if (!TEMP_CANVAS_CTX) {return};FRAME_LIST.push({ delay, data: TEMP_CANVAS_CTX.getImageData(0, 0, GIF_INFO.width, GIF_INFO.height) });};// 解析function parseExt(block) {function parseGCExt(block) {STREAM.readByte(); // Always 4 var bits = byteToBitArr(STREAM.readByte());block.reserved = bits.splice(0, 3); // Reserved; should be 000.block.disposalMethod = bitsToNum(bits.splice(0, 3));LAST_DISPOSA_METHOD = block.disposalMethodblock.userInput = bits.shift();block.transparencyGiven = bits.shift();block.delayTime = STREAM.readUnsigned();DELAY = block.delayTime;block.transparencyIndex = STREAM.readByte();block.terminator = STREAM.readByte();pushFrame(block.delayTime);TRANSPARENCY = block.transparencyGiven ? block.transparencyIndex : null;};function parseComExt(block) {block.comment = readSubBlocks();};function parsePTExt(block) {// No one *ever* uses this. If you use it, deal with parsing it yourself.STREAM.readByte(); // Always 12 这个必须得这样执行一次block.ptHeader = STREAM.readBytes(12);block.ptData = readSubBlocks();};function parseAppExt(block) {var parseNetscapeExt = function (block) {STREAM.readByte(); // Always 3 这个必须得这样执行一次block.unknown = STREAM.readByte(); // ??? Always 1? What is this?block.iterations = STREAM.readUnsigned();block.terminator = STREAM.readByte();};function parseUnknownAppExt(block) {block.appData = readSubBlocks();// FIXME: This won't work if a handler wants to match on any identifier.// handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);};STREAM.readByte(); // Always 11 这个必须得这样执行一次block.identifier = STREAM.read(8);block.authCode = STREAM.read(3);switch (block.identifier) {case 'NETSCAPE':parseNetscapeExt(block);break;default:parseUnknownAppExt(block);break;}};function parseUnknownExt(block) {block.data = readSubBlocks();};block.label = STREAM.readByte();switch (block.label) {case 0xF9: block.extType = 'gce';parseGCExt(block);break;case 0xFE:block.extType = 'com';parseComExt(block);break;case 0x01:block.extType = 'pte';parsePTExt(block);break;case 0xFF:block.extType = 'app';parseAppExt(block);break;default:block.extType = 'unknown';parseUnknownExt(block);break;}};function parseImg(img) {function deinterlace(pixels, width) {// Of course this defeats the purpose of interlacing. And it's *probably*// the least efficient way it's ever been implemented. But nevertheless...let newPixels = new Array(pixels.length);const rows = pixels.length / width;function cpRow(toRow, fromRow) {const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));};// See appendix E.const offsets = [0, 4, 2, 1],steps = [8, 8, 4, 2];let fromRow = 0;for (var pass = 0; pass < 4; pass++) {for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {cpRow(toRow, fromRow)fromRow++;}}return newPixels;};img.leftPos = STREAM.readUnsigned();img.topPos = STREAM.readUnsigned();img.width = STREAM.readUnsigned();img.height = STREAM.readUnsigned();let bits = byteToBitArr(STREAM.readByte());img.lctFlag = bits.shift();img.interlaced = bits.shift();img.sorted = bits.shift();img.reserved = bits.splice(0, 2);img.lctSize = bitsToNum(bits.splice(0, 3));if (img.lctFlag) {img.lct = parseCT(1 << (img.lctSize + 1));}img.lzwMinCodeSize = STREAM.readByte();const lzwData = readSubBlocks();img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);if (img.interlaced) { // Moveimg.pixels = deinterlace(img.pixels, img.width);}doImg(img);};// LZW (GIF-specific)function parseCT(entries) { // Each entry is 3 bytes, for RGB.let ct = [];for (let i = 0; i < entries; i++) {ct.push(STREAM.readBytes(3));}return ct;};function parseHeader() {GIF_INFO.sig = STREAM.read(3);GIF_INFO.ver = STREAM.read(3);if (GIF_INFO.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.GIF_INFO.width = STREAM.readUnsigned();GIF_INFO.height = STREAM.readUnsigned();let bits = byteToBitArr(STREAM.readByte());GIF_INFO.gctFlag = bits.shift();GIF_INFO.colorRes = bitsToNum(bits.splice(0, 3));GIF_INFO.sorted = bits.shift();GIF_INFO.gctSize = bitsToNum(bits.splice(0, 3));GIF_INFO.bgColor = STREAM.readByte();GIF_INFO.pixelAspectRatio = STREAM.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64if (GIF_INFO.gctFlag) {GIF_INFO.gct = parseCT(1 << (GIF_INFO.gctSize + 1));}// 给 TEMP_CANVAS 设置大小TEMP_CANVAS.width = GIF_INFO.width;TEMP_CANVAS.height = GIF_INFO.height;TEMP_CANVAS.style.width = GIF_INFO.width + 'px';TEMP_CANVAS.style.height = GIF_INFO.height + 'px';TEMP_CANVAS.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);};function parseBlock() {let block = {};block.sentinel = STREAM.readByte();switch (String.fromCharCode(block.sentinel)) { // For ease of matchingcase '!':block.type = 'ext';parseExt(block);break;case ',':block.type = 'img';parseImg(block);break;case ';':block.type = 'eof';pushFrame(DELAY);// 已经结束啦。结束就跑这playGif();break;default:throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.}// 递归if (block.type !== 'eof') {setTimeout(parseBlock, 0);}};// 播放giffunction playGif() {let len = FRAME_LIST.length;let index = 0;function play() {TEMP_CANVAS.getContext("2d").putImageData(FRAME_LIST[index].data, 0, 0);CTX.globalCompositeOperation = "copy";CTX.drawImage(TEMP_CANVAS, 100, 200);index++;if (index >= len) {index = 0;}setTimeout(play,  FRAME_LIST[index].delay * 10);}play();}// 用xhr请求本地文件function loadGIF(url) {const h = new XMLHttpRequest();h.open('GET', url, true);// 浏览器兼容处理if ('overrideMimeType' in h) {h.overrideMimeType('text/plain; charset=x-user-defined');}// old browsers (XMLHttpRequest-compliant)else if ('responseType' in h) {h.responseType = 'arraybuffer';}// IE9 (Microsoft.XMLHTTP-compliant)else {h.setRequestHeader('Accept-Charset', 'x-user-defined');}h.onload = function (e) {if (this.status != 200) {doLoadError('xhr - response');}// emulating response field for IE9if (!('response' in this)) {this.response = new VBArray(this.responseText).toArray().map(String.fromCharCode).join('');}let data = this.response;if (data.toString().indexOf("ArrayBuffer") > 0) {data = new Uint8Array(data);}STREAM = new Stream(data);parseHeader();parseBlock();};h.onerror = function (e) {console.log("摆烂 error", e)};h.send();}// 测试loadGIF("./example_gifs/lll.gif");</script>
</body>
</html>







后记:

看见评论说多gif放一个canvas上播放有问题,我做了个例子放下面

思路是记住多个 FRAME_LIST 然后统一播放

js canvas绘制gif相关推荐

  1. PHP绘制99的棋盘,JS canvas绘制五子棋的棋盘

    本文为大家分享了JS canvas绘制五子棋棋盘的具体代码,供大家参考,具体内容如下 box-shadow:给元素块周边添加阴影效果. 语法:box-shadow: h-shadow v-shadow ...

  2. D3.js + Canvas 绘制组织结构图

    D3.js + Canvas 绘制组织结构图 使用 D3.js 默认的 svg 渲染 D3默认的树状图画图使用的是svg, 比如这个来自D3作者的例子: https://bl.ocks.org/mbo ...

  3. JS canvas绘制进度条

    JS canvas绘制进度条 在前端开发中,我比较喜欢Cavans画布,通过Cavans可以绘制自己想要的东西,在早之前,我通过Canvas播放视频,绘制图片,绘制banner图等,复杂点可以用来做数 ...

  4. js canvas绘制 一箭双心

    使用面向对象方法编程: 博主最近做视频,无奈播放量惨淡,所以..... 一键三连视频私聊获得源码: "面向对象"劝学视频_哔哩哔哩_bilibili

  5. html5绘制股票图形,股票数据分析(五):绘制股票k线图(js+canvas + Python + json)

    本文介绍:利用 js+canvas 绘制股票k线图 HTML5 标签用于绘制图像(通过脚本,通常是 JavaScript). 不过, 元素本身并没有绘制能力(它仅仅是图形的容器) - 您必须使用脚本来 ...

  6. html5canvas下绘制gif,JS+canvas操作gif动图

    这次给大家带来JS+canvas操作gif动图,JS+canvas操作gif动图的注意事项有哪些,下面就是实战案例,一起来看一下. HTML5 canvas可以读取图片信息,绘制当前图片.于是可以实现 ...

  7. JS打砖块小游戏 canvas绘制

    html5 canvas绘制打砖块小游戏 源代码 github源码下载地址 项目展示 下面放上主要代码 index.js代码 var game = {canvas : document.getElem ...

  8. js html 卡通 学生,JavaScript+html5 canvas绘制的小人效果

    本文实例讲述了JavaScript+html5 canvas绘制的小人效果.分享给大家供大家参考,具体如下: 运行效果截图如下: index.html代码如下: canvas中的缩放 #canvas ...

  9. vue.js 利用canvas绘制仪表盘圆环进度条-带动画

    vue.js 利用canvas绘制简易仪表盘进度条 html代码 因为动画效果比较消耗性能,所以进度条单独canvas绘制 <template><div class="ci ...

最新文章

  1. 车道线检测--End-to-end Lane Detection through Differentiable Least-Squares Fitting
  2. 如何用python做词云图_科学网—如何用Python做词云?(基础篇视频教程) - 王树义的博文...
  3. C++类里面的哪些成员函数是内联函数?
  4. Linux学习:文件描述符相关函数
  5. PHP实现四种排序-插入排序
  6. SharpMap分析手记
  7. How to create and apply a patch with Git
  8. win10运行Git出现警告 :warning: LF will be replaced by CRLF in ...
  9. jquery validate验证remote时的多状态问题
  10. Golang 参数传递本质
  11. 基于 go-cqhttp 开发的 SSPU的QQ机器人小助手
  12. 没有寻线仪怎么找网线_乱七八糟的网线怎么找?寻线仪来帮你
  13. 联想笔记本加固态后没声音(关于固态那些事)
  14. 语法错误与语义错误(所有语言通用版)
  15. 操作系统-存储器管理实验
  16. 员工被提拔,还是被干掉,就看这3点......
  17. pc系统设计演变_设计师的演变
  18. uni-app 数据循环
  19. 猿创征文|OpenCV编程——计算机视觉的登堂入室
  20. 老娘舅:困于疫情,囿于长三角

热门文章

  1. 【无标题】Python 应用POS信息写入JPG exif
  2. android 4.3 刷机,安卓4.3,oppofind7,刷5.0后卡刷4.3官方
  3. python 类 对象 知乎_GitHub - egrcc/zhihu-python: 获取知乎内容信息,包括问题,答案,用户,收藏夹信息...
  4. flink cdc 初识
  5. Unity 客户端简单框架(手游)
  6. 生活-晒黑后变白的好方法
  7. Python一看就懂系列(一)
  8. 各种排序算法复杂度比较
  9. 101 个 MySQL 的调节和优化的提示
  10. css height为auto的transition过渡效果--max-height