Animated GIF on Fabric.js Canvas

前端 未结 3 937
青春惊慌失措
青春惊慌失措 2020-12-16 07:00

I\'m working on a project where I\'ve been asked to support animated GIF on a fabric.js canvas.

As per https://github.com/kangax/fabric.js/issues/560, I\'ve followed

相关标签:
3条回答
  • 2020-12-16 07:04

    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>

    0 讨论(0)
  • 2020-12-16 07:25

    According to specs about the Canvas 2DRenderingContext drawImage method,

    Specifically, when a CanvasImageSource object represents an animated image in an HTMLImageElement, the user agent must use the default image of the animation (the one that the format defines is to be used when animation is not supported or is disabled), or, if there is no such image, the first frame of the animation, when rendering the image for CanvasRenderingContext2D APIs.

    This means that only the first frame of our animated canvas will be drawn on the canvas.
    This is because we don't have any control on animations inside an img tag.

    And fabricjs is based on canvas API and thus regulated by the same rules.

    The solution is then to parse all the still-images from your animated gif and to export it as a sprite-sheet. You can then easily animate it in fabricjs thanks to the sprite class.

    0 讨论(0)
  • 2020-12-16 07:26

    Here is my implementation, very efficient with small Gifs, not so well with larger ones (memory limits).

    live demo : https://codesandbox.io/s/red-flower-27i85

    Using two files/methods

    1 . gifToSprite.js: Import, parse and decompress the gif with gifuct-js library to frames, create the sprite sheet return its dataURL. You can set a maxWidth, maxHeight to scale the gif and a maxDuration in millisecond to reduce the number of frames.

    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: Mainly a wrapper for gifToSprite, take the same parameters return an instance of fabric.Image, override the _render method to redraw the canvas after each delay, add three methods to play, pause, and 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 . Implementation:

    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();
    
    0 讨论(0)
提交回复
热议问题