Course notes 27
syllabus | schedule | exercises | assignments | class notes | resources | students | ARTC courses

Canvas Drag & Drop

Drag and Drop behavior on the canvas is an applied combination of the fundamentals of canvas interaction. While the code is not difficult, it does require a certain amount of sequential problem solving to break down the interaction itself. The process of implementing a drag and drop system can be an excellent foundation for any complex behavior, as the process involved in designing other interactions will be very similar.

The Drag and Drop behavior can be broken down into several steps

  1. Move to the object
  2. Grab the object
  3. Move the object to another location
  4. Release the object
  5. Move away from the object

When writing the code to facilitate the interaction, it is important to understand the individual steps that make up the interaction. The following in-depth example is broken down and explained in individual code segments. Notice how the programming logic takes into account each of the steps involved in the interaction.

Show Source Code
var canvas,ctx,mouseX=9999,mouseY=9999,distX,distY,circle;

var cursor_grab ="url(data:application/cur;base64,AAACAAEAICACAAcABQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA%2F%2F%2F%2FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA%2FAAAAfwAAAP%2BAAAH%2FgAAB%2F8AAA%2F%2FAAAd%2FwAAGf%2BAAAH9gAADbYAAA2yAAAZsAAAGbAAAAGAAAAAAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FgH%2F%2F%2F4B%2F%2F%2F8Af%2F%2F%2BAD%2F%2F%2FAA%2F%2F%2FwAH%2F%2F4AB%2F%2F8AAf%2F%2FAAD%2F%2F5AA%2F%2F%2FgAP%2F%2F4AD%2F%2F8AF%2F%2F%2FAB%2F%2F%2F5A%2F%2F%2F%2F5%2F%2F%2F8%3D), move";
var cursor_drag ="url(data:application/cur;base64,AAACAAEAICACAAcABQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA%2F%2F%2F%2FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA%2FAAAAfwAAAP%2BAAAH%2FgAAB%2F8AAAH%2FAAAB%2FwAAA%2F0AAANsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FgH%2F%2F%2F4B%2F%2F%2F8Af%2F%2F%2BAD%2F%2F%2FAA%2F%2F%2FwAH%2F%2F%2BAB%2F%2F%2FwAf%2F%2F4AH%2F%2F%2BAD%2F%2F%2FyT%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8%3D), move";

function init(){
    canvas = document.getElementById('dnd');
    ctx = canvas.getContext('2d');
    
    circle = {
        x: canvas.width/2,
        y: canvas.height/2,
        r: 50,
        mouse: false,
        drag: false
    }
    
    canvas.addEventListener('mousemove',updateCanvas,false);
    canvas.addEventListener('mousedown',startDrag,false);
    canvas.addEventListener('mouseup',stopDrag,false);
    
    canvas.addEventListener('selectstart',function(e){e.preventDefault();},false);	
    canvas.style.MozUserSelect = "none";
        
        
    drawCanvas();	
    
}

function findOffset(obj) {
    var curX = curY = 0;
    if (obj.offsetParent) {
        do {
            curX += obj.offsetLeft;
            curY += obj.offsetTop;
        } while (obj = obj.offsetParent);
    return {x:curX,y:curY};
    }
}

function updateCanvas(e){
    
    var pos = findOffset(canvas);
    
    mouseX = e.pageX - pos.x;
    mouseY = e.pageY - pos.y;
    
    if(circle.mouse && !circle.drag){
        canvas.style.cursor = cursor_grab;
    } else if(circle.drag) {
        canvas.style.cursor = cursor_drag;
    } else {
        canvas.style.cursor = 'auto';
    }
    
    if(circle.drag){
        circle.x = mouseX - distX;
        circle.y = mouseY - distY;
    }
    
    drawCanvas();
}

function startDrag(){
    if(circle.mouse == true){
        circle.drag = true;
        distX = mouseX - circle.x;
        distY = mouseY - circle.y;
        ctx.save();
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowColor="rgba(0,0,0,.3)";
        ctx.shadowBlur = 5;
        drawCanvas();
    }
}

function stopDrag(){
    if(circle.drag == true){
        circle.drag = false;
        ctx.restore();
        drawCanvas();
    }
}

function drawCanvas(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.beginPath();
    ctx.arc(circle.x,circle.y,circle.r,0,Math.PI*2,false);
    ctx.isPointInPath(mouseX,mouseY) ? circle.mouse = true : circle.mouse = false;
    ctx.fill();

}

Global Variables

var canvas,ctx,mouseX=9999,mouseY=9999,distX,distY,circle;

var cursor_grab ="url(DATA URI), move";
var cursor_drag ="url(DATA URI), move";

