最近在使用fabric遇到了一个问题,老板让渲染一个gif动图,但是new fabric.Image他只可以渲染gif动图的第一帧,这样就变成了一个静态图,
最近也浏览了各大网站找到了一个方法,那就是使用第三方的gifuct-js库
主要原理:将gif动图解析为一帧一帧的图片,将这些帧图整合成一大张图片,利用fabric.util.requestAnimFrame在每次刷新时,截取一帧图片进行绘制。
gifuct-js这个库的作用就是将gif解析成帧序列图。
效果图
话不多说上代码
安装gifuct-js库
npm install gifuct-js
index.vue
<template>
<canvas id="canvas"></canvas>
</template>
index.js
<script >
import { fabricGif } from "./fabricGif";
//我这里简化写了懂得都懂
mounted() {
this.init()
},
methods: {
async init() {
const canvas = new fabric.Canvas("canvas");
canvas.setDimensions({
width: window.innerWidth,
height: window.innerHeight
});
const gif = await fabricGif(
"https://cdn-icons-gif.flaticon.com/8629/8629614.gif",
200,
200
);
gif1.set({ top: 50, left: 50 });
canvas.add(gif);
fabric.util.requestAnimFrame(function render() {
canvas.renderAll();
fabric.util.requestAnimFrame(render);
});
}
}
</script
fabricGif.js文件
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);
});
});
};
gifToSprite.js 文件
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
};
};