# Dragging Path Shapes & Images on Canvas

# How shapes & images REALLY(!) "move" on the Canvas

A problem: Canvas only remembers pixels, not shapes or images

This is an image of a circular beach ball, and of course, you can't drag the ball around the image.

enter image description here

It may surprise you that just like an image, if you draw a circle on a Canvas you cannot drag that circle around the canvas. That's because the canvas won't remember where it drew the circle.

// this arc (==circle) is not draggable!!
context.beginPath();
context.arc(20, 30, 15, 0, Math.PI*2);
context.fillStyle='blue';
context.fill();

What the Canvas DOESN'T know...

  • ...where you drew the circle (it does not know x,y =[20,30]).
  • ...the size of the circle (it does not know radius=15).
  • ...the color of the circle. (it does not know the circle is blue).

What the Canvas DOES know...

Canvas knows the color of every pixel on it's drawing surface.

The canvas can tell you that at x,y==[20,30] there is a blue pixel, but it does not know if this blue pixel is part of a circle.

What this means...

This means everything drawn on the Canvas is permanent: immovable and unchangeable.

  • Canvas can't move the circle or resize the circle.
  • Canvas can't recolor the circle or erase the circle.
  • Canvas can't say if the mouse is hovering over the circle.
  • Canvas can't say if the circle is colliding with another circle.
  • Canvas can't let a user drag the circle around the Canvas.

But Canvas can give the I-L-L-U-S-I-O-N of movement

