I wrote the following 1000 bounding squares demo as a test of the capabilities of HTML5 canvas. It runs fine at first but then a noticeable drop in fps after a few seconds. I am not sure why. Any pointers would be appreciated.
var c = document.getElementById("canvas");
var context = c.getContext("2d");
var WIDTH = 600;
var HEIGHT = 800;
c.width = WIDTH;
c.height = HEIGHT;
image = loadImage("square.png");
function loadImage(imageName){
var i = new Image();
i.src = imageName;
return i;
}
function clear(){
context.fillStyle = "#d0e7f9";
context.rect(0,0,WIDTH,HEIGHT);
context.fill();
}
clear();
var SpriteList = [];
var Sprite = (function() { //javascript class(?)... shredders
function Sprite(){ //constructor
this.x = Math.random()*WIDTH;
this.y = Math.random()*HEIGHT;
this.vx = Math.random()*10;
this.vy = Math.random()*10;
SpriteList.push(this);
}
Sprite.prototype.update = function(){
this.x += this.vx;
this.y += this.vy;
if (this.x<0 || this.x>WIDTH){
this.vx *= -1;
}
if (this.y<0 || this.y>HEIGHT){
this.vy *= -1;
}
};
return Sprite;
})();
for (var i = 0;i<1000;i++){
new Sprite();
}
function draw(){
clear();
for (i in SpriteList)
{
var s = SpriteList[i];
s.update();
context.drawImage(image, s.x, s.y);
}
}
setInterval(draw,1000/60);
There are a few issues with the code but the main reason for this to happen is this code:
This code will require you to use beginPath():
function clear(){
context.fillStyle = "#d0e7f9";
context.beginPath();
context.rect(0,0,WIDTH,HEIGHT); /// this will require beginPath();
context.fill();
}
or to avoid it, you can simply modify the code to do this:
function clear(){
context.fillStyle = "#d0e7f9";
context.fillRect(0,0,WIDTH,HEIGHT); /// this does not require beginPath();
}
Live fiddle here
/// use a var here
var image = loadImage("square.png");
/// your image loader is missing - image may not show up
function loadImage(imageName){
var i = new Image();
i.onload = nextStep; /// something like this
i.src = imageName;
return i;
}
var SpriteList = [];
/// create this as an object
function Sprite(){ //constructor
this.x = Math.random()*WIDTH;
this.y = Math.random()*HEIGHT;
this.vx = Math.random()*10;
this.vy = Math.random()*10;
return this;
}
Sprite.prototype.update = function(){
this.x += this.vx;
this.y += this.vy;
if (this.x<0 || this.x>WIDTH){
this.vx *= -1;
}
if (this.y<0 || this.y>HEIGHT){
this.vy *= -1;
}
};
/// separate pushing of the instances
for (var i = 0;i<1000;i++){
SpriteList.push(new Sprite());
}
var oldTime = 0;
function draw(timeElapsed){ /// in milliseconds
clear();
var diffTime = timeElapsed - oldTime;
/// use vars here too
for (var i = 0, s; s = SpriteList[i]; i++ )
{
s.update();
context.drawImage(image, s.x, s.y);
}
oldTime = timeElapsed;
/// use rAF here
requestAnimationFrame(draw);
}
draw(0); /// start
The setInterval may cause the whole thing to stack calls if the browser is not fast enough processing the sprites within the time budget you give,.
By using rAF the browser will only request a frame when it can even if that means lower frame rates - you will at least not lock up/slow down the browser.
(as you didn't provide a link to the image you're using I substituted it with a temp canvas - you will still need to consider a onload event handler for the actual image).
A few suggestions:
Use image.onload to be sure your square.png is fully loaded before it's used.
Put the image loading at the bottom of your code after you create your 1000 sprites.
var image=new Image();
image.onload=function(){
draw();
}
image.src="square.png";
Don't iterate using for(i in SpriteList). Do this instead:
for(var i=0;i<SpriteList.length;i++)
Your draw functions are probably stacking--the current draw() isn't being completed before setInterval is requesting another draw().
Replace setInterval with requestAnimationFrame to stop your stacking problems.
function draw(){
// request another animation frame
requestAnimationFrame(draw);
// draw the current frame
clear();
for(var i=0;i<SpriteList.length;i++)
{
var s = SpriteList[i];
s.update();
context.drawImage(image, s.x, s.y);
}
}
Related
I have created an element called Particle. Single animation of this element also works. But as soon as I try to run multiple animations, only one animation gets performed. I think the problem is the requestAnimationFrame (this.animate.bind(this))-call, but I don't know how to change it to accept multiple animations at once. Any ideas on how to fix this?
Code:
//gloabl vars
let particels = [];
let numberParticels = 120;
let canvas;
let ctx;
let title;
let mesaureTitle;
let boundRadius;
let animations;
window.onload = function () {
this.init();
for(let i = 0; i < numberParticels; i++){
particels[i].update();
particels[i].draw();
particels[i].animate(0);
}
}
function init(){
canvas = document.getElementById("c");
ctx = canvas.getContext("2d");
title = document.getElementById("title");
mesaureTitle = title.getBoundingClientRect();
bound = {
x: mesaureTitle.x,
y: mesaureTitle.y,
width: mesaureTitle.width,
height: mesaureTitle.height,
};
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
for(let i = 0; i < numberParticels; i++){
let x = Math.floor(Math.random() * window.innerWidth);
let y = Math.floor(Math.random() * window.innerHeight);
let size = Math.floor(Math.random() * 25) + 3;
let weight = Math.floor(Math.random() * 11) + 2;
particels.push(new Particel(x, y, size, weight));
}
}
class Particel {
constructor (x,y,size, weight) {
this.x = x;
this.y = y;
this.size = size;
this.directionX = 0.15332;
this.resetWeight = weight;
this.weight = weight;
this.lastTime = 0;
this.interval = 1000/60;
this.timer = 0;
}
update(){
this.weight += 0.02;
this.y = this.y + this.weight;
this.x += this.directionX;
//check for collision with textField
if (this.x < bound.x + bound.width
&& this.x + this.size > bound.x &&
this.y < bound.y + bound.height &&
this.y + this.size > bound.y) {
this.y -= 3;
this.weight *= -0.3;
}
}
draw(){
if(this.y > canvas.height){
this.y = 0 - this.size;
this.weight = this.resetWeight;
//create random start point
this.x = Math.floor(Math.random() * canvas.width);
}
ctx.fillStyle = "rgb(0, 180, 97)";
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
animate(timeStamp){
const deltaTime = timeStamp - this.lastTime;
this.lastTime = timeStamp;
if(this.timer > this.interval){
ctx.clearRect(0,0, canvas.width, canvas.height);
this.update();
this.draw();
this.timer = 0;
}else {
this.timer += deltaTime;
}
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
requestAnimationFrame(this.animate.bind(this));
}
}
The major problem is clearing the canvas inside the animate method of each particle. So if you draw multiple particles, each particle update call clears the canvas, overwriting previous particle data, which only leaves the last particle visible.
You could try removing the
ctx.clearRect(0,0, canvas.width, canvas.height);
line from where it is and create a createAnimationFrame call back in init to clear the canvas before amimating particles:
function init() {
// ....
requestAnimationFrame( ()=> ctx.clearRect(0,0, canvas.width, canvas.height));
// existing for loop:
for(let i = 0; i < numberParticels; i++){
// ... .
}
However this creates (one plus the number of particles) requests for an animation frame. A better solution would be to remove requesting animation frames from the Particel class and create a single requestAnimationFrame callback which goes through the particels array and calls a class method to redraw each particle on the canvas with updated position.
Also the code generates an error in strict mode that bound has not been declared. I suggest declaring it globally rather than relying on sloppy mode JavaScript creating it as a window property for you.
I'm currently developing a small game for my capstone project. In the game, the user tries to avoid rectangles of random sizes the move from the right side of the screen to the left at a set speed.
It's built using object-oriented Javascript, and I've assigned it an anonymous function, however, I can't seem to get it to generate a shape and animate it more than the initial time the function is called. The problem can be solved if I create more than one object, but I would like this function to run automatically and generate more than just the first rectangle.
I've tried to call the function with an interval to force it to re-run the function with no results. I also attempted to separate the initialization function to call it with a parameter to generate the number of shapes given to it.
This is the function that generates the shape with the initial call, and determines the color, size, and location as well as draws it on the canvas.
var randomRectangle = function(){
this.init = function() {
this.speed = 4;
this.x = canvas.width-50;
this.y = Math.floor(Math.random()*280) + 40;
this.w = Math.floor(Math.random()*200) + 50;
this.h = Math.floor(Math.random()*150) + 20;
this.col = "#b5e61d";
}
this.move = function(){
this.x -= this.speed;
}
this.draw = function(num){
draw.rectangles(this.x, this.y, this.w, this.h, this.col);
}
};
This is where the object is initialized and the loop generates all objects and animations on the canvas.
randRecs = new randomRectangle();
randRecs.init();
function loop(){
draw.clear();
player.draw();
player.move();
wall1.draw();
wall2.draw();
randRecs.draw();
randRecs.move();
}
var handle = setInterval(loop, 30);
I expected the rectangle to continuously be generated at a new y-coordinate with a new size, then move from the right side of the screen to the left. However, only one rectangle is created and animated.
var list = [];
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var randomRectangle = function() {
this.init = function() {
this.speed = 4;
this.x = canvas.width - 50;
this.y = Math.floor(Math.random() * 280) + 40;
this.w = Math.floor(Math.random() * 200) + 50;
this.h = Math.floor(Math.random() * 150) + 20;
this.col = "#b5e61d";
}
this.move = function() {
this.x -= this.speed;
// restart x position to reuse rectangles
// you can change the y value here to a new random value
// or you can just remove with array.splice
if (this.x < -50) this.x = canvas.width - 50;
}
this.draw = function(num) {
draw.rectangles(this.x, this.y, this.w, this.h, this.col);
}
};
function loop() {
draw.clear();
//player.draw();
//player.move();
//wall1.draw();
//wall2.draw();
// call the methods draw and move for each rectangle on the list
for (var i=0; i<list.length; i++) {
rec = list[i];
rec.draw();
rec.move();
}
}
// spawn any number of new rects in a specific interval
var rectsPerSpawn = 1;
function addRects() {
for (var i=0; i<rectsPerSpawn; i++) {
if (list.length < 100) {
var rec = new randomRectangle();
list.push(rec);
rec.init();
}
}
}
// every half second will spawn a new rect
var spawn = setInterval(addRects, 500);
var draw = {
clear: function () {
ctx.clearRect(0,0,canvas.width,canvas.height);
},
rectangles: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x,y,w,h);
}
}
var handle = setInterval(loop, 30);
<canvas></canvas>
Hello I have been watching some tutorials on HTML canvas and animations and I wanted to create my own. I am having trouble though. :/
I am trying to create some clouds that start on the left side of the screen and move to the right side of the screen, and will eventually disappear when they get to a certain point. I don't have that code yet. I don't know how to handle transparency. But, that is not where my troubles lie.
Currently, my clouds do not move. I can generate 20 different clouds in different locations but they are failing to move. I have checked my code with other tutorials and I can not seem to find why it's not working. Maybe because I am using an image?? If I could find some help I would really appreciate it. Thank you.
$(function(){
var leftcloudsrc = "ls/pics/cloud1.png";
var rightcloudsrc = "ls/pics/cloud2.png";
var canvas = document.getElementById('cloud');
var cw = canvas.width;
var ch = canvas.height;
var cloudsArray = [];
createclouds();
function createclouds(){
for (var i=0; i < 20; i++){
var x = Math.random() * 150;
var y = Math.random() * 300;
var v = Math.random() * 4;
cloudsArray.push(new Cloud(x, y, v));
}
animate();
console.log(cloudsArray);
}
function animate(){
requestAnimationFrame(animate);
for (var i = 0; i<cloudsArray.length; i++){
cloudsArray[i].move();
//new Cloud(x, y, v).create();
}
}
function Cloud(x, y, v){
this.x = x;
this.y = y;
this.v = v;
this.create = function(){
img = new Image,
ctx = document.getElementById('cloud').getContext('2d');
img.src = leftcloudsrc;
var iw = img.naturalWidth;
var ih = img.naturalHeight;
ctx.drawImage(img, x, y);
}
this.move = function(){
this.x += this.v;
this.create();
}
}
// var cloud = new Cloud(0,0,0);
// cloud.create();
});
i have tried writing to the console to make sure the information is saving and sticking, and yes, it is. i have even tried writing the .move() function to console to make sure the data changes, and it does. but it does not reflect visually???
ctx.drawImage(img, x, y); // wrong
ctx.drawImage(img, this.x, this.y); // right
You arent updating x and y. You are updating this.x and this.y
full code
// test
var leftcloudsrc = "http://www.freepngimg.com/download/cloud/10-cloud-png-image.png";
var rightcloudsrc = "ls/pics/cloud2.png";
var canvas = document.getElementById('cloud');
// you dont have to define ctx again and again
var ctx = canvas.getContext('2d');
var cw = canvas.width;
var ch = canvas.height;
var cloudsArray = [];
createclouds();
function createclouds() {
for (var i = 0; i < 20; i++) {
var x = Math.random() * 150;
var y = Math.random() * 300;
var v = Math.random() * 4;
cloudsArray.push(new Cloud(x, y, v));
}
animate();
console.log(cloudsArray);
}
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, cw, ch)
for (var i = 0; i < cloudsArray.length; i++) {
var c = cloudsArray[i]
c.move();
// remove when crosses the canvas width
if(c.x >= 500) {
cloudsArray.splice(i, 1);
}
//new Cloud(x, y, v).create();
}
}
function Cloud(x, y, v) {
this.x = x;
this.y = y;
this.v = v;
this.create = function() {
// invoke the constructor
var img = new Image;
img.src = leftcloudsrc;
var iw = img.naturalWidth;
var ih = img.naturalHeight;
ctx.drawImage(img, this.x, this.y);
}
this.move = function() {
this.x += this.v;
this.create();
}
}
<canvas id="cloud" width="500" height="400"></canvas>
I'm trying to show multiple pipes in the canvas of same height but even after using a for loop it is showing a single pipe and not a lot of it
<script>
var tryCanvas = document.getElementById("myCanvas");
var c = tryCanvas.getContext("2d");
var myCall = [];
function Squares() {
for(var i =0; i < 10; i++){
this.x = Math.random()* tryCanvas.clientWidth;
this.y = 0;
this.w = 20;
this.h = 60;
this.counter = 0;
this.draw = function() {
c.beginPath();
c.rect(this.x, this.y, this.w, this.h)
c.fill();
}
}
this.update = function() {
if(this.x < 0){
this.x = 0;
}
this.x -= 1;
this.draw();
}
}
var holder = new Squares;
setInterval(callFun, 10);
function callFun() {
c.clearRect(0,0,tryCanvas.clientWidth, tryCanvas.clientHeight);
holder.update();
}
</script>
If I push the constructor function in an array it's not showing anything in the canvas and in the console it's giving undefined or NaN.
But if I do it without "this" its generating the number of rects.
What am I doing wrong here?
Updated to move the bars along the screen
See this working example:
https://codepen.io/bkfarns/pen/braWQB?editors=1010
This.draw will only get created with the values from the last iteration of the for loop.
Also as a side node, usually instead of new Squares you call the constructor like new Squares(). When you call the constructor you are calling a method.
But I think the code below fixes your issues. Try it out:
<body>
<canvas id="myCanvas"/>
</body>
<script>
var tryCanvas = document.getElementById("myCanvas");
var c = tryCanvas.getContext("2d");
var myCall = [];
function Squares() {
this.draw = function(xOffset) {
for(var i =0; i < 10; i++){
this.x = (i * xOffset) + (5*i)//Math.random()* tryCanvas.clientWidth;
this.y = 0;
this.w = 20;
this.h = 60;
c.beginPath();
c.rect(this.x, this.y, this.w, this.h)
c.fill();
}
}
}
var holder = new Squares();
var xOffset = 20;
setInterval(function() {
c.clearRect(0,0,tryCanvas.clientWidth, tryCanvas.clientHeight);
holder.draw(xOffset)
xOffset--;
}, 1000)
</script>
So I made this code (with the help of a tutorial) and I want to create the effect wherever I click.
The code does what it has to, but every time I do another click and call the 'drawParticle'-function, the process speeds up and less particles are been shown.
I think this is because I call on multiple setIntervals at once while JavaScript is single threaded. But how can I fix it?
var canvas,
c,
particles,
particleIndex,
particleNum,
w,
h;
window.onload =function(){
canvas = document.createElement("canvas");
c = canvas.getContext("2d");
particles = {};
particleIndex = 0;
particleNum = 3;
w = window.innerWidth;
h = window.innerHeight;
canvas.width = w;
canvas.height = h;
document.body.appendChild(canvas);
c.fillStyle ="black";
c.fillRect(0,0,canvas.width,canvas.height);
canvas.addEventListener('click', drawParticle, false);
};
function drawParticle(e){
function Particle(){
this.x = e.clientX;
this.y = e.clientY;
this.vx = Math.random() * 10 - 5;
this.vy = Math.random() * 10 - 5;
this.gravity = 0.2;
particleIndex++;
particles[particleIndex] = this;
this.id = particleIndex;
this.life = 0;
this.maxLife = Math.random() * 30 + 50;
this.color = "rgb("+parseInt(Math.random()*255,10)+",0,0)";
}
Particle.prototype.draw = function(){
this.x += this.vx;
this.y += this.vy;
if(Math.random() < 0.1 ){
this.vx = Math.random() * 10 - 5;
this.vy = Math.random() * 10 - 5;
}
//this.vy += this.gravity;
this.life++;
if(this.life >= this.maxLife){
delete particles[this.id];
}
c.fillStyle = this.color;
c.fillRect(this.x,this.y,10,10);
};
interval = setInterval(function(){
//c.globalCompositeOperation ="source-over";
c.fillStyle ="rgba(0,0,0,0.3)";
c.fillRect(0,0,canvas.width,canvas.height);
for (var i = 0; i < particleNum; i++){
new Particle();
}
//c.globalCompositeOperation ="lighter";
for (var i in particles){
particles[i].draw();
}
},30);
};
I think you're correct about the interval. I'm not sure if this is the reaction you want but if you clear the interval before redrawing, things don't speed up. Try something like:
var interval = null;
function drawParticle(e){
if (interval) {
clearInterval(interval)
}
function Particle(){
...
There's a codepen here http://codepen.io/bunnymatic/pen/BjwVBM
UPDATE:
I've changed the code pen to include the new code which is included below. The primary change was to make a Particles object which would hold the set of particles being drawn for each click. By moving this collection of particles and their particleIndex seems to have solved the issue. I suspect there was something strange about adding each element to the global particles list. Now the Particles instance manages just the particles associated with that group.
I also took out particleId because it seemed unnecessary, though you may need to put it back in if it's useful for other stuff out side of this code.
Here's my updated code:
var canvas,
c,
particleNum,
w,
h;
window.onload =function(){
canvas = document.createElement("canvas");
c = canvas.getContext("2d");
particleNum = 3;
w = window.innerWidth;
h = window.innerHeight;
canvas.width = w;
canvas.height = h;
document.body.appendChild(canvas);
c.fillStyle ="black";
c.fillRect(0,0,canvas.width,canvas.height);
canvas.addEventListener('click', drawParticles, false);
};
function Particles() {
this.particles = {};
this.particleIndex = 0;
}
Particles.prototype.addParticle = function(particle) {
this.particles[++this.particleIndex] = particle;
}
Particles.prototype.killDeadParticles = function() {
for (var i in this.particles) {
var particle = this.particles[i];
if (particle.isDead()) {
delete this.particles[i]
}
}
}
Particles.prototype.draw = function() {
// draw particles
for (var i in this.particles){
this.particles[i].draw();
}
this.killDeadParticles();
}
function Particle(e){
this.x = e.clientX;
this.y = e.clientY;
this.vx = Math.random() * 10 - 5;
this.vy = Math.random() * 10 - 5;
this.gravity = 0.2;
this.life = 0;
this.maxLife = Math.random() * 30 + 50;
this.color = "rgb("+parseInt(Math.random()*255,10)+",0,0)";
// console.log('new particle', particleIndex);
}
Particle.prototype.isDead = function() {
return (this.life >= this.maxLife);
}
Particle.prototype.draw = function(){
this.x += this.vx;
this.y += this.vy;
if(Math.random() < 0.1 ){
this.vx = Math.random() * 10 - 5;
this.vy = Math.random() * 10 - 5;
}
//this.vy += this.gravity;
this.life++;
c.fillStyle = this.color;
c.fillRect(this.x,this.y,10,10);
};
function drawParticles(e){
var particles = new Particles(e);
setInterval(function(){
//c.globalCompositeOperation ="source-over";
c.fillStyle ="rgba(0,0,0,0.3)";
c.fillRect(0,0,canvas.width,canvas.height);
for (var i = 0; i < particleNum; i++){
var particle = new Particle(e);
particles.addParticle(particle);
}
//c.globalCompositeOperation ="lighter";
particles.draw();
},30);
};