Reputation: 2517
JavaScript syntax:
context.drawImage(img,srcX,srcY,srcWidth,srcHeight,x,y,width,height);
In Javascript, if I wanted to animate the following spritesheet, I would simply update srcX
and srcY
every animation frame in order to capture segments of the image.
This results in each frame being clipped and displayed individually onto the canvas
, which when updated at a fixed frame rate results in fluid sprite animation, like this:
How can I do this using the "Fabric.js" library?
Note: One way to achieve this would be to set canvasSize = frameSize
so that only one frame can be seen at any given time. Then by moving the image around, different frames can be placed inside the canvas in order to simulate animation. This will not work however with a large canvas, or with variable frame sizes.
Upvotes: 1
Views: 2399
Reputation: 880
Look at this,it does the same thing.
A walking human figure. Fabric.js,image animation.
var URL = 'https://i.sstatic.net/M06El.jpg';
var canvas = new fabric.Canvas('canvas');
var positions = {
topSteps:2,
leftSteps:4
};
canWalk(URL,positions);
function canWalk(URL,positions){
var myImage = new Image();
myImage.src = URL;
myImage.onload = function() {
var topStep = myImage.naturalHeight/positions.topSteps;
var leftStep = myImage.naturalWidth/positions.leftSteps;
var docCanvas = document.getElementById('canvas');
docCanvas.height = topStep;
docCanvas.width = leftStep;
fabricImageFromURL(0,0);
var y = 0;
var x = 0;
setInterval(function(){
if(x == positions.leftSteps)
{
x = 0;
y++;
if(y==positions.topSteps)
{
y=0;
}
}
fabricImageFromURL(-y*topStep,-x*leftStep);
x++;
},100);
};
}
function fabricImageFromURL(top, left)
{
console.log(top, left);
fabric.Image.fromURL(URL, function (oImg) {
oImg.set('left', left).set('top',top);
oImg.hasControls = false;
oImg.hasBorders = false;
oImg.selectable = false;
canvas.add(oImg);
canvas.renderAll();
}, {"left": 0, "top": 0, "scaleX": 1, "scaleY": 1});
}
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.min.js"></script>
Upvotes: 1
Reputation: 1
You can use a combination of setting the clipTo-function and invoking setLeft / setTop in a loop. Within the fabric.Image constructor's option you pass the property clipTo and tell fabric to cut out a specific part of the image. Then, with setTop / setLeft inside a loop you trigger repainting and thereby invoking clipTo and at the same time re-position the cut image so it always stays in the same place.
I had the same problem and extracted the logic into two functions. Quick rundown of the options:
spriteWidth - width of one animation frame of the sprite
spriteHeight - height of one animation frame of the sprite
totalWidth - width of the whole sprite image
totalHeight - height of the whole sprite image
animationFrameDuration - how long one sprite frame should be shown
startRandom - if you want to start the animation not right away, but randomly within 1 second
left - just like the normal left option of fabric.Image
top - just like the normal top option of fabric.Image
Synchronous version (passing the HTMLImageElement):
/**
* @param imgObj HTMLImageElement
* @param options {
* spriteWidth: number
* spriteHeight: number
* totalWidth: number
* totalHeight: number
* animationFrameDuration: number
* startRandom: boolean (optional)
* left: number (optional)
* top: number (optional)
* }
* @returns fabric.Image
*/
function animateImg(imgObj, options) {
const left = options.left || 0;
const top = options.top || 0;
let x = 0;
let y = 0;
const image = new fabric.Image(imgObj, {
width: options.totalWidth,
height: options.totalHeight,
left: left,
top: top,
clipTo: ctx => {
ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
}
});
setTimeout(() => {
setInterval(() => {
x = (x - options.spriteWidth) % options.totalWidth;
if (x === 0) {
y = (y - options.spriteHeight) % options.totalHeight;
}
image.setLeft(x + left);
image.setTop(y + top);
}, options.animationFrameDuration)
}, options.startRandom ? Math.random() * 1000 : 0);
return image;
}
Asynchronous version (passing the image URL):
/**
* @param imgURL string
* @param options {
* spriteWidth: number
* spriteHeight: number
* totalWidth: number
* totalHeight: number
* animationFrameDuration: number
* startRandom: boolean (optional)
* left: number (optional)
* top: number (optional)
* }
* @param callback (image : fabric.Image) => void
*/
function animateImgFromURL(imgURL, options, callback) {
const left = options.left || 0;
const top = options.top || 0;
let x = 0;
let y = 0;
fabric.Image.fromURL(
imgURL,
image => {
setTimeout(() => {
setInterval(() => {
x = (x - options.spriteWidth) % options.totalWidth;
if (x === 0) {
y = (y - options.spriteHeight) % options.totalHeight;
}
image.setLeft(x);
image.setTop(y);
}, options.animationFrameDuration)
}, options.startRandom ? Math.random() * 1000 : 0);
callback(image);
}, {
width: options.totalWidth,
height: options.totalHeight,
left: 0,
top: 0,
left: left,
top: top,
clipTo: ctx => {
ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
}
});
Note that the above functions do not rerender the canvas, you have to do that yourself.
You can use the above code like this to animate your sprite two times side by side (once synchronous version, once asynchronous):
// Assuming:
// 1. canvas was created
// 2. Sprite is in html with id 'walking'
// 3. Sprite is within folder 'images/walking.jpg'
const img1 = animateImg(document.getElementById('walking'), {
spriteWidth: 125,
spriteHeight: 125,
totalWidth: 500,
totalHeight: 250,
startRandom: true,
animationFrameDuration: 150,
left: 125,
top: 0
});
canvas.add(img1);
animateImgFromURL('images/walking.jpg', {
spriteWidth: 125,
spriteHeight: 125,
totalWidth: 500,
totalHeight: 250,
startRandom: true,
animationFrameDuration: 150
}, image => canvas.add(image));
// hacky way of invoking renderAll in a loop:
setInterval(() => canvas.renderAll(), 10);
Upvotes: 0
Reputation: 1
By default Fabric won't do it. You need to present 'source' properties in fabric.Image object & extend fabric.Image _render method. Original looks this:
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} noTransform
*/
_render: function(ctx, noTransform) {
var x, y, imageMargins = this._findMargins(), elementToDraw;
x = (noTransform ? this.left : -this.width / 2);
y = (noTransform ? this.top : -this.height / 2);
if (this.meetOrSlice === 'slice') {
ctx.beginPath();
ctx.rect(x, y, this.width, this.height);
ctx.clip();
}
if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) {
this._lastScaleX = this.scaleX;
this._lastScaleY = this.scaleY;
elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, true);
}
else {
elementToDraw = this._element;
}
elementToDraw && ctx.drawImage(elementToDraw,
x + imageMargins.marginX,
y + imageMargins.marginY,
imageMargins.width,
imageMargins.height
);
this._stroke(ctx);
this._renderStroke(ctx);
},
And you need to change it:
fabric.util.object.extend(fabric.Image.prototype, {
_render: function(ctx, noTransform) {
// ...
elementToDraw && ctx.drawImage(
elementToDraw,
this.source.x,
this.source.y,
this.source.width,
this.source.height,
x + imageMargins.marginX,
y + imageMargins.marginY,
imageMargins.width,
imageMargins.height
);
this._renderStroke(ctx);
}
});
Upvotes: 0