【问题标题】:Animated GIF on Fabric.js CanvasFabric.js 画布上的动画 GIF
【发布时间】:2015-03-19 08:20:51
【问题描述】:

我正在做一个项目,我被要求在 fabric.js 画布上支持动画 GIF。

根据https://github.com/kangax/fabric.js/issues/560,我已按照建议使用fabric.util.requestAnimFrame 定期渲染。使用此方法可以很好地呈现视频,但 GIF 似乎没有更新。

var canvas = new fabric.StaticCanvas(document.getElementById('stage'));

fabric.util.requestAnimFrame(function render() {
    canvas.renderAll();
    fabric.util.requestAnimFrame(render);
});

var myGif = document.createElement('img');
myGif.src = 'http://i.stack.imgur.com/e8nZC.gif';

if(myGif.height > 0){
    addImgToCanvas(myGif);
} else {
    myGif.onload = function(){
        addImgToCanvas(myGif);
    }
}

function addImgToCanvas(imgToAdd){
    var obj = new fabric.Image(imgToAdd, {
        left: 105,
        top: 30,
        crossOrigin: 'anonymous',
        height: 100,
        width:100
    }); 
    canvas.add(obj);
}

这里的JSFiddle:http://jsfiddle.net/phoenixrizin/o359o11f/

任何建议将不胜感激!我一直在到处寻找,但没有找到可行的解决方案。

【问题讨论】:

  • 找到好的解决方案了吗?
  • @3244611user 我最终选择了一条不同的路线并使用了非画布解决方案,因为我找不到这个问题的答案。
  • 我也有同样的问题。请问@PhoenixRizin 你最后选择了什么方法?
  • @fongoh-martin 查看我之前的回复。没有进一步追求。

标签: javascript canvas fabricjs animated-gif


【解决方案1】:

根据to specs关于Canvas 2DRenderingContextdrawImage方法,

具体来说,当 CanvasImageSource 对象表示动画时 HTMLImageElement 中的图像,用户代理必须使用默认值 动画的图像(使用格式定义的图像 当不支持或禁用动画时),或者,如果没有 这样的图像,动画的第一帧,在渲染图像时 用于 CanvasRenderingContext2D API。

这意味着我们只会在画布上绘制动画画布的第一帧。
这是因为我们无法控制 img 标签内的动画。

而fabricjs是基于canvas API的,因此受同样的规则监管。

然后解决方案是从您的动画 gif 中解析所有静止图像并将其导出为 sprite-sheet。 借助sprite class,您可以轻松地在fabricjs 中对其进行动画处理。

