How to Drag and Drop Objects with JavaScript and HTML5 Canvas

2020-10-02 23:10:03 | #programming #javascript #html5 #canvas

Tested On

  • Linux Ubuntu 20.04
  • Windows 10
  • macOS Catalina

HTML5 Canvas is not just limited to producing visuals. It can produce complex interactions, as well. A good place to start is by giving the user the ability to pick up objects and drag them around the screen. This involves a bit of math for the hit detection, but I promise it won't be difficult.

Before we dive in, try playing around with the following demo. Feel free to update the values in the code editor, below. If the code looks daunting, don't worry. We explain all of the math and logic, in the next section, as well as provide you with the full source code.

Code Editor

Drag and Drop Logic

For drag and drop functionality, the basic logic flow is as follows:

  1. Detect when the user presses down
  2. If the coordinates of the user's interaction fall within the bounds of a shape, set isDragging for target shape to true
  3. If the user moves the mouse/touch before releasing, update the position of all target shapes to match the mouse/touch coordinates
  4. If the user releases, set isDragging for all shapes to false

How to See If the Mouse is Inside a Rectangle

When dealing with unrotated rectangles, determining if the user's mouse/touch is colliding with the shape is fairly straightforward. We just take the XY coordinates of the mouse or touch, and check to see if it's greater than the top and left sides and less than the bottom and right sides. The cursor at 3, on the x-axis, and 1 on the y-axis falls within the bounds, but the cursor at 5,1 misses.

How to Use the Pythagorean Theorem for Circle Hit Detection

With a circle, we have a round perimeter to deal with. Using the same rectangular bounding box hit detection would be insufficient because of the gaps between the curved arc of a circle and the straight edges of a rectangle. To get pixel-perfect collision detection with round shapes, we can rely on the Pythagorean Theorem.

Given a right triangle, if you add both sides of a triangle and square them, they will equal the hypotenuse squared. So in the above example, c2 = a2 + b2. Why are we talking about triangles, you might be asking? Because a right triangle is formed in the space between a shape and the user's mouse, and the hypotenuse of that triangle gives us the distance between those two points. Take a look at the following diagram:

Notice that wherever the user position's the mouse/touch, a right triangle is formed. Using the pythagorean theorem, we can determine that distance = (mouseX - circleX)2 + (mouseY - circleY)2. So if the distance between the shape and the mouse is less than the distance between the center of the circle and its radius, we can conclude that the user is clicking on the circle.

Let's Write Some JavaScript Code

Let's translate all this math into some JavaScript code. Just import it into an HTML file, with a canvas element with an ID of "canvas", and you will have drag and drop functionality. If this sounds confusing, you might want to take our Learn JavaScript and ES6 with HTML5 Canvas course, which sets you up, from scratch.

var Rectangle = function(x, y, width, height) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
  this.isDragging = false;

  this.render = function(ctx) {
    ctx.save();

    ctx.beginPath();
    ctx.rect(this.x - this.width * 0.5, this.y - this.height * 0.5, this.width, this.height);
    ctx.fillStyle = '#2793ef';
    ctx.fill();

    ctx.restore();
  }
}

var Arc = function(x, y, radius, radians) {
  this.x = x;
  this.y = y;
  this.radius = radius;
  this.radians = radians;
  this.isDragging = false;

  this.render = function(ctx) {
    ctx.save();

    ctx.beginPath();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, this.radians, false);
    ctx.fillStyle = '#2793ef';
    ctx.fill();

    ctx.restore();
  }
}

var MouseTouchTracker = function(canvas, callback){

  function processEvent(evt) {
    var rect = canvas.getBoundingClientRect();
    var offsetTop = rect.top;
    var offsetLeft = rect.left;

    if (evt.touches) {
      return {
        x: evt.touches[0].clientX - offsetLeft,
        y: evt.touches[0].clientY - offsetTop
      }
    } else {
      return {
        x: evt.clientX - offsetLeft,
        y: evt.clientY - offsetTop
      }
    }
  }

  function onDown(evt) {
    evt.preventDefault();
    var coords = processEvent(evt);
    callback('down', coords.x, coords.y);
  }

  function onUp(evt) {
    evt.preventDefault();
    callback('up');
  }

  function onMove(evt) {
    evt.preventDefault();
    var coords = processEvent(evt);
    callback('move', coords.x, coords.y);
  }

  canvas.ontouchmove = onMove;
  canvas.onmousemove = onMove;

  canvas.ontouchstart = onDown;
  canvas.onmousedown = onDown;
  canvas.ontouchend = onUp;
  canvas.onmouseup = onUp;
}

