user37422 - 1 year ago 100
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.

Regards

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)

``````/*-------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------*/

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;
balls.push({
dist : dist,
angle : -Math.PI / 2,
x : 0,
y : 0,
});
}
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));

}else
if(mouseOverMass){
var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
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();
}

/*-------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------*/

/** 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();
}
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.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
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