Search code examples
rotationspritegame-physics2d-games

Calculation of a swivel rotation (without physics engine)


I try to get the rotation angle of a swiveling wheel, given the rotation angle of the connected body:

enter image description here

Using wheels angles was my first attempt:i already have this information stored. To get the rotation angle of the wheel, i try to calculate the distance from the rotation axis, by using a coefficient as simple friction simulation:

var dx = cartCX - wheelCX,
    dy = cartCY - wheelCY,
    dist = Math.sqrt(cartCX * wheelCX + cartCY * wheelCY);

var wheelRotation = Math.atan2(-dy, -dx) * dist * friction;

Consider following case:

  1. the wheel is in a stable position, aligned with the rotation circumference of the car after a long-enough clock-wise rotation of the whole system.

  2. now, the whole system is rotating CCW: then the swivel should lead some wheels perform an initial CCW-rotation (wheel right-top in the example picture) which shall be then adequately tweened until the wheel is aligned. Wheels left-top, left-bottom and right bottom shall be rotate CW.

Is there a simple way to calculate the rotation angles of the wheels without any phisics engine?

I don't need an exact physics rigid body simulation, just a simple swivel effect.


Solution

  • Simple swivels

    Assuming the wheels are casters.

    To describe one

    // ? for numbers save me making up numbers
    var wheel = {
       swivel : {x : ?, y : ?}, // world position of swivel (rotation point 
       length : ?,  // distance from swivel to ground Contact when viewed from above
       angle : ?, // current angle
    } 
    

    When the cart moves rotates and/or travel there is a local vector of movement at the wheels swivel

    var delta = { x : ? , y : ? };
    

    This movement is then applied in the reverse direction on the wheel where it contacts the ground.

    var wheelForce = {};
    wheelForce.x = -delta.x;
    wheelForce.y = -delta.y;
    

    There will be two types of motion on the wheel, one is rotation and one is translation. It is constrained by the swingarm so all we need is the rotation as translation will come from the movement of the cart.

    First normalize the wheelForce

    nwf = {};  // for (n)ormalised (w)heel (f)orce
    var dist = Math.hypot(wheelForce.x,wheelForce.y);
    nwf.x = wheelForce.x / dist;
    nwf.y = wheelForce.y / dist;
    

    Then get the normalised vector from the swivel to the wheel ground contact point

    var dir = {};
    dir.x = Math.cos(wheel.angle);
    dir.y = Math.sin(wheel.angle);
    

    Then get the sin of the angle between nwf and dir using cross product

    var fs = nwf.x * dir.y - nwf.y * dir.x;
    

    Now just get the angle from the inverse sin and rotate the wheel by that amount.

    wheel.angle -= Math.asin(fs); // I never get this right (may have to subtract, been up to long to think this out see demo)
    

    UPDATE

    OP has requested some improvements. Skip the line above Update

    To allow for the wheels to slip (or turn) will reduce the turning force on the swivel.

    THe amount of slipping is related to the cos of the angle between the wheelForce and wheel directions. If the angle is 0 then cos will return 1 meaning the wheels are totally free to roll / slip, if the wheel direction is at 90 deg to the force then they will not turn (like dragging a car sideways) cos 90 return 0

    But if we allow for the wheels to turn completely without resistance then that does not work well. So with a limiting factor this modification will improve the sim.

    Thus calculate the slip amount

        var wheelTurnResistance = 0.8;
        var slip = Math.abs(Math.cos(Math.asin(fs))) * wheelTurnResistance;
    

    Now apply the turning force reduced to account for the slip, I have also added an extra reduction to ease the speed of rotation

        var beNiceFactor = 0.6; // this further reduces the tendency to turn valid values 1-0 where smaller number reduce the tendency to swivel
        wheel.angle -= Math.asin(fs) * Math.abs(fs) * (1 - slip) * beNiceFactor;
    

    Will also add this to the demo code below.

    That is as far as this method will allow. If you want a better sim we will have to start from scratch and use a more complex solution

    That is it.

    DEMO

    It seams way to simple so had to try it in code, Below is a demo using the above method. You could improve it by limiting the amount you add the the wheel angle, Multiplying by the sin of the angle you add makes it a little less responsive, and to add slippage just multiply by another fraction.

    Thus where the wheel angle is changed in the function updateTrolly change

    ww.angle -= Math.asin(cross); 
    

    to

    ww.angle -= Math.asin(cross) * Math.abs(cross) * 0.1; 
    

    where Math.abs(cross) is the positive sin of the angle added and 0.1 is the amount of slippage ( any value < 1 and > 0, with 1 no slippage)

    Click and drag on or near the cart to move it. The wheels will follow.

    The demo has an updateTrolly function that does the wheels, but is dependent on the wheel positions calculated in the displayTrolly function.

    The code you are interested in starts about halfway down the rest just handles the mouse and canvas.

    DEMO UPDATED See update in answer re slippage.

    /** SimpleFullCanvasMouse.js begin **/
    const CANVAS_ELEMENT_ID = "canv";
    const U = undefined;
    var w, h, cw, ch; // short cut vars 
    var canvas, ctx, mouse;
    var globalTime = 0; 
    var createCanvas, resizeCanvas, setGlobals;
    var L = typeof log === "function" ? log : function(d){ console.log(d); }
    createCanvas = function () {
        var c,cs;
        cs = (c = document.createElement("canvas")).style; 
        c.id = CANVAS_ELEMENT_ID;    
        cs.position = "absolute";
        cs.top = cs.left = "0px";
        cs.zIndex = 1000;
        document.body.appendChild(c); 
        return c;
    }
    resizeCanvas = function () {
        if (canvas === U) { canvas = createCanvas(); }
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        ctx = canvas.getContext("2d"); 
        if (typeof setGlobals === "function") { setGlobals(); }
    }
    setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; }
    mouse = (function(){
        function preventDefault(e) { e.preventDefault(); }
        var mouse = {
            x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
            over : false,  // mouse is over the element
            bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.x = e.offsetX; m.y = e.offsetY;
            if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
            m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
            if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }  
            else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
            else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
            else if (t === "mouseover") { m.over = true; }
            else if (t === "mousewheel") { m.w = e.wheelDelta; }
            else if (t === "DOMMouseScroll") { m.w = -e.detail; }
            if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
            e.preventDefault();
        }
        m.addCallback = function (callback) {
            if (typeof callback === "function") {
                if (m.callbacks === U) { m.callbacks = [callback]; }
                else { m.callbacks.push(callback); }
            } else { throw new TypeError("mouse.addCallback argument must be a function"); }
        }
        m.start = function (element, blockContextMenu) {
            if (m.element !== U) { m.removeMouse(); }        
            m.element = element === U ? document : element;
            m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
            m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
            if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
        }
        m.remove = function () {
            if (m.element !== U) {
                m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
                if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
                m.element = m.callbacks = m.contextMenuBlocked = U;
            }
        }
        return mouse;
    })();
    var done = function(){
        window.removeEventListener("resize",resizeCanvas)
        mouse.remove();
        document.body.removeChild(canvas);    
        canvas = ctx = mouse = U;
        L("All done!")
    }
    
    resizeCanvas(); // create and size canvas
    mouse.start(canvas,true); // start mouse on canvas and block context menu
    window.addEventListener("resize",resizeCanvas); // add resize event
    
    
    // ================================================================================
    // Start of answer code
    
    const SYSTEM_DRAG = 0.99; // add drag to stop everything flying around. value > 0 and < 1 the closer to 1 the less the drag (friction)
    const MOUSE_FORCE = 600; // multiplies mouse movement force bigger number more force
    const TROLLY_WIDTH = 100;
    const TROLLY_LENGTH = 200;
    const WHEEL_INSET = 0;
    const WHEEL_WIDTH = 10;
    
    const WHEEL_SWING_LENGTH = 20;
    //const WHEEL_LENGTH = WHEEL_SWING_LENGTH * (2/3);
    const WHEEL_LENGTH =30;
    const PIXEL_MASS = 2; // mass per pixel. Need mass for better sim
    
    var trolly = {
        wheels : [],
        x : 200, 
        y : 200,
        r : 0,
        dx : 0,
        dy : 0,
        dr : 0,
        w : TROLLY_WIDTH,
        l : TROLLY_LENGTH,
        mass : TROLLY_WIDTH * TROLLY_LENGTH * PIXEL_MASS
    };
    function addWheel(t,x,y,dist,angle){
        t.wheels.push({
            x:x,
            y:y, // relative to the trolly
            rx : x,  // to keep it simple r is for real worl position
            ry : y,
            lrx : x, // and lr is for last real world position. That will give delta at wheel
            lry : y,
            length : dist,
            angle : angle, // absolute angle relative to the world
        })
        t.mass += WHEEL_WIDTH * WHEEL_LENGTH * PIXEL_MASS;
    }
    addWheel(trolly,-(TROLLY_LENGTH / 2 - WHEEL_INSET),-(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
    addWheel(trolly,(TROLLY_LENGTH / 2 - WHEEL_INSET),-(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
    addWheel(trolly,(TROLLY_LENGTH / 2 - WHEEL_INSET),(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
    addWheel(trolly,-(TROLLY_LENGTH / 2 - WHEEL_INSET),(TROLLY_WIDTH / 2 - WHEEL_INSET),WHEEL_SWING_LENGTH, 0);
    
    function drawTrolly(t){
        ctx.setTransform(1,0,0,1,t.x,t.y);
        ctx.rotate(t.r);
        ctx.lineWidth = 2;
        ctx.strokeStyle = "black";
        ctx.beginPath();
        ctx.moveTo(-t.l/2,-t.w/2);
        ctx.lineTo(t.l/2,-t.w/2);
        ctx.lineTo(t.l/2,t.w/2);
        ctx.lineTo(-t.l/2,t.w/2);
        ctx.closePath();
        ctx.setTransform(1,0,0,1,0,0); // reset transform
        var dx = Math.cos(t.r); // x axis
        var dy = Math.sin(t.r);
        for(var i = 0; i < t.wheels.length; i ++){
            var w = t.wheels[i];
            var x = w.x * dx + w.y * - dy; 
            var y = w.x * dy + w.y *  dx; 
            var wx = Math.cos(w.angle);  // vector to the wheel
            var wy = Math.sin(w.angle);
            w.lrx = w.rx;  // save last pos
            w.lry = w.ry;
            w.rx = t.x + x;  // save new pos
            w.ry = t.y + y;
            // get ground contact point 
            var gx = t.x + x + wx * w.length;
            var gy = t.y + y + wy * w.length;
            ctx.setTransform(1,0,0,1,w.rx, w.ry); // reset transform
            ctx.moveTo(0,0);
            ctx.setTransform(1,0,0,1,gx,gy); // move to the wheel
            ctx.lineTo(0,0);
            ctx.rotate(w.angle);
            ctx.moveTo(-WHEEL_LENGTH / 2, -WHEEL_WIDTH / 2);
            ctx.lineTo(WHEEL_LENGTH / 2, -WHEEL_WIDTH / 2);
            ctx.lineTo(WHEEL_LENGTH / 2, WHEEL_WIDTH / 2);
            ctx.lineTo(-WHEEL_LENGTH / 2, WHEEL_WIDTH / 2);
            ctx.closePath();    
        }
        ctx.stroke();
    }
    function updateTrolly(t){
        for(var i = 0; i < t.wheels.length; i ++){
            var ww = t.wheels[i];
            var dx = ww.rx - ww.lrx; // get delta change at wheels
            var dy = ww.ry - ww.lry;
            var dist = Math.hypot(dx,dy);
            if(dist > 0.00001){ // not to small to bother
                var nx = -dx / dist;
                var ny = -dy / dist;
                var wx = Math.cos(ww.angle);
                var wy = Math.sin(ww.angle);
                var cross = nx * wy - ny * wx;
                var slip = Math.abs(Math.cos(Math.asin(cross))) * 0.7;
                ww.angle -= Math.asin(cross) * Math.abs(cross) * (1-slip) * 0.6;
            }
        }
        t.x += t.dx;
        t.y += t.dy;
        t.r += t.dr;
        t.dx *= SYSTEM_DRAG;
        t.dy *= SYSTEM_DRAG;
        t.dr *= SYSTEM_DRAG;
        t.x = ((t.x % w) + w) % w; // keep onscreen
        t.y = ((t.y % h) + h) % h; // keep onscreen
        
    }
    
    function applyForceCenter(object, force, direction){ // force is a vector
        force /= object.mass; // now use F = m * a in the form a = F/m
        object.dx += Math.cos(direction) * force;
        object.dy += Math.sin(direction) * force;
    }
    function applyForce(object, force, direction, locx,locy){ // force is a vector, loc is a coordinate
        var radius = Math.hypot(object.y - locy, object.x - locx);
        if(radius <= 0.00001){
            applyForceCenter(object,force,direction);
            return;
        }
        var toCenter = Math.atan2(object.y - locy, object.x - locx);
        var pheta = toCenter - direction;
        var Fv = Math.cos(pheta) * force;
        var Fa = Math.sin(pheta) * force;
        Fv /= object.mass; // now use F = m * a in the form a = F/m
        var Fvx = Math.cos(toCenter) * Fv;
        var Fvy = Math.sin(toCenter) * Fv;
        object.dx += Fvx; 
        object.dy += Fvy;
        Fa /= (radius  * object.mass); // for the angular component get the rotation
                                                       // acceleration
        object.dr += Fa;// now add that to the box delta r
    }
    function applyForceToTrolly(t,x,y,dx,dy){
        var f = Math.hypot(dx,dy) * MOUSE_FORCE;
        var dir = Math.atan2(dy,dx);
        applyForce(t,f,dir,x,y);
    }
    
    var lx,ly;
    function display(){  // put code in here
        ctx.setTransform(1,0,0,1,0,0); // reset transform
        ctx.globalAlpha = 1;           // reset alpha
        ctx.clearRect(0,0,w,h);
        if(mouse.buttonRaw & 1){
            applyForceToTrolly(trolly,mouse.x,mouse.y,mouse.x-lx,mouse.y-ly);
            
        }
        updateTrolly(trolly);
        drawTrolly(trolly);
        lx = mouse.x;
        ly = mouse.y;
    }
    function update(timer){ // Main update loop
        globalTime = timer;
        display();  // call demo code
        requestAnimationFrame(update);
    }
    requestAnimationFrame(update);
    
    /** SimpleFullCanvasMouse.js end **/