Fabric.js Canvas 上的动画 GIF
Animated GIF on Fabric.js Canvas
我正在做一个项目,有人要求我在 fabric.js canvas 上支持 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/
任何建议将不胜感激!我一直在到处搜索,但没有找到可行的解决方案。
根据 to specs 关于 Canvas 2DRenderingContext drawImage
方法,
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.
这意味着我们的动画 canvas 只会在 canvas 上绘制第一帧。
这是因为我们无法控制 img 标签内的动画。
而 fabricjs 基于 canvas API,因此受相同的规则约束。
然后解决方案是解析动画 gif 中的所有静止图像并将其导出为 sprite-sheet。
由于 sprite class.
,您可以轻松地在 fabricjs 中对其进行动画处理
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>
这是我的实现,对小 Gif 非常有效,对大 Gif 不太有效(内存限制)。
现场演示:https://codesandbox.io/s/red-flower-27i85
使用两个 files/methods
1。 gifToSprite.js
:使用gifuct-js 库将gif 导入、解析和解压到帧,创建精灵sheet return 其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
的包装器,取相同参数return fabric.Image
的一个实例,覆盖_render
方法重绘canvas每次延迟后,在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();
我们在自己的项目中使用了 答案中的示例,但发现它缺少一些功能并且存在局限性。以下是改进:
- 每帧延迟而不只是第一帧
- 更大的 gif 文件性能更高,而且巨大的 gif 文件不会再因 max canvas size 溢出而崩溃。它现在可以使用相应交换位置的多个精灵
- 移植到 TypeScript
gif.utils.ts
import {parseGIF, decompressFrames, ParsedFrame} from 'gifuct-js';
import fetch from 'node-fetch';
export async function gifToSprites(gif: string | File, maxWidth?: number, maxHeight?: number) {
const arrayBuffer = await getGifArrayBuffer(gif);
const frames = decompressFrames(parseGIF(arrayBuffer), true);
if (!frames[0]) {
throw new Error('No frames found in gif');
}
const totalFrames = frames.length;
// get the frames dimensions and delay
let width = frames[0].dims.width;
let height = frames[0].dims.height;
// 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;
const dataCanvas = document.createElement('canvas');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const dataCtx = dataCanvas.getContext('2d')!;
const frameCanvas = document.createElement('canvas');
frameCanvas.width = width;
frameCanvas.height = height;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const frameCtx = frameCanvas.getContext('2d')!;
// 4096 is the max canvas width in IE
const framesPerSprite = Math.floor(4096 / width);
const totalSprites = Math.ceil(totalFrames / framesPerSprite);
let previousFrame: ParsedFrame | undefined;
const sprites: Array<HTMLCanvasElement> = [];
for (let spriteIndex = 0; spriteIndex < totalSprites; spriteIndex++) {
const framesOffset = framesPerSprite * spriteIndex;
const remainingFrames = totalFrames - framesOffset;
const currentSpriteTotalFrames = Math.min(framesPerSprite, remainingFrames);
const spriteCanvas = document.createElement('canvas');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const spriteCtx = spriteCanvas.getContext('2d')!;
spriteCanvas.width = width * currentSpriteTotalFrames;
spriteCanvas.height = height;
frames.slice(framesOffset, framesOffset + currentSpriteTotalFrames).forEach((frame, i) => {
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);
if (previousFrame?.disposalType === 2) {
const {width, height, left, top} = previousFrame.dims;
frameCtx.clearRect(left, top, width, height);
}
// draw a frame from the imageData
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);
previousFrame = frame;
});
sprites.push(spriteCanvas);
spriteCanvas.remove();
}
// clean the dom, dispose of the unused canvass
dataCanvas.remove();
frameCanvas.remove();
return {
framesPerSprite,
sprites,
frames,
frameWidth: width,
frameHeight: height,
totalFrames
};
}
async function getGifArrayBuffer(gif: string | File): Promise<ArrayBuffer> {
if (typeof gif === 'string') {
return fetch(gif).then((resp) => resp.arrayBuffer());
} else {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(gif);
});
}
}
image.fabric.ts
:
import {gifToSprites} from '../utils/gif.utils';
const [PLAY, PAUSE, STOP] = [0, 1, 2];
export async function fabricGif(
gif: string | File,
maxWidth?: number,
maxHeight?: number
): Promise<{image: fabric.Image}> {
const {framesPerSprite, sprites, frames, frameWidth, frameHeight, totalFrames} =
await gifToSprites(gif, maxWidth, maxHeight);
const frameCanvas = document.createElement('canvas');
frameCanvas.width = frameWidth;
frameCanvas.height = frameHeight;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const frameCtx = frameCanvas.getContext('2d')!;
frameCtx.drawImage(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sprites[0]!,
0,
0,
frameWidth,
frameHeight
);
return new Promise((resolve) => {
window.fabric.Image.fromURL(frameCanvas.toDataURL(), (image) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const firstFrame = frames[0]!;
let framesIndex = 0;
let start = performance.now();
let status: number;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let accumulatedDelay = firstFrame.delay;
image.width = frameWidth;
image.height = frameHeight;
image._render = function (ctx) {
if (status === PAUSE || (status === STOP && framesIndex === 0)) return;
const now = performance.now();
const delta = now - start;
if (delta > accumulatedDelay) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
accumulatedDelay += frames[framesIndex]!.delay;
framesIndex++;
}
if (framesIndex === totalFrames || status === STOP) {
framesIndex = 0;
start = now;
accumulatedDelay = firstFrame.delay;
}
const spriteIndex = Math.floor(framesIndex / framesPerSprite);
ctx.drawImage(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sprites[spriteIndex]!,
frameWidth * (framesIndex % framesPerSprite),
0,
frameWidth,
frameHeight,
-frameWidth / 2,
-frameHeight / 2,
frameWidth,
frameHeight
);
};
const methods = {
play: () => {
status = PLAY;
image.dirty = true;
},
pause: () => {
status = PAUSE;
image.dirty = false;
},
stop: () => {
status = STOP;
image.dirty = false;
},
getStatus: () => ['Playing', 'Paused', 'Stopped'][status]
};
methods.play();
resolve({
...methods,
image
});
});
});
}
实现还是一样
感谢@Fennec 提供原始代码,希望这些对您也有用。
我正在做一个项目,有人要求我在 fabric.js canvas 上支持 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/
任何建议将不胜感激!我一直在到处搜索,但没有找到可行的解决方案。
根据 to specs 关于 Canvas 2DRenderingContext drawImage
方法,
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.
这意味着我们的动画 canvas 只会在 canvas 上绘制第一帧。
这是因为我们无法控制 img 标签内的动画。
而 fabricjs 基于 canvas API,因此受相同的规则约束。
然后解决方案是解析动画 gif 中的所有静止图像并将其导出为 sprite-sheet。 由于 sprite class.
,您可以轻松地在 fabricjs 中对其进行动画处理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>
这是我的实现,对小 Gif 非常有效,对大 Gif 不太有效(内存限制)。
现场演示:https://codesandbox.io/s/red-flower-27i85
使用两个 files/methods
1。 gifToSprite.js
:使用gifuct-js 库将gif 导入、解析和解压到帧,创建精灵sheet return 其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
的包装器,取相同参数return fabric.Image
的一个实例,覆盖_render
方法重绘canvas每次延迟后,在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();
我们在自己的项目中使用了
- 每帧延迟而不只是第一帧
- 更大的 gif 文件性能更高,而且巨大的 gif 文件不会再因 max canvas size 溢出而崩溃。它现在可以使用相应交换位置的多个精灵
- 移植到 TypeScript
gif.utils.ts
import {parseGIF, decompressFrames, ParsedFrame} from 'gifuct-js'; import fetch from 'node-fetch'; export async function gifToSprites(gif: string | File, maxWidth?: number, maxHeight?: number) { const arrayBuffer = await getGifArrayBuffer(gif); const frames = decompressFrames(parseGIF(arrayBuffer), true); if (!frames[0]) { throw new Error('No frames found in gif'); } const totalFrames = frames.length; // get the frames dimensions and delay let width = frames[0].dims.width; let height = frames[0].dims.height; // 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; const dataCanvas = document.createElement('canvas'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const dataCtx = dataCanvas.getContext('2d')!; const frameCanvas = document.createElement('canvas'); frameCanvas.width = width; frameCanvas.height = height; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const frameCtx = frameCanvas.getContext('2d')!; // 4096 is the max canvas width in IE const framesPerSprite = Math.floor(4096 / width); const totalSprites = Math.ceil(totalFrames / framesPerSprite); let previousFrame: ParsedFrame | undefined; const sprites: Array<HTMLCanvasElement> = []; for (let spriteIndex = 0; spriteIndex < totalSprites; spriteIndex++) { const framesOffset = framesPerSprite * spriteIndex; const remainingFrames = totalFrames - framesOffset; const currentSpriteTotalFrames = Math.min(framesPerSprite, remainingFrames); const spriteCanvas = document.createElement('canvas'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const spriteCtx = spriteCanvas.getContext('2d')!; spriteCanvas.width = width * currentSpriteTotalFrames; spriteCanvas.height = height; frames.slice(framesOffset, framesOffset + currentSpriteTotalFrames).forEach((frame, i) => { 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); if (previousFrame?.disposalType === 2) { const {width, height, left, top} = previousFrame.dims; frameCtx.clearRect(left, top, width, height); } // draw a frame from the imageData 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); previousFrame = frame; }); sprites.push(spriteCanvas); spriteCanvas.remove(); } // clean the dom, dispose of the unused canvass dataCanvas.remove(); frameCanvas.remove(); return { framesPerSprite, sprites, frames, frameWidth: width, frameHeight: height, totalFrames }; } async function getGifArrayBuffer(gif: string | File): Promise<ArrayBuffer> { if (typeof gif === 'string') { return fetch(gif).then((resp) => resp.arrayBuffer()); } else { const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onload = () => resolve(reader.result as ArrayBuffer); reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(gif); }); } }
image.fabric.ts
:import {gifToSprites} from '../utils/gif.utils'; const [PLAY, PAUSE, STOP] = [0, 1, 2]; export async function fabricGif( gif: string | File, maxWidth?: number, maxHeight?: number ): Promise<{image: fabric.Image}> { const {framesPerSprite, sprites, frames, frameWidth, frameHeight, totalFrames} = await gifToSprites(gif, maxWidth, maxHeight); const frameCanvas = document.createElement('canvas'); frameCanvas.width = frameWidth; frameCanvas.height = frameHeight; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const frameCtx = frameCanvas.getContext('2d')!; frameCtx.drawImage( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sprites[0]!, 0, 0, frameWidth, frameHeight ); return new Promise((resolve) => { window.fabric.Image.fromURL(frameCanvas.toDataURL(), (image) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const firstFrame = frames[0]!; let framesIndex = 0; let start = performance.now(); let status: number; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion let accumulatedDelay = firstFrame.delay; image.width = frameWidth; image.height = frameHeight; image._render = function (ctx) { if (status === PAUSE || (status === STOP && framesIndex === 0)) return; const now = performance.now(); const delta = now - start; if (delta > accumulatedDelay) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion accumulatedDelay += frames[framesIndex]!.delay; framesIndex++; } if (framesIndex === totalFrames || status === STOP) { framesIndex = 0; start = now; accumulatedDelay = firstFrame.delay; } const spriteIndex = Math.floor(framesIndex / framesPerSprite); ctx.drawImage( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sprites[spriteIndex]!, frameWidth * (framesIndex % framesPerSprite), 0, frameWidth, frameHeight, -frameWidth / 2, -frameHeight / 2, frameWidth, frameHeight ); }; const methods = { play: () => { status = PLAY; image.dirty = true; }, pause: () => { status = PAUSE; image.dirty = false; }, stop: () => { status = STOP; image.dirty = false; }, getStatus: () => ['Playing', 'Paused', 'Stopped'][status] }; methods.play(); resolve({ ...methods, image }); }); }); }
实现还是一样
感谢@Fennec 提供原始代码,希望这些对您也有用。