Плавная анимация передвижения одной окружности внутри другой

147
29 января 2020, 08:10

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

class Circle { 
    constructor(node) { 
        this.DOMElement = node; 
        this.ctx = this.DOMElement.getContext('2d'); 
        this.dx = 0; 
        this.dy = 0; 
        this.x = this.DOMElement.width / 2, 
        this.y = this.DOMElement.height / 2, 
        this.ballRadius = 39; 
        this.cirleRadius = this.DOMElement.width / 2 - 50 
    } 
    drawCircle(x, y, radius, fill) { 
        this.ctx.beginPath(); 
        this.ctx[`${fill.fill}Style`] = fill.color; 
        this.ctx.lineWidth = 3; 
        this.ctx.arc(x, y, radius, 0, Math.PI*2, true); 
        this.ctx[fill.fill](); 
        this.ctx.closePath(); 
    } 
    render() { 
        this.ctx.clearRect(0, 0, this.DOMElement.width, this.DOMElement.height); 
        this.drawCircle( 
            this.DOMElement.width / 2,  
            this.DOMElement.height / 2,  
            this.cirleRadius, 
            {fill: 'stroke', color: '#34648e'} 
        ); 
        this.drawCircle( 
            this.x,  
            this.y,  
            this.ballRadius, 
            {fill: 'fill', color: '#0294bf'} 
        ); 
        if(Math.pow(this.x - this.DOMElement.width / 2, 2) + Math.pow(this.y - this.DOMElement.height / 2, 2) >= Math.pow(this.cirleRadius - this.ballRadius, 2) ) { 
          // ???? 
        } 
        const frame = requestAnimationFrame(this.render.bind(this)) 
    } 
     
    init() { 
        let currentX = 0; 
        let currentY = 0; 
        let distance = 0; 
        let speed = 10; 
 
        this.DOMElement.onmousemove = (event) => { 
            let mouseX = event.clientX - this.DOMElement.offsetLeft - 10; 
            let mouseY = event.clientY - this.DOMElement.offsetTop - this.DOMElement.scrollTop + window.pageYOffset - 14; 
             
            let xResult = mouseX - currentX; 
            let yResult = mouseY - currentY; 
            distance = Math.sqrt(xResult * xResult + yResult * yResult); 
            if(Math.pow(mouseX - this.x, 2) + Math.pow(mouseY - this.y, 2) <= Math.pow(this.ballRadius + 5, 2) ) { 
                this.x += xResult / distance * speed; 
                this.y += yResult / distance * speed; 
            } 
            currentX = mouseX; 
            currentY = mouseY; 
             
        } 
        this.render(); 
         
    } 
     
     
} 
const circle = new Circle(document.querySelector('#canvas')); 
    circle.init();
.canvas { 
    border: 15px solid #ebebeb; 
    margin: auto; 
    background-color: #f8f8f8; 
}
<canvas id="canvas" class="canvas" width="590" height="600">

Подскажите как реализовать планый "отскок" шара от прикосновения к нему курсором на некую дистанцию и при этом такой же плавный "отскок" от границы внешней окружности при столкновении.

Answer 1

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

Т.е. зная положение объекта в прошлом кадре, его направление, скорость движения и сколько прошло с прошлого кадра секунд - вычисляем новое положение.

Собрал маленький пример:

  1. Я завел объект, описывающий состояние шарика.

  2. Повесил слушатель, который обновляет глобальное положение мыши по mousemove

  3. Написал цикл отрисовки (рекурсивный вызов через requestAnimationFrame), который берет значение положения шарика из объекта-состояния и рисует его + рисует внешний круг.

  4. Запустил независимый от отрисовки цикл(setInterval), который меняет состояние шарика. Он делает 3 вещи:

    • Обрабатывает положение мыши относительно шарика, и когда произошло столкновение устанавливает шарику скорость и вектор движения.

    • Считает новое положения шарика исходя из его текущего положения, вектора его движения и скорости. Постепенно уменьшает скорость движения.

    • Обрабатывает столкновения с внешним кругом, подсчитывая новый отраженный вектор движения

Отраженный вектор равен исходному вектору минус вектор нормали умноженный на двойное скалярное произведение вектора нормали в точке соударения и исходного вектора.

r = i−2(i⋅n)n

Вот рабочий пример:

// координаты мыши относительно центра канвы. 
let x = 0, y = 0;  
 
// глобальный слушатель мышки 
window.addEventListener('mousemove', e => { 
  let z = window.getComputedStyle(canvas).zoom || 1;      
  x = e.pageX/z - e.target.offsetLeft - canvas.width/2, 
  y = e.pageY/z - e.target.offsetTop - canvas.height/2; 
}); 
 
