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>