0

i followed Draw on HTML5 Canvas using a mouse to draw free share in convas, provided code snippet is not working properly, but when i try to use, it it not working properly. I mean position of cursor is different than in canvas.

Screenshot enter image description here

and here is code snippets


function init() {
  canvas = document.getElementById("whiteboard-canvas");
  ctx = canvas.getContext("2d");
  w = canvas.width;
  h = canvas.height;

  canvas.addEventListener(
    "mousemove",
    function (e) {
      findxy("move", e);
      console.log("on mouse move", e);
    },
    false
  );
#othere mouse event handler goes here..
}

function draw() {
  ctx.beginPath();
  ctx.moveTo(prevX, prevY);
  ctx.lineTo(currX, currY);
  ctx.strokeStyle = x;
  ctx.lineWidth = y;
  ctx.stroke();
  ctx.closePath();
}

function findxy(res, e) {
  if (res == "down") {
    prevX = currX;
    prevY = currY;
    currX = e.clientX - canvas.offsetLeft;;
    currY = e.clientY - canvas.offsetTop;;

    flag = true;
    dot_flag = true;
    if (dot_flag) {
      ctx.beginPath();
      ctx.fillStyle = x;
      ctx.fillRect(currX, currY, 2, 2);
      ctx.closePath();
      dot_flag = false;
    }
  }
  if (res == "up" || res == "out") {
    flag = false;
  }
  if (res == "move") {
    if (flag) {
      prevX = currX;
      prevY = currY;
      currX = e.clientX - canvas.offsetLeft;;
      currY = e.clientY - canvas.offsetTop;;
      draw();
    }
  }
}

My code snippets are almost identical as in the link.

Sandeep
  • 33
  • 1
  • 5
  • First of, JS comments don't start with `#` - thats invalid and you should notice that immediately when you start up your console in the browser. Secondly, from you snippet you I can't see anything inhereently wrong, except that `ctx.fillStyle = x;` makes no sense, `x` is usually a coordinate, so using it as a fill makes no sense but since I don't know where `x` is coming from, I can't say if that's causing thee issue. In short, add your variable definitions to this post as well (including `canvas`, `ctx`, etc...), as well as turning this into a SO snippet (its the `<>` document icon). – somethinghere Sep 20 '20 at 18:15
  • You must scale the mouse coordinates (which is in CSS pixels) to match the canvas resolution (canvas pixels). The example does not contain the code to do that and there are many caveats. Most basic is `canvasPixelX = currX * (canvas.width / canvas.getBoundingClientRect().width * devicePixelRatio)` and same for height. see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect for correct use` getBoundingClientRect`and https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio for devicePixelRatio – Blindman67 Sep 20 '20 at 18:32
  • @Blindman67 Thats not true, `offsetX` provides the correct value, no need for scaling. – somethinghere Sep 20 '20 at 19:39
  • @somethinghere `offsetX` and Y gives the top left corner, it does not provide any information on the size of canvas pixels compared to CSS pixels. Canvas resolution is independent of the canvas display size. In default state canvas coordinates are in canvas pixels not CSS pixels. If OP has a retina (or one of many HD devices ) display `devicePixelRatio` must also be used to scale correctly – Blindman67 Sep 20 '20 at 20:05
  • You are getting off in the woods here man. The issue is not that. There is no indication this has anything to do with DPI. You’re making the problem unnecessarily complicated. With no indicators of retina, all of this matches up. And even then, you can solve that with a simple context.scale() and forget anout it altogether. This guy is just starting out, defo no retine problem. – somethinghere Sep 20 '20 at 20:59
  • @somethinghere sorry of some issue, actually I'm python developer, that why, By mistakenly i added # for comment only in this question. Actually my problem was scaling, My actual canvas size is `width:300 Height: 150`, and also this is the actual reason behind facing problem. I've used css to increase height and width of canvas to match div size. Is there any way to fix this problem. – Sandeep Sep 21 '20 at 03:45
  • @somethinghere value of x is coming from another function, which i 've included here, due to limitation of stackoverflow. I hope you understand. – Sandeep Sep 21 '20 at 03:47
  • Set the width and height of the canvas directly with camvas.width and csnvas.height. Though again, since you use offset it isn’t necessarily to do with scaling. Your csnvas might not fill the screen but it should still work. – somethinghere Sep 21 '20 at 03:48

1 Answers1

0

CSS to canvas pixel coords

Canvas resolution, measured in pixels, and canvas page size, measured in CSS pixels, are independent.

When using the 2D API rendering is done in canvas pixels (not CSS pixels)

