Шарики отскакивают друг от друга - PullRequest
2 голосов
/ 17 марта 2020

Я работаю над этим сценарием, где у меня есть прыгающие шарики с x-числом (в данном случае 20 шариков) на холсте. Мой вопрос: как мне заставить их отражаться друг от друга, когда они ударяются, а также отражаться от желтого прямоугольника, когда они ударяются о него?

var mycanvas =document.getElementById("mycanvas");
var ctx=mycanvas.getContext("2d");
var w=500,h=500;

mycanvas.height=h;
mycanvas.width=w;

var ball=[];

function Ball(x,y,r,c,vx,vy){
  this.x=x; //starting x coordinate
  this.y=y; //starting x coordinate
  this.r=r; //radius  
  this.c=c; //color 
  this.vx=vx; // x direction speed
  this.vy=vy; // y direction speed
  this.update=function(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, false);
        ctx.fillStyle = this.c;
        ctx.fill();
        ctx.closePath();
        this.x += this.vx;
        this.y += this.vy;
        //changing direction on hitting wall
        if(this.y>=(w-10)||this.y<=10){
        this.vy=-this.vy;
         }
        if(this.x>=(h-10)||this.x<=10){
        this.vx=-this.vx;
         }
}
}

function clearCanvas(){
ctx.clearRect(0, 0, w, h);
}

var count;
for (count = 0; count < 20; count++) {
  var rndColor=Math.floor((Math.random() * 9) + 1); //random color
    ball[count]= new Ball(Math.floor((Math.random() * 490) + 1),Math.floor((Math.random() * 490)+1),5,'red',5,5);
}

function update(){
  var i;
  clearCanvas();
    //draw rectangle 
    ctx.rect(250, 200, 10, 100);
    ctx.fillStyle = 'yellow';
    ctx.fill();

  for(i=0;i<count;i++){
        ball[i].update();
    }
}

setInterval(update, 1000/60);

Ответы [ 2 ]

2 голосов
/ 17 марта 2020

Чтобы отскочить друг от друга, ему нужно знать, что он знает

  1. Столкнулись ли шары? Способ определить - измерить расстояние между центрами двух окружностей. Если это меньше, чем объединенные радиусы, шары столкнулись

  2. Какое направление они должны иметь после столкновения? Используйте команду atan2, чтобы вычислить угол между центрами двух шаров. Затем установите их в противоположных направлениях под этим углом таким образом, чтобы они не оказывались глубже друг в друге. Конечно, это простое решение игнорирует существующий импульс, который могут иметь шары. Но вычисление импульса (которое включает массу, скорость и текущий угол) более сложное.

1 голос
/ 17 марта 2020

Есть несколько методов, которые вы можете использовать. Следующие методы являются наиболее простыми.

Обновление

Я добавил пример, в котором используется второй метод. См. Фрагмент внизу.

Определение шаров

Каждый пример представляет собой объект с именем Ball.

// x,y position of center, 
// vx,vy is velocity, 
// r is radius defaults 45, 
// m is mass defaults to the volume of the sphere of radius r
function Ball(x, y, vx, vy, r = 45, m = (4 / 3 * Math.PI * (r ** 3)) {
    this.r = r;
    this.m = m;
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
}
Ball.prototype = {
    // add collision functions here
};

Код предполагает, что шары касаются.

Elasti c столкновения

Используемые логи c можно найти на wikis elasti c столкновение стр.

Расчет делит силы на две части для каждого шара. (Всего 4 на 2 шара)

  1. Передача энергии по линии между шарами,
  2. Регулировка энергии на шар вдоль тангенса точки столкновения

Равная масса

Каждый шар имеет одинаковую массу, что означает, что передача энергии сбалансирована и может игнорироваться

После вызова функции каждый шар имеет новую скорость vector.

Обратите внимание, что если вы называете столкновение, а скорости означают, что шары удаляются друг от друга (парадокс столкновения), то в результате шары будут двигаться друг к другу (парадокс разрешения)

Для упрощения математики векторные величины u1, u2, u3 и u4 преобразуются в единицу, представляющую собой длину линии между центрами шаров (квадрат root из * 1041) *)

