4

I want to get coordinates of the painted area of an image placed in an HTML canvas tag and send it to the database. And populate it on the same area of that image in another page. And how can I reset or clear the painted area by clicking the reset button?

JSfiddle example

var canvas = document.getElementById("canvas");
var img = document.getElementById("imagearea"),
    ctx = canvas.getContext("2d"),
    painting = false,
    lastX = 0,
    lastY = 0,
    lineThickness = 1;

canvas.width = canvas.height = 600;
ctx.fillRect(0, 0, 600, 600);
ctx.drawImage(img, 10, 10);

canvas.onmousedown = function(e) {
    painting = true;
    ctx.fillStyle = "#ff0000";
    lastX = e.pageX - this.offsetLeft;
    lastY = e.pageY - this.offsetTop;
};

canvas.onmouseup = function(e){
    painting = false;
}

canvas.onmousemove = function(e) {
    if (painting) {
        mouseX = e.pageX - this.offsetLeft;
        mouseY = e.pageY - this.offsetTop;

        // find all points between        
        var x1 = mouseX,
            x2 = lastX,
            y1 = mouseY,
            y2 = lastY;


        var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));
        if (steep){
            var x = x1;
            x1 = y1;
            y1 = x;

            var y = y2;
            y2 = x2;
            x2 = y;
        }
        if (x1 > x2) {
            var x = x1;
            x1 = x2;
            x2 = x;

            var y = y1;
            y1 = y2;
            y2 = y;
        }

        var dx = x2 - x1,
            dy = Math.abs(y2 - y1),
            error = 0,
            de = dy / dx,
            yStep = -1,
            y = y1;
        
        if (y1 < y2) {
            yStep = 1;
        }
       
        lineThickness = 5 - Math.sqrt((x2 - x1) *(x2-x1) + (y2 - y1) * (y2-y1))/10;
        if(lineThickness < 1){
            lineThickness = 1;   
        }

        for (var x = x1; x < x2; x++) {
            if (steep) {
                ctx.fillRect(y, x, lineThickness , lineThickness );
            } else {
                ctx.fillRect(x, y, lineThickness , lineThickness );
            }
            
            error += de;
            if (error >= 0.5) {
                y += yStep;
                error -= 1.0;
            }
        }



        lastX = mouseX;
        lastY = mouseY;

    }
}
<canvas id="canvas">
</canvas>
<img id="imagearea" src="https://media.istockphoto.com/photos/green-apple-with-leaf-and-cut-isolated-on-white-picture-id1141004606?k=6&m=1141004606&s=170667a&w=0&h=zwbN4lLc7MFb6f_aZ4npNL3i4Tgde-yINlYTztlI1QQ=" style="display: none;" />

<button> Reset </button>
KetZoomer
  • 2,701
  • 3
  • 15
  • 43
Peace
  • 45
  • 11
  • u need to use [toDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) – Burham B. Soliman Jan 20 '21 at 14:25
  • @BurhamB.Soliman could you give a working example in answer or JSfiddle because i haven't found any coordinates example in that site. – Peace Jan 20 '21 at 17:18

4 Answers4

3

To achieve what you're looking for, you need to store the min(x, y) coords for the (top, left) position and the max(x, y) coords for the (bottom, right) position, and you can't get the image on the same canvas if you are looking to remove the drawing on the area. use an HTML element with absolute position relative to the canvas for the area frame, attach an event to crop, and display the target area another one to remove it.

Here is a working example with a lot of comments, it should be clear. Click on the area to display the preview on the "x" to remove it with the drawing, it can handle multiple areas.

const source = "https://media.istockphoto.com/photos/green-apple-with-leaf-and-cut-isolated-on-white-picture-id1141004606?k=6&m=1141004606&s=170667a&w=0&h=zwbN4lLc7MFb6f_aZ4npNL3i4Tgde-yINlYTztlI1QQ=";
const container = document.querySelector("#container");
const canvas = container.querySelector("canvas");
const ctx = canvas.getContext("2d");
const resetButton = document.querySelector("button");

