I try to get the rotation angle of a swiveling wheel, given the rotation angle of the connected body:
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:
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.
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.
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 **/