Skip to main content
1 of 5

javascript top down circle to circle smooth collision detection

BH

HI I've been looking through a lot of answers here:

Resolving Circle-Circle collision

Circle to Circle collision, checking each circle against all others

Nestling into contact with a group of physics objects without exerting forces on them

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

Sliding character inside the circle

2D Circle detection algorithm partially working

Box2d too much for Circle/Circle collision detection?

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

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 implemmenting things from other answers here but whenever the character moves around circle, for some reason it just goes inside it (was able to get it to spin around edges at one point but was unpredictable)

BH
<br>
game test 2d<br>
<meta charset="utf-8">
<button onclick="start()" class=wow>ok</button>
<canvas id=can>

</canvas>
<style>
    canvas{
        width:100%;
        height:100%;
        position:absolute;
        top:0;  
        left:0px
    }

    .wow{
        float:right;
        z-index:1298737198
    }

</style>
<script>
var aD =[]
var r
function start() {
    r = new CanvasRenderer(can),
        my = new scene();
    window.my = my
    eventHandler();
    my.add(new mesh({
        verts: [
            0,   0,
            100, 15,
            115, 60,
            50, 100,
            20, 75,2,8
        ],
        position: {
            x: 100,
            y:100
        },
        scale: {

            x:4,y:5
        },
        color:"orange",
        onupdate(me) {
        //  me.position.x++
        }
    }));
    
    my.add(new mesh({
        position:{
            x:700,
            y:200
        },
        radius:70,
        color:"yellow",
        primitive:"circle",
        collision:"circle"
    }))
    
    var g = false
    corn=false
    nv = [0,0]
    my.add(new mesh({
        primitive:"rect",
        name: "player",
        scale: {
            x: 50,
            y:50
        },
        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
            
            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
            if(nv[0]||nv[1]){
                //xvl = -nv[0]
                //yvl = -nv[1]
            }
            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 lastdify = 0;
            var lastdifx = 0;
            var dify = 0
            var difx = 0
            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=[]
                
                //console.log("COL",col.type,col.name)
                ob.active=false
                ob.wowow=292
                var op=[0,0]
                //corn=false
            //  if(col.type=="mesh") console.log("wow")
            //  console.log("K",col.x,col.y,me.cols.length,isColliding.length,isme)
                
                if(isColliding.length==1){
                    
                    if(me.obs.length>0){
                        console.log("undid")
                        me.obs.forEach(ob=>{
                            //if(ob.active) {
                                ob.lastPoint = null 
                                me.obs.splice(me.obs.indexOf(ob),1)
                            //}
                        })
                    }
                    var backlash = col.x
                    var backlashy = col.y
                    
                    if(col.type=="circle"){
                        let vecBetweenX = me.position.x - ob.position.x;
                        let vecBetweenY = me.position.y - ob.position.y;
                        
                        let heading = Math.atan2(vecBetweenY, vecBetweenX);
                        
                    
                        
                        
                        var rt= me.rotation.x+heading
                        
                        
                        var ax = Math.cos(rt)*speed*forward
                        var ay = Math.sin(rt)*speed*forward
                        
                        difx = -ax
                        dify = -ay
                        
                        me.position.x +=difx
                        me.position.y += dify
                    //  return
                        
                        
                    } else {
                        difx += col.x-next.x
                        dify += col.y-next.y
                    }
                }else {
                    me.eq=false
                    me.curc=null
                    ob.active=true 
                    if(!col.line) return;
                    var xd = col.line.end[0]-col.line.start[0]
                    var slope1=slope(col.line)
                    
                    var b = findb(col.x,col.y,slope1)
                    var amy = slope1
                    var amx = Math.abs(xd)
                        
                    var y;
                    if(!ob.lastPoint) {
                        y = col.y+amy
                    } else {
                        y = ob.lastPoint.y+amy; 
                        ob.lastPoint.y+=amy;
                    }
                    var x = xatywithslope(y,slope1,b)

                    me.tst={
                        b,x,y,slope1,amx,amy    
                    }
                    
                
                    if(col.percentOfWall == 0 || col.percentOfWall == 1){
                        me.eq=true
            
                        difx += x-next.x
                        dify += y-next.y
                        
                        if(!ob.lastPoint){
                            ob.lastPoint = {
                                x:col.x+amx,
                                y:col.y+amy
                            }
                            
                            me.obs.push(ob)
                        }
                    } else {
                
                        me.curc=col;
                        difx += col.x-next.x
                        dify += col.y-next.y
                        
                        
                        lastdify = dify
                        lastdifx = difx

                        if(difx>0)
                            nv[0] = difx
                        if(dify>0)
                            nv[1]=dify

                    }


                }
                dify+=op[1]
                difx+=op[0] 
            }
            
            if(!noad) {
                //xvl+=difx
                //yvl+=dify

                me.position.x+=difx+xvl
                me.position.y+=dify+yvl

            } else {
            //  xvl+=nv[0]//*forward
        //      yvl-=nv[1]//*forward
            }
            if(noad) {
                
            } else {
                
            }
        }

    }));
    
    let i = setInterval(() => render(r, my), 16);
    r.on("resize", () => render(r, my));

}

