user37422 user37422 - 4 months ago 23
Javascript Question

Nice Drag and drop on a canvas HTML5

I try to implement a nice drag and drop on a canvas simulating the motion of a pendulum with 3 masses.

I would like to change with mouse the position of each mass. My main problem is that I am constrained by the length of axe for each of these 3 spheres.

For the moment, I have implemented the following function when mouse is moving inside the canvas ( value of indexMass indicates which mass is moved :

1, 2 or 3
and
t1, t2, t3
represents respectively
the angle of mass 1, 2, 3
) :

// Happens when the mouse is moving inside the canvas
function myMove(event) {

if (isDrag) {
var x = event.offsetX;
var y = event.offsetY;

if (indexMass == 1)
{ // Update theta1 value
t1 = t1 + 0.1*Math.atan(y/x);
}
else if (indexMass == 2)
{ // Update theta2 value
t2 = t2 + 0.1*Math.atan(y/x);
}
else if (indexMass == 3)
{ // Update theta3 value
t3 = t3 + 0.1*Math.atan(y/x);
}

// Update drawing
DrawPend(canvas);

}

}


As you can see, I did for each angle :

t = t + 0.1*Math.atan(y/x);


with :

var x = event.offsetX;
var y = event.offsetY;


But this effect is not very nice. Once the sphere is selected with mouse (on mouse click), I would like the cursor to be stucked with this sphere or the sphere to follow the "
delta
" of the mouse coordinates when I am not on sphere anymore.

To summarize, I don't know how to create a fine and user friendly drag and drop, if someone could help me or give me some advices, this would be great.

Thanks

UPDATE

@Blindman67 : thanks for your help, your code snippet is pretty complex for me, I didn't understand it all. But I am on the right way.

I am starting by the first issue : make rotate the selected disk with mouse staying very closed to it or over it, when dragging.

For the moment, I have modified my function
myMove
(which is called when I have clicked down and move the mouse for dragging) like :

// Happens when the mouse is moving inside the canvas
function myMove(event) {

// If dragging
if (isDrag) {

// Compute dx and dy before calling DrawPend
var lastX = parseInt(event.offsetX - mx);
var lastY = parseInt(event.offsetY - my);

var dx = lastX - window['x'+indexMass];
var dy = lastY - window['y'+indexMass];

// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);

// Update drawing
DrawPend(canvas);

// Highlight dragging disk
fillDisk(indexMass, 'pink');

}

}


where
indexMass
is the index of dragged disk and
window['x'+indexMass]
,
window['y'+indexMass]
are the current coordinates of the selected disk center.

After, I compute the
dx, dy
respectively from coordinates mouse clicked when starting drag (
mx, my
returned by
getMousePos function
) and mouse coordinates with moving.

Finally, I change the angle of disk by set, for global variable (theta of selected disk), i.e
window['t'+indexMass]
:

// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);


I have took your part of code with
Math.atan2
.

But the result of this function doesn't make a good animation with mouse dragging, I would like to know where this could come from.

Right now, I would like to implement only the dragging without modifying the length of axis, I will see more later for this functionality.

You can see this bad behavior dragging on this link

Regards

Answer

You can not move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw a mouse on the canvas your self but it will lag behind by one frame because when you get the mouse coordinates the OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF) the next presentation of the canvas will be at the next display refresh interval. If you don't use RAF you may or may not present the canvas on the current display refresh, but you will get occasional flicker and shearing.

To solve the problem (which is subjective) draw a line from the rotation point through the ball to the mouse position this will at least give the user some feedback as to what is happening.

I would also add some handles to the balls so you could change the mass (volume of sphere * density) and the length of the pendulum.. The resize cursors are a problem as the will not match the direction of required movement when the angles have changes. You would need to find one closest to the correct angle or render a cursor to a canvas and use that.

Example code shows what I mean. (does not include sim) Move mouse over balls to move, when over you will also see two circles appear to change distance and radius (mass)

/*-------------------------------------------------------------------------------------
 answer code
---------------------------------------------------------------------------------------*/






