Я не могу настроить столкновение и гравитацию в canvas
//Функция для ограничения количества кадров в секунду
var limitLoop = function (fn, fps) {
// Use var then = Date.now(); if you
// don't care about targetting < IE9
var then = new Date().getTime();
// custom fps, otherwise fallback to 60
fps = fps || 60;
var interval = 1000 / fps;
return (function loop(time){
requestAnimationFrame(loop);
// again, Date.now() if it's available
var now = new Date().getTime();
var delta = now - then;
if (delta > interval) {
// Update time
// now - (delta % interval) is an improvement over just
// using then = now, which can end up lowering overall fps
then = now - (delta % interval);
// call the fn
fn();
}
}(0));
};
//Полезные функции
function randomIntFromRange(min,max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function frameCheck(ball){
if(ball.y + ball.radius + ball.velocity.y > canvas.height){
ball.velocity.y = -ball.velocity.y * 0.6;
} else {
if(!ball.colideState){
ball.velocity.y += gravity;
}
}
if(ball.x + ball.radius + ball.velocity.x >= canvas.width || ball.x - ball.radius + ball.velocity.x<= 0){
ball.velocity.x = -ball.velocity.x;
}
}
function getDistance(x1, y1, x2, y2){
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2))
}
//Утилита для рассчета коллизии
/**
* Rotates coordinate system for velocities
*
* Takes velocities and alters them as if the coordinate system they're on was rotated
*
* @param Object | velocity | The velocity of an individual particle
* @param Float | angle | The angle of collision between two objects in radians
* @return Object | The altered x and y velocities after the coordinate system has been rotated
*/
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) - velocity.y * Math.sin(angle),
y: velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
/**
* Swaps out two colliding particles' x and y velocities after running through
* an elastic collision reaction equation
*
* @param Object | particle | A particle object with x and y coordinates, plus velocity
* @param Object | otherParticle | A particle object with x and y coordinates, plus velocity
* @return Null | Does not return a value
*/
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.velocity.x - otherParticle.velocity.x;
const yVelocityDiff = particle.velocity.y - otherParticle.velocity.y;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
// Prevent accidental overlap of particles
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
// Grab angle between the two colliding particles
const angle = -Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
// Store mass in var for better readability in collision equation
const m1 = particle.mass;
const m2 = otherParticle.mass;
// Velocity before equation
const u1 = rotate(particle.velocity, angle);
const u2 = rotate(otherParticle.velocity, angle);
// Velocity after 1d collision equation
const v1 = { x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2), y: u1.y };
const v2 = { x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2), y: u2.y };
// Final velocity after rotating axis back to original location
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
if(particle.y != particle.pastY){
// Swap particle velocities for realistic bounce effect
if(particle.y + particle.radius + particle.velocity.y < canvas.height){
particle.velocity.y = vFinal2.y * 0.8;
}
if(otherParticle.y + otherParticle.radius + otherParticle.velocity.y < canvas.height){
otherParticle.velocity.y = vFinal2.y * 0.8;
}
}
if(particle.x + particle.radius + particle.velocity.x < canvas.width && particle.x - particle.radius + particle.velocity.x > 0){
particle.velocity.x = vFinal1.x ;
} else {}
if(otherParticle.x + otherParticle.radius + otherParticle.velocity.x < canvas.width && otherParticle.x - otherParticle.radius + otherParticle.velocity.x > 0){
otherParticle.velocity.x = vFinal2.x ;
} else {}
}
}
//game
const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d');
canvas.height = window.screen.height - window.screen.height * 0.6; // window heigh - 20 percents
canvas.width = document.querySelector('.container').clientWidth * 0.5; // container width
let objects = []
//Массив шариков
let ballsTemplates = {
1:{
y: 50,
direction: 0,
radius: 20,
color: "blue",
level: 1
},
2:{
y: 50,
direction: 0,
radius: 25,
color: "green",
level: 2
},
3:{
y: 50,
direction: 0,
radius: 30,
color: "black",
level: 3
},
4:{
y: 50,
direction: 0,
radius: 35,
color: "red",
level: 4
},
}
const gravity = 0.5;
//objects
class Ball {
constructor(x, y, dx, dy, bp, radius, color, level) {
this.x = x
this.y = y
this.pastX = 0
this.pastY = 0
this.colideState = false;
this.velocity = {
x: dx,
y: dy
}
this.level = level
this.bp = bp;
this.radius = radius
this.color = color
this.mass = 1;
}
draw() {
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
c.fillStyle = this.color
c.fill()
c.closePath()
}
update = objects => {
this.pastX = this.x;
this.pastY = this.y
this.y += this.velocity.y
this.velocity.x = this.velocity.x * 0.99;
this.x += this.velocity.x;
//Не даём шарику возможности упать за границы карты
frameCheck(this);
for (let i=0; i < objects.length; i++){
if(objects[i]){
if(this === objects[i]) continue;
if(getDistance(this.x, this.y, objects[i].x, objects[i].y) - (this.radius + objects[i].radius) < -0.8){
//Вызов функции коллизии
resolveCollision(this, objects[i]);
let lvl = this.level;
let lvl2 = objects[i].level;
if(lvl === lvl2){
if(lvl < 4){
let x = (this.x + objects[i].x) / 2
let y = (this.y + objects[i].y) / 2
this.x = x;
this.y = y;
this.direction = ballsTemplates[lvl + 1].direction;
this.dy = 0.5;
this.bp = 0.9;
this.radius = ballsTemplates[lvl + 1].radius;
this.color = ballsTemplates[lvl + 1].color;
this.level = ballsTemplates[lvl + 1].level;
delete objects[i];
}
}
if(this.y == this.pastY){
this.colideState = true;
}
} else {
this.colideState = false;
}
}
}
this.draw()
}
}
//функция для создания шарика по клику
canvas.addEventListener('mouseup', function (event) {
// Implementation
const rect = canvas.getBoundingClientRect();
const computedStyle = getComputedStyle(canvas);
const borderLeftWidth = parseInt(computedStyle.borderLeftWidth, 10);
const borderTopWidth = parseInt(computedStyle.borderTopWidth, 10);
let numb = randomIntFromRange(1,4);
let x = event.clientX - rect.left - borderLeftWidth
let y = event.clientY - rect.top - borderTopWidth
let radius = 55;
let dxWerid = 5;
if(x + radius * 2 > canvas.width){
x = canvas.width - radius;
} else if (x - radius * 2 < canvas.width){
x = x + radius;
}
let ball = new Ball( x , y, ballsTemplates[numb].direction , 1 , 0.9,ballsTemplates[numb].radius, ballsTemplates[numb].color, ballsTemplates[numb].level)
objects.push(ball)
});
const animate = async () => {
c.fillStyle="white"
c.fillRect(0, 0, canvas.width, canvas.height) //background :/
objects.forEach(object => {
object.update(objects)
})
}
limitLoop(animate, 60);
.gameScreen {
border: 10px solid white;
background-color: black;
}
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"><div class="container" height="200" width="332"><div><div><canvas class="gameScreen"></canvas> <- С попощью левой кнопки мыши заспавни много шариков. Чтобы было несколько рядок</div></div></div></div>
</body>
Я пытаюсь реализовать с помощью canvas довольно простую игру merge fruit, в которой шарики должны падать в стакан, и если шарики одинакового размера, соприкасаются друг с другом. Они превращаются в один большой шар. Но есть проблема с гравитацией и коллизией. Со временем они просто начинают продавливаться друг в друга. Скорее всего, это из-за силы тяжести, которая притягивает их, пока они не коснутся пола. Но я не знаю, как манипулировать ею. И не терять силу тяжести при столкновении. Потому что нужно чтобы, шарики имели по прежнему возможность кататься друг по другу. Пожалуйста, помогите мне!
Извините, если код выглядит неуклюже. Я просто перепробовал все, что мог, и со временем начал путаться в своем собственном коде
Ответы (2 шт):
Тут такое крайне не приветствуется, но к сожалению по другому у меня не получилось. Я не смог полностью разобраться в вашем коде, потому написал сам с нуля :)
Пытался назвать переменные максимально понятно и оставлял комментарии в некоторых местах и везде пытался оставлять JSDoc. Но всё же если будут вопросы, то обязательно пишите, постараюсь ответить
Вот сам код:
const CANVAS_WIDTH = window.innerWidth;
const CANVAS_HEIGHT = window.innerHeight;
// FRAME PER SECOND
const FPS = 60;
// PHYSIC PER SECOND
const PPS = FPS;
const Gravity = 9.8;
const G = Gravity / PPS;
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d', {alpha: false});
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// CLASSES (START)
class Dot {
/**
* @type {number}
*/
x;
/**
* @type {number}
*/
y;
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Vector extends Dot {
}
class CircleLevel {
/**
* @type {number}
*/
#level = 0;
/**
* @type {string}
*/
#color = '';
/**
* @type {number}
*/
#radius = 0;
/**
* @type {number}
*/
#mass = 0;
/**
* @param {number} level
* @param {string} color
* @param {number} radius
* @param {number} mass
*/
constructor(level, color, radius, mass) {
this.#level = level;
this.#color = color;
this.#radius = radius;
this.#mass = mass
}
/**
* @returns {number}
*/
get level() {
return this.#level;
}
/**
* @returns {string}
*/
get color() {
return this.#color;
}
/**
* @returns {number}
*/
get radius() {
return this.#radius;
}
/**
* @returns {number}
*/
get mass() {
return this.#mass;
}
}
class CircleLevelsManager {
/**
* @type {CircleLevel[]}
*/
static #levels = [
new CircleLevel(1, 'red', 20, 1),
new CircleLevel(2, 'green', 30, 2),
new CircleLevel(3, 'blue', 40, 3),
new CircleLevel(4, 'yellow', 50, 4)
];
/**
* @param {number} levelIndex
* @returns {CircleLevel}
*/
static getLevel(levelIndex) {
return CircleLevelsManager.#levels[levelIndex];
}
/**
* @param {CircleLevel} prevLevel
* @returns {CircleLevel}
*/
static getNextLevel(prevLevel) {
return CircleLevelsManager.#levels[prevLevel.level];
}
/**
* @param {CircleLevel} prevLevel
* @returns {boolean}
*/
static hasNextLevel(prevLevel) {
return prevLevel.level < CircleLevelsManager.#levels.length;
}
}
class CirclesManager {
/**
* @type {Set<Circle>}
*/
static #circles = new Set();
/**
* @param {Circle} circle
*/
static add(circle) {
CirclesManager.#circles.add(circle);
}
/**
* @param {Circle} circle
*/
static delete(circle) {
CirclesManager.#circles.delete(circle);
}
/**
* @returns {Circle[]}
*/
static getCircles() {
return [...CirclesManager.#circles.values()];
}
}
class Circle {
/**
* @type {Dot}
*/
#center = new Dot(0, 0);
/**
* @type {{
* horizontal: number,
* vertical: number
* }}
*/
#friction = {
horizontal: 0.98,
vertical: 0.5,
};
/**
* @type {Vector}
*/
velocity = new Vector(0, 0);
/**
* @type {CircleLevel}
*/
level = CircleLevelsManager.getLevel(0);
/**
* @param {number} x
* @param {number} y
* @param {CircleLevel} [level]
*/
constructor(x, y, level) {
CirclesManager.add(this);
this.#center.x = x;
this.#center.y = y;
if (level !== undefined) this.level = level;
}
/**
* @returns {number}
*/
get radius() {
return this.level.radius;
}
/**
* @returns {number}
*/
get mass() {
return this.level.mass;
}
/**
* @returns {Dot}
*/
get center() {
return this.#center;
}
/**
* @param {Dot} newCenter
*/
set center(newCenter) {
return this.#center = newCenter;
}
/**
* @returns {number}
*/
get x() {
return this.#center.x;
}
/**
* @returns {number}
*/
get y() {
return this.#center.y;
}
draw() {
ctx.beginPath();
ctx.fillStyle = this.level.color;
ctx.arc(this.#center.x, this.#center.y, this.level.radius, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
update() {
const radius = this.level.radius;
// IF TOUCHES HORIZONTAL WALLS
if (this.#center.y + radius >= CANVAS_HEIGHT || this.center.y - radius <= 0) {
this.velocity.y = Math.abs(this.velocity.y) < 1 ? 0 : -this.velocity.y * this.#friction.vertical;
if (this.velocity.y === 0 && this.center.y - radius === 0) this.velocity.y += 1;
} else {
// IF FALLING DOWN
this.velocity.y += G * this.level.mass;
}
// IF TOUCHES VERTICAL WALLS
if (this.#center.x + radius >= CANVAS_WIDTH || this.center.x - radius <= 0) {
this.velocity.x = Math.abs(this.velocity.x) < 1 ? 0 : -this.velocity.x * this.#friction.horizontal;
}
// PUT INSIDE FRAME
this.#center.y = Math.max(radius, Math.min(this.#center.y + this.velocity.y, CANVAS_HEIGHT - radius));
// IF ON GROUND
if (this.center.y + radius === CANVAS_HEIGHT) {
this.velocity.x *= this.#friction.horizontal;
}
// PUT INSIDE FRAME
this.#center.x = Math.max(radius, Math.min(this.#center.x + this.velocity.x, CANVAS_WIDTH - radius));
}
/**
* @param {Circle} circle
* @returns {boolean}
*/
hasCollision(circle) {
return distanceSquare(circle.center, this.center) <= (circle.radius + this.radius) ** 2;
}
}
// CLASSES (END)
// UTILS (START)
/**
* @param {Dot} dot1
* @param {Dot} dot2
* @returns {number}
*/
const distanceSquare = (dot1, dot2) => (dot1.x - dot2.x) ** 2 + (dot1.y - dot2.y) ** 2;
/**
* @param {Dot} dot1
* @param {Dot} dot2
* @returns {number}
*/
const distance = (dot1, dot2) => Math.sqrt(distanceSquare(dot1, dot2));
/**
* @param {Dot} vector
* @returns {number}
*/
const lengthOfVector = vector => Math.sqrt(vector.x ** 2 + vector.y ** 2);
/**
* @param {Dot} dot1
* @param {Dot} dot2
* @returns {Dot}
*/
const dotsSub = (dot1, dot2) => new Dot(dot1.x - dot2.x, dot1.y - dot2.y);
/**
* @param {Dot} dot1
* @param {Dot} dot2
* @returns {Dot}
*/
const dotsSum = (dot1, dot2) => new Dot(dot1.x + dot2.x, dot1.y + dot2.y);
/**
* @param {Dot} dot
* @param {number} coefficient
* @returns {Dot}
*/
const multiplyDot = (dot, coefficient) => new Dot(dot.x * coefficient, dot.y * coefficient);
/**
* @param {Dot} dot1
* @param {Dot} dot2
* @returns {number}
*/
const dotsProduct = (dot1, dot2) => dot1.x * dot2.x + dot1.y * dot2.y;
/**
* @param {Circle} circle1
* @param {Circle} circle2
*/
function elasticCollision(circle1, circle2) {
const massSum = circle1.mass + circle2.mass;
let dist = distance(circle1.center, circle2.center);
const correctionLength = (circle1.radius + circle2.radius - dist) / 2;
const impact = dotsSub(circle1.center, circle2.center);
const dir = multiplyDot(impact, correctionLength / lengthOfVector(impact));
circle1.center = dotsSum(circle1.center, dir);
circle2.center = dotsSub(circle2.center, dir);
dist = circle1.radius + circle2.radius;
const distSquare = dist ** 2;
const oldVelocity1 = circle1.velocity;
const oldVelocity2 = circle2.velocity;
circle1.velocity = dotsSub(
oldVelocity1,
multiplyDot(
dotsSub(circle1.center, circle2.center),
(2 * circle2.mass / massSum) * dotsProduct(
dotsSub(oldVelocity1, oldVelocity2),
dotsSub(circle1.center, circle2.center),
) / distSquare
)
);
circle2.velocity = dotsSub(
oldVelocity2,
multiplyDot(
dotsSub(circle2.center, circle1.center),
(2 * circle1.mass / massSum) * dotsProduct(
dotsSub(oldVelocity2, oldVelocity1),
dotsSub(circle2.center, circle1.center),
) / distSquare
)
);
}
function upgradeLevel(circle1, circle2) {
let primaryCircle = circle1;
let secondaryCircle = circle2;
if (circle2.y < circle1.y) {
primaryCircle = circle2;
secondaryCircle = circle1;
}
CirclesManager.delete(secondaryCircle);
primaryCircle.level = CircleLevelsManager.getNextLevel(primaryCircle.level);
}
// UTILS (END)
canvas.addEventListener('mousemove', e => {
new Circle(e.clientX, e.clientY);
})
// UPDATE GAME (START)
let prevPhysicsTimeStamp = performance.now();
/**
* @param {DOMHighResTimeStamp} timeStamp
*/
const updatePhysics = (timeStamp) => {
requestAnimationFrame(updatePhysics);
if (timeStamp - prevPhysicsTimeStamp < (1000 / PPS)) return;
prevPhysicsTimeStamp = timeStamp;
const circles = CirclesManager.getCircles();
for (let i = 0; i < circles.length; ++i) {
circles[i].update();
}
// CHECK COLLISIONS
for (let i = 0; i < circles.length - 1; ++i) {
const circle1 = circles[i];
for (let j = i + 1; j < circles.length; ++j) {
const circle2 = circles[j];
if (!circle1.hasCollision(circle2)) continue;
if (circle1.level === circle2.level && CircleLevelsManager.hasNextLevel(circle1.level)) {
upgradeLevel(circle1, circle2);
} else {
elasticCollision(circle1, circle2);
}
}
}
}
requestAnimationFrame(updatePhysics);
let prevPictureTimeStamp = performance.now();
/**
* @param {DOMHighResTimeStamp} timeStamp
*/
const updatePicture = (timeStamp) => {
requestAnimationFrame(updatePicture);
if (timeStamp - prevPictureTimeStamp < (1000 / FPS)) return;
prevPictureTimeStamp = timeStamp;
// ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.fillStyle = `rgba(0, 0, 0, 0.5)`;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const circles = CirclesManager.getCircles();
for (let i = 0; i < circles.length; ++i) {
circles[i].draw();
}
}
requestAnimationFrame(updatePicture);
// UPDATE GAME (END)
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
overflow: hidden;
}
canvas {
background-color: black;
}
<canvas></canvas>
Основные моменты:
Формулы я взял отсюда: Two-dimensional collision with two moving objects
Были пару багов, с которыми разобрался благодаря этому видео: Collisions Without a Physics Library! (Coding Challenge 184). В видел автор исползьует другие формулы, чем в википедии, насколько я понял, потому что не разобрался в них (хотя они крайне похожи)
Вообще очень советую этого автора, очень много чему учит. Правда испоользует библиотеку
p5
, но он настолько подробно и просто объясняет, что закодить самому - не проблемаРазделил обновление физики и картинки на две отдельные, чтобы они не мешали друг другу
Формулы, я по сути просто скопипастил, но автор на похожих формулах показал как можно их оптимизировать
Добавил след для кружков :)
Надеюсь мой ответ вам поможет!
Я решил проблему отредактировав функцию resolveCollision
Добавил константу overlap которая рассчитывает перекрытие (пересечение) между двумя частицами, когда они сталкиваются.
Вот как это работает:
overlap вычисляется как разница между суммой радиусов двух частиц (particle.radius + otherParticle.radius) и расстоянием между их центрами (Math.hypot(xDist, yDist)). Если overlap больше нуля, это означает, что частицы перекрываются, т.е. находятся так близко друг к другу, что пересекаются.
Переменная используется для корректировки позиций частиц после столкновения, чтобы они не оставались в состоянии пересечения. Это помогает избежать проблемы, когда частицы визуально "залипают" друг на друге после столкновения.
//Функция для ограничения количества кадров в секунду
var limitLoop = function (fn, fps) {
// Use var then = Date.now(); if you
// don't care about targetting < IE9
var then = new Date().getTime();
// custom fps, otherwise fallback to 60
fps = fps || 60;
var interval = 1000 / fps;
return (function loop(time){
requestAnimationFrame(loop);
// again, Date.now() if it's available
var now = new Date().getTime();
var delta = now - then;
if (delta > interval) {
// Update time
// now - (delta % interval) is an improvement over just
// using then = now, which can end up lowering overall fps
then = now - (delta % interval);
// call the fn
fn();
}
}(0));
};
//Полезные функции
function randomIntFromRange(min,max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function frameCheck(ball){
if(ball.y + ball.radius + ball.velocity.y > canvas.height){
ball.velocity.y = -ball.velocity.y * 0.6;
} else {
if(!ball.colideState){
ball.velocity.y += gravity;
}
}
if(ball.x + ball.radius + ball.velocity.x >= canvas.width || ball.x - ball.radius + ball.velocity.x<= 0){
ball.velocity.x = -ball.velocity.x;
}
}
function getDistance(x1, y1, x2, y2){
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2))
}
//Утилита для рассчета коллизии
/**
* Rotates coordinate system for velocities
*
* Takes velocities and alters them as if the coordinate system they're on was rotated
*
* @param Object | velocity | The velocity of an individual particle
* @param Float | angle | The angle of collision between two objects in radians
* @return Object | The altered x and y velocities after the coordinate system has been rotated
*/
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) - velocity.y * Math.sin(angle),
y: velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
/**
* Swaps out two colliding particles' x and y velocities after running through
* an elastic collision reaction equation
*
* @param Object | particle | A particle object with x and y coordinates, plus velocity
* @param Object | otherParticle | A particle object with x and y coordinates, plus velocity
* @return Null | Does not return a value
*/
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.velocity.x - otherParticle.velocity.x;
const yVelocityDiff = particle.velocity.y - otherParticle.velocity.y;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist > 0) {
const angle = -Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate(particle.velocity, angle);
const u2 = rotate(otherParticle.velocity, angle);
const v1 = { x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2), y: u1.y };
const v2 = { x: u2.x * (m2 - m1) / (m1 + m2) + u1.x * 2 * m1 / (m1 + m2), y: u2.y };
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.velocity.x = vFinal1.x * 0.8;
particle.velocity.y = vFinal1.y * 0.8;
otherParticle.velocity.x = vFinal2.x * 0.8;
otherParticle.velocity.y = vFinal2.y * 0.8;
const overlap = particle.radius + otherParticle.radius - Math.hypot(xDist, yDist);
if (overlap > 0) {
const correctionFactor = overlap / 2;
const correctionX = (xDist / Math.hypot(xDist, yDist)) * correctionFactor;
const correctionY = (yDist / Math.hypot(xDist, yDist)) * correctionFactor;
particle.x -= correctionX;
particle.y -= correctionY;
otherParticle.x += correctionX;
otherParticle.y += correctionY;
}
function handleBoundaryCollision(p, canvas) {
if (p.x - p.radius < 0) {
p.x = p.radius;
p.velocity.x = Math.abs(p.velocity.x) < 0.01 ? 0 : -p.velocity.x * 0.8;
} else if (p.x + p.radius > canvas.width) {
p.x = canvas.width - p.radius;
p.velocity.x = Math.abs(p.velocity.x) < 0.01 ? 0 : -p.velocity.x * 0.8;
}
if (p.y - p.radius < 0) {
p.y = p.radius;
p.velocity.y = Math.abs(p.velocity.y) < 0.01 ? 0 : -p.velocity.y * 0.8;
} else if (p.y + p.radius > canvas.height) {
p.y = canvas.height - p.radius;
p.velocity.y = Math.abs(p.velocity.y) < 0.01 ? 0 : -p.velocity.y * 0.8;
}
}
handleBoundaryCollision(particle, canvas);
handleBoundaryCollision(otherParticle, canvas);
}
}
//game
const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d');
canvas.height = window.screen.height - window.screen.height * 0.6; // window heigh - 20 percents
canvas.width = document.querySelector('.container').clientWidth * 0.5; // container width
let objects = []
//Массив шариков
let ballsTemplates = {
1:{
y: 50,
direction: 0,
radius: 20,
color: "blue",
level: 1
},
2:{
y: 50,
direction: 0,
radius: 25,
color: "green",
level: 2
},
3:{
y: 50,
direction: 0,
radius: 30,
color: "black",
level: 3
},
4:{
y: 50,
direction: 0,
radius: 35,
color: "red",
level: 4
},
}
const gravity = 0.5;
//objects
class Ball {
constructor(x, y, dx, dy, bp, radius, color, level) {
this.x = x
this.y = y
this.pastX = 0
this.pastY = 0
this.colideState = false;
this.velocity = {
x: dx,
y: dy
}
this.level = level
this.bp = bp;
this.radius = radius
this.color = color
this.mass = 1;
}
draw() {
c.beginPath()
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
c.fillStyle = this.color
c.fill()
c.closePath()
}
update = objects => {
this.pastX = this.x;
this.pastY = this.y
this.y += this.velocity.y
this.velocity.x = this.velocity.x * 0.99;
this.x += this.velocity.x;
//Не даём шарику возможности упать за границы карты
frameCheck(this);
for (let i=0; i < objects.length; i++){
if(objects[i]){
if(this === objects[i]) continue;
if(getDistance(this.x, this.y, objects[i].x, objects[i].y) - (this.radius + objects[i].radius) < -0.8){
//Вызов функции коллизии
resolveCollision(this, objects[i]);
let lvl = this.level;
let lvl2 = objects[i].level;
if(lvl === lvl2){
if(lvl < 4){
let x = (this.x + objects[i].x) / 2
let y = (this.y + objects[i].y) / 2
this.x = x;
this.y = y;
this.direction = ballsTemplates[lvl + 1].direction;
this.dy = 0.5;
this.bp = 0.9;
this.radius = ballsTemplates[lvl + 1].radius;
this.color = ballsTemplates[lvl + 1].color;
this.level = ballsTemplates[lvl + 1].level;
delete objects[i];
}
}
if(this.y == this.pastY){
this.colideState = true;
}
} else {
this.colideState = false;
}
}
}
this.draw()
}
}
//функция для создания шарика по клику
canvas.addEventListener('mouseup', function (event) {
// Implementation
const rect = canvas.getBoundingClientRect();
const computedStyle = getComputedStyle(canvas);
const borderLeftWidth = parseInt(computedStyle.borderLeftWidth, 10);
const borderTopWidth = parseInt(computedStyle.borderTopWidth, 10);
let numb = randomIntFromRange(1,4);
let x = event.clientX - rect.left - borderLeftWidth
let y = event.clientY - rect.top - borderTopWidth
let radius = 55;
let dxWerid = 5;
if(x + radius * 2 > canvas.width){
x = canvas.width - radius;
} else if (x - radius * 2 < canvas.width){
x = x + radius;
}
let ball = new Ball( x , y, ballsTemplates[numb].direction , 1 , 0.9,ballsTemplates[numb].radius, ballsTemplates[numb].color, ballsTemplates[numb].level)
objects.push(ball)
});
const animate = async () => {
c.fillStyle="white"
c.fillRect(0, 0, canvas.width, canvas.height) //background :/
objects.forEach(object => {
object.update(objects)
})
}
limitLoop(animate, 60);
.gameScreen {
border: 10px solid white;
background-color: black;
}
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"><div class="container" height="200" width="332"><div><div><canvas class="gameScreen"></canvas> <- С попощью левой кнопки мыши заспавни много шариков. Чтобы было несколько рядок</div></div></div></div>
</body>