javascripthtmlcanvasselection

Movable selection on a canvas


Here below is how to draw a "selection rectangle" on a <canvas> with drag-and-drop, see How to draw a selection rectangle with drag and drop on a HTML canvas?.

Is there a simple way to detect the selection rectangle on hover at a distance of a few pixels, and allow to move the selection rectangle with drag-and-drop?

var c1 = document.getElementById("c1"), c2 = document.getElementById("c2");
var ctx1 = c1.getContext("2d"), ctx2 = c2.getContext("2d");
ctx2.setLineDash([5, 5]);
var origin = null;
window.onload = () => { ctx1.drawImage(document.getElementById("img"), 0, 0); }
c2.onmousedown = e => { origin = {x: e.offsetX, y: e.offsetY}; };
window.onmouseup = e => { origin = null; };
c2.onmousemove = e => { 
    if (!!origin) { 
        ctx2.strokeStyle = "#ff0000";
        ctx2.clearRect(0, 0, c2.width, c2.height);
        ctx2.beginPath();
        ctx2.rect(origin.x, origin.y, e.offsetX - origin.x, e.offsetY - origin.y); 
        ctx2.stroke(); 
    } 
};
#img { display: none; }
#canvas-container { position: relative; }
canvas { position: absolute; left: 0; top: 0; }
#c1 { z-index: 0; }
#c2 { z-index: 1; }
<img id="img" src="https://i.imgur.com/okSIKkW.jpg">
<div id="canvas-container">
<canvas id="c1" height="200" width="200"></canvas>
<canvas id="c2" height="200" width="200"></canvas>
</div>