var balls = [];
var startX,startY;
var mouseOverBallIndex = -1;
var mouseOverDist = false;
var mouseOverMass = false;
const DRAG_CURSOR = "move";
const MASS_CURSOR = "ew-resize";
const DIST_CURSOR = "ns-resize";
var dragging = false;
var dragStartX = 0;
var dragStartY = 0;
function addBall(dist,radius){
    balls.push({
        dist : dist,
        radius : Math.max(10,radius),
        angle : -Math.PI / 2,
        x : 0,
        y : 0,
        mass : (4/3) * radius * radius * radius * Math.PI,
    });
}
function drawBalls(){
    var i = 0;
    var len = balls.length;
    var x,y,dist,b,minDist,index,cursor;
    ctx.lineWidth = 2;
    ctx.strokeStyle = "black";
    ctx.fillStyle = "blue"
    ctx.beginPath();
    x = startX;
    y = startY;
    ctx.moveTo(x, y)
    for(; i < len; i += 1){
        b = balls[i];
        x += Math.cos(b.angle) * b.dist;
        y += Math.sin(b.angle) * b.dist;
        ctx.lineTo(x, y);
        b.x = x;
        b.y = y;
    }
    ctx.stroke();
    minDist = Infinity;
    index = -1;
    for(i = 0; i < len; i += 1){
        b = balls[i];
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
        ctx.fill();
        if(!dragging){
            x = b.x - mouse.x;
            y = b.y - mouse.y;
            dist = Math.sqrt(x * x + y * y);
            if(dist < b.radius + 5 && dist < minDist){
                minDist = dist;
                index = i;
            }
        }
    }
    if(!dragging){
        mouseOverBallIndex = index;
        if(index !== -1){
            cursor = DRAG_CURSOR;
            b = balls[index];
            ctx.fillStyle = "Red"
            ctx.beginPath();
            ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
            ctx.fill();
            dx = b.x - Math.cos(b.angle) * b.radius;
            dy = b.y - Math.sin(b.angle) * b.radius;
            x = dx - mouse.x;
            y = dy - mouse.y;
            dist = Math.sqrt(x * x + y * y);
            ctx.beginPath();
            if(dist < 6){
                ctx.strokeStyle = "Yellow"
                mouseOverDist = true;
                ctx.arc(dx, dy, 12, 0, Math.PI * 2);
                cursor = DIST_CURSOR;
            }else{
                ctx.strokeStyle = "black"
                mouseOverDist = false;
                ctx.arc(dx, dy, 5, 0, Math.PI * 2);

            }
            ctx.stroke();MASS_CURSOR
            dx = b.x - Math.cos(b.angle + Math.PI/2) * b.radius;
            dy = b.y - Math.sin(b.angle + Math.PI/2) * b.radius;
            x = dx - mouse.x;
            y = dy - mouse.y;
            dist = Math.sqrt(x * x + y * y);
            ctx.beginPath();
            if(dist < 6){
                ctx.strokeStyle = "Yellow"
                mouseOverMass = true;
                ctx.arc(dx, dy, 12, 0, Math.PI * 2);
                cursor = MASS_CURSOR;
            }else{
                ctx.strokeStyle = "black"
                mouseOverMass = false;
                ctx.arc(dx, dy, 5, 0, Math.PI * 2);

            }
            ctx.stroke();
            canvas.style.cursor = cursor;
        }else{
            canvas.style.cursor = "default";
        }
    }else{
        b = balls[mouseOverBallIndex];
        ctx.fillStyle = "Yellow"
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
        ctx.fill();        
        
    }

}
function display(){  // put code in here
    var x,y,b
  
    if(balls.length === 0){
        startX = canvas.width/2;
        startY = canvas.height/2;
        addBall((startY * 0.8) * (1/4), startY * 0.04);
        addBall((startY * 0.8) * (1/3), startY * 0.04);
        addBall((startY * 0.8) * (1/2), startY * 0.04);
        
    }
    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) && mouseOverBallIndex > -1){
        b = balls[mouseOverBallIndex];
        if(dragging === false){
            dragging = true;
            dragStartX = balls[mouseOverBallIndex].x;
            dragStartY = balls[mouseOverBallIndex].y;
        }else{
            b = balls[mouseOverBallIndex];
            if(mouseOverBallIndex === 0){
                x = startX;
                y = startY;
            }else{
                x = balls[mouseOverBallIndex-1].x
                y = balls[mouseOverBallIndex-1].y
            }
            if(mouseOverDist){
                var dist = Math.sqrt(Math.pow(x-mouse.x,2)+Math.pow(y-mouse.y,2));
                b.dist = dist + b.radius;
                
            }else    
            if(mouseOverMass){
                var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
                b.radius = Math.max(10,dist);
                b.mass = dist * dist * dist * (4/3) * Math.PI;
            }else{
                b.angle = Math.atan2(mouse.y - y, mouse.x - x);
                ctx.beginPath();
                ctx.lineWidth = 1;
                ctx.strokeStyle = "grey";
                ctx.moveTo(x,y);
                ctx.lineTo(mouse.x, mouse.y);
                ctx.stroke();
            }
        }
        
    }else if(dragging){
        dragging = false;
    }

    drawBalls();
}

/*-------------------------------------------------------------------------------------
 answer code END
---------------------------------------------------------------------------------------*/





































/** 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; balls.length = 0; }
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

function update(timer){ // Main update loop
    globalTime = timer;
    display();  // call demo code
    // continue until mouse right down
    if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);

/** SimpleFullCanvasMouse.js end **/