let lastDrawnArea = [[Infinity, Infinity], [0, 0]];
let image;
let painting = false;
let lastX = 0;
let lastY = 0;
let lineThickness = 1;

init();

async function init() {

  await loadDrawImage();
  
  // Start Event Listening
  canvas.onmousedown = function(e) {
    painting = true;
    ctx.fillStyle = "#ff0000";
    lastX = e.pageX - this.offsetLeft;
    lastY = e.pageY - this.offsetTop;
  };

  canvas.onmouseup = function(e){
      painting = false;
      // Set the drawing area  
      setDrawingArea();
  }

  canvas.onmousemove = function(e) {
      if (painting) {
          mouseX = e.pageX - this.offsetLeft;
          mouseY = e.pageY - this.offsetTop;

          // find all points between        
          var x1 = mouseX,
              x2 = lastX,
              y1 = mouseY,
              y2 = lastY;


          var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));
          if (steep){
              var x = x1;
              x1 = y1;
              y1 = x;

              var y = y2;
              y2 = x2;
              x2 = y;
          }
          if (x1 > x2) {
              var x = x1;
              x1 = x2;
              x2 = x;

              var y = y1;
              y1 = y2;
              y2 = y;
          }

          var dx = x2 - x1,
              dy = Math.abs(y2 - y1),
              error = 0,
              de = dy / dx,
              yStep = -1,
              y = y1;

          if (y1 < y2) {
              yStep = 1;
          }

          lineThickness = 5 - Math.sqrt((x2 - x1) *(x2-x1) + (y2 - y1) * (y2-y1))/10;
          if(lineThickness < 1){
              lineThickness = 1;   
          }

          for (var x = x1; x < x2; x++) {
              if (steep) {
                  ctx.fillRect(y, x, lineThickness , lineThickness );
              } else {
                  ctx.fillRect(x, y, lineThickness , lineThickness );
              }

              error += de;
              if (error >= 0.5) {
                  y += yStep;
                  error -= 1.0;
              }
          }

          lastX = mouseX;
          lastY = mouseY;
          
          // Set The min, max coordinate of the current drawing 
          // to define the current drawing area
          lastDrawnArea = [
            [// Top left min([x, y]) coords
              Math.min(lastDrawnArea[0][0], mouseX),
              Math.min(lastDrawnArea[0][1], mouseY)
            ],
            [// Bottom right max([x, y]) coords
              Math.max(lastDrawnArea[1][0], mouseX),
              Math.max(lastDrawnArea[1][1], mouseY)
            ]
          ]
      }
  }
}

async function loadDrawImage() {
  image = new Image();
  
  // Load the image
  await new Promise(resolve => {
    image.onload = resolve;
    image.src = source;
  });

  const [width, height] = [image.naturalWidth, image.naturalHeight];

  // Set the container and canvas size
  container.style.width = `${width}px`;
  container.style.height = `${height}px`;
  canvas.width = width;
  canvas.height = height;
  
  // Set the container in the background
  container.style.background = `url(${image.src})`;
}

function setDrawingArea(){
  const [TOP_LEFT, BOTTOM_RIGHT, X, Y] = [0, 1, 0, 1];
  const container = document.querySelector("#container");
  const template = document.querySelector("#areaTemplate");
  const area = template.content.firstElementChild.cloneNode(true);
  
  // You should replace this with the lineThickness 
  const offset = 10;
  
  // Get the area size
  const width = lastDrawnArea[BOTTOM_RIGHT][X] - lastDrawnArea[TOP_LEFT][X];
  const height = lastDrawnArea[BOTTOM_RIGHT][Y] - lastDrawnArea[TOP_LEFT][Y];
  
  area.style.left = `${lastDrawnArea[TOP_LEFT][X] - offset}px`;
  area.style.top = `${lastDrawnArea[TOP_LEFT][Y] - offset}px`;
  area.style.width = `${width + (offset * 2)}px`;
  area.style.height = `${height + (offset * 2)}px`;

  // Draw the template
  container.append(area);
  
  // Add the events
  area.onclick = previewArea; // Preveiw event
  area.querySelector("b").onclick = removeArea; // Remove event
  
  // Reset "lastDrawnArea" value
  lastDrawnArea = [[Infinity, Infinity], [0, 0]];  
  
}