Solution

  • Storing the current selection

    To be able to move an existing selection, you have to save its state. Right now, your code "forgets" about it after drawing it once.

    You can save your selection in a variable on mouseup like so:

    const dx = origin.x - mouse.x;
    const dy = origin.y - mouse.y;
    
    selection = {
      left:   Math.min(mouse.x, origin.x),  
      top:    Math.min(mouse.y, origin.y),
      width:  Math.abs(dx),
      height: Math.abs(dy)
    }
    

    Intersection check

    Your selection is a rectangle. You can check whether the mouse is intersecting the rectangle like so:

    const intersects = (
      mouse.x >= selection.left &&
      mouse.x <= selection.left + selection.width &&
      mouse.y >= selection.top &&
      mouse.y <= selection.top + selection.height
    );
    

    If you want to add some padding around the rectangle, you can change the checks to be, for example, mouse.x >= selection.left - PADDING.

    Make or Move

    You now have to support 2 types of interactions:

    1. Making new selections, and
    2. Moving existing selections

    This is where my implementation gets a bit messy, but you can probably refactor it yourself 🙂

    I didn't change much in the making-selections code, other than saving the selection to a variable.

    When moving selections, you take the dx and dy of your mouse drag and add them to the selection's original position (ox, oy):

    const dx = origin.x - mouse.x;
    const dy = origin.y - mouse.y;
    
    selection.left = ox - dx;
    selection.top = oy - dy;
    
    

    Here's everything in a runnable snippet:

    var c1 = document.getElementById("c1"),
        c2 = document.getElementById("c2");
    var ctx1 = c1.getContext("2d"),
        ctx2 = c2.getContext("2d");
      
    ctx2.setLineDash([5, 5]);
    
    var origin = null;
    let selection = null;
    let selectionHovered = false;
    let interaction = null;
    let ox = null;
    let oy = null;
    
    window.onload = () => { ctx1.drawImage(document.getElementById("img"), 0, 0); }
    
    c2.onmousedown = e => { 
      origin = {x: e.offsetX, y: e.offsetY};
      
      if (selectionHovered) {
        interaction = "MOVE_SELECTION";
        ox = selection.left;
        oy = selection.top;
      } else {
        interaction = "MAKE_SELECTION";
      }
      
    };
    
    window.onmouseup = e => {
      interaction = null;
      origin = null;
    };
    
    c2.onmousemove = e => {
      const x = e.offsetX;
      const y = e.offsetY;
      
      if (!interaction) {
       selectionHovered = (
          selection &&
          x >= selection.left &&
          x <= selection.left + selection.width &&
          y >= selection.top &&
          y <= selection.top + selection.height
        );
      } else {
        const dx = origin.x - x;
        const dy = origin.y - y;
    
        // Update
        switch (interaction) {
          case "MOVE_SELECTION":
            selection.left = ox - dx;
            selection.top = oy - dy;
    
            break;
          case "MAKE_SELECTION":
            selection = {
              left:   Math.min(x, origin.x),
              top:    Math.min(y, origin.y),
              width:  Math.abs(dx),
              height: Math.abs(dy)
            }
            break
          default:
            // Do nothing
        }
    
    
        // Set selectionHovered
        if (selection) {
    
        } else {
          selectionHovered = false;
        }
      }
    
      // Draw
      if (selection) drawSelection(selection);
    };
    
    function drawSelection({ top, left, width, height }) {
      // Draw rect
      ctx2.strokeStyle = "#ff0000";
      ctx2.clearRect(0, 0, c2.width, c2.height);
      ctx2.beginPath();
      ctx2.rect(left, top, width, height); 
      ctx2.stroke(); 
      
      // Set mouse 
      c2.style.cursor = selectionHovered ? "move" : "default";
      
    }
    #img { display: none; }
    body { margin: 0; }
    #canvas-container { position: relative; }
    canvas { position: absolute; left: 0; top: 0; }
    #c1 { z-index: 0; }
    #c2 { z-index: 1; }
    <img id="img" src="https://i.imgur.com/okSIKkW.jpg">
    <div id="canvas-container">
      <canvas id="c1" height="200" width="200"></canvas>
      <canvas id="c2" height="200" width="200"></canvas>
    </div>

    Edit: by a commenter's request, here's an example of implementing edge edits. Only done for the left edge; doing all is a bit more work and would probably benefit from some refactoring.

    var c1 = document.getElementById("c1"),
        c2 = document.getElementById("c2");
    var ctx1 = c1.getContext("2d"),
        ctx2 = c2.getContext("2d");
      
    ctx2.setLineDash([5, 5]);
    
    var origin = null;
    let selection = null;
    let selectionHovered = false;
    let edgeHovered = null;
    let interaction = null;
    let ox = null;
    let oy = null;
    
    window.onload = () => { ctx1.drawImage(document.getElementById("img"), 0, 0); }
    
    c2.onmousedown = e => { 
      origin = {x: e.offsetX, y: e.offsetY};
      
      if (edgeHovered === "left") {
        origin = { x: selection.left + selection.width, y: selection.top };
        interaction = "MAKE_SELECTION";
      } else if (selectionHovered) {
        interaction = "MOVE_SELECTION";
        ox = selection.left;
        oy = selection.top;
      } else {
        interaction = "MAKE_SELECTION";
      }
      
    };
    
    window.onmouseup = e => {
      interaction = null;
      origin = null;
    };
    
    c2.onmousemove = e => {
      const x = e.offsetX;
      const y = e.offsetY;
      
      if (!interaction) {
       selectionHovered = (
          selection &&
          x >= selection.left &&
          x <= selection.left + selection.width &&
          y >= selection.top &&
          y <= selection.top + selection.height
        );
        
        if (
          selection &&
          x >= selection.left - 5 &&
          x <= selection.left + 5 && 
          y >= selection.top &&
          y <= selection.top + selection.height
        ) {
          edgeHovered = "left";
        } else {
          edgeHovered = "";
        }
      } else {
        const dx = origin.x - x;
        const dy = origin.y - y;
    
        // Update
        switch (interaction) {
          case "MOVE_SELECTION":
            selection.left = ox - dx;
            selection.top = oy - dy;
    
            break;
          case "MAKE_SELECTION":
            selection = {
              left:   Math.min(x, origin.x),
              top:    Math.min(y, origin.y),
              width:  Math.abs(dx),
              height: edgeHovered === "left" ? selection.height : Math.abs(dy)
            }
            break
          default:
            // Do nothing
        }
    
    
        // Set selectionHovered
        if (selection) {
    
        } else {
          selectionHovered = false;
        }
      }
    
      // Draw
      if (selection) drawSelection(selection);
    };
    
    function drawSelection({ top, left, width, height }) {
      // Draw rect
      ctx2.strokeStyle = "#ff0000";
      ctx2.clearRect(0, 0, c2.width, c2.height);
      ctx2.beginPath();
      ctx2.rect(left, top, width, height); 
      ctx2.stroke(); 
      
      // Set mouse 
      c2.style.cursor = edgeHovered === "left" ? "ew-resize" : selectionHovered ? "move" : "default";
      
    }
    #img { display: none; }
    body { margin: 0; }
    #canvas-container { position: relative; }
    canvas { position: absolute; left: 0; top: 0; }
    #c1 { z-index: 0; }
    #c2 { z-index: 1; }
    <img id="img" src="https://i.imgur.com/okSIKkW.jpg">
    <div id="canvas-container">
      <canvas id="c1" height="200" width="200"></canvas>
      <canvas id="c2" height="200" width="200"></canvas>
    </div>