I've been looking through a lot of answers here: - https://gamedev.stackexchange.com/questions/158825/resolving-circle-circle-collision - https://gamedev.stackexchange.com/questions/26439/circle-to-circle-collision-checking-each-circle-against-all-others - https://gamedev.stackexchange.com/questions/136520/nestling-into-contact-with-a-group-of-physics-objects-without-exerting-forces-on/136595#136595 - https://stackoverflow.com/questions/345838/ball-to-ball-collision-detection-and-handling - https://stackoverflow.com/questions/53010947/detect-collision-between-two-circles-and-sliding-them-on-each-other - https://gamedev.stackexchange.com/questions/199274/sliding-character-inside-the-circle - https://gamedev.stackexchange.com/questions/147493/2d-circle-detection-algorithm-partially-working - https://gamedev.stackexchange.com/questions/40547/box2d-too-much-for-circle-circle-collision-detection - https://gamedev.stackexchange.com/questions/103278/circle-collision-resolution-and-angular-momentum - https://flatredball.com/documentation/tutorials/math/circle-collision/ - https://stackoverflow.com/questions/61161303/how-do-i-simulate-ball-ball-collision-without-having-the-balls-bounce --doesnt show sliding - https://stackoverflow.com/questions/26216173/accelerometer-rolling-ball-in-ball -- not sure how to implement in my case for angle `a` I couldn't find exactly the same thing I was looking for, which is proper top-down character sliding of circle to circle movement. I tried implementing things from other answers here but whenever the character moves around circle, for some reason it just goes inside it (I was able to get it to spin around edges at one point but was unpredictable) <!-- begin snippet: js hide: false console: true babel: false --> <!-- language: lang-js --> var aD = [] var r function start() { r = new CanvasRenderer(can) my = new scene(); eventHandler(); my.add(new mesh({ position: { x: 700, y: 200 }, radius: 70, color: "yellow", primitive: "circle", collision: "circle" })) my.add(new mesh({ primitive: "circle", collision: "circle", color: "blue", name: "player", radius: 40, position: { x: 311, y: 75 }, origin: { x: 0.5, y: 0.5 }, onupdate(me) { aD.push(() => { r.ctx.beginPath() r.ctx.fillStyle = "red" r.ctx.arc(me.position.x, me.position.y, 5, 0, Math.PI * 2); r.ctx.fill() r.ctx.closePath(); }) if (!window.pl) pl = me var upKey = keys[38], downKey = keys[40], rightKey = keys[39], leftKey = keys[37], drx = 0, dx = 0, speed = 5, turningSpeed = 3 //console.log(upKey) drx = leftKey ? -1 : rightKey ? 1 : 0 forward = upKey ? 1 : downKey ? -1 : 0 me.rotation.x += ( (drx * Math.PI / 180 * turningSpeed) ) me.rotation.y = 1; var xDir = Math.cos(me.rotation.x) var yDir = Math.sin(me.rotation.x) var xvl = xDir * forward * speed var yvl = yDir * forward * speed me.position.y //=col.y me.position.x // = col.x me.xvl = xvl me.yvl = yvl var next = { x: me.position.x + xvl, y: me.position.y + yvl } me.next = next var isColliding = [] var coll; var curColls = [] me.cols = [] me.speed = speed me.yDir = yDir for (var i = 0; i < my.objects.length; i++) { let cur = my.objects[i]; if (cur.name == me.name || cur == me) { break; } curColls = checkCollision( next.x, next.y, me.radius || me.scale.x / 2, cur ) curColls.forEach(coll => { if (!coll) return; if (coll == me) { // console.log("WH") return } cur.isCol = true isColliding.push({ coll, object: cur }) // console.log(2222,coll.line) }) } Array.from(me.cols) .forEach(q => { var w = isColliding.indexOf(q) if (w == -1 || q.object == me) { me.cols.splice(me.cols.indexOf(q), 1) } }) var isme = me.cols.findIndex(q => q.object == me) if (isme > -1) { me.cols.splice(isme, 1) var ok = isColliding.findIndex(q => q.object == me) if (ok > -1) { isColliding.splice(ok, 1) } } if (!me.obs) me.obs = [] var noad = false if (isColliding.length > 0) { isColliding.forEach(q => { var w = me.cols.indexOf(q) if (w == -1) { me.cols.push(q) } }) var col = me.cols[0].coll var ob = me.cols[0].object if (!me.obs) me.obs = [] if (col.type == "circle") { let vecBetweenX = next.x - ob.position.x; let vecBetweenY = next.y - ob.position.y; let heading = Math.atan2(vecBetweenY, vecBetweenX); var dst = Math.sqrt( vecBetweenX * vecBetweenX + vecBetweenY * vecBetweenY ) var dirX = vecBetweenX / dst var dirY = vecBetweenY / dst var rt = heading var ris = ob.radius + me.radius var ax = Math.cos(rt) * ris var ay = Math.sin(rt) * ris /* var nx=ob.position.x+ax var ny=ob.position.y+ay */ var nx = ob.position.x + dirX * ris var ny = ob.position.y + dirY + ris me.position.x = nx me.position.y = ny return } } me.position.x += xvl me.position.y += yvl } })); let i = setInterval(() => render(r, my), 16); r.on("resize", () => render(r, my)); } function render(r, s) { s.update(); r.render(s) aD.forEach(x => x()); aD = [] } onload = start; function checkCollision(x1, y1, rad, ob) { var colls = []; if (ob.collision == "circle") { /* Ball 1: center: p1=(x1,y1) radius: r1 Ball 2: center: p2=(x2,y2) radius: r2 collision distance: R= r1 + r2 actual distance: r12= sqrt( (x2-x1)^2 + (y2-y1)^2 ) collision vector: d12= (x2-x1,y2-y1) = (dx,dy) actual distance: r12= sqrt( dx*dx + dy*dy ) */ var r1 = rad; var r2 = ob.radius; var p1 = [x1, y1] var p2 = [ob.position.x, ob.position.y] var R = r1 + r2; var R2 = R * R; var dx = p2[0] - p1[0] var dy = p2[1] - p1[1] var r12 = ( dx * dx + dy * dy ) var r12s = Math.sqrt(r12) var isCollision = r12s < R var colPoint = { x: (x1 * r2 + p2[0] * r1) / (r1 + r2), y: (y1 * r2 + p2[1] * r1) / (r1 + r2), type: "circle" }; aD.push(() => { r.ctx.beginPath() r.ctx.fillStyle = "salmon" r.ctx.arc(colPoint.x, colPoint.y, 5, 0, Math.PI * 2); r.ctx.fill() r.ctx.closePath(); }) if (isCollision) { colls.push(colPoint) } } return colls; } function dot(x1, y1, x2, y2) { return ( x1 * x2 + y1 * y2 ) } function eventHandler() { window.keys = {}; addEventListener("keyup", e => { keys[e.keyCode] = false; }); addEventListener("keydown", e => { keys[e.keyCode] = true; }); } function CanvasRenderer(dom) { if (!dom) dom = document.createElement("canvas"); var events = {}, self = this; function rsz() { dom.width = dom.clientWidth; dom.height = dom.clientHeight; self.dispatchEvent("resize"); } window.addEventListener("resize", rsz); let ctx = dom.getContext("2d"); function render(scene) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); for (let i = 0; i < scene.objects.length; i++) { let o = scene.objects[i], verts = o.realVerts; if (o.primitive == "circle") { var fnc = o.drawPrimitive[o.primitive] if (typeof(fnc) == "function") fnc(ctx) } ctx.fillStyle = o.color || "blue"; ctx.lineWidth = 1; ctx.fill() ctx.stroke(); } } Object.defineProperties(this, { domElement: { get: () => dom }, ctx: { get: () => ctx }, render: { get: () => render }, on: { get: () => (nm, cb) => { if (!events[nm]) { events[nm] = []; } events[nm].push(data => { if (typeof cb == "function") { cb(data); } }); } }, dispatchEvent: { get: () => (name, data) => { if (events[name]) { events[name].forEach(x => { x(data); }); } } } }); rsz(); } function scene() { let objects = []; Object.defineProperties(this, { add: { get: () => obj => { objects.push(obj); } }, objects: { get: () => objects }, update: { get: () => () => { objects.forEach(x => { if (typeof x.update == "function") { x.update(); } }); } } }); } function mesh(data = {}) { let self = this, holder = { position: {}, scale: { }, rotation: {}, origin: {} }, position = {}, scale = {}, rotation = {}, collision = "mesh", shape = null, origin = {}, color, radius = 0, name, primitive, eventNames = "update", events = {}, drawPrimitive = { circle(ctx) { ctx.beginPath(); ctx.arc( self.position.x, self.position.y, radius, 0, 360 * Math.PI / 180 ); ctx.closePath(); } }, width = 1, height = 1; Object.defineProperties(position, { x: { get: () => holder.position.x || 0, set: v => holder.position.x = v }, y: { get: () => holder.position.y || 0, set: v => holder.position.y = v }, z: { get: () => holder.position.z || 0, set: v => holder.position.z = v } }); Object.defineProperties(scale, { x: { get: () => holder.scale.x || 1, set: v => holder.scale.x = v }, y: { get: () => holder.scale.y || 1, set: v => holder.scale.y = v }, z: { get: () => holder.scale.z || 1, set: v => holder.scale.z = v } }); Object.defineProperties(rotation, { x: { get: () => holder.rotation.x || 0, set: v => holder.rotation.x = v }, y: { get: () => holder.rotation.y || 0, set: v => holder.rotation.y = v }, z: { get: () => holder.rotation.z || 0, set: v => holder.rotation.z = v } }); Object.defineProperties(this, { name: { get: () => name, set(v) { name = v } }, primitive: { get: () => primitive, set(v) { primitive = v; } }, drawPrimitive: { get: () => drawPrimitive }, width: { get: () => width, set(v) { width = v } }, height: { get: () => height, set(v) { height = v } }, position: { get: () => position, set: v => { position.x = v.x || 0; position.y = v.y || 0; position.z = v.z || 0; } }, radius: { get: () => radius || (width * scale.x) / 2, set: v => { radius = v; this.width = radius / 2 this.height = radius / 2 } }, scale: { get: () => scale, set: v => { scale.x = v.x || v.x === 0 ? v.x : 1; scale.y = v.y || v.y === 0 ? v.y : 1; scale.z = v.z || v.z === 0 ? v.z : 1; } }, rotation: { get: () => rotation, set: v => { rotation.x = v.x || 0; rotation.y = v.y || 0; rotation.z = v.z || 0; } }, color: { get: () => color, set: v => { color = v; } }, update: { get: () => () => { if (events["update"]) { events.update.forEach(x => { x(this); }); } } }, on: { get: () => (nm, fnc) => { if (!events[nm]) events[nm] = []; events[nm].push(stuff => { if (typeof fnc == "function") { fnc(stuff); } }); } } }); eventNames.split(" ").forEach(x => { var name = "on" + x; if (!this.hasOwnProperty(name)) { Object.defineProperty(this, name, { get: () => events[name], set(v) { events[x] = [ data => { typeof v == "function" && v(data) } ]; } }); } }); for (let k in data) { this[k] = data[k] } } <!-- language: lang-css --> canvas { width: 100%; height: 100%; position: absolute; top: 0; left: 0px } .wow { float: right; z-index: 1298737198 } <!-- language: lang-html --> <br> game test 2d minimal<br> <meta charset="utf-8"> <button onclick="start()" class=wow>ok</button> <canvas id=can></canvas> <!-- end snippet --> Just arrow keys to move Relevant part for circle sliding (contains code from different examples that was used at different times but nothing works): ``` if(col.type=="circle"){ let vecBetweenX = next.x - ob.position.x; let vecBetweenY = next.y - ob.position.y; let heading = Math.atan2(vecBetweenY, vecBetweenX); var dst = Math.sqrt( vecBetweenX*vecBetweenX + vecBetweenY*vecBetweenY ) var dirX=vecBetweenX/dst var dirY=vecBetweenY/dst var rt= heading var ris = ob.radius + me.radius var ax = Math.cos(rt)*ris var ay = Math.sin(rt)*ris /* var nx=ob.position.x+ax var ny=ob.position.y+ay */ var nx=ob.position.x+dirX*ris var ny=ob.position.y+dirY+ris me.position.x = nx me.position.y = ny return } ``` I tried using method suggested in answer, but player is still getting stuck at circle, while the ideal intended behavior is for the player to slide along the edges