collide(b) {  // b is the ball that the collision is with
    const a = this;
    const x = a.x - b.x;
    const y = a.y - b.y;  
    const d = x * x + y * y;

    const u1 = (a.vx * x + a.vy * y) / d;  // From this to b
    const u2 = (x * a.vy - y * a.vx) / d;  // Adjust self along tangent
    const u3 = (b.vx * x + b.vy * y) / d;  // From b to this
    const u4 = (x * b.vy - y * b.vx) / d;  // Adjust b  along tangent

    // set new velocities
    b.vx = x * u1 - y * u4;
    b.vy = y * u1 + x * u4;
    a.vx = x * u3 - y * u2;
    a.vy = y * u3 + x * u2;     
},

Разные массы

Каждый шар имеет свою собственную массу, поэтому для передачи необходимо рассчитать количество энергии, связанной с передаваемой массой. .

Разница в массе влияет только на энергию, передаваемую вдоль линии между шариками

collideMass(b) {
    const a = this;
    const m1 = a.m;
    const m2 = b.m;        
    const x = a.x - b.x;
    const y = a.y - b.y;
    const d = x * x + y * y;

    const u1 = (a.vx * x + a.vy * y) / d; 
    const u2 = (x * a.vy - y * a.vx) / d;  
    const u3 = (b.vx * x + b.vy * y) / d;    
    const u4 = (x * b.vy - y * b.vx) / d; 

    const mm = m1 + m2;
    const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
    const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;

    b.vx = x * vu1 - y * u4;
    b.vy = y * vu1 + x * u4;
    a.vx = x * vu3 - y * u2;
    a.vy = y * vu3 + x * u2;     
},

Пример

Пример простого столкновения шариков. Шарики, связанные линиями (линии примечаний имеют внешнюю и внутреннюю стороны, если смотреть от начала до конца, внутренняя сторона находится справа)

Столкновения полностью разрешаются в хронологическом порядке между кадрами. Используемое время - это кадр, где 0 - предыдущий кадр, а 1 - текущий кадр.

canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 20;  // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 13;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5; 
const MAX_RESOLUTION_CYCLES = 100;
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
    const A = a * a, B = b * b, C = c * c, D = d * d;
    const E = e * e, F = f * f, G = g * g, H = h * h;
    var R = (r1 + r2) ** 2;
    const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
    const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
    const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
    return Math.quadRoots(AA, BB, CC);
}   
Math.quadRoots = (a, b, c) => { // find roots for quadratic
    if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : []  }
    b /= a;
    var d = b * b - 4 * (c / a);
    if (d > 0) {
        d = d ** 0.5;
        return  [0.5 * (-b + d), 0.5 * (-b - d)]
    }
    return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2,  r) => {
    const xx = x2 - x1;
    const yy = y2 - y1;
    const d = vx * yy - vy * xx;
    if (d > 0) {  // only if moving towards the line
        const dd = r / (xx * xx + yy * yy) ** 0.5;
        const nx = xx * dd;
        const ny = yy * dd;
        return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
    }
}
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
}
Line.prototype = {
    draw() {
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
    },
    reverse() {
        const x = this.x1;
        const y = this.y1;
        this.x1 = this.x2;
        this.y1 = this.y2;
        this.x2 = x;
        this.y2 = y;
        return this;
    }
}
    
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
    this.r = r;
    this.m = m
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
}
Ball.prototype = {
    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.vy += GRAVITY;
    },
    draw() {
        ctx.moveTo(this.x + this.r, this.y);
        ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
    },
    interceptLineTime(l, time) {
        const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2,  this.r);
        if(u >= time && u <= 1) {
            return u;
        }
    },
    checkBallBallTime(t, minTime) {
        return t > minTime && t <= 1;
    },
    interceptBallTime(b, time) {
        const x = this.x - b.x;
        const y = this.y - b.y;
        const d = (x * x + y * y) ** 0.5;
        if(d > this.r + b.r) {
            const times = Math.circlesInterceptUnitTime(
                this.x, this.y, 
                this.x + this.vx, this.y + this.vy, 
                b.x, b.y,
                b.x + b.vx, b.y + b.vy, 
                this.r, b.r
            );
            if(times.length) {
                if(times.length === 1) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    return;
                }
                if(times[0] <= times[1]) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    if(this.checkBallBallTime(times[1], time)) { return times[1] }
                    return
                }
                if(this.checkBallBallTime(times[1], time)) { return times[1] }                
                if(this.checkBallBallTime(times[0], time)) { return times[0] }
            }
        }
    },
    collideLine(l, time) {
        const x1 = l.x2 - l.x1;
        const y1 = l.y2 - l.y1;
        const d = (x1 * x1 + y1 * y1) ** 0.5;
        const nx = x1 / d;
        const ny = y1 / d;            
        const u = (this.vx  * nx + this.vy  * ny) * 2;
        this.x += this.vx * time;   
        this.y += this.vy * time;   
        this.vx = (nx * u - this.vx) * WALL_LOSS;
        this.vy = (ny * u - this.vy) * WALL_LOSS;
        this.x -= this.vx * time;
        this.y -= this.vy * time;
    },
    collide(b, time) {
        const a = this;
        const m1 = a.m;
        const m2 = b.m;
        const x = a.x - b.x
        const y = a.y - b.y  
        const d = (x * x + y * y);
        const u1 = (a.vx * x + a.vy * y) / d
        const u2 = (x * a.vy - y * a.vx ) / d
        const u3 = (b.vx * x + b.vy * y) / d
        const u4 = (x * b.vy - y * b.vx ) / d
        const mm = m1 + m2;
        const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
        const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
        a.x = a.x + a.vx * time;
        a.y = a.y + a.vy * time;
        b.x = b.x + b.vx * time;
        b.y = b.y + b.vy * time;
        b.vx = x * vu1 - y * u4;
        b.vy = y * vu1 + x * u4;
        a.vx = x * vu3 - y * u2;
        a.vy = y * vu3 + x * u2;
        a.x = a.x - a.vx * time;
        a.y = a.y - a.vy * time;
        b.x = b.x - b.vx * time;
        b.y = b.y - b.vy * time;
    },
    doesOverlap(ball) {
        const x = this.x - ball.x;
        const y = this.y - ball.y;
        return  (this.r + ball.r) > ((x * x + y * y) ** 0.5);  
    }       
}

