Tengo un círculo básico que rebota en las paredes de un lienzo rectangular (que adapté de un ejemplo).

https://jsfiddle.net/n5stvv52/1/

El código para verificar este tipo de colisión es algo tosco, pero funciona:

if (p.x > canvasWidth - p.rad) {
  p.x = canvasWidth - p.rad
  p.velX *= -1
}
if (p.x < p.rad) {
  p.x = p.rad
  p.velX *= -1
}
if (p.y > canvasHeight - p.rad) {
  p.y = canvasHeight - p.rad
  p.velY *= -1
}
if (p.y < p.rad) {
  p.y = p.rad
  p.velY *= -1
}

Donde p es el elemento que se mueve.

Sin embargo, los límites de mi lienzo ahora deben ser un círculo, por lo que compruebo la colisión con lo siguiente:

const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad

if (collision) {
  console.log('Out of circle bounds!')
}

Cuando mi bola golpea los bordes del círculo, la instrucción if (collision) se ejecuta como verdadera y veo el log. Entonces puedo detectarlo, pero no puedo saber cómo calcular la dirección que debería seguir después de eso.

Obviamente, comparar x con el ancho del lienzo no es lo que necesito porque ese es el rectángulo y se corta un círculo en las esquinas.

¿Alguna idea de cómo puedo actualizar mis declaraciones if para dar cuenta de este círculo recién detectado?

Parece absolutamente terrible con la trigonometría básica, ¡así que tengan paciencia conmigo! Gracias.

2
Michael Giovanni Pumo 2 mar. 2018 a las 00:32

3 respuestas

La mejor respuesta

Puedes usar las coordenadas polares para normalizar el vector:

var theta = Math.atan2(dy, dx)
var R = canvasRadius - p.rad

p.x = canvasRadius + R * Math.cos(theta)
p.y = canvasRadius + R * Math.sin(theta)

p.velX *= -1
p.velY *= -1

https://jsfiddle.net/d3k5pd94/1/

Actualización : el movimiento puede ser más natural si agregamos aleatoriedad a la aceleración:

 p.velX *= Math.random() > 0.5 ? 1 : -1
 p.velY *= Math.random() > 0.5 ? 1 : -1

https://jsfiddle.net/1g9h9jvq/

1
stdob-- 1 mar. 2018 a las 22:10

Entonces, para hacer esto, necesitarás un buen trigonometraje. Los ingredientes básicos que necesitará son:

  • El vector que apunta desde el centro del círculo hasta el punto de colisión.
  • El vector de velocidad de la pelota.

Luego, dado que las cosas rebotan aproximadamente con un "ángulo igual y opuesto", necesitará encontrar la diferencia de ángulo entre ese vector de velocidad y el vector de radio, que puede obtener utilizando un producto de puntos.

Luego, haz algunos disparos para obtener un nuevo vector que esté tan alejado del vector de radio, en la otra dirección (este es tu igual y opuesto). Establezca que sea el nuevo vector de velocidad y listo.

Sé que es un poco denso, especialmente si estás oxidado con tus matemáticas trigonométricas / vectoriales, así que aquí está el código para que funcione. Este código probablemente podría simplificarse, pero al menos demuestra los pasos esenciales:

function canvasApp (selector) {
  const canvas = document.querySelector(selector)
  const context = canvas.getContext('2d')

  const canvasWidth = canvas.width
  const canvasHeight = canvas.height
  const canvasRadius = canvasWidth / 2
  const particleList = {}
  const numParticles = 1
  const initVelMax = 1.5
  const maxVelComp = 2.5
  const randAccel = 0.3
  const fadeColor = 'rgba(255,255,255,0.1)'
  let p

  context.fillStyle = '#050505'
  context.fillRect(0, 0, canvasWidth, canvasHeight)

  createParticles()
  draw()

  function createParticles () {
    const minRGB = 16
    const maxRGB = 255
    const alpha = 1

    for (let i = 0; i < numParticles; i++) {
      const vAngle = Math.random() * 2 * Math.PI
      const vMag = initVelMax * (0.6 + 0.4 * Math.random())
      const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const color = `rgba(${r},${g},${b},${alpha})`
      const newParticle = {
        x: Math.random() * canvasWidth,
        y: Math.random() * canvasHeight,
        velX: vMag * Math.cos(vAngle),
        velY: vMag * Math.sin(vAngle),
        rad: 15,
        color
      }

      if (i > 0) {
        newParticle.next = particleList.first
      }

      particleList.first = newParticle
    }
  }

  function draw () {
    context.fillStyle = fadeColor
    context.fillRect(0, 0, canvasWidth, canvasHeight)

    p = particleList.first

    // random accleration
    p.velX += (1 - 2 * Math.random()) * randAccel
    p.velY += (1 - 2 * Math.random()) * randAccel

    // don't let velocity get too large
    if (p.velX > maxVelComp) {
      p.velX = maxVelComp
    } else if (p.velX < -maxVelComp) {
      p.velX = -maxVelComp
    }
    if (p.velY > maxVelComp) {
      p.velY = maxVelComp
    } else if (p.velY < -maxVelComp) {
      p.velY = -maxVelComp
    }

    p.x += p.velX
    p.y += p.velY

    // boundary
    const dx = p.x - canvasRadius
    const dy = p.y - canvasRadius
    const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad

    if (collision) {
      console.log('Out of circle bounds!')
      // Center of circle.
      const center = [Math.floor(canvasWidth/2), Math.floor(canvasHeight/2)];
      // Vector that points from center to collision point (radius vector):
      const radvec = [p.x, p.y].map((c, i) => c - center[i]);
      // Inverse vector, this vector is one that is TANGENT to the circle at the collision point.
      const invvec = [-p.y, p.x];
      // Direction vector, this is the velocity vector of the ball.
      const dirvec = [p.velX, p.velY];
      
      // This is the angle in radians to the radius vector (center to collision point).
      // Time to rememeber some of your trig.
      const radangle = Math.atan2(radvec[1], radvec[0]);
      // This is the "direction angle", eg, the DIFFERENCE in angle between the radius vector
      // and the velocity vector. This is calculated using the dot product.
      const dirangle = Math.acos((radvec[0]*dirvec[0] + radvec[1]*dirvec[1]) / (Math.hypot(...radvec)*Math.hypot(...dirvec)));
      
      // This is the reflected angle, an angle that is "equal and opposite" to the velocity vec.
    	const refangle = radangle - dirangle;
      
      // Turn that back into a set of coordinates (again, remember your trig):
      const refvec = [Math.cos(refangle), Math.sin(refangle)].map(x => x*Math.hypot(...dirvec));
      
      // And invert that, so that it points back to the inside of the circle:
      p.velX = -refvec[0];
      p.velY = -refvec[1];
      
      // Easy peasy lemon squeezy!
    }

    context.fillStyle = p.color
    context.beginPath()
    context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
    context.closePath()
    context.fill()

    p = p.next

    window.requestAnimationFrame(draw)
  }
}