let ball = { 
   
  r: 50,    // радиус шарика 
  x: 0,     // координата по х центра шарика 
  y: 0,     // координата по y центра шарика 
  speed: 0, // скорость движения 
  dirx: 0,  // компонент x вектора движения шарика 
  diry: 0,  // компонент y вектора движения шарика 
  damp: 10, // скорость уменьшения скорости движения (сопротивление) 
  collision: false, // признак коллизии с внешним кругом 
   
  // функция, которая проверяет наличие коллизии шарика с внешним кругом 
  hitOuterCircleCheck: function() { 
   
   let dr = 195-this.r; //разница радиусов 
   // по теореме пифагора проверяем выход за пределы круга (коллизию) 
   if (this.x*this.x + this.y*this.y > dr*dr) { 
    
      // если коллизия уже была обсчитана, но шарик еще не вернулся в круг,  
      // чтобы он не застревал больше не надо обсчитывать коллизии, поэтому выходим 
      if (this.collision) 
          return; 
           
      // устанавливаем для шарика признак коллизии     
      this.collision = true;  
 
      // далее идет код расчета нового вектора движения 
 
      // найдем вектор нормали. тут он берется приближенно,  
      // в точке центра шарика в момент обсчета коллизии,  
      // при том что шарик уже проскочил границу. по идее тут  
      // необходимо посчитать точку соударения геометрически.  
      let max = Math.max(Math.abs(this.x), Math.abs(this.y));  
      let nx = -this.x/max; 
      let ny = -this.y/max; 
 
      // найдем новый вектор движения по формуле  
      // r = i−2(i⋅n)n , где 
      // i - исходный вектор 
      // n - нормаль  
      // ⋅ знак скалярного произведения 
 
      let dot2 = this.dirx * nx * 2 + this.diry * ny * 2 
      this.dirx = this.dirx-dot2*nx; 
      this.diry = this.diry-dot2*ny; 
 
      // нормализуем вектор движения 
      max = Math.max(Math.abs(this.dirx), Math.abs(this.diry)); 
      this.dirx /= max; 
      this.diry /= max; 
    } else { 
     
      // сбрасываем признак коллизии когда шарик вернулся в круг. 
      this.collision = false; 
    } 
  }, 
   
  // функция проверки коллизии шарика и мышки 
  hitMouseCheck: function() { 
   
    // если есть коллизия с внешним кругом игнорируем мышку 
    if (this.collision)  
      return; 
   
    // разница координат мышки и шарика 
    let dx = this.x - x;  
    let dy = this.y - y; 
 
    // проверяем по теореме Пифагора столкновение с мышкой 
    if (dx*dx + dy*dy < ball.r*ball.r) {  
      // задаем вектор движения и нормализуем его 
      let max = Math.max(Math.abs(dx), Math.abs(dy)); 
      if (!max) return; 
      this.dirx = dx/max; 
      this.diry = dy/max; 
 
      // задаем скорость 
      this.speed = 300; 
    } 
  }, 
   
  // тут осуществляется передвижение 
  // dt - кол-во секунд с прошлого обсчета 
  doMove: function(dt) {  
   
    // к текущей координате прибавляем вектор скорости помноженный  
    // на значение скорости помноженные на прошедшее время 
    this.x += this.dirx*this.speed*dt;  
    this.y += this.diry*this.speed*dt; 
     
    // тормозим объект, так же на значение зависящее от времени 
    this.speed = Math.max(0, this.speed - this.damp*dt);  
  } 
}; 
 
// цикл прверок, не зависит от цикла отрисовки,  
// все проверки запускаются дискретно, через фиксированное время 10 мс 
// если считать "физику" в requestAnimationFrame все будет печально. 
let t = 0; 
setInterval(() => { 
  // считаем сколько времени прошло с прошлого обсчета 
  let dt = new Date().getTime() - t;  
  ball.hitMouseCheck(); 
  ball.doMove(dt/1000); 
  ball.hitOuterCircleCheck(); 
  t = new Date().getTime(); 
}, 5); 
 
// далее нет никаких фокусов 
 
let canvas = document.querySelector('canvas'); 
let ctx = canvas.getContext('2d'); 
 
requestAnimationFrame(draw); 
 
function draw() { 
  ctx.clearRect(0, 0, canvas.width, canvas.height); 
  ctx.translate(canvas.width/2, canvas.height/2) 
  circle(0, 0, 195); 
  circle(ball.x, ball.y, ball.r); 
  ctx.translate(-canvas.width/2, -canvas.height/2) 
  requestAnimationFrame(draw); 
} 
 
function circle(x,y,r) { 
  ctx.beginPath(); 
  ctx.strokeStyle = 'red'; 
  ctx.lineWidth = 3; 
  ctx.arc(x, y, r, 0, Math.PI*2, true); 
  ctx.stroke(); 
  ctx.closePath(); 
}
<body style="margin:0"><canvas width="400" height="400"/></body>

В этом алгоритме есть неточности, точнее недоработки, но в этом примере они несущественны.

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

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

READ ALSO
Как нарисовать такую фигуру на D3?

Как нарисовать такую фигуру на D3?

Подскажите, как её нарисовать не используя две фигурыТак же известен радиус, высота и расстояние между верхней точкой окружности и верхним...

166
Выравнивание window.open() окна по центру монитора

Выравнивание window.open() окна по центру монитора

Открываю на сайте всплывающее окно, токо оно открывается в левом верхнем углуwindow

199
Как преобразовать объект массива

Как преобразовать объект массива

Необходимо изменить структуру из такой:

147
Ошибка при подключении к MySQL 8.0.15-a2

Ошибка при подключении к MySQL 8.0.15-a2

У меня возникла проблема с MySQL я недавно ее поставил настроил и вот тут уже проблема с коннектором Внимание Phpmyadmin работает без проблем а коннект...

162