function canAdd(ball) {
    for(const b of balls) {
        if (ball.doesOverlap(b)) { return false }
    }
    return true;
}
function create(bCount) {
    lines.push(new Line(-10, 10, ctx.canvas.width + 10, 5));
    lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
    lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
    lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10)); 
    while (bCount--) {
        let tries = 100;
        debugger
        while (tries--) {
            const dir = Math.rand(0, Math.TAU);
            const vel = Math.rand(VEL_MIN, VEL_MAX);
            const ball = new Ball(
                Math.rand(MAX_BALL_SIZE + 10, canvas.width - MAX_BALL_SIZE - 10), 
                Math.rand(MAX_BALL_SIZE + 10, canvas.height - MAX_BALL_SIZE - 10),
                Math.cos(dir) * vel,
                Math.sin(dir) * vel,
                Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
            );
            if (canAdd(ball)) {
                balls.push(ball);
                break;
            }
        }
    }
}
function resolveCollisions() {
    var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
    while(resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
        resolving = false;
        minObj = undefined;
        minBall = undefined;
        minTime = 1;
        idx = 0;
        for(const b of balls) {
            idx1 = idx + 1;
            while(idx1 < balls.length) {
                const b1 = balls[idx1++];
                const time = b.interceptBallTime(b1, after);
                if(time !== undefined) {
                    if(time <= minTime) {
                        minTime = time;
                        minObj = b1;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            for(const l of lines) {
                const time = b.interceptLineTime(l, after);
                if(time !== undefined) {
                    if(time <= minTime) {
                        minTime = time;
                        minObj = l;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            idx ++;
        }
        if(resolving) {
            if (minObj instanceof Ball) {
                minBall.collide(minObj, minTime);
            } else {
                minBall.collideLine(minObj, minTime);
            }
            after = minTime;
        }
    }
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
    resolveCollisions();
    for(const b of balls) { b.update() }
    ctx.strokeStyle = "#000";
    ctx.beginPath();
    for(const b of balls) { b.draw() }
    for(const l of lines) { l.draw() }
    ctx.stroke();
    requestAnimationFrame(mainLoop);
}
<canvas id="canvas"></canvas>
...