Nice Drag and Drop on HTML5 Canvas

I am trying to implement a nice drag and drop on a canvas representing 3 discs.

I want to change the position of each mass with the mouse. My main problem is that the ax length for each of these three spheres limits me.

At the moment, I have implemented the following function when the mouse moves inside the canvas (the indexMass value indicates which mass moves: 1, 2 or 3 and t1, t2, t3 respectively correspond to 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 corner:

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

with:

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

But this effect is not very pleasant. When the ball is selected with the mouse (with a mouse click), I would like the cursor to be attached to this sphere or sphere in order to follow the " delta " coordinates of the mouse when I am no longer on the sphere.

To summarize, I don’t know how to create a beautiful and user-friendly drag and drop, if someone can help me or give me some advice, that would be great.

thanks

UPDATE 1

@ Blindman67: thanks for your help, the code snippet is rather complicated for me, I didn’t understand all this. But I'm on the right track.

I start with the first problem: rotate the selected drive when the mouse remains very closed to it or above it when dragging.

At the moment, I have changed my myMove function (which is called when I click and drag the mouse to drag and drop), for example:

 // 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 the dragged disk, and window['x'+indexMass] , window['y'+indexMass] are the current coordinates of the selected disk center.

After that, I calculate dx, dy respectively, from the coordinates, by clicking the mouse when running drag ( mx, my , returned by the getMousePos function ), and the coordinates of the mouse with the move.

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

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

I took your piece of code using Math.atan2 .

But the result of this function does not create a good drag and drop animation, I would like to know where this could have come from.

Right now I would like to implement only drag and drop without changing the axis length, I will see a later version for this function.

UPDATE 2

I keep looking for a solution to drag the selected mass with the mouse.

To try to synthesize what I did earlier, I think the following method is good, but this drag and drop method does not work very well: the selected drive does not match the mouse correctly , and I don’t know why.

In myMove function (function called when the drag myMove function ), I decided:

  • Calculate dx, dy between the coordinates of the mouse and the selected coordinates of the disk, i.e.:

var dx = parseInt (event.offsetX - window ['x' + indexMass]);

var dy = parseInt (event.offsetY - window ['y' + indexMass]);

indexMass represents the index of the selected drive.

  • Increase the position of the selected disk (stored in temporary variables tmpX, tmpY ) by dx, dy .

  • Calculate the new angle theta (identified in the global variable code window['t'+indexMass]

  • Calculate the new positions of the selected disk with this new theta value, for example, using disk1 ( indexMass=1 and theta = t1 ):

     x1= x0 +l1 * sin(t1) y1= y0 +l1 * sin(t1) 

I have to make you notice that I want to drag with the mouse so as not to change the length of the axes with the mouse, this is a limitation.

Here's the whole myMove function (called when drag starts):

 // Happens when the mouse is moving inside the canvas function myMove(event) { // If dragging if (isDrag) { console.log('offsetX', event.offsetX); console.log('offsetY', event.offsetY); var dx = parseInt(event.offsetX - window['x'+indexMass]); var dy = parseInt(event.offsetY - window['y'+indexMass]); console.log('dx', dx); console.log('dy', dy); // Temp variables var tmpX = window['x'+indexMass]; var tmpY = window['y'+indexMass]; // Increment temp positions tmpX += dx; tmpY += dy; // Compute new angle for indexMass window['t'+indexMass] = Math.atan2(tmpX, tmpY); console.log('printf', window['t'+indexMass]); // Compute new positions of disks dragComputePositions(); // Update drawing DrawPend(canvas); // Highlight dragging disk fillDisk(indexMass, 'pink'); } } 

UPDATE 4 - Bounty:

The problem is solved! I forgot to consider the position of the "indexMass-1" drive to calculate the new angle using the Math.atan2 function.

+5
source share
1 answer

You cannot move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw the mouse on the canvas yourself, but it will be one frame behind, because when you get the mouse coordinates, OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF), the next canvas presentation will be on the next display refresh interval. If you are not using RAF, you may or may not be able to display the canvas in the current display update, but you will interfere and cut periodically.

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

I would also add a few handles to the balls so that you can change the mass (volume sphere * density) and axis length. Resize cursors are a problem because they will not correspond to the direction of the required movement when the angles have changes. You need to find the one closest to the right corner or make a cursor on the canvas and use it.

The sample code shows what I mean. (does not include sim) Move your mouse over the balls to move when you also see above that two circles seem to change the 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); bx = x; by = y; } ctx.stroke(); minDist = Infinity; index = -1; for(i = 0; i < len; i += 1){ b = balls[i]; ctx.beginPath(); ctx.arc(bx, by, b.radius, 0, Math.PI * 2); ctx.fill(); if(!dragging){ x = bx - mouse.x; y = by - 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(bx, by, b.radius, 0, Math.PI * 2); ctx.fill(); dx = bx - Math.cos(b.angle) * b.radius; dy = by - 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 = bx - Math.cos(b.angle + Math.PI/2) * b.radius; dy = by - 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(bx, by, 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; mx = e.offsetX; my = e.offsetY; if (mx === U) { mx = e.clientX; my = 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") { mw = e.wheelDelta; } else if (t === "DOMMouseScroll") { mw = -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 **/ 
+5
source

All Articles