js canvas绘制gif

效果大概这样

                 (图中用来播放的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++];
                else
                    return 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 functions
        function 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 to
            be treated after being displayed.

            Values :    0 - No disposal specified. The decoder is
                            not required to take any action.
                        1 - Do not dispose. The graphic is to be left
                            in place.
                        2 - Restore to background color. The area used by the
                            graphic must be restored to the background color.
                        3 - Restore to previous. The decoder is required to
                            restore the area overwritten by the graphic with
                            what was there prior to rendering the graphic.

                            Importantly, "previous" means the frame state
                            after 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#p86079
                    TEMP_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) {

                pushFrame(DELAY);

                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.disposalMethod
 
                block.userInput = bits.shift();
                block.transparencyGiven = bits.shift();
                block.delayTime = STREAM.readUnsigned();
                DELAY = block.delayTime;
                block.transparencyIndex = STREAM.readByte();
                block.terminator = STREAM.readByte();
                
                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) { // Move
                img.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) / 64
            if (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 matching
                case '!':
                    block.type = 'ext';
                    parseExt(block);
                    break;
                case ',':
                    block.type = 'img';
                    parseImg(block);
                    break;
                case ';':
                    block.type = 'eof';
                    pushFrame(DELAY);
                    // 已经结束啦。结束就跑这
                    playGif();
                    console.log(FRAME_LIST);
                    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);
            }
        };
 
        // 播放gif
        function 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);
                setTimeout(play,  FRAME_LIST[index].delay * 100);
                index++;
                if (index >= len) {
                    index = 0;
                }
            }
            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 IE9
                if (!('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/333.gif");
    </script>
</body>
</html>







后记:

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

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

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
您好!要在Canvas中生成GIF动画,您可以使用一些库或工具来帮助实现此功能。以下是一种常见的方法: 1. 首先,您需要将每一帧的图像绘制Canvas上。您可以使用Canvas API中的`drawImage`方法来加载每一帧的图像。 2. 将每一帧的图像保存为单独的Canvas对象或图像对象。 3. 使用一个库,比如gif.js(https://jnordberg.github.io/gif.js/),它是一个用于在浏览器中生成GIFJavaScript库。您可以通过将每一帧的图像传递给gif.js来创建GIF动画。 4. 通过调用gif.js的方法来设置GIF的参数,例如帧速率、循环等。 5. 调用gif.js的`render`方法来生成GIF动画。 下面是一个简单的示例代码,演示如何使用gif.jsCanvas中生成GIF动画: ```javascript // 创建一个GIF实例 var gif = new GIF({ workers: 2, quality: 10 }); // 获取Canvas元素和上下文 var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); // 循环遍历每一帧 for (var i = 0; i < numFrames; i++) { // 在Canvas绘制当前帧 drawFrame(i); // 将当前帧的Canvas对象添加到GIF实例中 gif.addFrame(canvas, { delay: 200 }); } // 生成GIF动画 gif.render(); // 绘制每一帧的函数 function drawFrame(frameIndex) { // 清除Canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制当前帧 // ... // 示例:在Canvas绘制一个简单的矩形 ctx.fillStyle = "red"; ctx.fillRect(0, 0, canvas.width, canvas.height); } ``` 您需要根据您的具体需求和图像生成逻辑来调整代码。希望对您有所帮助!如果您有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值