function isHit(shape, x, y) {
  if (shape.constructor.name === 'Arc') {
    var dx = shape.x - x;
    var dy = shape.y - y;
    if (dx * dx + dy * dy < shape.radius * shape.radius) {
      return true
    }
  } else {
    if (x > shape.x - shape.width * 0.5 && y > shape.y - shape.height * 0.5 && x < shape.x + shape.width - shape.width * 0.5 && y < shape.y + shape.height - shape.height * 0.5) {
      return true;
    }
  }

  return false;
}
  
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var startX = 0;
var startY = 0;

var rectangle = new Rectangle(50, 50, 100, 100);
rectangle.render(ctx);

var circle = new Arc(200, 140, 50, Math.PI * 2);
circle.render(ctx);

var mtt = new MouseTouchTracker(canvas,
  function(evtType, x, y) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    switch(evtType) {

      case 'down':
        startX = x;
        startY = y;
        if (isHit(rectangle, x, y)) {
          rectangle.isDragging = true;
        }
        if (isHit(circle, x, y)) {
          circle.isDragging = true;
        }
        break;

      case 'up':
        rectangle.isDragging = false;
        circle.isDragging = false;
        break;

      case 'move':
        var dx = x - startX;
        var dy = y - startY;
        startX = x;
        startY = y;

        if (rectangle.isDragging) {
          rectangle.x += dx;
          rectangle.y += dy;
        }

        if (circle.isDragging) {
          circle.x += dx;
          circle.y += dy;
        }
        break;
    }

    circle.render(ctx);
    rectangle.render(ctx);
  }
);

Here's an explanation of the code, above.

Lines 1-38 give us a Rectangle and Arc that we can instantiate with our desired parameters. We won't be explaining this, here, since our primary focus is drag and drop functionality. If you'd like an in-depth explaination of shapes, please head over to our Learn JavaScript and ES6 with HTML5 Canvas course.

Lines 40-84 define all the logic for detecting user mouse/touch input and returning their coordinates.

Lines 86-100 is where our hit detection code lives. We first check to see if the shape is an Arc. If so, we calculate the distance between the mouse and the shape (dx and dy), and see if squaring both distances is less than the circle's radius squared. If so, we can determine that the user that clicked inside the circle.

Lines 94-96 is the hit detection for rectangular shapes. We use the same logic mentioned at the beginning of this tutorial, but we offset everything by half the rectangle's width and height because our rectangle's centerpoint is at the center, not the top left.

Lines 119-151 is our drag functionality. We toggle the isDragging flag for each shape on mouse/touch down and up. And we update the shape's position if the user moves around the screen with their mouse pressed down.

Conclusion

So there you have it—a tutorial explaining how to drag and drop objects with canvas. We hope you find this guide useful.

If you're interested in making interactives and games, we recommend you take our Learn JavaScript While Building a Game Framework course, which teaches you JavaScript and ES6 while you build an HTML5 Canvas, physics-based framework, from scratch. You'll also learn how to get shapes to follow the mouse, velocity, acceleration, friction, and many other useful functionality you can build interactives and games with. In addition, it compiles into a browser-compatible ES2015 build. Start making canvas applications, today.

Want To See More Exercises?

View Exercises View Courses

Comments

You must log in to comment. Don't have an account? Sign up for free.

Subscribe to comments for this post

Want To Receive More Free Content?

Would you like to receive free resources, tailored to help you reach your IT goals? Get started now, by leaving your email address below. We promise not to spam. You can also sign up for a free account and follow us on and engage with the community. You may opt out at any time.



Tell Us About Your Project









Contact Us

Do you have a specific IT problem that needs solving or just have a general IT question? Use the contact form to get in touch with us and an IT professional will be with you, momentarily.

Hire Us

We offer web development, enterprise software development, QA &amp; testing, google analytics, domains and hosting, databases, security, IT consulting, and other IT-related services.

Free IT Tutorials

Head over to our tutorials section to learn all about working with various IT solutions.

Contact