ashishraaj
ashishraaj

Reputation: 761

Handling click and drag events in JavaScript

I am using pure JavaScript to develop a simple paint app using the canvas element. The app creates circles when the user drags on the screen. Secondly, double-clicking on a circle will delete the particular circle. Thirdly, I want to drag a circle when I click on it.

var canvas,
context, shapes,
dragging = false, draggingtoMove = false,dragstopped = 0,
    dragStartLocation,dragEndLocation,
    snapshot;
 var numShapes;
function initiate() {
    numShapes = 100;
    shapes = [];
    canvas = document.getElementById('canvas');
    context = canvas.getContext('2d');
    canvas.addEventListener('mousedown', dragStart, false);
    canvas.addEventListener('mousemove', drag, false);
    canvas.addEventListener('mouseup', dragStop, false);
    canvas.addEventListener('dblclick', dblclickerase);
}
function dblclickerase(evt){
    dragstopped = 0;
    shapes.pop();
    shapes.pop();
        var i, j;
		var highestIndex = -1;
		
		//getting mouse position correctly, being mindful of resizing that may have occured in the browser:
		var bRect = canvas.getBoundingClientRect();
		mouseX = (evt.clientX - bRect.left)*(canvas.width/bRect.width);
		mouseY = (evt.clientY - bRect.top)*(canvas.height/bRect.height);
				
		//Now find which shape was clicked
		for (i=0; i < shapes.length; i++) {
			if	(hitTest(shapes[i], mouseX, mouseY)) {
                // That particular circle is I am going to delete at position i
			    shapes[i].x = -1;
                shapes[i].y = -1;
                shapes[i].rad = -1;
                shapes[i].color = -1;                
			    eraseCanvas(); // clear canvas
			}
		}
		redraw();
}
function redraw() {
    var j;
    eraseCanvas(); // clear canvas and redraw it
            for(j= 0; j< shapes.length; j++)
                {
                    if(shapes[j].x != -1)
                    {
                   
			        context.beginPath();
			        context.arc(shapes[j].x, shapes[j].y, shapes[j].rad, 0, 2*Math.PI, false);
                    context.fillStyle = shapes[j].color;
			        context.fill();
                    }
                }
}
function getCanvasCoordinates(event) {
    var x = event.clientX - canvas.getBoundingClientRect().left,
        y = event.clientY - canvas.getBoundingClientRect().top;
    return {
        x: x,
        y: y
    };
}
function takeSnapshot() {
    snapshot = context.getImageData(0, 0, canvas.width, canvas.height);
}
function restoreSnapshot() {
    context.putImageData(snapshot, 0, 0);
}
function draw(position) {
     var radius = Math.sqrt(Math.pow((dragStartLocation.x - position.x), 2) + Math.pow((dragStartLocation.y - position.y), 2));
        var i=0;
		var tempX;
		var tempY;
		var tempRad;
		var tempR;
		var tempG;
		var tempB;
		var tempColor;
			tempRad = radius;
			tempX = dragStartLocation.x;
			tempY = dragStartLocation.y;
			tempColor = getRndColor();
			tempShape = {x:tempX, y:tempY, rad:tempRad, color:tempColor};
			if (dragstopped) { shapes.push(tempShape); }
			context.beginPath();
			context.arc(tempX, tempY, tempRad, 0, 2*Math.PI, false);
			//context.closePath();
            context.fillStyle = tempColor;
			context.fill();
			i++;
}
function dragStart(evt) {
   
    //// Here I will check whether if circle overlaps then drag the circle else draw new one.
        var i, j;
		var highestIndex = -1;
		
		//getting mouse position correctly, being mindful of resizing that may have occured in the browser:
		var bRect = canvas.getBoundingClientRect();
		mouseX = (evt.clientX - bRect.left)*(canvas.width/bRect.width);
		mouseY = (evt.clientY - bRect.top)*(canvas.height/bRect.height);
				
		//Now find which shape was clicked
		for (i=0; i < shapes.length; i++) {
			if	(hitTest(shapes[i], mouseX, mouseY)) {
                // That particular circle is I am going to delete at position i                
			    //eraseCanvas(); // clear canvas
			    console.log("clicking on circle");
			   // return;
			}
		}
    
    //Draw Circle
            dragging = true;
            dragstopped = 0;
            dragStartLocation = getCanvasCoordinates(evt);
            takeSnapshot();
             } 

 function hitTest(shape,mx,my) {
		var dx;
		var dy;
		dx = Math.abs(mx - shape.x);
		dy = Math.abs(my - shape.y);
		//if it is inside any circle radius will let us know	
		return (Math.sqrt(dx*dx + dy*dy) < shape.rad);
	}