The mouse event holds coordinates as CSS pixels and as such will not always directly translate to canvas pixel coordinates.

One can get the page size of the canvas using getBoundingClientRect which can be used to scale from CSS pixels to canvas pixels.

However getBoundingClientRect includes the element's padding and border which must also be considered when scaling the CSS pixels to canvas pixels.

Note in my comments I talked about the device pixel ratio. I was wrong in my assessment of it's use, it is not required in this situation

Example

The example uses a default canvas (300 by 150px) and then uses CSS rules to scale it to the page, adding padding and a border in units other than CSS pixels. The canvas is also scaled such that it can be scrolled up and down.

Switch between the full page and snippet to see how scaling effects the example.

The function getInnerRect uses getBoundingClientRect and getComputedStyle to get the bounds of the canvas on the page in CSS pixels. From the top left of the top left pixel on the canvas to the bottom right of the bottom right most pixel.

The draw function then uses the inner rect (named bounds in example) to calculate the scales (x, y). Then offsets the mouse coordinates and scales them to draw to the canvas.

  • Note That the function mouseEvent captures the mouse coordinates from event.pageX and event.pageY

  • Note The function getBoundingClientRect is relative to the page scroll position. Thus when using it's return one must adjust the coordinates to take in acount the page scroll position. The example uses scrollX and scrollY to do this.

  • Note that the scale and offset can also be encapsulated as a 2D transformation. As the coordinates in the example will only change when the page is resized the transform can be calculated once on resize and applied to the canvas. Thus the draw function can use the CSS pixels to render directly to the canvas. This make the code in the example a little simpler. (See second snippet);

  • Note That because the transform will transform all rendering sizes it is necessary to compute the inverse scale of the transform. This is used to scale the line width in the second example. If this is not done then the line width rendered will change as the page size changes.

const ctx = canvas.getContext("2d");
var bounds;
const mouse = {x:0,y:0,b:0,px:0,py:0}; // p for previouse b for button
function mouseEvent(event) {
    mouse.px = mouse.x;
    mouse.py = mouse.y;
    mouse.x = event.pageX;
    mouse.y = event.pageY;
    if (event.type === "mousedown") {
        mouse.b = true;
        canvas.classList.add("drawing");
    } else if (event.type === "mouseup" || event.type === "mouseout") { 
        mouse.b = false;
        canvas.classList.remove("drawing");
    }
    draw();
}
addEventListener("mousemove", mouseEvent);
addEventListener("mousedown", mouseEvent);
addEventListener("mouseup", mouseEvent);
addEventListener("mouseout", mouseEvent);

const CSSPx2Number = cssPx => Number(cssPx.replace("px",""));
function getInnerRect(element) {
    var top, left, right, bottom;
    const bounds = element.getBoundingClientRect();
    const canStyle = getComputedStyle(element);
    
    left  = CSSPx2Number(canStyle.paddingLeft);
    left += CSSPx2Number(canStyle.borderLeftWidth);
    
    top  = CSSPx2Number(canStyle.paddingTop);
    top += CSSPx2Number(canStyle.borderTopWidth);
    
    right  = CSSPx2Number(canStyle.paddingRight);
    right += CSSPx2Number(canStyle.borderRightWidth);
    
    bot  = CSSPx2Number(canStyle.paddingBottom);
    bot += CSSPx2Number(canStyle.borderBottomWidth);       
    
    return {
        top: bounds.top + top  + scrollY,
        left: bounds.left + left  + scrollX,
        bottom: bounds.bottom + bot + scrollY,
        right: bounds.right + right + scrollX,
        width: bounds.width - left - right,
        height: bounds.height - top - bot,
    };


}
function resizeEvent() { 
    bounds = getInnerRect(canvas);
    widthText.textContent = "Canvas page size: " + bounds.width.toFixed(1) + " by " + bounds.height.toFixed(1) + "px Canvas width: " + canvas.width + " by " + canvas.height + "px";
    ctx.lineWidth = 3;
    ctx.strokeStyle = "black";
    ctx.lineCap = ctx.lineJoin = "round";
}
addEventListener("resize",resizeEvent);
resizeEvent();

