Sphere Collider

HTML

<canvas width="200" height="200" id="c"></canvas>

CSS

canvas {
  border: 1px solid silver;
}

JavaScript

class Circle { 
	constructor(x, y, r, color="black") {
  	this.x = x;
    this.y = y;
    this.r = r;
    this.color = color;
  }
  
  draw(ctx) {
  	ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x,this.y,this.r,0, 2*Math.PI);
	  ctx.fill();
  }
}

class CircleCollider {
	static collides(c1, c2) {
  	if (!(c1 instanceof Circle) || !(c2 instanceof Circle)) {
    	return false;
    } 
    
    const dx = c1.x - c2.x;
    const dy = c1.y - c2.y;
    return dx*dx + dy*dy <= (c1.r + c2.r)**2;
  }
  
  static block(circle, fixed) {
  	if (!CircleCollider.collides(circle, fixed)) {
    	return {x: circle.x, y: circle.y};
    }
    
    var dx = fixed.x - circle.x;
    var dy = fixed.y - circle.y;
    const len = Math.sqrt(dx**2 + dy**2);
    
    dx /= len;
    dy /= len;
    
    return {
    	x: fixed.x - dx * (fixed.r + circle.r),
      y: fixed.y - dy * (fixed.r + circle.r)
    }
  }
}

const ctx = c.getContext("2d");
const fixedCircles = [];

for (let i = 0; i < 5; i++) {
	fixedCircles.push(new Circle(Math.random() * c.width, Math.random() * c.height, 15))
}

const mouseCircle = new Circle(0, 0, 10, "#f00");

function render() {
	ctx.clearRect(0, 0, c.width, c.height);
	for (const c of fixedCircles) {
    c.draw(ctx);
  }
  
  mouseCircle.draw(ctx);
  window.requestAnimationFrame(render);
}
window.requestAnimationFrame(render)

c.addEventListener('mousemove', (evt) => {
	mouseCircle.x = evt.clientX - mouseCircle.r;
  mouseCircle.y = evt.clientY - mouseCircle.r;
  
	for (const fixed of fixedCircles) {
  	const blocked = CircleCollider.block(mouseCircle, fixed);
    mouseCircle.x = blocked.x;
    mouseCircle.y = blocked.y;
  }
});