1
votes

I'm struggling to find a method/strategy to handle drawing with stored coordinates and the variation in canvas dimensions across various devices and screen sizes for my web app.

Basically I want to display an image on the canvas. The user will mark two points on an area of image and the app records where these markers were placed. The idea is that the user will use the app every odd day, able to see where X amount of previous points were drawn and able to add two new ones to the area mentioned in places not already marked by previous markers. The canvas is currently set up for height = window.innerHeight and width = window.innerWidth/2.

My initial thought was recording the coordinates of each pair of points and retrieving them as required so they can be redrawn. But these coordinates don't match up if the canvas changes size, as discovered when I tested the web page on different devices. How can I record the previous coordinates and use them to mark the same area of the image regardless of canvas dimensions?

2

2 Answers

3
votes

Use percentages! Example:

So lets say on Device 1 the canvas size is 150x200,
User puts marker on pixel 25x30. You can do some math to get the percentage.
And then you SAVE that percentage, not the location,
Example:

let userX = 25; //where the user placed a marker
let canvasWidth = 150;
//Use a calculator to verify :D
let percent = 100 / (canvasWidth / userX); //16.666%

And now that you have the percent you can set the marker's location based on that percent.
Example:

let markerX = (canvasWidth * percent) / 100; //24.999
canvasWidth = 400; //Lets change the canvas size!
markerX = (canvasWidth * percent) / 100; //66.664;

And voila :D just grab the canvas size and you can determine marker's location every time.

0
votes

Virtual Canvas

You must define a virtual canvas. This is the ideal canvas with a predefined size, all coordinates are relative to this canvas. The center of this virtual canvas is coordinate 0,0

When a coordinate is entered it is converted to the virtual coordinates and stored. When rendered they are converted to the device screen coordinates.

Different devices have various aspect ratios, even a single device can be tilted which changes the aspect. That means that the virtual canvas will not exactly fit on all devices. The best you can do is ensure that the whole virtual canvas is visible without stretching it in x, or y directions. this is called scale to fit.

Scale to fit

To render to the device canvas you need to scale the coordinates so that the whole virtual canvas can fit. You use the canvas transform to apply the scaling.

To create the device scale matrix

const vWidth = 1920;  // virtual canvas size
const vHeight = 1080;

function scaleToFitMatrix(dWidth, dHeight) {
    const scale = Math.min(dWidth / vWidth, dHeight / vHeight);
    return [scale, 0, 0, scale, dWidth / 2, dHeight / 2];
}

const scaleMatrix = scaleToFitMatrix(innerWidth, innerHeight);

Scale position not pixels

Point is defined as a position on the virtual canvas. However the transform will also scale the line widths, and feature sizes which you would not want on very low or high res devices.

To keep the same pixels size but still render in features in pixel sizes you use the inverse scale, and reset the transform just before you stroke as follows (4 pixel box centered over point)

const point = {x : 0, y : 0}; // center of virtual canvas
const point1 = {x : -vWidth / 2, y : -vHeight / 2}; // top left of virtual canvas
const point2 = {x : vWidth / 2, y : vHeight / 2}; // bottom right of virtual canvas

function drawPoint(ctx, matrix, vX, vY, pW, pH) { // vX, vY virtual coordinate
     const invScale = 1 / matrix[0]; // to scale to pixel size
     ctx.setTransform(...matrix); 
     ctx.lineWidth = 1; // width of line
     ctx.beginPath();
     ctx.rect(vX - pW * 0.5 * invScale, vY - pH * 0.5 * invScale, pW * invScale, pH * invScale);
     ctx.setTransform(1,0,0,1,0,0); // reset transform for line width to be correct
     ctx.fill();
     ctx.stroke();
}
const ctx = canvas.getContext("2d");
drawPoint(ctx, scaleMatrix, point.x, point.y, 4, 4);

Transforming via CPU

To convert a point from the device coordinates to the virtual coordinates you need to apply the inverse matrix to that point. For example you get the pageX, pageY coordinates from a mouse, you convert using the scale matrix as follows

function pointToVirtual(matrix, point) {
    point.x = (point.x - matrix[4]) / matrix[0];
    point.y = (point.y - matrix[5]) / matrix[3];
    return point;
}

To convert from virtual to device

function virtualToPoint(matrix, point) {
    point.x = (point.x * matrix[0]) + matrix[4];
    point.y = (point.y * matrix[3]) + matrix[5];
    return point;
}

Check bounds

There may be an area above/below or left/right of the canvas that is outside the virtual canvas coordinates. To check if inside the virtual canvas call the following

function isInVritual(vPoint) {
     return ! (vPoint.x < -vWidth / 2 || 
         vPoint.y < -vHeight / 2 ||
         vPoint.x >= vWidth / 2 ||
         vPoint.y >= vHeight / 2);
}
const dPoint = {x: page.x, y: page.y};  // coordinate in device coords
if (isInVirtual(pointToVirtual(scaleMatrix,dPoint))) {
     console.log("Point inside");
} else {
     console.log("Point out of bounds.");
}

Extra points

  • The above assumes that the canvas is aligned to the screen.
  • Some devices will be zoomed (pinch scaled). You will need to check the device pixel scale for the best results.
  • It is best to set the virtual canvas size to the max screen resolution you expect.
  • Always work in virtual coordinates, only convert to device coordinates when you need to render.