Canvas can give the illusion of movement by continuously erasing the circle and redrawing it in a different position. By redrawing the Canvas many times per second, the eye is fooled into seeing the circle move across the Canvas.

  • Erase the canvas
  • Update the circle's position
  • Redraw the circle in it's new position
  • Repeat, repeat, repeat ...
  • This code gives the illusion of movement by continuously redrawing a circle in new positions.

    // create a canvas
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    ctx.fillStyle='red';
    document.body.appendChild(canvas);
    
    // a variable indicating a circle's X position
    var circleX=20;
    
    // start animating the circle across the canvas
    // by continuously erasing & redrawing the circle
    // in new positions
    requestAnimationFrame(animate);
    
    function animate(){
        // update the X position of the circle
        circleX++;      
        // redraw the circle in it's new position
        ctx.clearRect(0,0,canvas.width,canvas.height);
        ctx.beginPath();
        ctx.arc( circleX, 30,15,0,Math.PI*2 );
        ctx.fill();
        // request another animate() loop
        requestAnimationFrame(animate);
    }
    
    

    # Dragging circles & rectangles around the Canvas

    # What is a "Shape"?

    You typically save your shapes by creating a JavaScript "shape" object representing each shape.

    var myCircle = { x:30, y:20, radius:15 };
    
    

    Of course, you're not really saving shapes. Instead, you're saving the definition of how to draw the shapes.

    Then put every shape-object into an array for easy reference.

    // save relevant information about shapes drawn on the canvas
    var shapes=[];
    
    // define one circle and save it in the shapes[] array
    shapes.push( {x:10, y:20, radius:15, fillcolor:'blue'} );
    
    // define one rectangle and save it in the shapes[] array
    shapes.push( {x:10, y:100, width:50, height:35, fillcolor:'red'} );
    
    

    # Using mouse events to do Dragging

    Dragging a shape or image requires responding to these mouse events:

    On mousedown:

    Test if any shape is under the mouse. If a shape is under the mouse, the user is intending to drag that shape. So keep a reference to that shape and set a true/false isDragging flag indicating that a drag is in process.

    On mousemove:

    Calculate the distance that the mouse has been dragged since the last mousemove event and change the dragged shape's position by that distance. To change the shape's position, you change the x,y position properties in that shape's object.

    On mouseup or mouseout:

    The user is intending to stop the drag operation, so clear the "isDragging" flag. Dragging is completed.

    # Demo: Dragging circles & rectangles on the canvas

    This demo drags circles & rectangles on the canvas by responding to mouse events and giving the illusion of movement by clearing and redrawing.

    // canvas related vars
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    document.body.appendChild(canvas);
    canvas.style.border='1px solid red';
    
    // used to calc canvas position relative to window
    function reOffset(){
        var BB=canvas.getBoundingClientRect();
        offsetX=BB.left;
        offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }
    window.onresize=function(e){ reOffset(); }
    canvas.onresize=function(e){ reOffset(); }
    
    // save relevant information about shapes drawn on the canvas
    var shapes=[];
    // define one circle and save it in the shapes[] array
    shapes.push( {x:30, y:30, radius:15, color:'blue'} );
    // define one rectangle and save it in the shapes[] array
    shapes.push( {x:100, y:-1, width:75, height:35, color:'red'} );
    
    // drag related vars
    var isDragging=false;
    var startX,startY;
    
    // hold the index of the shape being dragged (if any)
    var selectedShapeIndex;
    
    // draw the shapes on the canvas
    drawAll();
    
    // listen for mouse events
    canvas.onmousedown=handleMouseDown;
    canvas.onmousemove=handleMouseMove;
    canvas.onmouseup=handleMouseUp;
    canvas.onmouseout=handleMouseOut;
    
    // given mouse X & Y (mx & my) and shape object
    // return true/false whether mouse is inside the shape
    function isMouseInShape(mx,my,shape){
        if(shape.radius){
            // this is a circle
            var dx=mx-shape.x;
            var dy=my-shape.y;
            // math test to see if mouse is inside circle
            if(dx*dx+dy*dy<shape.radius*shape.radius){
                // yes, mouse is inside this circle
                return(true);
            }
        }else if(shape.width){
            // this is a rectangle
            var rLeft=shape.x;
            var rRight=shape.x+shape.width;
            var rTop=shape.y;
            var rBott=shape.y+shape.height;
            // math test to see if mouse is inside rectangle
            if( mx>rLeft && mx<rRight && my>rTop && my<rBott){
                return(true);
            }
        }
        // the mouse isn't in any of the shapes
        return(false);
    }
    
    function handleMouseDown(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position
        startX=parseInt(e.clientX-offsetX);
        startY=parseInt(e.clientY-offsetY);
        // test mouse position against all shapes
        // post result if mouse is in a shape
        for(var i=0;i<shapes.length;i++){
            if(isMouseInShape(startX,startY,shapes[i])){
                // the mouse is inside this shape
                // select this shape
                selectedShapeIndex=i;
                // set the isDragging flag
                isDragging=true;
                // and return (==stop looking for 
                //     further shapes under the mouse)
                return;
            }
        }
    }
    
    function handleMouseUp(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseOut(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseMove(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position         
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);
        // how far has the mouse dragged from its previous mousemove position?
        var dx=mouseX-startX;
        var dy=mouseY-startY;
        // move the selected shape by the drag distance
        var selectedShape=shapes[selectedShapeIndex];
        selectedShape.x+=dx;
        selectedShape.y+=dy;
        // clear the canvas and redraw all shapes
        drawAll();
        // update the starting drag position (== the current mouse position)
        startX=mouseX;
        startY=mouseY;
    }
    
    // clear the canvas and 
    // redraw all shapes in their current positions
    function drawAll(){
        ctx.clearRect(0,0,cw,ch);
        for(var i=0;i<shapes.length;i++){
            var shape=shapes[i];
            if(shape.radius){
                // it's a circle
                ctx.beginPath();
                ctx.arc(shape.x,shape.y,shape.radius,0,Math.PI*2);
                ctx.fillStyle=shape.color;
                ctx.fill();
            }else if(shape.width){
                // it's a rectangle
                ctx.fillStyle=shape.color;
                ctx.fillRect(shape.x,shape.y,shape.width,shape.height);
            }
        }
    }
    
    

    # Dragging irregular shapes around the Canvas

    Most Canvas drawings are either rectangular (rectangles, images, text-blocks) or circular (circles).

    Circles & rectangles have mathematical tests to check if the mouse is inside them. This makes testing circles and rectangles easy, quick and efficient. You can "hit-test" hundreds of circles or rectangles in a fraction of a second.

    You can also drag irregular shapes. But irregular shapes have no quick mathematical hit-test. Fortunately, irregular shapes do have a built-in hit-test to determine if a point (mouse) is inside the shape: context.isPointInPath. While isPointInPath works well, it is not nearly as efficient as purely mathematical hit-tests -- it is often up to 10X slower than pure mathematical hit-tests.

    One requirement when using isPointInPath is that you must "redefine" the Path being tested immediately before calling isPointInPath. "Redefine" means you must issue the path drawing commands (as above), but you don't need to stroke() or fill() the Path before testing it with isPointInPath. This way you can test previously drawn Paths without having to overwrite (stroke/fill) those previous Paths on the Canvas itself.

    The irregular shape doesn't need to be as common as the everyday triangle. You can also hit-test any wildly irregular Paths.

    This annotated example shows how to drag irregular Path shapes as well as circles and rectangles:

    // canvas related vars
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    document.body.appendChild(canvas);
    canvas.style.border='1px solid red';
    
    // used to calc canvas position relative to window
    function reOffset(){
        var BB=canvas.getBoundingClientRect();
        offsetX=BB.left;
        offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }
    window.onresize=function(e){ reOffset(); }
    canvas.onresize=function(e){ reOffset(); }
    
    // save relevant information about shapes drawn on the canvas
    var shapes=[];
    // define one circle and save it in the shapes[] array
    shapes.push( {x:20, y:20, radius:15, color:'blue'} );
    // define one rectangle and save it in the shapes[] array
    shapes.push( {x:100, y:-1, width:75, height:35, color:'red'} );
    // define one triangle path and save it in the shapes[] array
    shapes.push( {x:0, y:0, points:[{x:50,y:30},{x:75,y:60},{x:25,y:60}],color:'green'} );
    
    // drag related vars
    var isDragging=false;
    var startX,startY;
    
    // hold the index of the shape being dragged (if any)
    var selectedShapeIndex;
    
    // draw the shapes on the canvas
    drawAll();
    
    // listen for mouse events
    canvas.onmousedown=handleMouseDown;
    canvas.onmousemove=handleMouseMove;
    canvas.onmouseup=handleMouseUp;
    canvas.onmouseout=handleMouseOut;
    
    // given mouse X & Y (mx & my) and shape object
    // return true/false whether mouse is inside the shape
    function isMouseInShape(mx,my,shape){
        if(shape.radius){
            // this is a circle
            var dx=mx-shape.x;
            var dy=my-shape.y;
            // math test to see if mouse is inside circle
            if(dx*dx+dy*dy<shape.radius*shape.radius){
                // yes, mouse is inside this circle
                return(true);
            }
        }else if(shape.width){
            // this is a rectangle
            var rLeft=shape.x;
            var rRight=shape.x+shape.width;
            var rTop=shape.y;
            var rBott=shape.y+shape.height;
            // math test to see if mouse is inside rectangle
            if( mx>rLeft && mx<rRight && my>rTop && my<rBott){
                return(true);
            }
        }else if(shape.points){
            // this is a polyline path
            // First redefine the path again (no need to stroke/fill!)
            defineIrregularPath(shape);
            // Then hit-test with isPointInPath
            if(ctx.isPointInPath(mx,my)){
                return(true);
            }                
        }
        // the mouse isn't in any of the shapes
        return(false);
    }
    
    function handleMouseDown(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position
        startX=parseInt(e.clientX-offsetX);
        startY=parseInt(e.clientY-offsetY);
        // test mouse position against all shapes
        // post result if mouse is in a shape
        for(var i=0;i<shapes.length;i++){
            if(isMouseInShape(startX,startY,shapes[i])){
                // the mouse is inside this shape
                // select this shape
                selectedShapeIndex=i;
                // set the isDragging flag
                isDragging=true;
                // and return (==stop looking for 
                //     further shapes under the mouse)
                return;
            }
        }
    }
    
    function handleMouseUp(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseOut(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseMove(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position         
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);
        // how far has the mouse dragged from its previous mousemove position?
        var dx=mouseX-startX;
        var dy=mouseY-startY;
        // move the selected shape by the drag distance
        var selectedShape=shapes[selectedShapeIndex];
        selectedShape.x+=dx;
        selectedShape.y+=dy;
        // clear the canvas and redraw all shapes
        drawAll();
        // update the starting drag position (== the current mouse position)
        startX=mouseX;
        startY=mouseY;
    }
    
    // clear the canvas and 
    // redraw all shapes in their current positions
    function drawAll(){
        ctx.clearRect(0,0,cw,ch);
        for(var i=0;i<shapes.length;i++){
            var shape=shapes[i];
            if(shape.radius){
                // it's a circle
                ctx.beginPath();
                ctx.arc(shape.x,shape.y,shape.radius,0,Math.PI*2);
                ctx.fillStyle=shape.color;
                ctx.fill();
            }else if(shape.width){
                // it's a rectangle
                ctx.fillStyle=shape.color;
                ctx.fillRect(shape.x,shape.y,shape.width,shape.height);
            }else if(shape.points){
                // its a polyline path
                defineIrregularPath(shape);
                ctx.fillStyle=shape.color;
                ctx.fill();
            }
        }
    }
    
    function defineIrregularPath(shape){
        var points=shape.points;
        ctx.beginPath();
        ctx.moveTo(shape.x+points[0].x,shape.y+points[0].y);
        for(var i=1;i<points.length;i++){
            ctx.lineTo(shape.x+points[i].x,shape.y+points[i].y);
        }
        ctx.closePath();
    }
    
    

    # Dragging images around the Canvas

    See this Example for a general explanation of dragging Shapes around the Canvas.

    This annotated example shows how to drag images around the Canvas

    // canvas related vars
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    canvas.width=378;
    canvas.height=378;
    var cw=canvas.width;
    var ch=canvas.height;
    document.body.appendChild(canvas);
    canvas.style.border='1px solid red';
    
    // used to calc canvas position relative to window
    function reOffset(){
        var BB=canvas.getBoundingClientRect();
        offsetX=BB.left;
        offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }
    window.onresize=function(e){ reOffset(); }
    canvas.onresize=function(e){ reOffset(); }
    
    // save relevant information about shapes drawn on the canvas
    var shapes=[];
    
    // drag related vars
    var isDragging=false;
    var startX,startY;
    
    // hold the index of the shape being dragged (if any)
    var selectedShapeIndex;
    
    // load the image
    var card=new Image();
    card.onload=function(){
        // define one image and save it in the shapes[] array
        shapes.push( {x:30, y:10, width:127, height:150, image:card} );
        // draw the shapes on the canvas
        drawAll();
        // listen for mouse events
        canvas.onmousedown=handleMouseDown;
        canvas.onmousemove=handleMouseMove;
        canvas.onmouseup=handleMouseUp;
        canvas.onmouseout=handleMouseOut;
    };
    // put your image src here!
    card.src='https://dl.dropboxusercontent.com/u/139992952/stackoverflow/card.png';
    
    
    // given mouse X & Y (mx & my) and shape object
    // return true/false whether mouse is inside the shape
    function isMouseInShape(mx,my,shape){
        // is this shape an image?
        if(shape.image){
            // this is a rectangle
            var rLeft=shape.x;
            var rRight=shape.x+shape.width;
            var rTop=shape.y;
            var rBott=shape.y+shape.height;
            // math test to see if mouse is inside image
            if( mx>rLeft && mx<rRight && my>rTop && my<rBott){
                return(true);
            }
        }
        // the mouse isn't in any of this shapes
        return(false);
    }
    
    function handleMouseDown(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position
        startX=parseInt(e.clientX-offsetX);
        startY=parseInt(e.clientY-offsetY);
        // test mouse position against all shapes
        // post result if mouse is in a shape
        for(var i=0;i<shapes.length;i++){
            if(isMouseInShape(startX,startY,shapes[i])){
                // the mouse is inside this shape
                // select this shape
                selectedShapeIndex=i;
                // set the isDragging flag
                isDragging=true;
                // and return (==stop looking for 
                //     further shapes under the mouse)
                return;
            }
        }
    }
    
    function handleMouseUp(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseOut(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // the drag is over -- clear the isDragging flag
        isDragging=false;
    }
    
    function handleMouseMove(e){
        // return if we're not dragging
        if(!isDragging){return;}
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // calculate the current mouse position         
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);
        // how far has the mouse dragged from its previous mousemove position?
        var dx=mouseX-startX;
        var dy=mouseY-startY;
        // move the selected shape by the drag distance
        var selectedShape=shapes[selectedShapeIndex];
        selectedShape.x+=dx;
        selectedShape.y+=dy;
        // clear the canvas and redraw all shapes
        drawAll();
        // update the starting drag position (== the current mouse position)
        startX=mouseX;
        startY=mouseY;
    }
    
    // clear the canvas and 
    // redraw all shapes in their current positions
    function drawAll(){
        ctx.clearRect(0,0,cw,ch);
        for(var i=0;i<shapes.length;i++){
            var shape=shapes[i];
            if(shape.image){
                // it's an image
                ctx.drawImage(shape.image,shape.x,shape.y);
            }
        }
    }