In the global scope the variables which will be needed across functions are defined. canvas and ctx are their normal canvas variables, which will later be initialized after the HTML has loaded. mouseX and mouseY will be used to hold the mouse coordinates. They are set to impossible values, because the first time they are referenced is before the actual mouse is recorded. distX and distY will be used to hold the offset value of the mouse from the center of the circle when drawing. circle will become an object which holds the properties of the circle being drawn on the canvas.

The last two variables define custom cursors which will be used to more thoroughly create the Drag and Drop experience. The cursors themselves are from the free, open source Google Web Toolkit

Initialization

function init(){
    canvas = document.getElementById('dnd');
    ctx = canvas.getContext('2d');
    
    circle = {
        x: canvas.width/2,
        y: canvas.height/2,
        r: 50,
        mouse: false,
        drag: false
    }
    
    canvas.addEventListener('mousemove',updateCanvas,false);
    canvas.addEventListener('mousedown',startDrag,false);
    canvas.addEventListener('mouseup',stopDrag,false);
    
    canvas.addEventListener('selectstart',function(e){e.preventDefault();},false);	
    canvas.style.MozUserSelect = "none";    
        
    drawCanvas();	
    
}

The first two lines are normal canvas commands. The second section defines our circle object. As an object literal, key:value pairs are used to hold parameters which we will later use to draw the circle ( x, y, radius) and values to monitor if the mouse is over the circle, and if it is being dragged. After that we attach three event listeners to the canvas. mousemove will track the position of the mouse. mousedown will check if the mouse is inside the circle, and start the dragging operation. mouseup will check if the user is dragging, and end it.

The next two lines are used to prevent the I-bar cursor from appearing while dragging. This is done simply by disabling the default browser behavior of dragging on the canvas element. This is not necessary to the actual functionality of the drag and drop behavior, but it is a part of the experience being presented.

After all that, the drawCanvas() function is called to render the surface.

Drawing the Canvas

function drawCanvas(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.beginPath();
    ctx.arc(circle.x,circle.y,circle.r,0,Math.PI*2,false);
    ctx.isPointInPath(mouseX,mouseY) ? circle.mouse = true : circle.mouse = false;
    ctx.fill();

}

The actual drawing function starts off by clearing the canvas. Then it proceeds to draw the circle using the properties defined in the circle object during initialization. While drawing, it also checks if the current mouse position is inside the circle. If so, it sets the mouse property of circle to true. Otherwise, it sets it to false.

Moving the Mouse - Updating the Canvas

function findOffset(obj) {
    var curX = curY = 0;
    if (obj.offsetParent) {
        do {
            curX += obj.offsetLeft;
            curY += obj.offsetTop;
        } while (obj = obj.offsetParent);
    return {x:curX,y:curY};
    }
}
    
function updateCanvas(e){
    
    var pos = findOffset(canvas);
    
    mouseX = e.pageX - pos.x;
    mouseY = e.pageY - pos.y;
    
    if(circle.mouse && !circle.drag){
        canvas.style.cursor = cursor_grab;
    } else if(circle.drag) {
        canvas.style.cursor = cursor_drag;
    } else {
        canvas.style.cursor = 'auto';
    }
    
    if(circle.drag){
        circle.x = mouseX - distX;
        circle.y = mouseY - distY;
    }
    
    drawCanvas();
}

In response to the mousemove event listener, updateCanvas is fired, and passed the event object. It starts by using the findOffset function to find the position of the canvas. Then, using that information, calculates the updated mouse coordinates relative to the canvas.

The first if block checks if the mouse is currently inside the circle, or if the circle is currently being dragged, and swaps in the correct custom cursor accordingly.

The second if block checks if the user is currently dragging, and if so, updates the positioning parameters of the circle based on the mouse coordinates and its distance from the center of the circle

Then, after all the values have been updated, the canvas is redrawn

Starting to Drag

function startDrag(){
    if(circle.mouse == true){
        circle.drag = true;
        distX = mouseX - circle.x;
        distY = mouseY - circle.y;
        
        ctx.save();
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowColor="rgba(0,0,0,.3)";
        ctx.shadowBlur = 5;
        
        drawCanvas();
    }
}

In response to the mousedown event listener, startDrag is fired. It begins by referencing the circle's mouse property to see if the mouse is currently inside the circle's path. If it is, the circle's drag property is set, and the mouse's offset from the center of the circle is calculated.

The next set of commands simply defines a shadow to aid the visual aesthetic of the circle being 'lifted' and moved on the canvas. After that, the canvas is redrawn.

Ending the Drag

function stopDrag(){
    if(circle.drag == true){
        circle.drag = false;
        ctx.restore();
        drawCanvas();
    }
}

In response to the mouseup event listener, stopDrag is fired. If the circle's drag property is true, meaning the circle is currently being dragged, it is set to false.

ctx.restore() is used to remove the shadow's parameters, and then the canvas is redrawn.