canvasApp('#canvas')
<canvas id="canvas" width="500" height="500" style="border: 1px solid red; border-radius: 50%;"></canvas>

DESCARGO DE RESPONSABILIDAD: Dado que su posición inicial es aleatoria, esto no funciona muy bien con el inicio de la pelota ya fuera del círculo. Así que asegúrese de que el punto inicial esté dentro de los límites.

2
CRice 1 mar. 2018 a las 22:08

No necesitas trigonometría en absoluto. Todo lo que necesita es la superficie normal, que es el vector desde el punto de impacto hasta el centro. Normalícela (divida ambas coordenadas entre la longitud) y obtendrá la nueva velocidad usando

v '= v - 2 * (v • n) * n

Donde v • n es el producto punto:

v • n = v.x * n.x + v.y * n.y

Traducido a su ejemplo de código, eso es

// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad

if (collision) {
  // the normal at the point of collision is -dx, -dy normalized
  var nx = -dx / nl
  var ny = -dy / nl
  // calculate new velocity: v' = v - 2 * dot(d, v) * n
  const dot = p.velX * nx + p.velY * ny
  p.velX = p.velX - 2 * dot * nx
  p.velY = p.velY - 2 * dot * ny
}
function canvasApp(selector) {
  const canvas = document.querySelector(selector)
  const context = canvas.getContext('2d')

  const canvasWidth = canvas.width
  const canvasHeight = canvas.height
  const canvasRadius = canvasWidth / 2
  const particleList = {}
  const numParticles = 1
  const initVelMax = 1.5
  const maxVelComp = 2.5
  const randAccel = 0.3
  const fadeColor = 'rgba(255,255,255,0.1)'
  let p

  context.fillStyle = '#050505'
  context.fillRect(0, 0, canvasWidth, canvasHeight)

  createParticles()
  draw()

  function createParticles() {
    const minRGB = 16
    const maxRGB = 255
    const alpha = 1

    for (let i = 0; i < numParticles; i++) {
      const vAngle = Math.random() * 2 * Math.PI
      const vMag = initVelMax * (0.6 + 0.4 * Math.random())
      const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
      const color = `rgba(${r},${g},${b},${alpha})`
      const newParticle = {
        // start inside circle
        x: canvasWidth / 4 +  Math.random() * canvasWidth / 2,
        y: canvasHeight / 4 +  Math.random() * canvasHeight / 2,
        velX: vMag * Math.cos(vAngle),
        velY: vMag * Math.sin(vAngle),
        rad: 15,
        color
      }

      if (i > 0) {
        newParticle.next = particleList.first
      }

      particleList.first = newParticle
    }
  }

  function draw() {
    context.fillStyle = fadeColor
    context.fillRect(0, 0, canvasWidth, canvasHeight)
    
    // draw circle bounds
    context.fillStyle = "black"
    context.beginPath()
    context.arc(canvasRadius, canvasRadius, canvasRadius, 0, 2 * Math.PI, false)
    context.closePath()
    context.stroke()

    p = particleList.first

    // random accleration
    p.velX += (1 - 2 * Math.random()) * randAccel
    p.velY += (1 - 2 * Math.random()) * randAccel

    // don't let velocity get too large
    if (p.velX > maxVelComp) {
      p.velX = maxVelComp
    } else if (p.velX < -maxVelComp) {
      p.velX = -maxVelComp
    }
    if (p.velY > maxVelComp) {
      p.velY = maxVelComp
    } else if (p.velY < -maxVelComp) {
      p.velY = -maxVelComp
    }

    p.x += p.velX
    p.y += p.velY

    // boundary
    const dx = p.x - canvasRadius
    const dy = p.y - canvasRadius
    const nl = Math.sqrt(dx * dx + dy * dy)
    const collision = nl >= canvasRadius - p.rad

		if (collision) {
    	// the normal at the point of collision is -dx, -dy normalized
      var nx = -dx / nl
      var ny = -dy / nl
      // calculate new velocity: v' = v - 2 * dot(d, v) * n
      const dot = p.velX * nx + p.velY * ny
      p.velX = p.velX - 2 * dot * nx
      p.velY = p.velY - 2 * dot * ny
    }

    context.fillStyle = p.color
    context.beginPath()
    context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
    context.closePath()
    context.fill()

    p = p.next

    window.requestAnimationFrame(draw)
  }
}

canvasApp('#canvas')
<canvas id="canvas" width="176" height="176"></canvas>
2
Chris G 1 mar. 2018 a las 22:43