I am making a simple game in HTML Canvas. As a part of it, in the background i want to make falling stars which create the illusion of travelling. After the star reaches the end of the canvas i want to remove it. Each star is an instance of Star class, and depending on it's radius it has a different velocity. This is where problems start. When using constant velocity for every star, they disappear one by one like they should be. When velocity is changed, stars that "overtake" slower stars, when reached the end of the canvas, do not only dissapear themselves but also remove every star that was in array before them.
I have tried many solutions described below:
let canvas = document.querySelector("canvas");
let c = canvas.getContext('2d');
Star Class declaration:
class Star{
constructor(x, y, radius, color, velocity){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.velocity = velocity
}
draw(){
c.globalCompositeOperation='destination-over'
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
c.fillStyle = this.color;
c.shadowColor= "white"
c.shadowBlur=12
c.fill();
c.shadowBlur=0
}
update(){
this.draw();
this.y = this.y + this.radius/2;
}
}
Creating stars and adding it to array
let stars = [];
function createStar(){
setInterval(()=>{
//Create random radius and X position
let randomRadius = Math.floor((Math.random()*5))
let randomXPosition = Math.floor((Math.random()*780)+15)
let velocity = 1;
stars.push(new Star(randomXPosition, -randomRadius, randomRadius, "white",velocity));
console.log("stars:"+ stars.length);
},300)
}
Below here I use a function calling itself to clear and refresh the star drawing. I have tried looping through stars array with forEach method, reversed loop (like in example below), I tried putting the if statement with splice function in seperate setTimeout(()=>{},0) or setTimeout(()=>{},10). I tried using the condition like
(forEach method removes stars, however the number of active stars do not remain more or less the same. It constantly slowly increases)
function animate(){
c.clearRect(0, 0, canvas.width, canvas.height);
for(let i = stars.length-1; i>=0; i--){
stars[i].update()
if (stars[i].y > canvas.height +stars[i].radius ){
stars.splice(stars[i], 1)
}
}
requestAnimationFrame(animate);
}
animate();
createStar();
I tried using the condition like:
if (stars[i].y > 5000 ){
stars.splice(stars[i], 1)
}
But it's not how it's supposed to be solved, because stars live for 5000 pixels longer,what makes game laggy.
Just to be clear on the problem.
If i generate 5 stars every 300ms and push the into the array called "stars" I get e.g.
[star1, star2, star3, star4, star5]
Lets say star1 and star 2 have small radius and they move slowly. Star3 is bigger therefore it moves faster and overtakes first two. When star3 reaches canvas.height + star[i].radius it disappear just over the canvas, but it also makes every star in array that was before star3 disappear (in this case it's star1 and star2).
This is my first post on stackoverflow. I apologise for all understatements in advance.
HTML Canvas
<body>
<div class="container">
<button class="logOutButton">Log Out</button>
<button class="topScores">Top 10</button>
<header>
<p class="hello">Hello <span id="spanName"></span></p>
<p class="topScore">Your TOP score: <span id="score"></span></p>
</header>
<div class="gameTitle">
<h2 class="title">Space Warrior</h2>
</div>
<canvas class="canvas" width="800" height="500"></canvas>
</div>
<script src="User.js"></script>
</body>
EDIT
I changed stars.splice(stars[i], 1) to stars.splice(i, 1) - not working
I tried adding another removal array like below but array stars just slowly gets bigger (even though some elements get removed)
var removeStar = [];
// stars.forEach((star,starIndex)=>{
let total = stars.length-1;
for(let i = total; i>=0; i--){
stars[i].update();
if (stars[i].y > canvas.height + stars[i].radius){
removeStar.push(i);
}
};
for(let i = removeStar.length; i>0; i--){
stars.splice(removeStar[i-1],1)
}
you wrote:
stars.splice(stars[i], 1)
first argument should be index which you want to remove ...just index stars.splice(i, 1)
..another problem is that you changing the array while looping within it, which is bad idea.
see this answer to very similar question:
https://stackoverflow.com/a/65725703/3054380
After fixing the bug mentioned above and adding minVelocity and star limiting condition (because you adding with interval and removing when star goes off the canvas - we need to limit in case interval goes faster) ...now everything looks good - working snippet below (added maxStars minVelocity)
let canvas = document.querySelector(".mycanvas");
let c = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 150;
let maxStars = 60;
let minVelocity = 0.5;
let stars = [];
class Star {
constructor(x, y, radius, color, velocity) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.velocity = velocity
}
draw() {
c.globalCompositeOperation = 'destination-over'
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
c.fillStyle = this.color;
c.shadowColor = "white"
c.shadowBlur = 12
c.fill();
c.shadowBlur = 0
}
update() {
this.draw();
this.y = this.y + Math.max(minVelocity, this.velocity * this.radius / 2);
}
}
function createStar() {
if (stars.length < maxStars) {
//Create random radius and X position
let randomRadius = Math.floor((Math.random() * 5));
let randomXPosition = Math.floor((Math.random() * (canvas.width - 20)) + 10);
let velocity = 1;
stars.push(new Star(randomXPosition, -randomRadius, randomRadius, "white", velocity));
console.log("stars:" + stars.length);
}
}
function animate() {
c.clearRect(0, 0, canvas.width, canvas.height);
for (let i = stars.length - 1; i >= 0; i--) {
stars[i].update()
if (stars[i].y > canvas.height) {
stars.splice(i, 1)
}
}
requestAnimationFrame(animate);
}
setInterval(createStar, 50);
animate();
.mycanvas {
width: 300px;
height: 150px;
border: 1px solid #f00;
}
<body style="background-color:#000;color:#fff;">
<div class="container">
<canvas class="mycanvas"></canvas>
</div>
<script src="User.js"></script>
</body>
I think I found the issue causing your everexpanding array. It has nothing to do with your splice function, which is implemented incorrectly as webdev-dan mentioned in his answer.
follow this logic
The radius has been set with Math.floor((Math.random()*5)).This means the radius can be 0.
In your update method you are increasing y based on the radius in this.y = this.y + this.radius/2. So this is possibly changing y with 0.
In if (stars[i].y > 5000 ) you are removing values of y over 5000.
So if the radius is 0, y doesn't change, stars are never removed. Nor can you see them visually.
solution
You could guarantee a minimal speed of 1. this.y += Math.max(1, this.radius/2).
PS: I had to do quite a bit of refactoring to figure this out. You are writing your code in a too complex way. Several types of logic are mixed up.
You really want to separate out your rendering logic from management of the stars object.
It is also quite hard to mentally keep track of the stars array because you are modifying it from anywhere; inside for loops, with async code ( the interval ).
This is what I ended up while trying to clean up your code a bit: https://jsfiddle.net/5hk0vscg/1/ Hope it's useful. Note that it is not a full cleanup, but it's an improvement over your current code.
Related
I'm trying to create a p5.js sketch based on an image that i inspired.
https://twitter.com/dmitricherniak/status/1287101982268948483?s=20 He made it with three.js.
I wrote a code that draw an array of circles and can control the length of circles by alpha.
But when i tried to modify color change by using Math.sin() function(method) it is not working.
As I increase this.hs, the result of this.Roff = sin(this.hs) keep returns 1.
I want them to be -1 to 1 and keep returning back and force.
Also I tried lerpColor() function to shift the color(also using sin()) but still returns 1.
I also wonder anyone knows how to generate more than 2 color gradient.
I want the gradient keep looping as well.
I made following animation example in Cinema 4D.
https://drive.google.com/file/d/1zTrIXJPMbqOsrx6VmLNrYdDCWvfmFSQ0/view?usp=sharing
class Particle {
constructor(x, y, v) {
this.x = x;
this.y = y;
this.vx = -v;
this.vy = v;
this.alp = 50;
// THIS.ROFF KEEPS RETURNING 1
this.hs = PI / 2;
this.inc = 0.1;
this.Roff = sin(this.hs);
this.Rshift = map(this.Roff, -1, 1, 0, 255);
}
finish() {
return this.alp < 0;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.alp -= 2;
this.hs += this.inc;
}
display() {
noStroke();
fill(this.Rshift, 0, 0, this.alp);
ellipse(this.x, this.y, 200);
}
}
For reference, I'm talking about the dark-gray space in the upper left of Discord's Login Page. For anyone who can't access that link, here's a screenshot:
It has a number of effects that are really cool, the dots and (darker shadows) move with the mouse, but I'm more interested in the "wobbly edge" effect, and to a lesser extent the "fast wobble/scale in" on page load (scaling in the canvas on load would give a similar, if not "cheaper" effect).
Unfortunately, I can't produce much in the way of a MCVE, because I'm not really sure where to start. I tried digging through Discord's assets, but I'm not familiar enough to Webpack to be able to determine what's going on.
Everything I've been able to dig up on "animated wave/wobble" is CSS powered SVG or clip-path borders, I'd like to produce something a bit more organic.
Very interesting problem. I've scaled the blob down so it is visible in the preview below.
Here is a codepen as well at a larger size.
const SCALE = 0.25;
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI / 2;
const canvas = document.createElement("canvas");
const c = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
class Blob {
constructor() {
this.wobbleIncrement = 0;
// use this to change the size of the blob
this.radius = 500;
// think of this as detail level
// number of conections in the `bezierSkin`
this.segments = 12;
this.step = HALF_PI / this.segments;
this.anchors = [];
this.radii = [];
this.thetaOff = [];
const bumpRadius = 100;
const halfBumpRadius = bumpRadius / 2;
for (let i = 0; i < this.segments + 2; i++) {
this.anchors.push(0, 0);
this.radii.push(Math.random() * bumpRadius - halfBumpRadius);
this.thetaOff.push(Math.random() * TWO_PI);
}
this.theta = 0;
this.thetaRamp = 0;
this.thetaRampDest = 12;
this.rampDamp = 25;
}
update() {
this.thetaRamp += (this.thetaRampDest - this.thetaRamp) / this.rampDamp;
this.theta += 0.03;
this.anchors = [0, this.radius];
for (let i = 0; i <= this.segments + 2; i++) {
const sine = Math.sin(this.thetaOff[i] + this.theta + this.thetaRamp);
const rad = this.radius + this.radii[i] * sine;
const theta = this.step * i;
const x = rad * Math.sin(theta);
const y = rad * Math.cos(theta);
this.anchors.push(x, y);
}
c.save();
c.translate(-10, -10);
c.scale(SCALE, SCALE);
c.fillStyle = "blue";
c.beginPath();
c.moveTo(0, 0);
bezierSkin(this.anchors, false);
c.lineTo(0, 0);
c.fill();
c.restore();
}
}
const blob = new Blob();
function loop() {
c.clearRect(0, 0, canvas.width, canvas.height);
blob.update();
window.requestAnimationFrame(loop);
}
loop();
// array of xy coords, closed boolean
function bezierSkin(bez, closed = true) {
const avg = calcAvgs(bez);
const leng = bez.length;
if (closed) {
c.moveTo(avg[0], avg[1]);
for (let i = 2; i < leng; i += 2) {
let n = i + 1;
c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
}
c.quadraticCurveTo(bez[0], bez[1], avg[0], avg[1]);
} else {
c.moveTo(bez[0], bez[1]);
c.lineTo(avg[0], avg[1]);
for (let i = 2; i < leng - 2; i += 2) {
let n = i + 1;
c.quadraticCurveTo(bez[i], bez[n], avg[i], avg[n]);
}
c.lineTo(bez[leng - 2], bez[leng - 1]);
}
}
// create anchor points by averaging the control points
function calcAvgs(p) {
const avg = [];
const leng = p.length;
let prev;
for (let i = 2; i < leng; i++) {
prev = i - 2;
avg.push((p[prev] + p[i]) / 2);
}
// close
avg.push((p[0] + p[leng - 2]) / 2, (p[1] + p[leng - 1]) / 2);
return avg;
}
There are lots of things going on here. In order to create this effect you need a good working knowledge of how quadratic bezier curves are defined. Once you have that, there is an old trick that I've used many many times over the years. To generate smooth linked quadratic bezier curves, define a list of points and calculate their averages. Then use the points as control points and the new averaged points as anchor points. See the bezierSkin and calcAvgs functions.
With the ability to draw smooth bezier curves, the rest is about positioning the points in an arc and then animating them. For this we use a little math:
x = radius * sin(theta)
y = radius * cos(theta)
That converts polar to cartesian coordinates. Where theta is the angle on the circumference of a circle [0 - 2pi].
As for the animation, there is a good deal more going on here - I'll see if I have some more time this weekend to update the answer with more details and info, but hopefully this will be helpful.
The animation runs on a canvas and it is a simple bezier curve animation.
For organic feel, you should look at perlin noise, that was introduced when developing original Tron video FX.
You can find a good guide to understand perlin noise here.
In the example I've used https://github.com/josephg/noisejs
var c = $('canvas').get(0).getContext('2d');
var simplex = new SimplexNoise();
var t = 0;
function init() {
window.requestAnimationFrame(draw);
}
function draw() {
c.clearRect(0, 0, 600, 300);
c.strokeStyle="blue";
c.moveTo(100,100);
c.lineTo(300,100);
c.stroke();
// Draw a Bézier curve by using the same line cooridinates.
c.beginPath();
c.lineWidth="3";
c.strokeStyle="black";
c.moveTo(100,100);
c.bezierCurveTo((simplex.noise2D(t,t)+1)*200,(simplex.noise2D(t,t)+1)*200,(simplex.noise2D(t,t)+1)*200,0,300,100);
c.stroke();
// draw reference points
c.fillRect(100-5,100-5,10,10);
c.fillRect(200-5,200-5,10,10);
c.fillRect(200-5,0-5,10,10);
c.fillRect(300-5,100-5,10,10);
t+=0.001;
window.requestAnimationFrame(draw);
}
init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.js"></script>
<canvas width="600" height="300"></canvas>
Note: further investigation on Discord source code, I've pointed out that's is using https://www.npm.red/~epistemex libraries. Epistemex NPM packages are still online, while GitHub repos and profile does not exists anymore.
Note 2: Another approach could be relying on physics libraries like this demo, but it can be an overkill, if you just need a single effect.
I have been given the following task, but I am getting errors that can be seen when the code snippet is run. I would like some help figuring out what exactly I am doing wrong.
Basically, I need to draw a circle, make it so that it moves and changes the direction/color when touching the walls of the screen.
Task: create a Circle class with the following properties:
x - the initial value of the coordinate x
y is the initial value of the y coordinate
radius - values of width and height
color - fill color Describe the methods:
draw () - marks off on the screen an element that is described by the given properties
setColor (newColor) - Changes the fill color to newColor
move ({x = 0, y = 0}) - moves the captured object by the vector (x, y) - each time period (for example, 100 ms) changes (adds \ subtracts)
to the values x and y, respectively. When a circle collides with any
edge of the screen it is necessary to realize its mirror reflection
(change the value of the corresponding coordinate of the vector on the
opposite of the value of the sign, and call this method with the new
vector) and generate the collision event, collision, which is captured
at the document level.Hang on this event a handler that will change
the color of the pouring of the circle into another (random) value.
Movement occurs until the stop method is called.
stop () - stops the circle movement
If the Escape button on the keyboard was pressed, the movement should stop.
I created a canvas and set the frame to move. I drew a circle and tried to move it using setInterval(), but it seems like I'm losing the context.
let c = document.getElementById("mycanvas");
let ctx = c.getContext("2d");
let xinc = 1;
let yinc = 1;
class Circle {
constructor(xpos, ypos, radius, color) {
this.xpos = xpos;
this.ypos = ypos;
this.radius = radius;
this.color = color;
}
draw() {
ctx.beginPath();
ctx.arc(this.xpos, this.ypos, this.radius, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
}
move(xpos, ypos) {
ctx.clearRect(0, 0, c.width, c.height);
ctx.beginPath();
this.draw();
xpos += xinc;
ypos += yinc;
console.log(xpos, ypos);
if ((this.xpos > c.width - this.radius) || (this.xpos < 0 + this.radius)) {
xinc = -xinc;
}
if ((this.ypos > c.height - this.radius) || (this.ypos < 0 + this.radius)) {
yinc = -yinc;
}
setInterval(this.move, 10);
//this.draw();
}
}
let circle = new Circle(200, 300, 50, "red");
circle.draw();
circle.move(200, 300);
<canvas id="mycanvas" width="1335" height="650" style="border: 1px solid"> </canvas>
I am just starting to learn events and DOMs, please help me correctly implement this task
You are passing this.move to setInterval with no context - just a function, with no this to call it in. You can pass in this.move.bind(this) to create a bound function. You can also do it once in the constructor: this.move = this.move.bind(this).
Also, the call to beginPath in move seems unnecessary.
I'm in the middle of creating this simple animation using HTML5 Canvas and JavaScript and I'm experiencing a problem with flickering objects.
I was trying to find the solution on the internet before I asked this question and all I found was basically:
avoid loading new image , object at each new frame
use requestAnimationFrame()
I think I've done that all and the flickering is still happening.
(blue rectangles (obstacles) in my case.
The only solution that works is reducing the number of pixels in method responsible for moving the object, here:
obstacle.prototype.moveObstacle = function(){
this.x -=3
}
but the the animation is too slow.
Is there any way around it?
JSFiddle: https://jsfiddle.net/wojmjaq6/
Code:
var cnv = document.getElementById("gameField");
var ctx = cnv.getContext("2d");
var speedY = 1
var obst1 = new obstacle(cnv.width + 50);
var myBird = new bird(100, 1);
function bird(x, y) {
this.x = x;
this.y = y;
this.gravity = 0.3
this.gravitySpeed = 0
}
bird.prototype.drawbird = function() {
ctx.fillStyle = "red"
ctx.fillRect(this.x, this.y, 20, 20);
}
bird.prototype.animate = function() {
this.gravitySpeed += this.gravity
this.y += speedY + this.gravitySpeed
}
function obstacle(x) {
this.x = x;
this.y = 0;
this.obstLen = Math.floor(Math.random() * 400)
}
obstacle.prototype.drawobstacle = function() {
ctx.fillStyle = "blue";
ctx.fillRect(this.x, this.y, 15, this.obstLen)
ctx.fillRect(this.x, cnv.height, 15, -(cnv.height - this.obstLen - 100))
}
obstacle.prototype.moveObstacle = function() {
this.x -= 3
}
function myFun() {
ctx.clearRect(0, 0, cnv.width, cnv.height);
myBird.animate();
myBird.drawbird();
obst1.moveObstacle();
obst1.drawobstacle();
if (obst1.x < 0) {
obst1 = new obstacle(cnv.width + 50);
}
window.requestAnimationFrame(myFun)
};
function test() {
if (myBird.gravity > 0) {
myBird.gravity = -1
} else {
myBird.gravity = 0.3
}
}
document.getElementById("gameField").onmousedown = test
document.getElementById("gameField").onmouseup = test
window.requestAnimationFrame(myFun)
I do see some stuttering with the blue obstacle - the animation is not smooth.
Changing the x position of the obstacle based on the raw requestAnimationFrame loop will not necessarily result in a smooth operation as requestAnimationFrame just requests that the browser re-draws when it can.
The time between calls to requestAnimationFrame can vary depending on the power of the device the animation is on and how much there is to do each frame. There is no guarantee that requestAnimationFrame will give you 60 FPS.
The solutions are to decouple the changing of objects positions with the actual drawing of them, or factor it the elapsed time between frames and calculate the new position based on that to give a smooth animation.
Normally in my canvas animations I just use a library like GreenSock's Animation Platform (GSAP) https://greensock.com/get-started-js which can animate any numeric property over time, then I only have to write code for the drawing part.
It is possible to compute a time based animation in your own requestAnimationFrame, though there is a bit of complexity involved. This looks like a good tutorial on it http://www.javascriptkit.com/javatutors/requestanimationframe.shtml
Cheers,
DouG
I am writing a script to draw an image of the sun, and have an image of the earth orbit around it.
I have defined a planet class as so:
function planet(name, size, rotateRate, startx, starty, colour, scale, oRad, oAng, oSpd){//container class for planets
this.name = name;
this.rotateRate = rotateRate;
this.x = startx;
this.y = starty;
this.colour = colour;
this.scale = scale;
this.size = size;
this.orbitRadius= oRad;
this.orbitAngle = oAng;
this.orbitSpeed = oSpd;
this.drawByArc =
function drawArcCircle(){//draws circles using the arc method
context.save();
context.strokeStyle = '#000000';
context.fillStyle = this.colour;
context.lineWidth=3;
context.beginPath();
context.arc(this.x,this.y,this.size*this.scale,0,Math.PI * 2,false)
context.stroke();
context.fill();
context.closePath();
context.restore();
}
}
Now I have created two instances of the class in the program and drawn them fine using the following functions:
function gameLoop(){//The Game Loop
var thisTime = Date.now();//current time
var deltaTime = thisTime - lastTime;//find difference, not yet used
update();
draw();
lastTime = thisTime;
setTimeout(gameLoop, 1000/60);
}
function draw(){// Draws all the objects
drawBackground();
Sun.drawByArc();
Earth.drawByArc();
}
function update(){// Updates for animation
//var newRotation = this.getCurrantRotation() + (this.getRotationRate()*deltaTime);
var gSegments;
gScale = 4;
simSpeed = 10;
Sun.scale = gScale;
Earth.scale = gScale;
Earth.orbitSpeed = 360/simSpeed;
//Earth.x = Sun.x + Earth.orbitRadius * Math.cos(Earth.orbitAngle * Math.pi / 180);
//Earth.y = Sun.y - Earth.orbitRadius * Math.sin(Earth.orbitAngle * Math.pi / 180);
}
When i have the last two lines of the update method commented out, both circles draw fine, however when i add the final two lines in to attempt to update the earths position in orbit, when i attempt to run the code in chrome the Earth sphere vanishes!
Chrome debugger shows no errors so i'm at a loss as to why it occurs.
EDITED::
Well, I found that thanks to a small typing error (math.pi instead of Math.PI) my planet x and y values were becoming NaN.
however now my earth is stuck at at 90 degree point in its orbit and simply doesnt move, at least it draws, any ideas?
Solved it.
Most of the issues came from using math.pi instead of Math.PI
on top of that, i had not set a value to change the angle of orbit, meaning the orbit always remained at 90, making there be no orbit.
Chrome debug very much helped me in figuring all this out, so thanks a lot user1816548