function drag(event) {
    dragstopped = 0;
    var position;
    if (dragging === true) {
        restoreSnapshot();
        position = getCanvasCoordinates(event);
        draw(position);
    }
}

function dragStop(event) {
    dragstopped += 1;
    dragging = false;
    restoreSnapshot();
   var position = getCanvasCoordinates(event);
  dragEndLocation = getCanvasCoordinates(event);
    draw(position);
}

function getRndColor() {
    var r = 255 * Math.random() | 0,
        g = 255 * Math.random() | 0,
        b = 255 * Math.random() | 0;
    return 'rgb(' + r + ',' + g + ',' + b + ')';
}

function eraseCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    shapes = [];
}

addEventListener("load",initiate);

 
body {
}

canvas{
    border: 1px solid gray;
}
button {
    font-size: 128%;
    margin-right: -12px;
    position: absolute;
}
<canvas id="canvas" width="600" height="400"></canvas>

My first question is this: How should I drag a circle using the same dragStart() function which is called when mousedown occurs? As the solution I am thinking that if I am clicking the circle then I need to drag it for sure. Then to find the position of the cursor I am checking the hitTest() which is giving whether the position is within the circle or not, but again if the circle is hit then I need to drag it, for that how can I bind with the mousemove event so that I can say that I want to drag the circle not to draw it as my code says in drag()?

My second question: I guess my code is not handling the double-click event and rather than deleting one circle it's clearing the canvas. I want to erase the canvas and redraw the whole canvas using the shapes array.

Upvotes: 0

Views: 2824

Answers (2)

Roger
Roger

Reputation: 1133

I haven't thought of a solution using your idea for the project, but I have some remarks that might help you.

1: I wouldn't recommend you to use canvas if you mean to interact with the shapes you are placing, mostly because the canvas is meant to be efficient in drawing, so it only holds the pixels information. You can alternatively try SVG or even the new HTML 5 feature for dragging and dropping elements (http://www.w3schools.com/html/html5_draganddrop.asp). But, if you wish to continue on canvas and javascript, you should try using onmousedown instead of onclick for triggering the drag function, since the order of those events is onmousedown, onmouseup and then onclick (http://www.w3schools.com/tags/ev_onmousedown.asp).

2: Maybe it would be the case for you to make an animation loop, that is, you draw and clear the whole canvas at a fixed time, this way you draw every shape in your "renderer" array, and if you wish to delete one shape, you just remove it from the array. Also, I haven't found your event listener for the double click, did you try attaching ondblclick="myFunction()" to your canvas? (http://www.w3schools.com/jsref/event_ondblclick.asp)

Upvotes: 1

Michael Laszlo
Michael Laszlo

Reputation: 12239

Instead of having a single mousemove handler that tries to deal with all possible actions, it would make sense to have specialized mousemove handlers.

  • Making a circle: take a snapshot of the canvas before doing anything, then attach a mousemove handler that restores the snapshot and paints a new circle of the desired size on top.

  • Dragging a circle: erase the canvas, paint all the circles except the one you want to drag, and take a snapshot. Now attach a mousemove handler that restores the snapshot and paints the circle in its new position.

In each case, you should also assign a mouseup handler that detaches the mousemove handler and the mouseup handler itself upon the conclusion of the dragging action.

As for the double-click problem, you were having trouble because you weren't erasing the shape properly. Your function started with a couple of shapes.pop() calls, thus deleting the last two shapes from the array, and then your eraseCanvas function discarded the whole array. When you erase the canvas, don't tamper with the shape array.

To delete the shape at position i from the middle of the array, shift all the array elements down by one and then pop the last element off the end:

      for (var j = i + 1; j < shapes.length; ++j) {
        shapes[j - 1] = shapes[j];
      }
      shapes.pop();

Once you've done that, you can erase the canvas and paint all the shapes that remain in the array.

I have implemented these corrections in the following snippet.

var canvas,
context, shapes,
dragging = false, draggingtoMove = false,dragstopped = 0,
    dragStartLocation,dragEndLocation,
    snapshot;
 var numShapes;
function initiate() {
    numShapes = 100;
    shapes = [];
    canvas = document.getElementById('canvas');
    context = canvas.getContext('2d');
    canvas.addEventListener('mousedown', click, false);
    canvas.addEventListener('dblclick', dblclickerase);
}
function dblclickerase(evt){
    var coordinates = getCanvasCoordinates(event),
        clickX = coordinates.x,
        clickY = coordinates.y;
				
		//Now find which shape was clicked
		for (var i = 0; i < shapes.length; ++i) {
			if	(hitTest(shapes[i], clickX, clickY)) {
			    console.log('double-clicked on circle ' + i);
          // Delete this shape from the array.
          for (var j = i + 1; j < shapes.length; ++j) {
            shapes[j - 1] = shapes[j];
          }
          shapes.pop();
          // Redraw the other shapes.
          eraseCanvas();
          for (var j = 0; j < shapes.length; ++j) {
            paintCircle(shapes[j]);
          }
          return;
			}
		}
}

