I'm working on a project where I simulate physics with balls.
Here is the link to the p5 editor of the project.
My problem is the following, when I add a lot of ball (like 200), balls are stacking but some of them will eventually collapse and I don't know why.
Can somebody explain why it does this and how to solve the problem ?
Thanks.
Here is the code of the sketch.
document.oncontextmenu = function () {
return false;
}
let isFlushing = false;
let isBallDiameterRandom = false;
let displayInfos = true;
let displayWeight = false;
let clickOnce = false;
let FRAME_RATE = 60;
let SPEED_FLUSH = 3;
let Y_GROUND;
let lastFR;
let balls = [];
function setup() {
frameRate(FRAME_RATE);
createCanvas(window.innerWidth, window.innerHeight);
Y_GROUND = height / 20 * 19;
lastFR = FRAME_RATE;
}
function draw() {
background(255);
if (isFlushing) {
for (let i = 0; i < SPEED_FLUSH; i++) {
balls.pop();
}
if (balls.length === 0) {
isFlushing = false;
}
}
balls.forEach(ball => {
ball.collide();
ball.move();
ball.display(displayWeight);
ball.checkCollisions();
});
if (mouseIsPressed) {
let ballDiameter;
if (isBallDiameterRandom) {
ballDiameter = random(15, 101);
} else {
ballDiameter = 25;
}
if (canAddBall(mouseX, mouseY, ballDiameter)) {
isFlushing = false;
let newBall = new Ball(mouseX, mouseY, ballDiameter, balls);
if (mouseButton === LEFT && !clickOnce) {
balls.push(newBall);
clickOnce = true;
}
if (mouseButton === RIGHT) {
balls.push(newBall);
}
}
}
drawGround();
if (displayInfos) {
displayShortcuts();
displayFrameRate();
displayBallCount();
}
}
function mouseReleased() {
if (mouseButton === LEFT) {
clickOnce = false;
}
}
function keyPressed() {
if (keyCode === 32) {//SPACE
displayInfos = !displayInfos;
}
if (keyCode === 70) {//F
isFlushing = true;
}
if (keyCode === 71) {//G
isBallDiameterRandom = !isBallDiameterRandom;
}
if (keyCode === 72) {//H
displayWeight = !displayWeight;
}
}
function canAddBall(x, y, d) {
let isInScreen =
y + d / 2 < Y_GROUND &&
y - d / 2 > 0 &&
x + d / 2 < width &&
x - d / 2 > 0;
let isInAnotherBall = false;
for (let i = 0; i < balls.length; i++) {
let d = dist(x, y, balls[i].position.x, balls[i].position.y);
if (d < balls[i].w) {
isInAnotherBall = true;
break;
}
}
return isInScreen && !isInAnotherBall;
}
function drawGround() {
strokeWeight(0);
fill('rgba(200,200,200, 0.25)');
rect(0, height / 10 * 9, width, height / 10);
}
function displayFrameRate() {
if (frameCount % 30 === 0) {
lastFR = round(frameRate());
}
textSize(50);
fill(255, 0, 0);
let lastFRWidth = textWidth(lastFR);
text(lastFR, width - lastFRWidth - 25, 50);
textSize(10);
text('fps', width - 20, 50);
}
function displayBallCount() {
textSize(50);
fill(255, 0, 0);
text(balls.length, 10, 50);
let twBalls = textWidth(balls.length);
textSize(10);
text('balls', 15 + twBalls, 50);
}
function displayShortcuts() {
let hStart = 30;
let steps = 15;
let maxTW = 0;
let controlTexts = [
'LEFT CLICK : add 1 ball',
'RIGHT CLICK : add 1 ball continuously',
'SPACE : display infos',
'F : flush balls',
'G : set random ball diameter (' + isBallDiameterRandom + ')',
'H : display weight of balls (' + displayWeight + ')'
];
textSize(11);
fill(0);
for (let i = 0; i < controlTexts.length; i++) {
let currentTW = textWidth(controlTexts[i]);
if (currentTW > maxTW) {
maxTW = currentTW;
}
}
for (let i = 0; i < controlTexts.length; i++) {
text(controlTexts[i], width / 2 - maxTW / 2 + 5, hStart);
hStart += steps;
}
fill(200, 200, 200, 100);
rect(width / 2 - maxTW / 2,
hStart - (controlTexts.length + 1) * steps,
maxTW + steps,
(controlTexts.length + 1) * steps - steps / 2
);
}
Here is the code of the Ball class.
class Ball {
constructor(x, y, w, e) {
this.id = e.length;
this.w = w;
this.e = e;
this.progressiveWidth = 0;
this.rgb = [
floor(random(0, 256)),
floor(random(0, 256)),
floor(random(0, 256))
];
this.mass = w;
this.position = createVector(x + random(-1, 1), y);
this.velocity = createVector(0, 0);
this.acceleration = createVector(0, 0);
this.gravity = 0.2;
this.friction = 0.5;
}
collide() {
for (let i = this.id + 1; i < this.e.length; i++) {
let dx = this.e[i].position.x - this.position.x;
let dy = this.e[i].position.y - this.position.y;
let distance = sqrt(dx * dx + dy * dy);
let minDist = this.e[i].w / 2 + this.w / 2;
if (distance < minDist) {
let angle = atan2(dy, dx);
let targetX = this.position.x + cos(angle) * minDist;
let targetY = this.position.y + sin(angle) * minDist;
this.acceleration.set(
targetX - this.e[i].position.x,
targetY - this.e[i].position.y
);
this.velocity.sub(this.acceleration);
this.e[i].velocity.add(this.acceleration);
//TODO : Effets bizarre quand on empile les boules (chevauchement)
this.velocity.mult(this.friction);
}
}
}
move() {
this.velocity.add(createVector(0, this.gravity));
this.position.add(this.velocity);
}
display(displayMass) {
if (this.progressiveWidth < this.w) {
this.progressiveWidth += this.w / 10;
}
stroke(0);
strokeWeight(2);
fill(this.rgb[0], this.rgb[1], this.rgb[2], 100);
ellipse(this.position.x, this.position.y, this.progressiveWidth);
if (displayMass) {
strokeWeight(1);
textSize(10);
let tempTW = textWidth(int(this.w));
text(int(this.w), this.position.x - tempTW / 2, this.position.y + 4);
}
}
checkCollisions() {
if (this.position.x > width - this.w / 2) {
this.velocity.x *= -this.friction;
this.position.x = width - this.w / 2;
} else if (this.position.x < this.w / 2) {
this.velocity.x *= -this.friction;
this.position.x = this.w / 2;
}
if (this.position.y > Y_GROUND - this.w / 2) {
this.velocity.x -= this.velocity.x / 100;
this.velocity.y *= -this.friction;
this.position.y = Y_GROUND - this.w / 2;
} else if (this.position.y < this.w / 2) {
this.velocity.y *= -this.friction;
this.position.y = this.w / 2;
}
}
}
I see this overlapping happen when the sum of ball masses gets bigger than the elasticity of the balls. At least it seems so. I made a copy with a smaller pool so it doesn't take so much time to reproduce the problem.
In the following example, with 6 balls (a mass of 150 units) pressing on the base row, we see that the 13 balls in the base row overlap. The base row has a width of ca. 300 pixels, which is only enough space for 12 balls of diameter 25. I think this is showing the limitation of the model: the balls are displayed circular but indeed have an amount of elasticity that they should display deformed instead. It's hard to say how this can be fixed without implementing drawing complicated shapes. Maybe less friction?
BTW: great physics engine you built there :-)
Meanwhile I was able to make another screenshot with even fewer balls. The weight of three of them (eq. 75 units) is sufficient to create overlapping in the base row.
I doubled the size of the balls and changed the pool dimensions as to detedt that there is a more serious error in the engine. I see that the balls are pressed so heavily under pressure that they have not enough space for their "volume" (area). Either they have to implode or it's elastic counter force must have greater impact of the whole scene. If you pay close attention to the pendulum movements made by the balls at the bottom, which have the least space, you will see that they are very violent, but apparently have no chance of reaching the outside.
Could it be that your evaluation order
balls.forEach(ball => {
ball.collide();
ball.move();
ball.display(displayWeight);
ball.checkCollisions();
});
is not able to propagate the collisions in a realistic way?
Related
I am trying the simple circle filling and it works. But when I try to fill the first circle with more circles, it hangs the program immediately. Here's my draw and and class code which fills the circles inside circles:
let krr=[];
function draw() {
background(0);
print(cir.length,krr.length)
if (cir.length<=100){
let temp=new c();
cir.push(temp);
}
else if (krr.length<100){
print(1)
fr=cir[0]
if (fr.r>50){
let re=new cirgain(fr.x,fr.y,(fr.r)/2)
krr.push(re);
}
}
for (let h of krr){
h.show();
}
for (let g of cir){
g.show();
g.grow();
}
}
class cirgain{
constructor(x,y,r){
this.smr=floor(r/3);
if (krr.length==0){
while (true){
this.x=random(x-r+1,x+r-1);
this.y=random(y-r+1,y+r-1)
if (this.x*this.x+this.y*this.y<r*r-2){
break
}
}
}
else{
let flag1=1
let count1=0
while (flag){
if (count1>=500){
count1=0;
this.smr--;
}
while (true){
this.x=random(x-r+1,x+r-1);
this.y=random(y-r+1,y+r-1);
if (this.x*this.x+this.y*this.y<r*r-2)
break
}
for (let i=0;i<krr.length;i++){
if (dist(krr[i].x,krr[i].y,this.x,this.y)<r+this.smr){
flag1=1
count1++;
break;
}
flag1=0;
}
}
}
this.ccc=createVector(random(255),random(100,255),random(100,255))
}
show(){
stroke(0);
noFill();
strokeWeight(3)
stroke(this.ccc.x,this.ccc.y,this.ccc.z);
circle(this.x,this.y,this.smr)
}
}
If the whole code (including setup and class c (which at first fills the space with circles)) is needed, let me know, I will edit to include it.
Edit: Okay, here is the whole code:
let cir = [];
let maxR;
let krr = [];
function setup() {
createCanvas(windowWidth, windowHeight);
maxR = width / 4;
if (height > width)
maxR = height / 4
colorMode(HSB);
angleMode(DEGREES);
}
function draw() {
background(0);
print(cir.length, krr.length)
if (cir.length <= 100) {
let temp = new c();
cir.push(temp);
} else if (krr.length < 100) {
print(1)
fr = cir[0]
if (fr.r > 50) {
let re = new cirgain(fr.x, fr.y, (fr.r) / 2)
krr.push(re);
}
}
for (let h of krr) {
h.show();
}
for (let g of cir) {
g.show();
g.grow();
}
}
class c {
constructor() {
this.tempr = 1
if (cir.length == 0) {
this.x = random(maxR + 1, width - maxR - 1);
this.y = random(maxR + 1, height - maxR - 1)
this.r = maxR;
} else {
let flag = 1
let count = 0
while (flag) {
if (count >= 500) {
count = 0;
maxR--;
}
this.x = random(maxR / 2 + 1, width - maxR / 2 - 1);
this.y = random(maxR / 2 + 1, height - maxR / 2 - 1);
this.r = maxR;
for (let i = 0; i < cir.length; i++) {
if (dist(cir[i].x, cir[i].y, this.x, this.y) < cir[i].r / 2 + this.r / 2 + 3) {
flag = 1
count++;
break;
}
flag = 0;
}
}
}
this.cc = createVector(random(255), random(100, 255), random(100, 255))
}
show() {
stroke(0);
noFill();
strokeWeight(3)
stroke(this.cc.x, this.cc.y, this.cc.z);
circle(this.x, this.y, this.tempr)
rectMode(CENTER);
}
grow() {
if (this.tempr <= this.r)
// this.tempr+=.5
this.tempr += this.r / 100
}
}
class cirgain {
constructor(x, y, r) {
this.smr = floor(r / 3);
if (krr.length == 0) {
while (true) {
this.x = random(x - r + 1, x + r - 1);
this.y = random(y - r + 1, y + r - 1)
if (this.x * this.x + this.y * this.y < r * r - 2) {
break
}
}
} else {
let flag1 = 1
let count1 = 0
while (flag) {
if (count1 >= 500) {
count1 = 0;
this.smr--;
}
while (true) {
this.x = random(x - r + 1, x + r - 1);
this.y = random(y - r + 1, y + r - 1);
if (this.x * this.x + this.y * this.y < r * r - 2)
break
}
for (let i = 0; i < krr.length; i++) {
if (dist(krr[i].x, krr[i].y, this.x, this.y) < r + this.smr) {
flag1 = 1
count1++;
break;
}
flag1 = 0;
}
}
}
this.ccc = createVector(random(255), random(100, 255), random(100, 255))
}
show() {
stroke(0);
noFill();
strokeWeight(3)
stroke(this.ccc.x, this.ccc.y, this.ccc.z);
circle(this.x, this.y, this.smr)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
Your logic for finding viable x/y values randomly in the cirgain constructor seems broken. Several of your while loops seem like they never exit.
let cir = [];
let krr = [];
let maxR;
function setup() {
createCanvas(windowWidth, windowHeight);
maxR = width / 4;
if (height > width) {
maxR = height / 4;
}
colorMode(HSB);
angleMode(DEGREES);
}
function draw() {
background(0);
print(cir.length, krr.length);
if (cir.length <= 100) {
let temp = new c();
cir.push(temp);
} else if (krr.length < 100) {
print(1)
fr = cir[0]
if (fr.r > 50) {
let re = new cirgain(fr.x, fr.y, (fr.r) / 2)
krr.push(re);
}
}
for (let h of krr) {
h.show();
}
for (let g of cir) {
g.show();
g.grow();
}
}
class c {
constructor() {
this.tempr = 1
if (cir.length == 0) {
this.x = random(maxR + 1, width - maxR - 1);
this.y = random(maxR + 1, height - maxR - 1)
this.r = maxR;
} else {
let flag = 1
let count = 0
while (flag) {
if (count >= 500) {
count = 0;
maxR--;
}
this.x = random(maxR / 2 + 1, width - maxR / 2 - 1);
this.y = random(maxR / 2 + 1, height - maxR / 2 - 1);
this.r = maxR;
for (let i = 0; i < cir.length; i++) {
if (dist(cir[i].x, cir[i].y, this.x, this.y) < cir[i].r / 2 + this.r / 2 + 3) {
flag = 1
count++;
break;
}
flag = 0;
}
}
}
this.cc = createVector(random(255), random(100, 255), random(100, 255))
}
show() {
stroke(0);
noFill();
strokeWeight(3)
stroke(this.cc.x, this.cc.y, this.cc.z);
circle(this.x, this.y, this.tempr)
rectMode(CENTER);
}
grow() {
if (this.tempr <= this.r)
// this.tempr+=.5
this.tempr += this.r / 100
}
}
class cirgain {
constructor(x, y, r) {
this.smr = floor(r / 3);
if (krr.length == 0) {
let n = 0;
while (++n < 1000) {
this.x = random(x - r + 1, x + r - 1);
this.y = random(y - r + 1, y + r - 1);
if (this.x * this.x + this.y * this.y < r * r - 2) {
break;
}
}
if (n >= 1000) {
console.warn('1. Unable to find an x/y value that satisfied the constraint');
debugger;
}
} else {
let flag1 = 1;
let count1 = 0;
let n = 0;
while (flag1 && ++n < 10000) {
if (count1 >= 500) {
count1 = 0;
this.smr--;
}
let n2 = 0;
while (++n2 < 1000) {
this.x = random(x - r + 1, x + r - 1);
this.y = random(y - r + 1, y + r - 1);
if (this.x * this.x + this.y * this.y < r * r - 2) {
break;
}
}
if (n2 >= 1000) {
console.warn('2. Unable to find an x/y value that satisfied the constraint');
debugger;
}
for (let i = 0; i < krr.length; i++) {
if (dist(krr[i].x, krr[i].y, this.x, this.y) < r + this.smr) {
flag1 = 1
count1++;
break;
}
flag1 = 0;
}
}
if (n >= 10000) {
console.warn('3. Flag never set to false');
debugger;
}
}
this.ccc = createVector(random(255), random(100, 255), random(100, 255))
}
show() {
stroke(0);
noFill();
strokeWeight(3)
stroke(this.ccc.x, this.ccc.y, this.ccc.z);
circle(this.x, this.y, this.smr);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
Update
You seem to have an error in your logic checking if the randomly generated x/y values are acceptable:
// Instead of this, which checks if the square of the distance from the top left (0, 0) to the randomly generated center is less than the radius squared
if (this.x * this.x + this.y * this.y < r * r - 2) {
break;
}
// You probably meant to check if the distance from the center of the circle to the randomly generated position is less than the radius:
if (dist(x, y, this.x, this.y) < r - 2) {
break;
}
Additionally I cannot make heads or tails of this logic:
for (let i = 0; i < krr.length; i++) {
if (dist(krr[i].x, krr[i].y, this.x, this.y) < r + this.smr) {
flag1 = 1
count1++;
break;
}
flag1 = 0;
}
For each cirgain that you create you are check that for every existing cirgain the new one has a distance from the existing that is less than the containing circle's radius plus the current circle's diameter*, and if that is the case (which it almost always is!) you try to find a different random position, and only after 500 attempts you decrease the current circle's radius. I can hardly tell what your objective is, but there has to be a better way.
* You're using the default ellipse mode, which means the third argument to circle() is the diameter, which makes this code especially confusing
I have written this code to demonstrate a basic visual p5js project. In here there are 10 balls of varying sizes and colors that spawn at random positions, move around in the canvas and might collide with each other. I am not looking for elastic collision or "realistic" collision physics for that matter. I just want the balls to change to a different direction (can be random as long as it works) and work accordingly.
Here is my code :
class Ball {
//create new ball using given arguments
constructor(pos, vel, radius, color) {
this.pos = pos;
this.vel = vel;
this.radius = radius;
this.color = color;
}
//collision detection
collide(check) {
if (check == this) {
return;
}
let relative = p5.Vector.sub(check.pos, this.pos);
let dist = relative.mag() - (this.radius + check.radius);
if (dist < 0) { //HELP HERE! <--
this.vel.mult(-1);
check.vel.mult(-1);
}
}
//give life to the ball
move() {
this.pos.add(this.vel);
if (this.pos.x < this.radius) {
this.pos.x = this.radius;
this.vel.x = -this.vel.x;
}
if (this.pos.x > width - this.radius) {
this.pos.x = width - this.radius;
this.vel.x = -this.vel.x;
}
if (this.pos.y < this.radius) {
this.pos.y = this.radius;
this.vel.y = -this.vel.y;
}
if (this.pos.y > height - this.radius) {
this.pos.y = height - this.radius;
this.vel.y = -this.vel.y;
}
}
//show the ball on the canvas
render() {
fill(this.color);
noStroke();
ellipse(this.pos.x, this.pos.y, this.radius * 2);
}
}
let balls = []; //stores all the balls
function setup() {
createCanvas(window.windowWidth, window.windowHeight);
let n = 10;
//loop to create n balls
for (i = 0; i < n; i++) {
balls.push(
new Ball(
createVector(random(width), random(height)),
p5.Vector.random2D().mult(random(5)),
random(20, 50),
color(random(255), random(255), random(255))
)
);
}
}
function draw() {
background(0);
//loop to detect collision at all instances
for (let i = 0; i < balls.length; i++) {
for (let j = 0; j < i; j++) {
balls[i].collide(balls[j]);
}
}
//loop to render and move all balls
for (let i = 0; i < balls.length; i++) {
balls[i].move();
balls[i].render();
}
}
Here is a link to the project : https://editor.p5js.org/AdilBub/sketches/TNn2OREsN
All I need is the collision to change the direction of the ball to a random direction and not get stuck. Any help would be appreciated. I am teaching kids this program so I just want basic collision, doesnot have to be "realistic".
Any help is appreciated. Thank you.
The issues you are currently encountering with balls being stuck has to do with randomly generating balls that overlap such that after one iteration of movement they still overlap. When this happens both balls will simply oscillate in place repeatedly colliding with each other. You can prevent this simply by checking for collisions before adding new balls:
class Ball {
//create new ball using given arguments
constructor(pos, vel, radius, color) {
this.pos = pos;
this.vel = vel;
this.radius = radius;
this.color = color;
}
isColliding(check) {
if (check == this) {
return;
}
let relative = p5.Vector.sub(check.pos, this.pos);
let dist = relative.mag() - (this.radius + check.radius);
return dist < 0;
}
//collision detection
collide(check) {
if (this.isColliding(check)) {
this.vel.x *= -1;
this.vel.y *= -1;
check.vel.x *= -1;
check.vel.y *= -1;
}
}
//give life to the ball
move() {
this.pos.add(this.vel);
if (this.pos.x < this.radius) {
this.pos.x = this.radius;
this.vel.x = -this.vel.x;
}
if (this.pos.x > width - this.radius) {
this.pos.x = width - this.radius;
this.vel.x = -this.vel.x;
}
if (this.pos.y < this.radius) {
this.pos.y = this.radius;
this.vel.y = -this.vel.y;
}
if (this.pos.y > height - this.radius) {
this.pos.y = height - this.radius;
this.vel.y = -this.vel.y;
}
}
//show the ball on the canvas
render() {
fill(this.color);
noStroke();
ellipse(this.pos.x, this.pos.y, this.radius * 2);
}
}
let balls = []; //stores all the balls
function setup() {
createCanvas(500, 500);
let n = 10;
//loop to create n balls
for (i = 0; i < n; i++) {
let newBall =
new Ball(
createVector(random(width), random(height)),
p5.Vector.random2D().mult(random(5)),
random(20, 40),
color(random(255), random(255), random(255))
);
let isOk = true;
// check for collisions with existing balls
for (let j = 0; j < balls.length; j++) {
if (newBall.isColliding(balls[j])) {
isOk = false;
break;
}
}
if (isOk) {
balls.push(newBall);
} else {
// try again
i--;
}
}
}
function draw() {
background(0);
//loop to detect collision at all instances
for (let i = 0; i < balls.length; i++) {
for (let j = 0; j < i; j++) {
balls[i].collide(balls[j]);
}
}
//loop to render and move all balls
for (let i = 0; i < balls.length; i++) {
balls[i].move();
balls[i].render();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
That said, fully elastic collisions (which means collisions are instantaneous and involve no loss of energy due to deformation and resulting heat emission) are actually quite simple to simulate. Here's a tutorial I made on OpenProcessing demonstrating the necessary concepts using p5.js: Elastic Ball Collision Tutorial.
Here's the final version of the code from that tutorial:
const radius = 30;
const speed = 100;
let time;
let balls = []
let boundary = [];
let obstacles = [];
let paused = false;
function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
ellipseMode(RADIUS);
boundary.push(createVector(60, 4));
boundary.push(createVector(width - 4, 60));
boundary.push(createVector(width - 60, height - 4));
boundary.push(createVector(4, height - 60));
obstacles.push(createVector(width / 2, height / 2));
balls.push({
pos: createVector(width * 0.25, height * 0.25),
vel: createVector(speed, 0).rotate(random(0, 360))
});
balls.push({
pos: createVector(width * 0.75, height * 0.75),
vel: createVector(speed, 0).rotate(random(0, 360))
});
balls.push({
pos: createVector(width * 0.25, height * 0.75),
vel: createVector(speed, 0).rotate(random(0, 360))
});
time = millis();
}
function keyPressed() {
if (key === "p") {
paused = !paused;
time = millis();
}
}
function draw() {
if (paused) {
return;
}
deltaT = millis() - time;
time = millis();
background('dimgray');
push();
fill('lightgray');
stroke('black');
strokeWeight(2);
beginShape();
for (let v of boundary) {
vertex(v.x, v.y);
}
endShape(CLOSE);
pop();
push();
fill('dimgray');
for (let obstacle of obstacles) {
circle(obstacle.x, obstacle.y, radius);
}
pop();
for (let i = 0; i < balls.length; i++) {
let ball = balls[i];
// update position
ball.pos = createVector(
min(max(0, ball.pos.x + ball.vel.x * (deltaT / 1000)), width),
min(max(0, ball.pos.y + ball.vel.y * (deltaT / 1000)), height)
);
// check for collisions
for (let i = 0; i < boundary.length; i++) {
checkCollision(ball, boundary[i], boundary[(i + 1) % boundary.length]);
}
for (let obstacle of obstacles) {
// Find the tangent plane that is perpendicular to a line from the obstacle to
// the moving circle
// A vector pointing in the direction of the moving object
let dirVector = p5.Vector.sub(ball.pos, obstacle).normalize().mult(radius);
// The point on the perimiter of the obstacle that is in the direction of the
// moving object
let p1 = p5.Vector.add(obstacle, dirVector);
checkCollision(ball, p1, p5.Vector.add(p1, p5.Vector.rotate(dirVector, -90)));
}
// Check for collisions with other balls
for (let j = 0; j < i; j++) {
let other = balls[j];
let distance = dist(ball.pos.x, ball.pos.y, other.pos.x, other.pos.y);
if (distance / 2 < radius) {
push();
let midPoint = p5.Vector.add(ball.pos, other.pos).div(2);
let boundaryVector = p5.Vector.sub(other.pos, ball.pos).rotate(-90);
let v1Parallel = project(ball.vel, boundaryVector);
let v2Parallel = project(other.vel, boundaryVector);
let v1Perpendicular = p5.Vector.sub(ball.vel, v1Parallel);
let v2Perpendicular = p5.Vector.sub(other.vel, v2Parallel);
ball.vel = p5.Vector.add(v1Parallel, v2Perpendicular);
other.vel = p5.Vector.add(v2Parallel, v1Perpendicular);
let bounce = min(radius, 2 * radius - distance);
ball.pos.add(p5.Vector.rotate(boundaryVector, -90).normalize().mult(bounce));
other.pos.add(p5.Vector.rotate(boundaryVector, 90).normalize().mult(bounce));
pop();
}
}
}
// Only draw balls after all position updates are complete
for (let ball of balls) {
circle(ball.pos.x, ball.pos.y, radius);
}
}
function drawLine(origin, offset) {
line(origin.x, origin.y, origin.x + offset.x, origin.y + offset.y);
}
// Handles collision with a plane given two points on the plane.
// It is assumed that given a vector from p1 to p2, roating that vector
// clockwise 90 degrees will give a vector pointing to the in-bounds side of the
// plane (i.e. a "normal").
function checkCollision(ball, p1, p2) {
let boundaryVector = p5.Vector.sub(p2, p1);
let objVector = p5.Vector.sub(ball.pos, p1);
let angle = boundaryVector.angleBetween(objVector);
let distance = objVector.mag() * sin(angle);
if (distance <= radius) {
// Collision
let vParallel = project(ball.vel, boundaryVector);
let vPerpendicular = p5.Vector.sub(ball.vel, vParallel);
ball.vel = p5.Vector.add(vParallel, p5.Vector.mult(vPerpendicular, -1));
let bounce = min(radius, (radius - distance) * 2);
// If the ball has crossed over beyond the plane we want to offset it to be on
// the in-bounds side of the plane.
let bounceOffset = p5.Vector.rotate(boundaryVector, 90).normalize().mult(bounce);
ball.pos.add(bounceOffset);
}
}
// p5.Vector helpers
function project(vect1, vect2) {
vect2 = p5.Vector.normalize(vect2);
return p5.Vector.mult(vect2, p5.Vector.dot(vect1, vect2));
}
function reject(vect1, vect2) {
return p5.Vector.sub(vect1, project(vect1, vect2));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
I'm still currently at high-school, and my self-taught programming knowledge isn't that accurate at all.
By the way, I worked on many projects(I code in C, C++, Python, HTML, JavaScript) and the one I'm currently working on has been giving me a hard time: a simulator for a gas sample's behavior(following Maxwell-Boltzmann's law of distribution for speeds).
M-B distribution is a kinda hard and advanced physics argument especially for my high-school standards, but I still managed to understand its functioning and found out the equation that gives the number of molecules with a velocity between v and v+dv:M-B equationhere.
Now, programming JS part takes place, and in the following snippet I inserted the
balls constructor part, the draw() core function and the part where I spawn balls(100, just as a test, since they should be around 1000) according to M-B equation; now, I think everything should be fine, but spawned balls behave very strangely changing direction whenever they want to, and sometimes getting stuck with each other instead of elastically bouncing off... could you please take a look at the "spawning" part and see if somethings wrong? Then, I also uploaded the whole HTML+JS+CSS code so you can try it, and also verify JS functions for ball-to-ball and ball-to-wall collisions and all that kind of stuff.
I would extremely appreciate it if someone could please help me... I'm confused.
Heartfelt thanks in advance for any answer,
Greg🙏.
class Ball {
constructor(x, y, dx, dy, radius){
this.radius = radius;
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
// mass is that of a sphere as opposed to circle
// it *does* make a difference in how realistic it looks
this.mass = this.radius * this.radius * this.radius;
this.color = 'red';
};
draw() {
ctx.beginPath();
ctx.arc(Math.round(this.x), Math.round(this.y), this.radius, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = this.color;
ctx.stroke();
ctx.closePath();
};
speed() {
// magnitude of velocity vector
return Math.sqrt(this.dx * this.dx + this.dy * this.dy);
};
angle() {
// velocity's angle with the x axis
return Math.atan2(this.dy, this.dx);
};
onGround() {
return (this.y + this.radius >= canvas.height)
};
};
function draw() {
currentTime = (new Date()).getTime();
dt = (currentTime - lastTime) / 1000; // delta time in seconds
// dirty and lazy solution
// instead of scaling down every velocity vector
// we decrease the speed of time
dt *= 0.1;
if (clearCanv) clearCanvas();
canvasBackground();
if (!paused) {
moveObjects();
ballCollision();
}
drawObjects();
//logger();
lastTime = currentTime;
window.requestAnimationFrame(draw);
}
let N = 500;
let m = 2.66e-26;
let T = 300;
let dV = 50;
let k = 1.38e-23;
let v = 50;
let balls = 0;
let anglex;
for(let i = 0; i < 29; i++) { //each 50m/s, with dv = 50, until 1500m/s,
probArray[i] = Math.floor(4 * Math.PI * N * (((m) / (2 * Math.PI * k * T))**1.5) * (v**2) * Math.exp((-m) / (2 * k * T) * (v**2)) * dV);//molecules num between v e v+50
v += 50;
}
v = 50;
for(let i = 0; i < 29; i++){
let n = 0;
while(n < probArray[i] && balls < 100) {
anglex = ((Math.random() * 360) * Math.PI) / 180; //converted in radians;
vel = Math.round((Math.random() * 50) + v);
objArray[objArray.length] = new Ball(randomX(), randomY(), Math.cos(anglex) * vel, Math.sin(anglex) * vel, 3);
n++;
balls++;
}
v += 50;
}
class Ball {
constructor(x, y, dx, dy, radius){
this.radius = radius;
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
// mass is that of a sphere as opposed to circle
// it *does* make a difference in how realistic it looks
this.mass = this.radius * this.radius * this.radius;
this.color = 'red';
};
draw() {
ctx.beginPath();
ctx.arc(Math.round(this.x), Math.round(this.y), this.radius, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = this.color;
ctx.stroke();
ctx.closePath();
};
speed() {
// magnitude of velocity vector
return Math.sqrt(this.dx * this.dx + this.dy * this.dy);
};
angle() {
// velocity's angle with the x axis
return Math.atan2(this.dy, this.dx);
};
onGround() {
return (this.y + this.radius >= canvas.height)
};
};
//FUNCTIONS
//will remove
function randomColor() {
let red = Math.floor(Math.random() * 3) * 127;
let green = Math.floor(Math.random() * 3) * 127;
let blue = Math.floor(Math.random() * 3) * 127;
// dim down the small balls
if (!bigBalls){
red *= 0.65
green *= 0.65
blue *= 0.65
}
let rc = "rgb(" + red + ", " + green + ", " + blue + ")";
return rc;
}
function randomX() {
let x = Math.floor(Math.random() * canvas.width);
if (x < 30) {
x = 30;
} else if (x + 30 > canvas.width) {
x = canvas.width - 30;
}
return x;
}
function randomY() {
let y = Math.floor(Math.random() * canvas.height);
if (y < 30) {
y = 30;
} else if (y + 30 > canvas.height) {
y = canvas.height - 30;
}
return y;
}
//will remove
function randomRadius() {
if (bigBalls) {
let r = Math.ceil(Math.random() * 10 + 20);
return r;
} else {
let r = Math.ceil(Math.random() * 2 + 2);
//let r = 5;
return r;
}
}
//will remove
function randomDx() {
let r = Math.floor(Math.random() * 10 - 4);
return r;
}
//will remove
function randomDy() {
let r = Math.floor(Math.random() * 10 - 3);
return r;
}
function distanceNextFrame(a, b) {
return Math.sqrt((a.x + a.dx - b.x - b.dx)**2 + (a.y + a.dy - b.y - b.dy)**2) - a.radius - b.radius;
}
function distance(a, b) {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2);
}
let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");
let objArray = [];
let probArray = [];
let paused = false;
let bumped = false;
let leftHeld = false;
let upHeld = false;
let rightHeld = false;
let downHeld = false;
let arrowControlSpeed = .25;
let gravityOn = false;
let clearCanv = true;
let bigBalls = false;
let lastTime = (new Date()).getTime();
let currentTime = 0;
let dt = 0;
let numStartingSmallBalls = 500;
let numStartingBigBalls = 0;
document.addEventListener("keydown", keyDownHandler);
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function keyDownHandler(event) {
if (event.keyCode == 80) { // p
paused = !paused;
} else if (event.keyCode == 82) { // r
objArray = [];
} else if (event.keyCode == 75) { // k
clearCanv = !clearCanv;
} else if (event.keyCode == 88) { // x
bigBalls = !bigBalls;
}
}
function canvasBackground() {
canvas.style.backgroundColor = "rgb(215, 235, 240)";
}
function wallCollision(ball) {
if (ball.x - ball.radius + ball.dx < 0 ||
ball.x + ball.radius + ball.dx > canvas.width) {
ball.dx *= -1;
}
if (ball.y - ball.radius + ball.dy < 0 ||
ball.y + ball.radius + ball.dy > canvas.height) {
ball.dy *= -1;
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius;
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
}
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius;
}
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
}
}
function ballCollision() {
for (let i=0; i<objArray.length-1; i++) {
for (let j=i+1; j<objArray.length; j++) {
let ob1 = objArray[i]
let ob2 = objArray[j]
let dist = distance(ob1, ob2)
if (dist < ob1.radius + ob2.radius) {
let theta1 = ob1.angle();
let theta2 = ob2.angle();
let phi = Math.atan2(ob2.y - ob1.y, ob2.x - ob1.x);
let m1 = ob1.mass;
let m2 = ob2.mass;
let v1 = ob1.speed();
let v2 = ob2.speed();
let dx1F = (v1 * Math.cos(theta1 - phi) * (m1-m2) + 2*m2*v2*Math.cos(theta2 - phi)) / (m1+m2) * Math.cos(phi) + v1*Math.sin(theta1-phi) * Math.cos(phi+Math.PI/2);
let dy1F = (v1 * Math.cos(theta1 - phi) * (m1-m2) + 2*m2*v2*Math.cos(theta2 - phi)) / (m1+m2) * Math.sin(phi) + v1*Math.sin(theta1-phi) * Math.sin(phi+Math.PI/2);
let dx2F = (v2 * Math.cos(theta2 - phi) * (m2-m1) + 2*m1*v1*Math.cos(theta1 - phi)) / (m1+m2) * Math.cos(phi) + v2*Math.sin(theta2-phi) * Math.cos(phi+Math.PI/2);
let dy2F = (v2 * Math.cos(theta2 - phi) * (m2-m1) + 2*m1*v1*Math.cos(theta1 - phi)) / (m1+m2) * Math.sin(phi) + v2*Math.sin(theta2-phi) * Math.sin(phi+Math.PI/2);
ob1.dx = dx1F;
ob1.dy = dy1F;
ob2.dx = dx2F;
ob2.dy = dy2F;
staticCollision(ob1, ob2)
}
}
wallCollision(objArray[i]);
}
if (objArray.length > 0)
wallCollision(objArray[objArray.length - 1])
}
function staticCollision(ob1, ob2, emergency = false)
{
let overlap = ob1.radius + ob2.radius - distance(ob1, ob2);
let smallerObject = ob1.radius < ob2.radius ? ob1 : ob2;
let biggerObject = ob1.radius > ob2.radius ? ob1 : ob2;
// When things go normally, this line does not execute.
// "Emergency" is when staticCollision has run, but the collision
// still hasn't been resolved. Which implies that one of the objects
// is likely being jammed against a corner, so we must now move the OTHER one instead.
// in other words: this line basically swaps the "little guy" role, because
// the actual little guy can't be moved away due to being blocked by the wall.
if (emergency) [smallerObject, biggerObject] = [biggerObject, smallerObject]
let theta = Math.atan2((biggerObject.y - smallerObject.y), (biggerObject.x - smallerObject.x));
smallerObject.x -= overlap * Math.cos(theta);
smallerObject.y -= overlap * Math.sin(theta);
if (distance(ob1, ob2) < ob1.radius + ob2.radius) {
// we don't want to be stuck in an infinite emergency.
// so if we have already run one emergency round; just ignore the problem.
if (!emergency) staticCollision(ob1, ob2, true)
}
}
function moveObjects() {
for (let i=0; i<objArray.length; i++) {
let ob = objArray[i];
ob.x += ob.dx * dt;
ob.y += ob.dy * dt;
}
}
function drawObjects() {
for (let obj in objArray) {
objArray[obj].draw();
}
}
function draw() {
currentTime = (new Date()).getTime();
dt = (currentTime - lastTime) / 1000; // delta time in seconds
// dirty and lazy solution
// instead of scaling down every velocity vector
// we decrease the speed of time
dt *= 0.1;
if (clearCanv) clearCanvas();
canvasBackground();
if (!paused) {
moveObjects();
ballCollision();
}
drawObjects();
//logger();
lastTime = currentTime;
window.requestAnimationFrame(draw);
}
/*
for (i = 0; i<numStartingSmallBalls; i++) {
objArray[objArray.length] = new Ball(randomX(), randomY(), 3);
}*/
let N = 500;
let m = 2.66e-26;
let T = 300;
let dV = 50;
let k = 1.38e-23;
let v = 50;
let balls = 0;
let angolox;
for(let i = 0; i < 29; i++) { //ogni 50 velocità, con dv = 50, fino a 1500m/s,
probArray[i] = Math.floor(4 * Math.PI * N * (((m) / (2 * Math.PI * k * T))**1.5) * (v**2) * Math.exp((-m) / (2 * k * T) * (v**2)) * dV);//num molecole tra v e v+50
v += 50;
}
v = 50;
for(let i = 0; i < 29; i++){
let n = 0;
while(n < probArray[i] && balls < 100) {
angolox = ((Math.random() * 360) * Math.PI) / 180; //converted in radians;
vel = Math.round((Math.random() * 50) + v);
objArray[objArray.length] = new Ball(randomX(), randomY(), Math.cos(angolox) * vel, Math.sin(angolox) * vel, 3);
n++;
balls++;
}
v += 50;
}
draw();
body {
background-color: khaki;
text-align: center;
font-family: Ubuntu Mono;
}
#title {
color: black;
font-size: 200%;
font-style: normal;
margin: 1px;
border: 1px;
}
#balls {
margin-top: 5px;
}
#myCanvas {
margin-top: -20px;
}
section.footer {
color: black;
font-family: Ubuntu Mono;
font-style: normal;
font-size: small;
}
#disclaimer {
font-size: 74%;
color: gray;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>2d collision</title>
<link rel="stylesheet" type="text/css" href="gas.css">
</head>
<body style="text-align: center">
<canvas id="myCanvas" width="1225%" height="650%" style="border:1px solid black; margin-top: 10px;"></canvas>
<script src="gas.js"></script>
<p>
<strong>[X]</strong> to toggle SIZE of future balls <br>
<strong>[K]</strong> to toggle clearCanvas(); <br>
<strong>[P]</strong>: pause/unpause || <strong>[R]</strong>: [RESET]
</p>
<p>
source code on github
</p>
<div id="disclaimer" align="center" style="word-break: break-word; width: 350px; display:inline-block;">
<p>
Make sure to press a few buttons and play around.<br>Made with pure javascript.
</p>
</div>
</section>
</body>
</html>
I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
I was wondering if it was possible to have mouseover events with multiple squares on a canvas
this is my code right now: http://jsfiddle.net/2j3u9f7m/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var Enemy = function (x, y, velx, vely) {
this.x = x;
this.y = y;
this.velx = 0;
this.vely = 0;
}
Enemy.prototype.update = function () {
var tx = 650 - this.x;
var ty = 250 - this.y;
var dist = Math.sqrt(tx * tx + ty * ty);
this.velx = (tx / dist);
this.vely = (ty / dist);
if (dist > 0) {
this.x += this.velx;
this.y += this.vely;
}
};
Enemy.prototype.render = function () {
context.fillStyle = '#000000';
context.beginPath();
context.rect(this.x, this.y, 25, 25);
context.fill();
context.closePath();
};
var enemies = [];
for (var i = 0; i < 10; i++) {
// random numbers from 0 (inclusive) to 100 (exclusive) for example:
var randomX = Math.random() * 896;
var randomY = Math.random() * 1303;
console.log(randomX);
console.log(randomY);
if (randomX > 100 && randomX < 1200) {
if (randomX % 2 == 0) {
randomX = 140;
} else {
randomX = 1281;
}
}
if (randomY > 100 && randomY < 75) {
if (randomY % 2 == 0) {
randomY = 15;
} else {
randomY = 560;
}
}
var enemy = new Enemy(randomX, randomY, 0, 0);
enemies.push(enemy);
}
for (var i = 0; i < 15; i++) {
// random numbers from 0 (inclusive) to 100 (exclusive) for example:
var randomX = Math.random() * 200;
var randomY = Math.random() * 403;
console.log(randomX);
console.log(randomY);
if (randomX > 100 && randomX < 1200) {
if (randomX % 2 == 0) {
randomX = 140;
} else {
randomX = 1281;
}
}
if (randomY > 100 && randomY < 75) {
if (randomY % 2 == 0) {
randomY = 15;
} else {
randomY = 560;
}
}
var enemy = new Enemy(randomX, randomY, 0, 0);
enemies.push(enemy);
}
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < enemies.length; i++) {
var one = enemies[i];
one.update();
one.render();
}
requestAnimationFrame(render);
}
render();
What I want to do is to have a mouseover event for each square; is there a way to do this without using a library?
You can extend your Enemy object doing a region check like this:
Enemy.prototype.isOnEnemy = function(x, y) {
return (x >= this.x && x < this.x + 25 && // 25 = width
y >= this.y && y < this.y + 25); // 25 = height
};
If the provided (x,y) position is inside the rectangle (here assuming width and height of 25) it will return true.
Then add a mousemove event listener to the canvas. Inside adjust the mouse position, then feed the muse position to each enemy object to check:
context.canvas.onmousemove = function(e) {
var rect = this.getBoundingClientRect(), // correct mouse position
x = e.clientX - rect.left,
y = e.clientY - rect.top,
i = 0;
for(; i < enemies.length; i++) { // check each enemy
if (enemies[i].isOnEnemy(x, y)) { // is inside?
console.log("AAAARG...", i); // some action...
}
}
};
Modified fiddle (see console for hits).