Fabric.js: using animate method on many object is so slow - fabricjs

I'm using animate method to animate 100 objects, but in my case, the performance is so slow, how do I fix it?
my demo code:
https://jsfiddle.net/cs6jqj2w/

Please take a look at fabricJS demo
Also, I modified little bit that demo using your function for generating random numbers and created 100 shpaes in this fiddle
(function() {
var canvas = this.__canvas = new fabric.Canvas('c');
fabric.Object.prototype.transparentCorners = false;
var Cross = fabric.util.createClass(fabric.Object, {
objectCaching: false,
initialize: function(options) {
this.callSuper('initialize', options);
this.animDirection = 'up';
this.width = 100;
this.height = 100;
this.w1 = this.h2 = 100;
this.h1 = this.w2 = 30;
},
animateWidthHeight: function() {
var interval = 2;
if (this.h2 >= 30 && this.h2 <= 100) {
var actualInterval = (this.animDirection === 'up' ? interval : -interval);
this.h2 += actualInterval;
this.w1 += actualInterval;
}
if (this.h2 >= 100) {
this.animDirection = 'down';
this.h2 -= interval;
this.w1 -= interval;
}
if (this.h2 <= 30) {
this.animDirection = 'up';
this.h2 += interval;
this.w1 += interval;
}
},
_render: function(ctx) {
ctx.fillRect(-this.w1 / 2, -this.h1 / 2, this.w1, this.h1);
ctx.fillRect(-this.w2 / 2, -this.h2 / 2, this.w2, this.h2);
}
});
for (var i = 0; i < 100; i++){
canvas.add(
new Cross({ top: getRandomInt(0,500), left: getRandomInt(0,500)})
);
}
setTimeout(function animate() {
canvas.forEachObject(function(obj){ obj.animateWidthHeight(); obj.dirty = true; });
canvas.renderAll();
setTimeout(animate, 10);
}, 10);
})();
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
UPDATE:
Your animation didn't work because you had to much rendering of the canvas.
You have to generate 99 items without rendering and last one with the rendering. Also, last item has to me with the maximum duration for the animation in order to complete animation for all shapes.
var fabric = window.fabric
var canvas = new fabric.StaticCanvas('c')
function createItem(canvas) {
var item = new fabric.Circle({
left: -100,
top: getRandomInt(0, 500),
opacity: Math.random().toFixed(2),
radius: getRandomInt(10, 50),
})
item.keepGoing = true
canvas.add(item)
// itemTopAnim(canvas, item, getNextTop(item.top))
// itemLeftAnim(canvas, item)
return item;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getNextTop(top) {
if (top < (canvas.height / 2)) {
return top + getRandomInt(50, 200)
}
return top - getRandomInt(50, 200)
}
function itemTopAnim(canvas, item, top) {
item.animate('top', top, {
duration: getRandomInt(1, 3) * 1000,
// onChange: canvas.renderAll.bind(canvas),
easing: fabric.util.ease.easeInOutCubic,
onComplete: function() {
item.keepGoing && itemTopAnim(canvas, item, getNextTop(item.top))
}
})
}
function itemTopAnimLast(canvas, item, top) {
item.animate('top', top, {
duration: 3 * 1000,
onChange: canvas.renderAll.bind(canvas),
easing: fabric.util.ease.easeInOutCubic,
onComplete: function() {
item.keepGoing && itemTopAnim(canvas, item, getNextTop(item.top))
}
})
}
function itemLeftAnim(canvas, item) {
item.animate('left', canvas.width - item.radius, {
duration: getRandomInt(5, 10) * 1000,
//onChange: canvas.renderAll.bind(canvas),
onComplete: function() {
item.keepGoing = false
}
})
}
function itemLeftAnimLast(canvas, item) {
item.animate('left', canvas.width - item.radius, {
duration: 10 * 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: function() {
item.keepGoing = true
}
})
}
for (var i = 0; i < 100; i++) {
var item = createItem(canvas);
if (i == 99){
itemLeftAnimLast(canvas, item)
itemTopAnimLast(canvas, item, getNextTop(item.top))
} else {
itemLeftAnim(canvas, item)
itemTopAnim(canvas, item, getNextTop(item.top))
}
}
Check this updated fiddle
Hopefully, it gives you more idea how it works right now.

Sounds like you've got it working now. Just thought I would add that the docs on http://fabricjs.com/fabric-intro-part-2 also mention that you might want to use requestAnimationFrame instead of the onChange callback when animating lots of objects:
The reason animate doesn't automatically re-render canvas after each change is due to performance. After all, we can have hundreds or thousands animating objects on canvas, and it wouldn't be good if every one of them tried to re-render screen. In the case of many objects, you could use something like requestAnimationFrame (or other timer-based) loop to render canvas continuosly on its own, without calling renderAll for each object. But most of the time, you'll probably need to explicitly specify canvas.renderAll as "onChange" callback.
I had similar problems to yours, and this advice worked quite well.

Related

Phaser Scrollable Text Box Tutorial not working on mobile

I recreated the scrolling text box tutorial in my game. However, it is running a bit glitchy on mobile. For example, if I swipe up, the text first goes down for a second and then follows my finger up. You’ll see the problem if you open the tutorial on mobile. Any thoughts? I copied my code below.
var graphics = scene.make.graphics();
graphics.fillRect(x, y + 10, width, height - 20);
var mask = new Phaser.Display.Masks.GeometryMask(scene, graphics);
var text = scene.add.text(x + 20, y + 20, content, {
fontFamily: 'Assistant',
fontSize: '28px',
color: '#000000',
wordWrap: { width: width - 20 }
}).setOrigin(0);
text.setMask(mask);
var minY = height - text.height - 20;
if (text.height <= height - 20) {
minY = y + 20;
}
// The rectangle they can 'drag' within
var zone = scene.add.zone(x, y - 3, width, height + 6).setOrigin(0).setInteractive({useHandCursor: true});
zone.on('pointermove', function (pointer) {
if (pointer.isDown) {
text.y += (pointer.velocity.y / 10);
text.y = Phaser.Math.Clamp(text.y, minY, y + 20);
}
});
I had the same issue. My solution is using "pointer.y" instead of "pointer.velocity.y".
Here is my code:
var previousPointerPositionY;
var currentPointerPositionY;
zone.on('pointerdown', function (pointer) {
previousPointerPositionY = pointer.y;
});
zone.on('pointermove', function (pointer) {
if (pointer.isDown) {
currentPointerPositionY = pointer.y;
if(currentPointerPositionY > previousPointerPositionY){
text.y += 5;
} else if(currentPointerPositionY < previousPointerPositionY){
text.y -= 5;
}
previousPointerPositionY = currentPointerPositionY;
text.y = Phaser.Math.Clamp(text.y, -360, 150);
}
});

Three.js shooting bullet

I'm a beginner in three.js. My task is to build a simple FPS game. I'm having many troubles with the gun and the bullets. When I press "spacebar" my weapon shoots but the problem is that the bullets go in the right direction only for a small part of the screen then they start to go in direction that are not the ones I want.
This is an example :
image1
image2
Here is the code I wrote for the bullet :
// SHOOT BULLET
for(var index=0; index<bullets.length; index+=1){
if( bullets[index] === undefined ) continue;
if( bullets[index].alive == false ){
bullets.splice(index,1);
continue;
}
bullets[index].position.add(bullets[index].velocity);
}
if(keyboard[32] && canShoot <= 0){ // spacebar key
// creates a bullet as a Mesh object
var bullet = new THREE.Mesh(
new THREE.SphereGeometry(0.2,8,8),
new THREE.MeshBasicMaterial({color:0x42FFFF})
);
// position the bullet to come from the player's weapon
bullet.position.set(
camera.position.x - 0.7*parseInt(-Math.cos(camera.rotation.z)),
camera.position.y - 0.3,
camera.position.z +1*parseInt(-Math.cos(camera.rotation.z))
);
// set the velocity of the bullet
bullet.velocity = new THREE.Vector3( (-mouse.x - Math.sin(camera.rotation.y + Math.PI/6) * 7),//*parseInt(-Math.cos(camera.rotation.z)) ,
mouse.y,
Math.cos(camera.rotation.y)*parseInt(-Math.cos(camera.rotation.z))
).normalize();
console.info(bullet.velocity);
// after 1000ms, set alive to false and remove from scene
// setting alive to false flags our update code to remove
// the bullet from the bullets array
bullet.alive = true;
setTimeout(function(){
bullet.alive = false;
scene.remove(bullet);
}, 1000);
// add to scene, array, and set the delay to 10 frames
bullets.push(bullet);
scene.add(bullet);
canShoot = 10;
}
if(canShoot > 0) canShoot -= 1;
A rough concept of how you can set direction and movement of bullets:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 10000);
camera.position.set(0, 0, 1);
scene.add(camera);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var background = new THREE.Mesh(new THREE.SphereGeometry(1000, 90, 45), new THREE.MeshBasicMaterial({
color: "gray",
wireframe: true
}));
scene.add(background);
var weapon = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 5), new THREE.MeshBasicMaterial({
color: 0x5555ff
}));
weapon.position.set(2, -1, -2.5);
camera.add(weapon);
var emitter = new THREE.Object3D();
emitter.position.set(2, -1, -5);
camera.add(emitter);
var plasmaBalls = [];
window.addEventListener("mousedown", onMouseDown);
function onMouseDown() {
let plasmaBall = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 4), new THREE.MeshBasicMaterial({
color: "aqua"
}));
plasmaBall.position.copy(emitter.getWorldPosition()); // start position - the tip of the weapon
plasmaBall.quaternion.copy(camera.quaternion); // apply camera's quaternion
scene.add(plasmaBall);
plasmaBalls.push(plasmaBall);
}
var speed = 50;
var clock = new THREE.Clock();
var delta = 0;
(function render() {
requestAnimationFrame(render);
delta = clock.getDelta();
plasmaBalls.forEach(b => {
b.translateZ(-speed * delta); // move along the local z-axis
});
renderer.render(scene, camera);
})()
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.115.0/examples/js/controls/OrbitControls.js"></script>