function previewArea(e) {
  const preview = document.querySelector("#preview");
  const previewCanvas = preview.querySelector("canvas");
  const previewCtx = previewCanvas.getContext("2d");
  
  // Get the drawing area coords
  const area = e.target;
  const [x, y, width, height] = [
    parseFloat(area.style.left),
    parseFloat(area.style.top),
    parseFloat(area.style.width),
    parseFloat(area.style.height)
  ];
  
  // Draw the preview area
  previewCanvas.width = width;
  previewCanvas.height = height;
  previewCtx.drawImage(image,
    x, y, width, height,
    0, 0, width, height);
}

function removeArea(e) {
  const area = e.target.parentElement;
  const [x, y, width, height] = [
    parseFloat(area.style.left),
    parseFloat(area.style.top),
    parseFloat(area.style.width),
    parseFloat(area.style.height)
  ];
  
  ctx.clearRect(x, y, width, height);
  
  area.remove();
}

resetButton.onclick = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  document.querySelectorAll('.area').forEach(el => el.remove());
}
body {
  display: flex;
}

#container {
  position: relative;
}

.area {
  position: absolute;
  border: 2px solid #333;
  color: #333;
  cursor: pointer;
}

.area b {
  position: absolute;
  right: 0;
  top: 0;
  transform: translate(100%, -100%);
  color: red;
  cursor: pointer;
}
<div id="container">
  <canvas></canvas>
</div>

<div id="preview">
  <canvas></canvas>
</div>

<template id="areaTemplate">
  <div class="area">
    <b>x</b>
  </div>
</template>

<button> Reset All </button>

There is one problem with this example if two areas overlap the drawing will be removed at the intersection of both areas, to overcome this issue you need to keep each drawing on its own canvas (... a lot of work).

Last but not least, you can achieve the same result with ease and have better control over the canvas and its elements if you use a library like fabric js, take a look at this example of freedrawing, you will get the drawing coordinates for free, and you can still have everything on the same canvas (the image or whatever you need to add to canvas and all the drawing with no overlap ...), the initial learning curve may take some time but by the end of the day, you'll get a better understanding of HTML canvas overall.

Side note: the image you're using has a cross-origin limitation, you should use images from the same domain or from domains that allow cross-origin.

Fennec
  • 1,535
  • 11
  • 26
  • Sorry for the late response. fabric js is the good option fulfilling all my requirements. I tried `var imgSave = JSON.stringify(canvas);` for saving image and drawing. But by populating it on canvas on another page with `canvas.loadFromJSON();` . Its giving this `error Uncaught SyntaxError: Unexpected token u in JSON at position 0 at JSON.parse () at klass.loadFromJSON (fabric.js:13647)` Could you resolve this issue? – Peace Feb 14 '21 at 07:49
  • Sorry for the late response ... try `var canvasJSON = canvas.toJSON()` then `canvas.loadFromJSON(canvasJSON)` ... http://fabricjs.com/docs/fabric.Canvas.html#toJSON) – Fennec Feb 15 '21 at 05:43
  • Its returning `[object Object]` with `alert(canvasJSON )` could you please give a working example in jsfiddle? – Peace Feb 16 '21 at 07:46
  • You have a problem differentiating `JSON` as object and `JSON` as a string, take a look here https://medium.com/@punitkmr/difference-between-javascript-object-vs-json-object-80be4bc752c0 .... any way here is a working fiddle https://jsfiddle.net/v9euzpq4/1/ ... if you need more help please consider opening a new question you will get more help – Fennec Feb 16 '21 at 22:31
  • So should i send it to database as an object OR string e.g... `$('#sendToDatabaseWithFormSubmit').val(canvas.toJSON())` OR `$('#sendToDatabaseWithFormSubmit').val(JSON.stringify(canvas))`. – Peace Feb 18 '21 at 11:28
  • Read this http://benalman.com/news/2010/03/theres-no-such-thing-as-a-json/ ... knowing that "There is no such thing as a JSON object", you have to send it stringified ... You should consider compressing it try https://jgranstrom.github.io/zipson/ or the more complex https://stuk.github.io/jszip/ ... if you want to go the extra mile save it as a file (convert string to blob) and reference it in the database (saving only the file info in the db), this require a good understanding of bolbs https://javascript.info/blob ... don't forget about securing it https://stackoverflow.com/q/3710204/12594730 – Fennec Feb 19 '21 at 01:21
