I have an Image loaded on canvas. I want to find the distance between to points on this image. But because the image can be bit small user can zoom in (scale image) and then click on two points find the distance. To get points clicked on any image is the easy part
myImage.on('mousedown', function(e){
console.log(e.layerX + " "+e.layerY);
}
though I am not quite sure that layerX is the better messurment. What If the user has scaled the image. Won't that mean that the distance will be bigger? So what is the correct way? Is it to scale down the layerX and layerY before calculating the distance?
myImage.on('mousedown', function(e){
var x, y;
x = e.layerX / this.getScaleX();
y = e.layerY / this.getScaleY();
calculateDistance(x, y);
});
Would the above calculate a more accurate distance? At a second level I want to to take this distance and then recalibrate another picture according to the distance I calibrated (doing the opposite thing). use case:
Users wants to take two points but scales the image up. The destination is calculated and the image is scaled down to its original state. User add a new picture on canvas, which shows something that I know its physical width and height. So system re-calculates pixel width and height of the image based on the calibration done before. So correct procedure would be
Take two points (on scaled up image)
Find distance
Divide distance with scale ratio
Use the distance
or
Take two points (on scaled up image)
divide layerX and layerY with scale ratio
calculate distance
Use the distance
Which one is more correct?
I would get the two points and divided by the image ratio.
See JSBin
html
<div id="holder">
<img src="http://30.media.tumblr.com/tumblr_lzq1rfNV7N1qhze6fo1_500.jpg" />
</div>
css
#holder {
width: 100%;
height:auto;
}
#holder img {
width: 100%;
height:auto;
}
JavaScript
var last = {
x:0,
y:0
};
//dim is the original image dimensions
var dim = {
width:500,
height:427
};
var img = $("#holder img");
img.bind('mousedown',checkpoint);
function checkpoint(e){
// ratio is the scaled image width/height
// divided by the original width/height
var ratio = {
x:img.width()/dim.width,
y:img.height()/dim.height
};
point = {
x:e.clientX / ratio.x,
y:e.clientY / ratio.y
};
var dis = distance(last.x,last.y,point.x,point.y);
last=point;
console.log(dis);
}
function distance(x1, y1, x2,y2){
var dx = x2-x1;
var dy = y2-y1;
return Math.sqrt(dx*dx+dy*dy);
}
updated per comment
Related
I'm playing with drawing on html canvas and I'm little confused of how different coordinate systems actually works. What I have learned so far is that there are more coordinate systems:
canvas coordinate system
css coordinate system
physical (display) coordinate system
So when I draw a line using CanvasRenderingContext2D
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(3, 1);
ctx.lineTo(3, 5);
ctx.stroke();
before drawing pixels to the display, the path needs to be
scaled according to the ctx transformation matrix (if any)
scaled according to the ratio between css canvas element dimensions (canvas.style.width and canvas.style.height) and canvas drawing dimensions (canvas.width and canvas.height)
scaled according to the window.devicePixelRatio (hi-res displays)
Now when I want to draw a crisp line, I found that there are two things to fight with. The first one is that canvas uses antialiasing. So when I draw a line of thikness 1 at integer coordinates, it will be blurred.
To fix this, it needs to be shifted by 0.5 pixels
ctx.moveTo(3.5, 1);
ctx.lineTo(3.5, 5);
The second thing to consider is window.devicePixelRatio. It is used to map logical css pixels to physical pixels. The snadard way how to adapt canvas to hi-res devices is to scale to the ratio
const ratio = window.devicePixelRatio || 1;
const clientBoundingRectangle = canvas.getBoundingClientRect();
canvas.width = clientBoundingRectangle.width * ratio;
canvas.height = clientBoundingRectangle.height * ratio;
const ctx = canvas.getContext('2d');
ctx.scale(ratio, ratio);
My question is, how is the solution of the antialiasing problem related to the scaling for the hi-res displays?
Let's say my display is hi-res and window.devicePixelRatio is 2.0. When I apply context scaling to adapt canvas to the hi-res display and want to draw the line with thickness of 1, can I just ignore the context scale and draw
ctx.moveTo(3.5, 1);
ctx.lineTo(3.5, 5);
which is in this case effectively
ctx.moveTo(7, 2);
ctx.lineTo(7, 10);
or do I have to consider the scaling ratio and use something like
ctx.moveTo(3.75, 1);
ctx.lineTo(3.75, 5);
to get the crisp line?
Antialiasing can occur both in the rendering on the canvas bitmap buffer, at the time you draw to it, and at the time it's displayed on the monitor, by CSS.
The 0.5px offset for straight lines works only for line widths that are odd integers. As you hinted to, it's so that the stroke, that can only be aligned to the center of the path, and thus will spread inside and outside of the actual path by half the line width, falls on full pixel coordinates. For a comprehensive explanation, see this previous answer of mine.
Scaling the canvas buffer to the monitor's pixel ratio works because on high-res devices, multiple physical dots will be used to cover a single px area. This allows to have more details e.g in texts, or other vector graphics. However, for bitmaps this means the browser has to "pretend" it was bigger in the first place. For instance a 100x100 image, rendered on a 2x monitor will have to be rendered as if it was a 200x200 image to have the same size as on a 1x monitor. During that scaling, the browser may yet again use antialiasing, or another scaling algorithm to "create" the missing pixels.
By directly scaling up the canvas by the pixel ratio, and scaling it down through CSS, we end up with an original bitmap that's the size it will be rendered, and there is no need for CSS to scale anything anymore.
But now, your canvas context is scaled by this pixel ratio too, and if we go back to our straight lines, still assuming a 2x monitor, the 0.5px offset now actually becomes a 1px offset, which is useless. A lineWidth of 1 will actually generate a 2px stroke, which doesn't need any offset.
So no, don't ignore the scaling when offsetting your context for straight lines.
But the best is probably to not use that offset trick at all, and instead use rect() calls and fill() if you want your lines to fit perfectly on pixels.
const canvas = document.querySelector("canvas");
// devicePixelRatio may not be accurate, see below
setCanvasSize(canvas);
function draw() {
const dPR = devicePixelRatio;
const ctx = canvas.getContext("2d");
// scale() with weird zoom levels may produce antialiasing
// So one might prefer to do the scaling of all coords manually:
const lineWidth = Math.round(1 * dPR);
const cellSize = Math.round(10 * dPR);
for (let x = cellSize; x < canvas.width; x += cellSize) {
ctx.rect(x, 0, lineWidth, canvas.height);
}
for (let y = cellSize; y < canvas.height; y += cellSize) {
ctx.rect(0, y, canvas.width, lineWidth);
}
ctx.fill();
}
function setCanvasSize(canvas) {
// We resize the canvas bitmap based on the size of the viewport
// while respecting the actual dPR
// Thanks to gman for the reminder of how to suppport all early impl.
// https://stackoverflow.com/a/65435847/3702797
const observer = new ResizeObserver(([entry]) => {
let width;
let height;
const dPR = devicePixelRatio;
if (entry.devicePixelContentBoxSize) {
width = entry.devicePixelContentBoxSize[0].inlineSize;
height = entry.devicePixelContentBoxSize[0].blockSize;
} else if (entry.contentBoxSize) {
if ( entry.contentBoxSize[0]) {
width = entry.contentBoxSize[0].inlineSize * dPR;
height = entry.contentBoxSize[0].blockSize * dPR;
} else {
width = entry.contentBoxSize.inlineSize * dPR;
height = entry.contentBoxSize.blockSize * dPR;
}
} else {
width = entry.contentRect.width * dPR;
height = entry.contentRect.height * dPR;
}
canvas.width = width;
canvas.height = height;
canvas.style.width = (width / dPR) + 'px';
canvas.style.height = (height / dPR) + 'px';
// we need to redraw
draw();
});
// observe the scrollbox size changes
try {
observer.observe(canvas, { box: 'device-pixel-content-box' });
}
catch(err) {
observer.observe(canvas, { box: 'content-box' });
}
}
canvas { width: 300px; height: 150px; }
<canvas></canvas>
Preventing anti-aliasing requires that the pixels of the canvas, which is a raster image, are aligned with the pixels of the screen, which can be done by multiplying the canvas size by the devicePixelRatio, while using the CSS size to hold the canvas to its original size:
canvas.width = pixelSize * window.devicePixelRatio;
canvas.height = pixelSize * window.devicePixelRatio;
canvas.style.width = pixelSize + 'px';
canvas.style.height = pixelSize + 'px';
You can then use scale on the context, so that the drawn images won't be shrunk by higher devicePixelRatios. Here I am rounding so that lines can be crisp on ratios that are not whole numbers:
let roundedScale = Math.round(window.devicePixelRatio);
context.scale(roundedScale, roundedScale);
The example then draws a vertical line from the center top of one pixel to the center top of another:
context.moveTo(100.5, 10);
context.lineTo(100.5, 190);
One thing to keep in mind is zooming. If you zoom in on the example, it will become anti-aliased as the browser scales up the raster image. If you then click run on the example again, it will become crisp again (on most browsers). This is because most browsers update the devicePixelRatio to include any zooming. If you are rendering in an animation loop while they are zooming, the rounding could cause some flickering.
I am able to get the intersects from my click event when I use the window object to acquire height and width, but getting the intersects position on a canvas that's dynamically sized is proving much harder. I'm not certain of the formula I would need to use to calculate the vector.x and vector.y values with a div that isn't always the same size.
The canvas is the size of a div that always has a width: height ratio of 4:3 and resizes to fit in the window and is always positioned in the center of the window.
If I resize the window to be 4:3 then the following code works perfectly:
mouse.x = (ecx/div_width) *2 -1;
mouse.y= -(ecy/div_height) *2 + 1;
when I resize the window, whichever dimension is larger than the size of the canvas has the incorrect value. I've linked an image to roughly describe how the problem presents itself
Image of horizontal dimension issue
I initially thought that the matches would be as simple as dividing the difference between the the sizes of the window and the canvas by
My question is, how would I acquire the correct values to pass to the vector object for it's x and y attributes? (using Vector3 and Raycaster)
here is the function I'm using to try and get the object(s) being clicked:
function getClicked(event){
event.preventDefault();
var ecx = event.clientX;
var ecy = event.clientY;
//elem is the div containing the canvas
//the canvas is not the same size as the window
var elem_w = elem.innerWidth();
var elem_h = elem.innerHeight();
//most examples suggest using the window height and width
//to get the position of the mouse in the scene.
//since the scene isn't the same size as the window, that doesn't work
var ww = window.innerWidth;
var wh = window.innerHeight;
mouse.x = (ecx/ww) *2 -1;
mouse.y= -(ecy/wh) *2 + 1;
var objlist = []
rc.setFromCamera(mouse, camera);
var intersects = rc.intersectObjects(scene.children, true);
for (var i=0;i<names_to_spin.length;i++){
var obj = intersects[i];
objlist.push(obj);
}
//ideally, this should return a list of the objects under the cursor
return objlist;
}
I made a function that transforms mouse coordinates to canvas pixel coordinates:
/* Returns pixel coordinates according to the pixel that's under the mouse cursor**/
HTMLCanvasElement.prototype.relativeCoords = function(event) {
var x,y;
//This is the current screen rectangle of canvas
var rect = this.getBoundingClientRect();
//Recalculate mouse offsets to relative offsets
x = event.clientX - rect.left;
y = event.clientY - rect.top;
//Also recalculate offsets of canvas is stretched
var width = rect.right - rect.left;
//I use this to reduce number of calculations for images that have normal size
if(this.width!=width) {
var height = rect.bottom - rect.top;
//changes coordinates by ratio
x = x*(this.width/width);
y = y*(this.height/height);
}
//Return as an array
return [x,y];
}
You can see demonstration of the pixel coordinate calculation. The problem is that the solutions fails for images having border property set.
How can I subtract the border width from rectangle? Performance does matter, as this calculation is often performed during mouse move events.
getComputedStyle contains the information you desire:
Fetch the border information once at the beginning of your app after the canvas border has been set.
// get a reference to the canvas element
var canvas=document.getElementById('yourCanvasId');
// get its computed style
var styling=getComputedStyle(canvas,null);
// fetch the 4 border width values
var topBorder=styling.getPropertyValue('border-top-width');
var rightBorder=styling.getPropertyValue('border-right-width');
var bottomBorder=styling.getPropertyValue('border-bottom-width');
var leftBorder=styling.getPropertyValue('border-left-width');
If you scope these border-width variables app-wide, you can use these prefetched variables in your HTMLCanvasElement.prototype.relativeCoords.
Good luck with your project!
I use fillRect(x, y, 9, 9) with integer values and see smoothed rectangles (see below). I tried the following with no luck:
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false;
var iStrokeWidth = 1;
var iTranslate = (iStrokeWidth % 2) / 2;
this.ctx.translate(iTranslate, iTranslate);
i would like to see a line of 1 pixel between blue blocks, but i see smoothed gap:
Just add 0.5 pixel to the positions (or pre-translate half pixel):
fillRect(x + 0.5, y + 0.5, 9, 9);
This works because for some reason the canvas coordinate system define start of a pixel from the pixel's center. When drawn canvas actually has to sub-pixel the single point producing 4 anti-aliased pixels. By adding 0.5 you move it from center to the pixel's corner so it matches with the screen's coordinate system and no sub-pixeling has to take place.
These only affects images, not shapes btw.:
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false;
i'm making a game in jquery/javascript. And the game is to hit one moving object by using your mouse to click on the screen. I have problem with the detection bit. Is there a jQuery/javascript function that can mesure the distance between the center of the two objects at all time? Becasue then i can easily make a control check the distance of the two centers. and se if the collide. They are both two circles.
<div id="box">
<div id="prepend">
<div id="hero"></div>
</div>
<div id="enemy"></div>
</div>
Where "box" is the area where the game takes place, "hero" the bullet that you are going to hit the "enemy".
To get the distance use the formula
function getDistance(obj1,obj2){
Obj1Center=[obj1.offset().left+obj1.width()/2,obj1.offset().top+obj1.height()/2];
Obj2Center=[obj2.offset().left+obj2.width()/2,obj2.offset().top+obj2.height()/2];
var distance=Math.sqrt( Math.pow( Obj2Center[0]-Obj1Center[0], 2) + Math.pow( Obj2Center[1]-Obj1Center[1], 2) )
return distance;
}
Call using
getDistance($("#obj1"),$("#obj2"));
To check for collision:
function hasCollision(obj1,obj2){
return getDistance(obj1,obj2)<obj1.width()/2+obj2.width()/2;
}
Here's a jsfiddle example demonstrating both
You can use the following:
var $this = $(this);
var offset = $this.offset();
var width = $this.width();
var height = $this.height();
var centerX = offset.left + width / 2;
var centerY = offset.top + height / 2;
Do this for both and then calculate the difference between those values.
There isn't one, but its pretty simple actually:
Get the absolute coords of the current item as well as width and height
Calculate center point (x + width/2, y + height/2)
Do that for both circles
use trigonometric functions to calculate distance between those two points: difference of x in square plus difference of y in square, take the square root of that = distance
if the distance minus the radius of both circles is greater than 0, no collision. If 0: they touch, if smaller than 0, they collided.