function draw() {
    if (mouse.b) {
        const xScale = canvas.width / bounds.width;
        const yScale = canvas.height / bounds.height;
        
        const x = (mouse.x - bounds.left) * xScale;
        const y = (mouse.y - bounds.top) * yScale;
        const px = (mouse.px - bounds.left) * xScale;
        const py = (mouse.py - bounds.top) * yScale;

        ctx.beginPath();
        ctx.lineTo(px, py);
        ctx.lineTo(x, y);
        ctx.stroke();
    }
}
canvas {
    position: absolute;
    top: 5%;
    left: 10%;
    width: 80%;
    height: 170%;
    border: 1pc solid blue;
    padding: 1%;
    cursor: crosshair;
}
.drawing { cursor: none; }
.info { 
   position: absolute;
   font-family: arial;
   font-size: x-small;
   pointer-events: none; 
   text-align: center;
   background: white;
   border: 1px solid black;

}
#widthText {
  top: 2%;
  left: 20%;
  width: 60%;
}
<!-- default size of canvas is 300 by 150 -->
<canvas id="canvas"></canvas>
<div id="widthText" class="info"></div>

Using transform

const ctx = canvas.getContext("2d");
const matrix = [1,0,0,1,0,0];
const mouse = {x:0,y:0,b:0,px:0,py:0}; // p for previouse b for button
function mouseEvent(event) {
    mouse.px = mouse.x;
    mouse.py = mouse.y;
    mouse.x = event.pageX;
    mouse.y = event.pageY;
    if (event.type === "mousedown") {
        mouse.b = true;
        canvas.classList.add("drawing");
    } else if (event.type === "mouseup" || event.type === "mouseout") { 
        mouse.b = false;
        canvas.classList.remove("drawing");
    }
    draw();
}
addEventListener("mousemove", mouseEvent);
addEventListener("mousedown", mouseEvent);
addEventListener("mouseup", mouseEvent);
addEventListener("mouseout", mouseEvent);



function draw() {
    if (mouse.b) {
        ctx.beginPath();  // using CSS pixels
        ctx.lineTo(mouse.px, mouse.py);
        ctx.lineTo(mouse.x, mouse.y);
        ctx.stroke();
    }
}


const CSSPx2Number = cssPx => Number(cssPx.replace("px",""));
function getInnerRect(element) {

    var top, left, right, bottom;
    const bounds = element.getBoundingClientRect();
    const canStyle = getComputedStyle(element);
    
    left  = CSSPx2Number(canStyle.paddingLeft);
    left += CSSPx2Number(canStyle.borderLeftWidth);
    
    top  = CSSPx2Number(canStyle.paddingTop);
    top += CSSPx2Number(canStyle.borderTopWidth);
    
    right  = CSSPx2Number(canStyle.paddingRight);
    right += CSSPx2Number(canStyle.borderRightWidth);
    
    bot  = CSSPx2Number(canStyle.paddingBottom);
    bot += CSSPx2Number(canStyle.borderBottomWidth);      
    
    const canvasInner = {
        top: bounds.top + top + scrollY,
        left: bounds.left + left  + scrollX,
        bottom: bounds.bottom + bot + scrollY,
        right: bounds.right + right + scrollX,
        width: bounds.width - left - right,
        height: bounds.height - top - bot,

    };
    const xScale = canvas.width / canvasInner.width;
    const yScale = canvas.height / canvasInner.height;

    // use mean of x,y scale to get inverse scale
    canvasInner.invScale = 1 / ((xScale + yScale) / 2);

    // set scale 
    matrix[0] = xScale;
    matrix[3] = yScale;
    
    // set offset in canvas pixels
    matrix[4] = -canvasInner.left * xScale;
    matrix[5] = -canvasInner.top * yScale;    

    ctx.setTransform(...matrix);
    return canvasInner;

}
function resizeEvent() { 
    const bounds = getInnerRect(canvas);
    widthText.textContent = "Canvas page size: " + bounds.width.toFixed(1) + " by " + bounds.height.toFixed(1) + "px Canvas width: " + canvas.width + " by " + canvas.height + "px";
    ctx.lineWidth = 3 * bounds.invScale;  // correcting line width for render
    ctx.strokeStyle = "black";
    ctx.lineCap = ctx.lineJoin = "round";  
}
addEventListener("resize",resizeEvent);
resizeEvent();
canvas {
    position: absolute;
    top: 5%;
    left: 10%;
    width: 80%;
    height: 170%;
    border: 1pc solid green;
    padding: 1%;
    cursor: crosshair;
}
.drawing { cursor: none; }
.info { 
   position: absolute;
   font-family: arial;
   font-size: x-small;
   pointer-events: none; 
   text-align: center;
   background: white;
   border: 1px solid black;
}
#widthText {
  top: 2%;
  left: 20%;
  width: 60%;
}
<!-- default size of canvas is 300 by 150 -->
<canvas id="canvas"></canvas>
<div id="widthText" class="info"></div>
Blindman67
  • 51,134
  • 11
  • 73
  • 136