function getCanvasCoordinates(event) {
    var x = event.clientX - canvas.getBoundingClientRect().left,
        y = event.clientY - canvas.getBoundingClientRect().top;
    return {
        x: x,
        y: y
    };
}
function takeSnapshot() {
    snapshot = context.getImageData(0, 0, canvas.width, canvas.height);
}
function restoreSnapshot() {
    context.putImageData(snapshot, 0, 0);
}
function draw(position) {
     var radius = Math.sqrt(Math.pow((dragStartLocation.x - position.x), 2) + Math.pow((dragStartLocation.y - position.y), 2));
        var i=0;
		var tempX;
		var tempY;
		var tempRad;
		var tempR;
		var tempG;
		var tempB;
		var tempColor;
			tempRad = radius;
			tempX = dragStartLocation.x;
			tempY = dragStartLocation.y;
			tempColor = getRndColor();
			tempShape = {x:tempX, y:tempY, rad:tempRad, color:tempColor};
			if (dragstopped) { shapes.push(tempShape); }
      paintCircle(tempShape);
			i++;
}
function click(event) {
   
    // Here I will check whether if circle overlaps then drag the circle else draw new one.
    var i, j;
		var highestIndex = -1;
		
		//getting mouse position correctly, being mindful of resizing that may have occured in the browser:
    var coordinates = getCanvasCoordinates(event),
        clickX = coordinates.x,
        clickY = coordinates.y;
				
		//Now find which shape was clicked
		for (i=0; i < shapes.length; i++) {
      var shape = shapes[i];
			if	(hitTest(shape, clickX, clickY)) {
			    console.log('clicked on circle ' + i);

          // Erase this circle and take a snapshot.
          eraseCanvas();
          for (var j = 0; j < shapes.length; ++j) {
            if (j != i) {
              paintCircle(shapes[j]);
            }
          }
          takeSnapshot();
          paintCircle(shape);

          var originalX = shape.x,
              originalY = shape.y;

          canvas.onmousemove = function (dragEvent) {
            var dragCoordinates = getCanvasCoordinates(dragEvent),
                dx = dragCoordinates.x - clickX,
                dy = dragCoordinates.y - clickY;
            shape.x = originalX + dx;
            shape.y = originalY + dy;
            restoreSnapshot();
            paintCircle(shape);
          };
          canvas.onmouseup = function (upEvent) {
            canvas.onmousemove = undefined;
            canvas.onmouseup = undefined;
          };

			    return;
			}
		}
    
    //Draw Circle
    dragging = true;
    dragstopped = 0;
    dragStartLocation = getCanvasCoordinates(event);
    takeSnapshot();
    canvas.onmousemove = makeCircle;
    canvas.onmouseup = finishCircle;
} 

function hitTest(shape,mx,my) {
    var dx;
    var dy;
    dx = Math.abs(mx - shape.x);
    dy = Math.abs(my - shape.y);
    //if it is inside any circle radius will let us know	
    return (Math.sqrt(dx*dx + dy*dy) < shape.rad);
}

function paintCircle(shape) {
  context.fillStyle = shape.color;
  context.beginPath();
  context.arc(shape.x, shape.y, shape.rad, 0, 2 * Math.PI);
  context.closePath();
  context.fill();
}

function makeCircle(event) {
    dragstopped = 0;
    var position;
    if (dragging === true) {
        restoreSnapshot();
        position = getCanvasCoordinates(event);
        draw(position);
    }
}

function finishCircle(event) {
    canvas.onmousemove = undefined;
    canvas.onmouseup = undefined;
    dragstopped += 1;
    dragging = false;
    restoreSnapshot();
    var position = getCanvasCoordinates(event);
    dragEndLocation = getCanvasCoordinates(event);
    draw(position);
}

function getRndColor() {
    var r = 255 * Math.random() | 0,
        g = 255 * Math.random() | 0,
        b = 255 * Math.random() | 0;
    return 'rgb(' + r + ',' + g + ',' + b + ')';
}

function eraseCanvas() {
    context.clearRect(0, 0, canvas.width, canvas.height);
}

addEventListener("load",initiate);
body {
}

canvas{
    border: 1px solid gray;
}
button {
    font-size: 128%;
    margin-right: -12px;
    position: absolute;
}
<canvas id="canvas" width="600" height="400"></canvas>

Upvotes: 1

Related Questions