我正在处理一个项目,我已经要求它在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/
任何建议将不胜感激!我到处都在搜索,但还没有找到合适的解决方案。
答案 0 :(得分:1)
根据to specs关于Canvas 2DRenderingContext drawImage
方法,
具体来说,当 CanvasImageSource 对象表示动画时 在 HTMLImageElement 中的图像,用户代理必须使用默认值 动画的图像(格式定义的图像将被使用 当动画不受支持或被禁用时),或者,如果没有 这样的图像,即动画的第一帧,在渲染图像时 for CanvasRenderingContext2D API。
这意味着只会在画布上绘制动画画布的第一帧 这是因为我们对img标签内的动画没有任何控制权。
Fabricjs基于canvas API,因此受相同规则的约束。
然后解决方案是解析动画gif中的所有静止图像并将其导出为精灵表。 然后,由于sprite class,您可以在fabricjs中轻松设置动画。
答案 1 :(得分:0)
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>
答案 2 :(得分:0)
这是我的实现,使用小Gif时非常有效,而使用大Gif(内存限制)时效果不佳。
实时演示:https://codesandbox.io/s/red-flower-27i85
使用两个文件/方法
1。 gifToSprite.js
:使用gifuct-js库将gif导入,解析并解压缩为帧,创建精灵表并返回其dataURL。您可以设置maxWidth
,maxHeight
缩放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
方法以在每次延迟后重绘画布,向其中添加三个方法play
,pause
和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();