I have a rectangular SVG object which is rotated via transform="rotate(45,0,0)". Using getBoundingClientRect() on the object gives the width and height of the bounding box which is not correct with the rotation.
I have searched and tried many calculations to get the original width and height taking off the rotation.
var rectangle = object.getBoundingClientRect()
var width = Math.sin(radian) * rectangle.height + Math.cos(radian) * rectangle.width
var height = Math.sin(radian) * rectangle.width + Math.cos(radian) * rectangle.height
And many other variations on this i have found, any help on the correct formula to use would be great. All i have is the values from getBoundingClientRect() and the radian.
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 have a slider which consists of iframes that are 1366px x 768px which I scale to fit on window resize. However, when 1366 is divided I can randomly get decimals on the actual width of the element which causes what I call as 'pixel breaking'.
Notice the while line on the second image. This is actually the second slide which is my problem. Example; 1366px becomes 1045.234234px so they don't line up properly.
I know I can also add a width by removing the decimals as parseInt(scaleAmount * 1366) but I don't think that can always be accurate with different resolutions.
Anything I can try to resolve or minimise this?
var $el = $(element);
var elHeight = 768;
var elWidth = 1366;
var $wrapper = $(parent);
function doResize(event, ui) {
var scale, origin;
scale = Math.min(
ui.size.width / elWidth,
ui.size.height / elHeight
);
$el.css({
'transform': "scale(" + scale + ")",
'-webkit-transform': "scale(" + scale + ")"
});
}
Since you're converting to a string, you can just trim off the decimal part of the number using scale.toFixed(0).
OK I think I've resolved it. I convert the transform scale to pixel value. Round up the pixel. And finally, convert the rounded pixel back to a transform scale. So the scaled pixel will always be an even number so therefore the pixels would not break anymore.
scale = Math.min(
ui.size.width / elWidth,
ui.size.height / elHeight
);
var scaleInPixels = scale * elWidth;
var evenPixel = 2 * Math.round(scaleInPixels / 2);
var finalScale = evenPixel / elWidth;
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 am scaling an image up and cropping it, but my x and y coordinates always seem to be slightly off compared to my selection. I think I'm scaling X and Y wrong, but I'm not sure what else to try.
var zoom = 1.5; //set dynamically by the user; ranges from 0.1 to infinity
var xPercent = cropDetails.x / imageW; // find how far x is from the left, relative to its size
var yPercent = cropDetails.y / imageH; // find how far y is from the top, relative to its size
var newImageSize = {width: imageW * zoom, height: imageH * zoom}; //find zoomed image width and height
cropDetails.x = newImageSize.width * xPercent; //find new x value for the resized image
cropDetails.y = newImageSize.height * yPercent; //find new y value for the resized image
EDIT: Kind of like this, but I don't want the inner rectangle to scale in size.
Multiplying X and Y by zoom:
Pre-crop, zoomed to 190%
Post-crop, seems like it's down and to the right
I also get the same result when I scale the coordinates proportionally.
You are finding the relative positions of x and y in your image. Then, you find the same relative points in a larger image. Seems correct but needlessly complex. Could rounding error be a problem? Are you setting the width and height of your crop anywhere?
You can actually find the new x and y points by scaling their locations by the zoom. e.g. the point (20, 20) in a (100, 100) image is in the same place as the point (30, 30) in the (150, 150) image. Thus, just multiply the coordinates, width, and height of your crop details by the zoom and you should be good!
var zoom = 1.5;
cropDetails.x *= zoom;
cropDetails.y *= zoom;
cropDetails.width *= zoom;
cropDetails.height *= zoom;
var newImageSize = {width: imageW * zoom, height: imageH * zoom};
Try that and let me know if it works! If not, you should post some examples of how it's failing so that I can help you debug.
For a 2d game, I have these concepts:
stage: container in which items and cameras are placed.
item: a visible entity, located on a certain point in the world, anchored from center.
camera: an invisible entity, used to generate relative images of world, located on a certain point in the world, anchored from center.
In the illustrations, you can see how they are related, and what the end result should be.
Here is the code I have: (dumbed down to make it easier to read)
Note1: This is not happening on canvas, so I will not use canvas translation or rotation (and even then, I don't think it would make the problem any easier).
Note2: Item and camera positions are center coordinates.
var sin = Math.sin(rotationRad);
var cos = Math.cos(rotationRad);
var difX = item.x - camera.x;
var difY = item.y - camera.y;
var offsetX = camera.width / 2;
var offsetY = camera.height / 2;
var view.x = (cos * difX) - (sin * difY) + _Ax + _Bx;
var view.y = (sin * difX) + (cos * difY) + _Ay + _By;
This is supposed to calculate an items new position by:
calculating new position of item by rotating it around camera center
(_A*) adjusting item position by offsetting camera position
(_B*) adjusting item position by offsetting camera size
I tried several different solutions to use for _A* and _B* here, but none of them work.
What is the correct way to do this, and if possible, what is the explanation?
You first subtract new origin position from object position. You then rotate it by the inverse of the rotation. New origin can be camera position of top left corner of viewport. Of course if you know viewport center its top left corner is computed by subtracting half of its dimensions.
Like this:
var topLeft = Camera.Position - Camera.Size / 2;
var newPosition = Object.Position - topLeft;
newPosition = Rotate(newPosition, -camera.Angle);
Rotation is very simple:
rotatedX = x * cos(angle) - y * sin(angle)
rotatedY = y * cos(angle) + x * sin(angle)