Search code examples
javascripthtmldrag-and-drophtml5-canvasimagemap

Drag and drop canvas objects onto scalable SVG image map


I'm looking for a recommendation on how to proceed with a ui design.

I may be going down the wrong path(I'm havving doubts), but I'm trying to combine these two concepts:

Drag and Drop Canvas

SVG Image Map

I need the equivalent of an image map and then need to be able to instantiate and then drag objects into the specific zones (defined as SVG(polygons). When objects are dropped into a zone, I need to be able to (raise an event)capture what zone(or no zone) they are in and the coordinates of the object so they can be persisted in a database. The whole thing should be responsive.

I've started a JsFiddle, but sort of hit a wall when using Z-indexes to put the SVG under the canvas. Not sure how to mix the two(if this is event the correct approach.)

Any advice or suggestions would be appreciated. Maybe there's an existing library/framwork out there for this?

JsFiddle
HTML:

<div style="position: absolute;">
  <canvas id="canvas" width="600" height="600" style="z-index: 2;"></canvas>

  <div id="artwork">
  <svg version="1.1" id="MAP" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 640 480" enable-background="new 0 0 640 480" xml:space="preserve">
  <circle id="C3" fill="none" stroke="#000000" stroke-miterlimit="10" cx="169" cy="204.333" r="53.333"/>
  <circle id="C2" fill="none" stroke="#000000" stroke-miterlimit="10" cx="317.667" cy="345.667" r="49.333"/>
  <circle id="C1" fill="none" stroke="#000000" stroke-miterlimit="10" cx="397.333" cy="113.333" r="47.667"/>
  <rect id="R1" x="74.333" y="26.333" fill="none" stroke="#000000" stroke-miterlimit="10" width="103.333" height="103.333"/>
  <polygon id="P1" fill="none" stroke="#000000" stroke-miterlimit="10" points="488.333,194.333 519.667,302.333 483,365.667 
    469.667,289.667 387,281.667 "/>
    </svg>

  </div>
</div>

JS:

let paths = Array.from(document.getElementsByTagName("polygon"));
paths.forEach((path) => {
  path.addEventListener("click", (e) => {
    console.log("hi");
    console.log(e.screenX);
    // do something with e.target
  });
});

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 = "rgba(0,0,200,.5)";//#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.arc(this.x, this.y, this.radius, 0, this.radians, false);
    ctx.fillStyle = "rgba(0,200,200,.5)";//"#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);
});

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);
});

CSS:

<style>
#artwork{
  background: url(artwork.jpg);
  background-size: cover;
  position: absolute;

}
#artwork:after{
  content: '';
  display: block;
  padding-bottom: 100%;
 
}

#artwork svg{

  position: absolute; 
  left: 0px; 
  top:0px;
  z-index: -1;
  width: 600px;
}

#artwork svg circle{
  cursor: pointer;
  fill: rgba(255, 0, 255, 1);
}

#artwork svg polygon{
  cursor: pointer;
  fill: rgba(0, 255, 255, 1);
}

</style>

Solution

  • I solved this using Konva JS library. It handled drawing the SVGs, layering the zones, drag-and-drop, responsive scaling, and determining when an object is within a zone.