javascript fabricjs - Adding an object to multiple canvases in a single click

I'm building an app that can design your own business card. I have to add an object to two canvases in a single click. Here are my codes:
$('#image-list').on('click','.image-option',function(e) {
var el = e.target;
/*temp code*/
var offset = 50;
var left = fabric.util.getRandomInt(0 + offset, 200 - offset);
var top = fabric.util.getRandomInt(0 + offset, 400 - offset);
var angle = fabric.util.getRandomInt(-20, 40);
var width = fabric.util.getRandomInt(30, 50);
var opacity = (function(min, max){ return Math.random() * (max - min) + min; })(0.5, 1);
var canvasObject;
// if ($('#flip').attr('data-facing') === 'front') {
// canvasObject = canvas;
// } else {
// canvasObject = canvas2;
// }
fabric.Image.fromURL(el.src, function(image) {
image.set({
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
canvas.add(image);
canvas2.add(image);
});
})
The problem is, when I resized or move the image on the 'front canvas', it also renders the same way in the 'back canvas'. In my case, I don't want the object to be that way. So is there a way to prevent the obect 'mirroring' on the other canvas? Thanks.
You cannot add the same object to 2 canvases.
You have to create 2 objects.
Also take note that if you have an html image element on your page you do not need to load it from URL again. Is already loaded, so pass the image element to the constructor directly
$('#image-list').on('click','.image-option',function(e) {
var el = e.target;
/*temp code*/
var offset = 50;
var left = fabric.util.getRandomInt(0 + offset, 200 - offset);
var top = fabric.util.getRandomInt(0 + offset, 400 - offset);
var angle = fabric.util.getRandomInt(-20, 40);
var width = fabric.util.getRandomInt(30, 50);
var opacity = (function(min, max){ return Math.random() * (max - min) + min; })(0.5, 1);
var canvasObject;
// if ($('#flip').attr('data-facing') === 'front') {
// canvasObject = canvas;
// } else {
// canvasObject = canvas2;
// }
image = new fabric.Image(el, {
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
image2 = new fabric.Image(el, {
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
canvas.add(image);
canvas2.add(image2);
});
})

Subclassed Image to add text but image hides it

I've subclassed the Image class so that I can add a label to it. However, the text seems to get rendered before the image such that the image is hiding the label. Below is the class declaration. Note that I tried calling callSuper() at the end of the _render() method, which didn't make sense to me anyway, and it didn't work. Seems to me if super is called first and then text is added after it should be on top right?
var Cabinet = fabric.util.createClass(fabric.Image, {
type: 'cabinet',
initialize: function(element, options) {
options || (options = { });
this.callSuper('initialize', element, options);
this.set('label', options.label || '');
this.set('height', options.height || 48);
this.set('width', options.width || 24);
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
});
},
_render: function(ctx) {
this.callSuper('_render', ctx);
var txtOffset = this.label.length * 6;
ctx.fillStyle = '#000';
if (this.get('angle') !== 0) {
ctx.font = '11px Arial Narrow';
//ctx.save();
ctx.rotate(90 * Math.PI/180);
ctx.fillText(this.label, (this.width / 2) - (txtOffset / 2) - 8, this.height - 10);
//ctx.restore();
} else {
ctx.font = "9px Arial Narrow";
ctx.fillText(this.label, (this.width / 2) - (txtOffset / 2) - 8, this.height - 10);
}
}
});
I would appreciate any help you can provide.
Thanks,
Rob

Wait for click before starting the game in phaser

The JSFiddle demo is here, but am pasting the code below also. My question is simple (though I can't seem to find any example in how to realize this): right now if you start the game in JSFiddle you will see that the rectangle immediately "falls" down. I would somehow like the game to start when I click with the mouse (basically that nothing happens before the first click of a mouse) - any points on how to accomplish this?
JS:
// Initialize Phaser, and creates a 400x490px game
var game = new Phaser.Game(400, 490, Phaser.AUTO, 'game_div');
var game_state = {};
// Creates a new 'main' state that wil contain the game
game_state.main = function () {};
game_state.main.prototype = {
preload: function () {
// Change the background color of the game
this.game.stage.backgroundColor = '#71c5cf';
// Load the bird sprite
this.game.load.image('bird', 'https://lh6.googleusercontent.com/Xrz0PnR6Ezg5_k5zyFKxGv0LzehAP9SMj_ga3qQzIF4JAfv8xHm7TxfliwtBD8ihfw=s190');
this.game.load.image('pipe', 'https://lh4.googleusercontent.com/RSMNhJ3KY4Xl0PQpUf6I9EayOdLhvOKKV9QV7_BXXYVedPy0oMNRFKANW14xV76NDA=s190');
},
create: function () {
// Display the bird on the screen
this.bird = this.game.add.sprite(100, 245, 'bird');
// Add gravity to the bird to make it fall
this.bird.body.gravity.y = 1000;
// Call the 'jump' function when the spacekey is hit
var space_key = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
space_key.onDown.add(this.jump, this);
this.pipes = game.add.group();
this.pipes.createMultiple(20, 'pipe');
this.timer = this.game.time.events.loop(1500, this.add_row_of_pipes, this);
this.score = 0;
var style = {
font: "30px Arial",
fill: "#ffffff"
};
this.label_score = this.game.add.text(20, 20, "0", style);
},
update: function () {
// If the bird is out of the world (too high or too low), call the 'restart_game' function
this.game.physics.overlap(this.bird, this.pipes, this.restart_game, null, this);
if (this.bird.inWorld == false) this.restart_game();
},
// Make the bird jump
jump: function () {
// Add a vertical velocity to the bird
this.bird.body.velocity.y = -350;
},
// Restart the game
restart_game: function () {
// Start the 'main' state, which restarts the game
this.game.time.events.remove(this.timer);
this.game.state.start('main');
},
add_one_pipe: function (x, y) {
// Get the first dead pipe of our group
var pipe = this.pipes.getFirstDead();
// Set the new position of the pipe
pipe.reset(x, y);
// Add velocity to the pipe to make it move left
pipe.body.velocity.x = -200;
// Kill the pipe when it's no longer visible
pipe.outOfBoundsKill = true;
},
add_row_of_pipes: function () {
this.score += 1;
this.label_score.content = this.score;
var hole = Math.floor(Math.random() * 5) + 1;
for (var i = 0; i < 8; i++)
if (i != hole && i != hole + 1) this.add_one_pipe(400, i * 60 + 10);
},
};
// Add and start the 'main' state to start the game
game.state.add('main', game_state.main);
game.state.start('main');
CSS:
#game_div {
width: 400px;
margin: auto;
margin-top: 50px;
}
HTML:
<div id="game_div"></div>
So, to not leave this question unanswered and to help further readers, here is the link to the updated jsFiddle where I solved my initial problem, and below is the code from jsFiddle.
However, if one will be interested I suggest taking a look at the freely available code on GitHub which uses game states and has a better structured code.
jsFiddle
CSS:
#game_div {
width: 400px;
margin: auto;
margin-top: 50px;
}
HTML:
<div id="game_div"></div>
JS:
// Initialize Phaser, and creates a 400x490px game
var game = new Phaser.Game(400, 490, Phaser.AUTO, 'game_div');
var game_state = {};
var gameStarted = false;
// Creates a new 'main' state that wil contain the game
game_state.main = function () {};
game_state.main.prototype = {
game_start: function(){
if (!gameStarted){
this.bird.body.gravity.y = 1000;
this.timer = this.game.time.events.loop(1500, this.add_row_of_pipes, this);
this.label_start.content = "";
}
gameStarted = true;
},
preload: function () {
// Change the background color of the game
this.game.stage.backgroundColor = '#71c5cf';
// Load the bird sprite
this.game.load.image('bird', 'https://lh6.googleusercontent.com/Xrz0PnR6Ezg5_k5zyFKxGv0LzehAP9SMj_ga3qQzIF4JAfv8xHm7TxfliwtBD8ihfw=s190');
this.game.load.image('pipe', 'https://lh4.googleusercontent.com/RSMNhJ3KY4Xl0PQpUf6I9EayOdLhvOKKV9QV7_BXXYVedPy0oMNRFKANW14xV76NDA=s190');
},
create: function () {
// Display the bird on the screen
this.bird = this.game.add.sprite(100, 245, 'bird');
// Add gravity to the bird to make it fall
// Call the 'jump' function when the spacekey is hit
this.game.input.onDown.add(this.game_start, this);
this.pipes = game.add.group();
this.pipes.createMultiple(20, 'pipe');
this.score = 0;
var style = {
font: "30px Arial",
fill: "#ffffff"
};
this.label_score = this.game.add.text(20, 20, "0", style);
this.label_start = this.game.add.text(35, 180, "Click to start the show", style);
game.input.onDown.add(this.jump, this);
},
update: function () {
// If the bird is out of the world (too high or too low), call the 'restart_game' function
this.game.physics.overlap(this.bird, this.pipes, this.restart_game, null, this);
if (this.bird.inWorld == false) this.restart_game();
},
// Make the bird jump
jump: function () {
// Add a vertical velocity to the bird
this.bird.body.velocity.y = -350;
},
// Restart the game
restart_game: function () {
// Start the 'main' state, which restarts the game
this.game.time.events.remove(this.timer);
this.game.state.start('main');
gameStarted=false;
},
add_one_pipe: function (x, y) {
// Get the first dead pipe of our group
var pipe = this.pipes.getFirstDead();
// Set the new position of the pipe
pipe.reset(x, y);
// Add velocity to the pipe to make it move left
pipe.body.velocity.x = -200;
// Kill the pipe when it's no longer visible
pipe.outOfBoundsKill = true;
},
add_row_of_pipes: function () {
this.score += 1;
this.label_score.content = this.score;
var hole = Math.floor(Math.random() * 5) + 1;
for (var i = 0; i < 8; i++)
if (i != hole && i != hole + 1) this.add_one_pipe(400, i * 60 + 10);
},
};
// Add and start the 'main' state to start the game
game.state.add('main', game_state.main);
game.state.start('main');

Resources