【讨论】:

    【解决方案2】:

    var canvas = new fabric.Canvas(document.getElementById('stage'));
    var url = 'https://themadcreator.github.io/gifler/assets/gif/run.gif';
    fabric.Image.fromURL(url, function(img) {
      img.scaleToWidth(80);
      img.scaleToHeight(80);
      img.left = 105;
      img.top = 30;
      gif(url, function(frames, delay) {
        var framesIndex = 0,
          animInterval;
        img.dirty = true;
        img._render = function(ctx) {
          ctx.drawImage(frames[framesIndex], -this.width / 2, -this.height / 2, this.width, this.height);
        }
        img.play = function() {
          if (typeof(animInterval) === 'undefined') {
            animInterval = setInterval(function() {
              framesIndex++;
              if (framesIndex === frames.length) {
                framesIndex = 0;
              }
            }, delay);
          }
        }
        img.stop = function() {
          clearInterval(animInterval);
          animInterval = undefined;
        }
        img.play();
        canvas.add(img);
      })
    
    })
    
    
    function gif(url, callback) {
    
      var tempCanvas = document.createElement('canvas');
      var tempCtx = tempCanvas.getContext('2d');
    
      var gifCanvas = document.createElement('canvas');
      var gifCtx = gifCanvas.getContext('2d');
    
      var imgs = [];
    
    
      var xhr = new XMLHttpRequest();
      xhr.open('get', url, true);
      xhr.responseType = 'arraybuffer';
      xhr.onload = function() {
        var tempBitmap = {};
        tempBitmap.url = url;
        var arrayBuffer = xhr.response;
        if (arrayBuffer) {
          var gif = new GIF(arrayBuffer);
          var frames = gif.decompressFrames(true);
          gifCanvas.width = frames[0].dims.width;
          gifCanvas.height = frames[0].dims.height;
    
          for (var i = 0; i < frames.length; i++) {
            createFrame(frames[i]);
          }
          callback(imgs, frames[0].delay);
        }
    
      }
      xhr.send(null);
    
      var disposalType;
    
      function createFrame(frame) {
        if (!disposalType) {
          disposalType = frame.disposalType;
        }
    
        var dims = frame.dims;
    
        tempCanvas.width = dims.width;
        tempCanvas.height = dims.height;
        var frameImageData = tempCtx.createImageData(dims.width, dims.height);
    
        frameImageData.data.set(frame.patch);
    
        if (disposalType !== 1) {
          gifCtx.clearRect(0, 0, gifCanvas.width, gifCanvas.height);
        }
    
        tempCtx.putImageData(frameImageData, 0, 0);
        gifCtx.drawImage(tempCanvas, dims.left, dims.top);
        var dataURL = gifCanvas.toDataURL('image/png');
        var tempImg = fabric.util.createImage();
        tempImg.src = dataURL;
        imgs.push(tempImg);
      }
    }
    render()
    
    function render() {
      if (canvas) {
        canvas.renderAll();
      }
    
      fabric.util.requestAnimFrame(render);
    }
    #stage {
      border: solid 1px #CCCCCC;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.13/fabric.min.js"></script>
    <script src="http://matt-way.github.io/gifuct-js/bower_components/gifuct-js/dist/gifuct-js.js"></script>
    <canvas id="stage" height="160" width="320"></canvas>

    【讨论】:

      【解决方案3】:

      这是我的实现,对于小 Gif 非常有效,对于较大的 Gif 则不太好(内存限制)。

      现场演示:https://codesandbox.io/s/red-flower-27i85

      使用两个文件/方法

      1 . gifToSprite.js:将带有gifuct-js 库的gif 导入、解析和解压缩到帧,创建精灵表返回其dataURL。您可以设置maxWidthmaxHeight 来缩放 gif 和maxDuration,以毫秒为单位减少帧数。

      import { parseGIF, decompressFrames } from "gifuct-js";
      
      /**
       * gifToSprite "async"
       * @param {string|input File} gif can be a URL, dataURL or an "input File"
       * @param {number} maxWidth Optional, scale to maximum width
       * @param {number} maxHeight Optional, scale to maximum height
       * @param {number} maxDuration Optional, in milliseconds reduce the gif frames to a maximum duration, ex: 2000 for 2 seconds
       * @returns {*} {error} object if any or a sprite sheet of the converted gif as dataURL
       */
      export const gifToSprite = async (gif, maxWidth, maxHeight, maxDuration) => {
        let arrayBuffer;
        let error;
        let frames;
      
        // if the gif is an input file, get the arrayBuffer with FileReader
        if (gif.type) {
          const reader = new FileReader();
          try {
            arrayBuffer = await new Promise((resolve, reject) => {
              reader.onload = () => resolve(reader.result);
              reader.onerror = () => reject(reader.error);
              reader.readAsArrayBuffer(gif);
            });
          } catch (err) {
            error = err;
          }
        }
        // else the gif is a URL or a dataUrl, fetch the arrayBuffer
        else {
          try {
        arrayBuffer = await fetch(gif).then((resp) => resp.arrayBuffer());
          } catch (err) {
            error = err;
          }
        }
      
        // Parse and decompress the gif arrayBuffer to frames with the "gifuct-js" library
        if (!error) frames = decompressFrames(parseGIF(arrayBuffer), true);
        if (!error && (!frames || !frames.length)) error = "No_frame_error";
        if (error) {
          console.error(error);
          return { error };
        }
      
        // Create the needed canvass
        const dataCanvas = document.createElement("canvas");
        const dataCtx = dataCanvas.getContext("2d");
        const frameCanvas = document.createElement("canvas");
        const frameCtx = frameCanvas.getContext("2d");
        const spriteCanvas = document.createElement("canvas");
        const spriteCtx = spriteCanvas.getContext("2d");
      
        // Get the frames dimensions and delay
        let [width, height, delay] = [
          frames[0].dims.width,
          frames[0].dims.height,
          frames.reduce((acc, cur) => (acc = !acc ? cur.delay : acc), null)
        ];
      
        // Set the Max duration of the gif if any
        // FIXME handle delay for each frame
        const duration = frames.length * delay;
        maxDuration = maxDuration || duration;
        if (duration > maxDuration) frames.splice(Math.ceil(maxDuration / delay));
      
        // Set the scale ratio if any
        maxWidth = maxWidth || width;
        maxHeight = maxHeight || height;
        const scale = Math.min(maxWidth / width, maxHeight / height);
        width = width * scale;
        height = height * scale;
      
        //Set the frame and sprite canvass dimensions
        frameCanvas.width = width;
        frameCanvas.height = height;
        spriteCanvas.width = width * frames.length;
        spriteCanvas.height = height;
      
        frames.forEach((frame, i) => {
          // Get the frame imageData from the "frame.patch"
          const frameImageData = dataCtx.createImageData(
            frame.dims.width,
            frame.dims.height
          );
          frameImageData.data.set(frame.patch);
          dataCanvas.width = frame.dims.width;
          dataCanvas.height = frame.dims.height;
          dataCtx.putImageData(frameImageData, 0, 0);
      
          // Draw a frame from the imageData
          if (frame.disposalType === 2) frameCtx.clearRect(0, 0, width, height);
          frameCtx.drawImage(
            dataCanvas,
            frame.dims.left * scale,
            frame.dims.top * scale,
            frame.dims.width * scale,
            frame.dims.height * scale
          );
      
          // Add the frame to the sprite sheet
          spriteCtx.drawImage(frameCanvas, width * i, 0);
        });
      
        // Get the sprite sheet dataUrl
        const dataUrl = spriteCanvas.toDataURL();
      
        // Clean the dom, dispose of the unused canvass
        dataCanvas.remove();
        frameCanvas.remove();
        spriteCanvas.remove();
      
        return {
          dataUrl,
          frameWidth: width,
          framesLength: frames.length,
          delay
        };
      };
      

      2 。 fabricGif.js:主要是对gifToSprite的一个包装器,取相同的参数返回fabric.Image的实例,重写_render方法在每次延迟后重绘画布,在playpause中添加三个方法,和stop

      import { fabric } from "fabric";
      import { gifToSprite } from "./gifToSprite";
      
      const [PLAY, PAUSE, STOP] = [0, 1, 2];
      
      /**
       * fabricGif "async"
       * Mainly a wrapper for gifToSprite
       * @param {string|File} gif can be a URL, dataURL or an "input File"
       * @param {number} maxWidth Optional, scale to maximum width
       * @param {number} maxHeight Optional, scale to maximum height
       * @param {number} maxDuration Optional, in milliseconds reduce the gif frames to a maximum duration, ex: 2000 for 2 seconds
       * @returns {*} {error} object if any or a 'fabric.image' instance of the gif with new 'play', 'pause', 'stop' methods
       */
      export const fabricGif = async (gif, maxWidth, maxHeight, maxDuration) => {
        const { error, dataUrl, delay, frameWidth, framesLength } = await gifToSprite(
          gif,
          maxWidth,
          maxHeight,
          maxDuration
        );
      
        if (error) return { error };
      
        return new Promise((resolve) => {
          fabric.Image.fromURL(dataUrl, (img) => {
            const sprite = img.getElement();
            let framesIndex = 0;
            let start = performance.now();
            let status;
      
            img.width = frameWidth;
            img.height = sprite.naturalHeight;
            img.mode = "image";
            img.top = 200;
            img.left = 200;
      
            img._render = function (ctx) {
              if (status === PAUSE || (status === STOP && framesIndex === 0)) return;
              const now = performance.now();
              const delta = now - start;
              if (delta > delay) {
                start = now;
                framesIndex++;
              }
              if (framesIndex === framesLength || status === STOP) framesIndex = 0;
              ctx.drawImage(
                sprite,
                frameWidth * framesIndex,
                0,
                frameWidth,
                sprite.height,
                -this.width / 2,
                -this.height / 2,
                frameWidth,
                sprite.height
              );
            };
            img.play = function () {
              status = PLAY;
              this.dirty = true;
            };
            img.pause = function () {
              status = PAUSE;
              this.dirty = false;
            };
            img.stop = function () {
              status = STOP;
              this.dirty = false;
            };
            img.getStatus = () => ["Playing", "Paused", "Stopped"][status];
      
            img.play();
            resolve(img);
          });
        });
      };
      

      3 .实施:

      import { fabric } from "fabric";
      import { fabricGif } from "./fabricGif";
      
      async function init() {
        const c = document.createElement("canvas");
        document.querySelector("body").append(c)
        const canvas = new fabric.Canvas(c);
        canvas.setDimensions({
          width: window.innerWidth,
          height: window.innerHeight
        });
      
        const gif = await fabricGif(
          "https://media.giphy.com/media/11RwocOdukxqN2/giphy.gif",
          200,
          200
        );
        gif.set({ top: 50, left: 50 });
        canvas.add(gif);
      
        fabric.util.requestAnimFrame(function render() {
          canvas.renderAll();
          fabric.util.requestAnimFrame(render);
        });
      }
      
      init();
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-03-04
        • 2018-03-15
        • 2021-05-17
        • 1970-01-01
        • 2017-05-17
        • 2013-01-06
        • 1970-01-01
        • 2021-06-24
        相关资源
        最近更新 更多