I have been asked to get the mouse coordinates in a game made in html5 with canvas.
As a first test, try reading the mouse position with the function below. But this function only reads the mouse position taking into account the dimensions of the canvas.
What happens is that the game has a larger stage than the canvas and this function does not show me the real location of the character on the stage.
I was doing a search and noticed that "behind" the canvas exists on a map (.png) with pixel dimensions already established. The canvas works like the camera to see a portion of the map.
Will it be possible to adapt my function to read the dimensions of the map and then locate the actual coordinates of the player?
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
canvas.addEventListener("click", function(e) {
var cRect = canvas.getBoundingClientRect();
var scaleX = canvas.width / cRect.width;
var scaleY = canvas.height / cRect.height;
var canvasX = Math.round((e.clientX - cRect.left) * scaleX);
var canvasY = Math.round((e.clientY - cRect.top) * scaleY);
console.log("X: "+canvasX+", Y: "+canvasY);
});
This function will only give me the position of the mouse based on the size of the canvas but the map is larger, I leave here an explanatory image.
I hope you have understood me. Thanks in advance.
World <=> View
To establish the vernacular, the terms used are
World: the coordinate system (in pixels) of world / playfield / (red box).
View: The coordinate system (in canvas pixels) of canvas / camera / (blue box).
As pointed out in the comments. You need the view origin. That is the coordinates that the top left of the canvas in world space.
You also need to know the view scale. That is the size of the canvas in relationship to the world.
Required information
const world = {width: 2048, height: 1024}; // Red box in pixels
const view = { // blue box
origin: {x: 500, y: 20}, // in world scale (pixels on world)
scale: {width: 1, height: 1}, // scale of pixels (from view to world)
}
Without this information you can not do the conversion. It must exist as it is required to render world content to the canvas.
Note that if the scales are 1 they may only be inferred in the canvas rendering system. If you can not find a scale then use 1.
Note This answer assumes there is no rotation of the view.
View => World
The following function will convert from view coordinates to world coordinates.
function viewToWorld(x, y) { // x,y pixel coordinates on canvas
return {
x: x * view.scale.width + view.origin.x,
y: y * view.scale.height + view.origin.y
}; // return x,y pixel coordinates in world
}
To use in a mouse event where the client is the canvas
function mouseEvent(event) {
// get world (red box) coords
const worldCoord = viewToWorld(event.clientX, event.clientY);
// normalize
worldCoord.x /= world.width;
worldCoord.y /= world.height;
}
World => View
You can reverse the conversion. That is move from world coordinates to view coordinates with the following functions.
function normalWorldToView(x, y) { // x,y normalized world coordinates
return {
x: (x * world.width - view.origin.x) / view.scale.width,
y: (y * world.height - view.origin.y) / view.scale.height
}; // return x,y pixel on canvas (view)
}
and in pixels
function worldToView(x, y) { // x,y world coordinates in pixels
return {
x: (x - view.origin.x) / view.scale.width,
y: (y - view.origin.y) / view.scale.height
}; // return x,y pixel on canvas (view)
}
Related
I have a requirement to make some annotations on an image. This image is scalable (can be zoomed in and out). Now the challenge is that the annotations should also move with the scaling. How can I achieve this? I understand that 'direction' of zooming depends on the point considered as 'centre' when zooming, so assuming that this 'centre' is the absolute centre of the iamge container (width/2, height/2), how do I get the coordinates of the same point on image after zooming?
As an example, consider the following two images:
Image-1 (Normal scale):
Image-2 (Zoomed-in):
If I know the coordinates of the red point in Image-1 (which is at normal scale), how do I get the coordinates (x,y) of the same red point in Image-2? Note that the image container's width and height will remain same throughout the zooming process.
This function should return your new X and Y measured from the left top of the image.
Bear in mind, that the new coordinates can be outside of the width/height of your image, as the point you picked might be "zoomed off the edge"
/**
* width: integer, width of image in px
* height: integer, height of image in px;
* x: integer, horizontal distance from left
* y: integer, vertical distance from top
* scale: float, scale factor (1,5 = 150%)
*/
const scaleCoordinates = (width, height, x, y, scale) =>{
const centerX = width/2;
const centerY = height/2;
const relX = x - centerX;
const relY = y - centerY;
const scaledX = relX * scale;
const scaledY= relY * scale;
return {x: scaledX + centerX, y: scaledY + centerY};
}
console.log(scaleCoordinates(100,100,25,50, 1.2));
First, you'd want to determine the coordinates of the annotation with respect to the center of the image.
So for example on an image of 200 x 100, the point (120,60) with the origin in the left top corner would be (20,-10) when you take the center of the image as your origin.
If you scale the image 150%, your new coordinates would be those coordinates multiplied by 1,5 (=150%).
In our example that would be 30, -15.
Than you can calculate that back to absolute values, with the original point of origin
I'm trying to capture an image in the browser using html2canvas. Capturing an image of the whole browser works. But I need to specify x,y start and end coordinates that I want to capture. In the docs I saw that html2canvas can accept x,y coordinates:
x: Default: Element x-offset Description: Crop canvas x-coordinate
y: Default: Element y-offset Description: Crop canvas y-coordinate
Passing my x,y coordinates to those parameters just captures the whole window.
So instead, I tried capturing the whole window, and then cropping an area from it using drawImage() (found at some other stackoverflow post, not sure which):
function snapImage(x1,y1,x2,y2, e){
html2canvas(document.body).then(function(canvas) {
// calc the size -- but no larger than the html2canvas size!
var width = Math.min(canvas.width,Math.abs(x2-x1));
var height = Math.min(canvas.height,Math.abs(y2-y1));
// create a new avatarCanvas with the specified size
var avatarCanvas = document.createElement('canvas');
avatarCanvas.width=width;
avatarCanvas.height=height;
avatarCanvas.id = 'avatarCanvas';
// put avatarCanvas into document body
document.body.appendChild(avatarCanvas);
// use the clipping version of drawImage to draw
// a clipped portion of html2canvas's canvas onto avatarCanvas
var avatarCtx = avatarCanvas.getContext('2d');
avatarCtx.drawImage(canvas,x1,y1,width,height,0,0,width,height);
});
}
This draws a shifted image with a wrong offset. For example, given the following website:
image taken from the example at: https://github.com/niklasvh/html2canvas/tree/master/examples
I mark "pluot?" area to snap it:
see the dotted rectangle
The dotted rectangle is drawn using js, given the mouse coordinates in 2 events: onmousedown and onmouseup. Because the rectangle is drawn correctly, I assume my coordinates are correct. But when I pass these coordinates to the function snapImage() above, I get the following captured image:
Looks like there's an offset. Maybe the start coordinates drawImage() operates on differ from my canvas start coordinates?
EDIT:
Turns out that my code works when I'm on 100% zoom. It doesn't though when I zoom in / out.
I guess this is because you get x and y from event with clientX and clientY. Use pageX and pageY instead. Have a look at this jsFiddle
let startX, startY;
document.getElementsByTagName('body')[0].addEventListener('mousedown', function(event) {
console.log("ok");
startX = Math.floor(event.pageX);
startY = Math.floor(event.pageY);
});
document.getElementsByTagName('body')[0].addEventListener('mouseup', function(event) {
snapImage(Math.min(event.pageX, startX), Math.min(event.pageY, startY), Math.max(event.pageX, startX), Math.max(event.pageY, startY));
});
function snapImage(x1,y1,x2,y2, e){
console.log(x1, x2, y1, y2);
html2canvas(document.body).then(function(canvas) {
// calc the size -- but no larger than the html2canvas size!
var width = Math.min(canvas.width,Math.abs(x2-x1));
var height = Math.min(canvas.height,Math.abs(y2-y1));
// create a new avatarCanvas with the specified size
var avatarCanvas = document.createElement('canvas');
avatarCanvas.width=width;
avatarCanvas.height=height;
avatarCanvas.id = 'avatarCanvas';
// put avatarCanvas into document body
document.body.appendChild(avatarCanvas);
// use the clipping version of drawImage to draw
// a clipped portion of html2canvas's canvas onto avatarCanvas
var avatarCtx = avatarCanvas.getContext('2d');
avatarCtx.drawImage(canvas,x1,y1,width,height,0,0,width,height);
});
}
Turns out there's a built-in chrome function captureVisibleTab that captures the image of the active tab. So I ended up using that instead of html2canvas. I got help from the Copyfish Chrome Extension. Github code here: Copyfish.
Here's my code:
Listener:
//listener in background.js which invokes the screen capture
chrome.tabs.captureVisibleTab(function (dataURL) {
sendResponse({
dataURL: dataURL,
});
});
Receiver:
//receiver in content.js which gets the captured image and crops it accordingly
function(response){
var img = new Image();
img.src = response.dataURL;
var dpf = window.innerWidth / img.width;
var scaleFactor = 1 / dpf,
sx = Math.min(x1, x2) * scaleFactor,
sy = Math.min(y1, y2) * scaleFactor,
width = Math.abs(x2 - x1),
height = Math.abs(y2 - y1);
// create a new avatarCanvas with the specified size
var avatarCanvas = document.createElement('canvas');
avatarCanvas.width = width;
avatarCanvas.height = height;
avatarCanvas.id = 'avatarCanvas';
// put avatarCanvas into document body
document.body.appendChild(avatarCanvas);
// use the clipping version of drawImage to draw
var avatarCtx = avatarCanvas.getContext('2d');
avatarCtx.drawImage(img, sx, sy, scaledWidth, scaledHeight, 0, 0, width, height);
}
x,y coordinates are taken by e.clientX and e.clientY respectively.
This method is zoom- and resolution- proof.
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?
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.
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.
I was searching for a couple of days how to solve this problem and I decided to ask here for the help.
The thing is, I made a canvas that is 640x480px and preloaded it with an image.
After I used the mouse to select the area that is going to be zoomed in (I used a draggable square, same type like if you would press mouse on windows desktop and select multiple icons) I changed the canvas to be 480x480px (since the zoom in part of the photo is a square), and within that new canvas I have displayed a new zoomed in part of that photo.
My question is: since I am doing all of this so I can zoom in on someones face so I can get a user to more precisely place dots on eyes and mouth (face recognition software like thing) how can I get real coordinates of these dots? In respect to an original image and original canvas that was 640x480px.
Everything is in pure javascript no jQuery, and without any js libraries
Thank you
The same way you'd convert between Fahrenheit and Celsius: decide on a reference point and adjust your scale. The reference point is easy: (0, 0) in the zoomed context is the upper left corner of the selected area in the original context. For the scale, convert the zoomed click point from pixels to percentages. A click at (120, 240) is a click at (25%, 50%). Then multiply that percentage by the size of the selected area and add the reference point offset.
// Assume the user selected in the 640x480 canvas a 223x223
// square whose upper left corner is (174, 36),
let zoomArea = {x: 174, y: 36, size: 223};
// and then clicked (120, 260) in the new 480x480 canvas.
let pointClicked = {x: 120, y: 260};
function getOriginalCoords(area, clicked) {
const ZOOMED_SIZE = 480;
// Get the coordinates of the clicked point in the zoomed
// area, on a scale of 0 to 1.
let clickedPercent = {
x: clicked.x / ZOOMED_SIZE,
y: clicked.y / ZOOMED_SIZE
};
return {
x: clickedPercent.x * area.size + area.x,
y: clickedPercent.y * area.size + area.y
};
}
console.log(getOriginalCoords(zoomArea, pointClicked));
At the end I did it this way
// get bounding rect of canvas
var rectangle = canvas.getBoundingClientRect();
// position of the point in respect to new 480x480 canvas
var xPositionZoom = e.clientX - crosshairOffSet - rectangle.left;
var yPositionZoom = e.clientY - crosshairOffSet - rectangle.top;
// position of the point in respect to original 640x480 canvas
var xPosition = rect.startX + (rect.w * (xPositionZoom / canvas.width));
var yPosition = rect.startY + (rect.h * (yPositionZoom / canvas.height));
I've been attempting to display the positions of players in a 3D game, on a web page, from an overhead view perspective (like a minimap). I'm simply superimposing markers (via SVG) on a 1024x1024 image of the game's level (an overhead view, taken from the game itself). I'm only concerned with the x, y coordinates, since I'm not using z in an overhead view.
The game's world has equal min/max coordinates for both x and y: -4096 to 4096. The 0, 0 coordinate is the center of the world.
To make things even more interesting, the initial position of a game level, within the game world, is arbitrary. So, for example, the upper-left world coordinate for the particular level I've been testing with is -2440, 3383.
My initial confusion comes from the fact that the 0,0 coordinate in a web page is the top-left, versus center in the world space.
How do I correctly convert the 3D world space coordinates to properly display in a web page viewport of any dimension?
Here's what I've tried (I've been attempting to use the viewbox attribute in svg to handle the upper left world coordinate offset)
scalePosition: function (targetWidth, targetHeight) {
// in-game positions
var gamePosition = this.get('pos');
var MAX_X = 8192,
MAX_Y = 8192;
// Flip y
gamePosition.y = -gamePosition.y;
// Ensure game coordinates are only positive values
// In game = -4096 < x|y < 4096 (0,0 = center)
// Browser = 0 < x|y < 8192 (0,0 = top-left)
gamePosition.x += 4096;
gamePosition.y += 4096;
// Target dimenions
targetWidth = (targetWidth || 1024),
targetHeight = (targetHeight || 1024);
// Find scale between game dimensions and target dimensions
var xScale = MAX_X / targetWidth,
yScale = MAX_Y / targetHeight;
// Convert in-game coords to target coords
var targetX = gamePosition.x / xScale,
targetY = gamePosition.y / yScale;
return {
x: targetX,
y: targetY
};
},