function angleOf2points(p1,p2) {
    return Math.atan2(p2[1] - p1[1], p2[0] - p1[0])
        //* 180 / Math.PI   
}
function xatywithslope(y,slope,b){
    /*y=mx+b
    mx=y-b
    x=(y-b)/m
    
    */
    return (y-b)/slope
}
    
function findb(x,y,s){
    /*y=mx+b
    y-mx=b
    b=y-mx
    */
    return y-s*x
}
function slope(line){
    var p1=line.start
    var p2=line.end
    return (
        (p1[1]-p2[1]) /
        (p1[0]-p2[0])
    )
}
    
function midSlope(slope1,slope2) {
    return -Math.tan(
        (
            Math.atan(slope1) +     
            Math.atan(slope2)
        ) / 2
    )
}
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)
        }
    } else {
//      console.log("cecking",rad,ob)
        ob.lineSegments.forEach(l => {
            var dist = distance2(
                l.start[0],
                l.start[1],

                l.end[0],
                l.end[1]
            ),
                vec1 = [
                    x1 - l.start[0],
                    y1 - l.start[1]
                ],

                vec2 = [
                    l.end[0] - l.start[0],
                    l.end[1] - l.start[1]
                ],

                percentOfWall = (
                    Math.max(
                        0,
                        Math.min(
                            1, 
                            dot(
                                vec1[0],
                                vec1[1],

                                vec2[0],
                                vec2[1]
                            ) / dist
                        )
                    )
                ),
                projection = [
                    l.start[0] + percentOfWall * vec2[0],
                    l.start[1] + percentOfWall * vec2[1],
                ],
                acDist = Math.sqrt(distance2(
                    x1, y1,
                    projection[0], projection[1]
                ))
            aD.push( () => {
                r.ctx.beginPath()
                r.ctx.fillStyle="green"
                r.ctx.arc(projection[0], projection[1], 5, 0, Math.PI*2);
                r.ctx.fill()
                r.ctx.closePath();
            })


            if(acDist < rad) {
                aD.push(() => {
                    r.ctx.beginPath()
                    r.ctx.fillStyle="orange"
                    r.ctx.arc(projection[0] + 

                    rad * (normal[1] ), projection[1] + 
                    rad* (-normal[0] ), 5, 0, Math.PI*2);
                    r.ctx.fill()
                    r.ctx.closePath()
                })
                var mag = Math.sqrt(dist),
                    delt = [
                        l.end[0] - l.start[0],
                        l.end[1] - l.start[1]
                    ],
                    normal = [
                        delt[0] / mag,
                        delt[1] / mag
                    ]

                colls.push({
                    percentOfWall,
                    type:"mesh",
                    x: projection[0] + 

                    rad * (normal[1] ),

                    y:projection[1] + 
                    rad* (-normal[0] ),
                    projection,
                    normal,
                    line:l
                })
            }
        })
    }
    
    return colls;

                    
}

function checkCollision1(x1, y1, rad,l) {
    var dist = distance2(
        l.start[0],
        l.start[1],

        l.end[0],
        l.end[1]
    ),
        vec1 = [
            x1 - l.start[0],
            y1 - l.start[1]
        ],

        vec2 = [
            l.end[0] - l.start[0],
            l.end[1] - l.start[1]
        ],

        percentOfWall = (
            Math.max(
                0,
                Math.min(
                    1, 
                    dot(
                        vec1[0],
                        vec1[1],

                        vec2[0],
                        vec2[1]
                    ) / dist
                )
            )
        ),
        projection = [
            l.start[0] + percentOfWall * vec2[0],
            l.start[1] + percentOfWall * vec2[1],
        ],
        acDist = Math.sqrt(distance2(
            x1, y1,
            projection[0], projection[1]
        ))
    aD.push( () => {
        r.ctx.beginPath()
        r.ctx.fillStyle="green"
        r.ctx.arc(projection[0], projection[1], 5, 0, Math.PI*2);
        r.ctx.fill()
        r.ctx.closePath();
    })


    if(acDist < rad) {
        var mag = Math.sqrt(dist),
            delt = [
                l.end[0] - l.start[0],
                l.end[1] - l.start[1]
            ],
            normal = [
                delt[0] / mag,
                delt[1] / mag
            ]

        return {

            x: projection[0] + 

            rad * (normal[1] ),

            y:projection[1] + 
            rad* (-normal[0] ),
            projection,
            normal
        }
    }

                    
}


function dot(x1, y1, x2, y2) {
    return (
        x1 * x2 + y1 * y2
    )
}

function distance2(x1, y1, x2, y2) {
    let dx = (x1 - x2), dy = (y1 - y2);
    return (
        dx * dx + dy * dy
    );
}

function render(r,s) {
//r.ctx.clearRect(0,0,r.ctx.canvas.width,r.ctx.canvas.height)
    s.update();
    r.render(s)
    aD.forEach(x=>x());
    aD = []
}