1

EDIT : Update with a working demo based on your fiddle

You may have to adapt this function to also include the thikness of the drawed lines (they may appear outside of the registered area). But like this, you have te position and the size of your drawed area.

You can now do a ROI on it if you want.


You can track the area drawed with a function like this one:

var drawedArea = [0,0,0,0];
function drawedAreaTrack(x, y) {
  // top left x
  if (drawedArea[0] === 0) {
  drawedArea[0] = x;
  } else {
  drawedArea[0] = Math.min(drawedArea[0], x);
  }
  

  // top left y
   if (drawedArea[1] === 0) {
  drawedArea[1] = y;
  } else {
  drawedArea[1] = Math.min(drawedArea[1], y);
  }

  // bottom right x
  drawedArea[2] = Math.max(drawedArea[2], x);

  // bottom right y
  drawedArea[3] = Math.max(drawedArea[3], y);
  
  console.log(drawedArea);
}

You could use those two point to get the total area drawed.

Here is a working example-> Fiddle :

http://jsfiddle.net/b90h6gaq/8/

Guimby
  • 31
  • 4
  • Could you give a working example because its returning `(4) [0, 0, 0, 0]` with `console.log(drawedArea );` – Peace Jan 23 '21 at 13:50
  • Hello, I have updated my answer with a working example. I hope that will be usefull to you. – Guimby Jan 25 '21 at 12:13
  • How can we get the coordinates of multiple areas paintedas we can get it in this [demo](https://rawgit.com/360Learning/jquery-select-areas/master/example/example.html). And with [Jcrop.js](https://stackoverflow.com/questions/9822371/select-a-portion-of-an-image-and-retrieve-its-coordinates-with-jquery#) – Peace Jan 25 '21 at 13:25
1

The 'onmousemove' is doing all the drawing, I would save all you need to an array then on some event send to the database or draw to another canvas...

Here is a very simple example. I'm keeping the code minimal to convey my point that you already have all you need, all that is needed is to store it in a variable, You can bring back the lineThickness, the rest of the calculations and logic later, that should not be a problem.

var button = document.getElementById("btn");
var canvas = document.getElementById("canvas1");
var canvas2 = document.getElementById("canvas2");

var ctx = canvas.getContext("2d");
var ctx2 = canvas2.getContext("2d");

var painting = false;
var coordinates = [];

canvas.onmousedown = function(e) {painting = true;}
canvas.onmouseup = function(e) {painting = false;}

canvas.onmousemove = function(e) {
  if (painting) {
    x = e.pageX - this.offsetLeft;
    y = e.pageY - this.offsetTop;
    coordinates.push({x, y})
    ctx.fillRect(x, y, 5, 5);
  }
}

button.onmousedown = function(e) {
  ctx.clearRect(0, 0, 300, 150);
  coordinates.forEach(coord => {
    ctx2.fillRect(coord.x, coord.y, 5, 5);
  });
};
<canvas id="canvas1" width=300 height=150></canvas>
<button id="btn"> Reset </button>
<canvas id="canvas2" width=300 height=150></canvas>

Here we have a new event on the button click, I'm clearing the initial canvas and drawing all the coordinates to a second canvas, we could also be sending that data to server to be stored that should not be a problem.

Helder Sepulveda
  • 15,500
  • 4
  • 29
  • 56
0

I don’t know if this is quite what you were thinking, but if you simply want the same image to appear on another webpage, you could use ctx.getImageData() to copy the drawing from the canvas as an object, convert it to json, then send it to the database. Then on the other end, turn it back into an object and use ctx.putImageData() to place it back on the new canvas.

Davedude
  • 160
  • 10