onload = start;

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)
            } else {
                
                ctx.beginPath();
                ctx.moveTo(
                    verts[0] , 

                    verts[1]
                );
                verts.forEach((v, i, ar) => {
                    let y = i;



                    ctx.lineTo(
                        v[0] , 

                        v[1]
                    );

                });
                ctx.lineTo(
                    verts[0],
                    verts[1] 
                );
                
                ctx.closePath();
            }
            
            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 verts = [],
        self = this,
        holder = {
            position:{},
            scale: {
                
            },
            rotation: {},
            origin:{}
        },
        actual = {
    
        },
        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();
            },
            rect(ctx) {
                ctx.strokeRect(
                    self.position.x,
                    self.position.y,
                    30, 30
                );
            }
        },
        width = 1,
        height = 1,
        primitiveToVerts = {
            rect: () =>  [
                    0, 0,
                    width , 0,
                    width, height,
                    0, height
            ]
        },
        realVerts = verts,
        lineSegments = [],
        o = this;
    
    function updateRealVerts() {
            
            let  actualVerts = [],
                originedVerts = [],
                adjustedVerts = [],
                rotatedVerts = [],
                stepSize = o.step || 2,
                curVerts = [];
            
            o.verts.forEach((v, i) => {
                curVerts.push(v);
                if(
                    (i - 1) % stepSize === 0 &&
                    i !== 0
                ) {
                    actualVerts.push(curVerts);
                    curVerts = [];
                }
            });
            actualVerts = actualVerts.filter(x => x.length == stepSize);
            
            originedVerts = actualVerts.map(v => [
                v[0] - o.origin.x,
                v[1] - o.origin.y,
                v[2] - o.origin.z
            ]);
    
            rotatedVerts = originedVerts.map(v => 
                [

                    v[0] * Math.cos(o.rotation.x) - 
                    v[1] * Math.sin(o.rotation.x),

                    v[0] * Math.sin(o.rotation.x) + 
                    v[1] *Math.cos(o.rotation.x),
v[2]
                ]
            );

            adjustedVerts = rotatedVerts.map(v => 
                [
                    v[0] * 
                    o.scale.x + 
                    o.position.x,
    
                    v[1] * 
                    o.scale.y + 
                    o.position.y,

                    v[2] * 
                    o.scale.z + 
                    o.position.z,
                ]
            );

            realVerts = adjustedVerts;
            updateLineSegments();
    }   

    function updateLineSegments() {
                let lines = [];
                for(let i = 0, a = realVerts; i < a.length;i++) {
                    let start = [], end = []
                    if(i < a.length - 1) {
                        start = a[i];
                        end = a[i + 1];
                    } else {
                        start = a[i];
                        end = a[0];
                    }

                    lines.push({
                        start, end
                    })
                }
                lineSegments = lines;
    }
    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(origin, {
        x: {
            get: () => holder.origin.x || 0,
            set: v => holder.origin.x = v
        },
        y: {
            get: () => holder.origin.y || 0,
            set: v => holder.origin.y = v
        },
        z: {
            get: () => holder.origin.z || 0,
            set: v => holder.origin.z = v
        }
    });
    

    Object.defineProperties(this, {
        verts: {
            get: ()=>verts,
            set(v) {
                verts = v
            }
        },
        
        shape: {
            get: ()=>shape,
            set(v) {
                shape = v
            }
        },
        name: {
            get: ()=>name,
            set(v) {
                name = v
            }
        },
        primitive: {
            get: ()=>primitive,
            set(v) {
                primitive = v;
                let newVerts = primitiveToVerts[v];
                if(newVerts) {
                    this.verts = newVerts();
                }
            }
        },
        
        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;
            }
        },
        origin: {
            get: () => origin,
            set: v => {
                origin.x = v.x || 0;
                origin.y = v.y || 0;
                origin.z = v.z || 0;
            }
        },
        color: {
            get: () => color,
            set: v => {
                color = v;
            }
        },
        realVerts: {
            get: () => realVerts
        },
        lineSegments: {
            get: () => lineSegments
        },
        update: {
            get: () => () => {
                if(events["update"]) {
                    events.update.forEach(x => {
                        updateRealVerts();
                        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]
    }

    updateRealVerts();

}
</script>

just arrow keys to move

relevant part for circle sliding:

if(col.type=="circle"){
                        let vecBetweenX = me.position.x - ob.position.x;
                        let vecBetweenY = me.position.y - ob.position.y;
                        
                        let heading = Math.atan2(vecBetweenY, vecBetweenX);
                        
                    
                        
                        
                        var rt= me.rotation.x+heading
                        
                        
                        var ax = Math.cos(rt)*speed*forward
                        var ay = Math.sin(rt)*speed*forward
                        
                        difx = -ax
                        dify = -ay
                        
                        me.position.x +=difx
                        me.position.y += dify
                    //  return
                        
                        
                    }

ive tried following the complex math questions here, but my mind can only understand code