I want to make a function that gives me a random point near the edges of a rectangle from a point. This is what I came up with so far, but I have absolutely no idea why it is not working.
function Point(x, y) {
this.x = x;
this.y = y;
}
function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomPointNearEdges(rectPos, width, height, border) {
var point = new Point(rectPos.x, rectPos.y);
if (randomNumber(0, 1) == 0) {
point.x = randomNumber(rectPos.x, rectPos.x + border);
if (randomNumber(0, 1) == 0) {
point.y = randomNumber(rectPos.y, rectPos.y + border);
}
else {
point.y = randomNumber(rectPos.y + height, (rectPos.y + height) + border);
}
}
else {
point.y = randomNumber(rectPos.y, rectPos.y + border);
if (randomNumber(0, 1) == 0) {
point.y = randomNumber(rectPos.x, rectPos.x + border);
}
else {
point.y = randomNumber(rectPos.x + height, (rectPos.x + width) + border);
}
}
return point;
};
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = 700;
canvas.height = 700;
var ctx = canvas.getContext("2d");
ctx.strokeRect(130, 130, 500, 500);
for (var i = 0; i < 30; i++) {
var point = getRandomPointNearEdges(new Point(130, 130), 500, 500, 100);
ctx.fillRect(point.x, point.y, 2, 2);
}
};
<canvas id="canvas"></canvas>
Just to clarify, the black region in this 'Not to scale' diagram is where I want to allow the point to generate. The width / height of that black region is the border property in the code snippet.
Why is my function not working?
Random with even distribution.
Just to point out that the answer by SimpleJ is statistical flawed with the distribution of random locations having a bias to the corners and then to the shorter sides, even though they cover much less area.
The ideal random location should be spread equally over the area in question, if the height of the box is less than the width then there is less chance of the the sides getting a point.
The example below provides a much faster and a much better distribution. I have added the given answers solution as well so you can compare.
The function that gets a random pos. The arguments x,y top left inside edge of rectangle, w,h inside width and height of the rectangle minDist, maxDist the min and max dist the random point can be from the inside edge of the box. You can also use negative values have the points outside the rectangle. Note that the distances are always from the inside edge of the box. The values are also floored when return (can easily be remove and still works)
function randomPointNearRect(x, y, w, h, minDist, maxDist) {
const dist = (Math.random() * (maxDist - minDist) + minDist) | 0;
x += dist;
y += dist;
w -= dist * 2
h -= dist * 2
if (Math.random() < w / (w + h)) { // top bottom
x = Math.random() * w + x;
y = Math.random() < 0.5 ? y : y + h -1;
} else {
y = Math.random() * h + y;
x = Math.random() < 0.5 ? x: x + w -1;
}
return [x | 0, y | 0];
}
Note there is a slight bias to the inside of the box. It can be removed with a little calculus with the bias rate of change f'(x) = 8*x 8 pixels per pixel inward and the anti derivative f(x)=4*(x**2) + c would directly relate to the distribution. Where x is dist from edge and c is related to perimeter length
Example to compare
The example has two canvases. Many random points are drawn. click the top canvas to add more points. Note how the bottom canvas sides and corners get darker due to the bias of the random points.
const ctx = canvas.getContext("2d");
canvas.onclick = ()=>{
getRandomPointsForBox(200, box,4, 18);
getRandomPoints(200);
}
const edgeClear = 30;
var box = {
x: edgeClear,
y: edgeClear,
w: canvas.width - edgeClear * 2,
h: canvas.height - edgeClear * 2,
edge: 4,
}
function drawBox(box) {
ctx.fillRect(box.x, box.y, box.w, box.h);
ctx.clearRect(box.x + box.edge, box.y + box.edge, box.w - box.edge * 2, box.h - box.edge * 2);
}
function drawPixel(x, y) {
ctx.fillRect(x, y, 1, 1);
}
function getRandomPointsForBox(count, box, min, max) {
min += box.edge;
max += box.edge;
while (count--) {
const [x, y] = randomPointNearRect(box.x, box.y, box.w, box.h, min, max);
drawPixel(x, y);
}
}
drawBox(box);
getRandomPointsForBox(200, box,4, 18);
ctx.font = "18px arial"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("Click to add more random points.",canvas.width / 2, canvas.height / 2);
function randomPointNearRect(x, y, w, h, minDist, maxDist) {
const dist = (Math.random() * (maxDist - minDist) + minDist) | 0;
x += dist;
y += dist;
w -= dist * 2
h -= dist * 2
if (Math.random() < w / (w + h)) { // top bottom
x = Math.random() * w + x;
y = Math.random() < 0.5 ? y : y + h -1;
} else {
y = Math.random() * h + y;
x = Math.random() < 0.5 ? x: x + w -1;
}
return [x | 0, y | 0];
}
/* The following is from the answer provided by SimpleJ https://stackoverflow.com/a/49581326/3877726 */
const ctx1 = canvas1.getContext('2d');
const rect = {
x: box.x, y: box.y,
width: box.w, height: box.h,
};
drawRect(rect);
ctx1.font = "18px arial"
ctx1.textAlign = "center"
ctx1.textBaseline = "middle"
ctx1.fillText("SimpleJ's method.",canvas1.width / 2, canvas1.height / 2);
ctx1.fillText("Note density of sides and corners.",canvas1.width / 2, canvas1.height / 2 + 20);
function getRandomPoints(count) {
while (count--) {
drawPoint(randomPointInRect(sample(rects)));
}
}
var rects = getBorderRects(rect, 10);
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
function drawRect({x, y, width, height}) {
ctx1.strokeRect(x, y, width, height);
}
function drawPoint({x, y}) {
ctx1.fillRect(x, y, 1,1);
}
getRandomPoints(200);
<canvas id="canvas" width="500" height="200"></canvas>
<canvas id="canvas1" width="500" height="200"></canvas>
If you think about the problem of getting a random point near an edge as getting a random point in one of four edge rectangles, this problem becomes much easier to break down:
Get edge rectangles.
Pick a random edge rectangle.
Generate a random point in the edge rectangle.
To generate edge rectangles, we need a max distance (how far from the edge can the point be?):
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
To pick a random rectangle from our array of edge rectangles, we can define a sample function:
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
Then to pick a random point in a rectangle, we just need some Math.random:
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
And putting everything together:
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const rect = {
x: 10, y: 20,
width: 300, height: 200,
};
drawRect(rect);
drawPoint(
randomPointInRect(
sample(
getBorderRects(rect, 10)
)
)
);
function getBorderRects(rect, distance) {
const { x, y, width, height } = rect;
return [
{x: x, y: y, width: width, height: distance}, // top
{x: x, y: y + height - distance, width: width, height: distance}, // bottom
{x: x, y: y, width: distance, height: height}, // left
{x: x + width - distance, y: y, width: distance, height: height}, // right
];
}
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
function randomPointInRect({x, y, width, height}) {
return {
x: x + (Math.random() * width),
y: y + (Math.random() * height),
};
}
function drawRect({x, y, width, height}) {
context.strokeRect(x, y, width, height);
}
function drawPoint({x, y}) {
context.arc(x, y, 1, 0, Math.PI * 2);
context.fill();
}
<canvas width="500" height="500"/>
For anybody here like me, looking for a short, simple solution, this post is closest I found that is not talking trigonometry .. An while what I came up with might not directly be a solution to OPs problem, maybe someone will find this useful..
The approach is fairly simple.
Math.random() a number between 0 & 800. Make use of modulus and divide what's left by 200 to get a random side and axis point. Push the random side all the way, assign the random value to the other axis and yeah, that's about it .. here's an ex:
let rndm = Math.floor(Math.random()*800-1);
let offset = rndm % 200;
let side = (rndm - offset) / 200; // 0:top 1:right 2:btm 3:left
let y = side % 2 > 0 ? offset+1 : 100 * side ;
let x = side % 2 < 1 ? offset+1 : 100 * (side - 1) ;
point.y = y - 100;
point.x = x - 100;
In my case, I needed both negative and positive values with an origin point.
And if you want to spawn a point inside a border, just do another random number spanning the width of the border.
Just remember to adjust the corners.
offset += rndmBorder * 2; // creates an inward line in the corners
point.x = x - 100 + rndmBorder; // still keeping the origin point nice and center
_____________
|\_________/| <-// inward line
| | | |
| | | |
All I was in need for is to offset some letters .. and most of what I found seemed like overkill .. This actually works fairly well, hope it helps.
Related
I have a 2D board made with KonvaJS and tokens that can move on a square grid. I can already add fog of war and remove it manually. However, I would like to make it so, when each token moves, it reveals a certain around it, taking into account walls. Most of the work is done, however it's not entirely accurate.
Basically for each wall, I'm checking if the token is on the top/right/bottom/left of it. And then depending on which one it is, I reduce the width/height of the revealing area so it doesn't go beyond the wall. Here is an image explaining what I have and what I need
Legend:
Gray is fog of war
Red area is the wall/obstacle
Token is the movable token
Blue area is the revealed area
Blue lines inside red area is where it intersects
Purple lines are squares that should be revealed (aka, it should be blue)
Basically, in this case, an intersection was detected and the token is on the right side of the obstacle. So I got the right side of the wall (the x coordinate), and made the blue area starting point be that x coordinate and removed from the total width of the blue area the intersection width(the blue lines, so 1 square of width was removed).
However, because of that, the purple lines don't get filled in. Unfortunately, I can't just check the intersection points between blue and red and only remove those, because if the blue area is bigger than the red area, it would reveal the other side of the obstacle(which I don't want).
Here is the code I'm using to iterate the walls, checking if there is an intersection, checking where the token is, and then removing the width or height according to the intersection.
const tokenPosition = { x: 10, y: 10 };
const haveIntersection = (r1, r2) => !(
r2.x > r1.x + r1.width || // Compares top left with top right
r2.x + r2.width < r1.x || // Compares top right with top left
r2.y > r1.y + r1.height || // Compare bottom left with bottom right
r2.y + r2.height < r1.y // Compare bottom right with bottom left
);
walls.forEach(wall => {
const redArea = { x: wall.x, y: wall.y, width: wall.width, height: wall.height };
// blueArea has the same properties as redArea
if (haveIntersection(blueArea, redArea)) {
const tokenToTheRight = tokenPosition.x > wall.x + wall.width;
const tokenToTheLeft = tokenPosition.x < wall.x;
const tokenToTheTop = tokenPosition.y < wall.y;
const tokenToTheBottom = tokenPosition.y > wall.y + wall.height;
if (tokenToTheRight) {
let diff = wall.x + wall.width - blueArea.x;
blueArea.x = wall.x + wall.width;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheLeft) {
let diff = blueArea.x + blueArea.width - wall.x;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheTop) {
let diff = blueArea.y + blueArea.height - wall.y;
blueArea.height = blueArea.height - diff;
}
if (tokenToTheBottom) {
let diff = wall.y + wall.height - blueArea.y;
blueArea.y = wall.y + wall.height;
blueArea.height = blueArea.height - diff;
}
}
});
Any idea on how to fix this or if I should be taking a different approach?
You'll have to do something ray-tracing like to get this to work.
In the snippet below, I:
Loop over each cell in your token's field-of-view
Check for that cell center whether
it is in a box, or
a line between the token and the cell center intersects with a wall of a box
Color the cell based on whether it intersects
Note: the occlusion from the boxes is quite aggressive because we only check the center for quite a large grid cell. You can play around with some of the settings to see if it matches your requirements. Let me know if it doesn't.
Legend:
Red: box
Light blue: in field of view
Orange: blocked field of view because box-overlap
Yellow: blocked field of view because behind box
// Setup
const cvs = document.createElement("canvas");
cvs.width = 480;
cvs.height = 360;
const ctx = cvs.getContext("2d");
document.body.appendChild(cvs);
// Game state
const GRID = 40;
const H_GRID = GRID / 2;
const token = { x: 7.5, y: 3.5, fow: 2 };
const boxes = [
{ x: 2, y: 3, w: 4, h: 4 },
{ x: 8, y: 4, w: 1, h: 1 },
];
const getBoxSides = ({ x, y, w, h }) => [
[ [x + 0, y + 0], [x + w, y + 0]],
[ [x + w, y + 0], [x + w, y + h]],
[ [x + w, y + h], [x + 0, y + h]],
[ [x + 0, y + h], [x + 0, y + 0]],
];
const renderToken = ({ x, y, fow }) => {
const cx = x * GRID;
const cy = y * GRID;
// Render FOV
for (let ix = x - fow; ix <= x + fow; ix += 1) {
for (let iy = y - fow; iy <= y + fow; iy += 1) {
let intersectionFound = false;
for (const box of boxes) {
if (
// Check within boxes
pointInBox(ix, iy, box) ||
// Check walls
// Warning: SLOW
getBoxSides(box).some(
([[ x1, y1], [x2, y2]]) => intersects(x, y, ix, iy, x1, y1, x2, y2)
)
) {
intersectionFound = true;
break;
}
}
if (!intersectionFound) {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(0, 255, 255, 0.5)", 0);
ctx.fillStyle = "lime";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
} else {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(255, 255, 0, 0.5)", 0);
ctx.fillStyle = "red";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
}
}
}
ctx.lineWidth = 5;
ctx.fillStyle = "#efefef";
ctx.beginPath();
ctx.arc(cx, cy, GRID / 2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
const renderBox = ({ x, y, w, h }, color = "red", strokeWidth = 5) => {
ctx.fillStyle = color;
ctx.strokeWidth = strokeWidth;
ctx.beginPath();
ctx.rect(x * GRID, y * GRID, w * GRID, h * GRID);
ctx.closePath();
ctx.fill();
if (strokeWidth) ctx.stroke();
}
const renderGrid = () => {
ctx.lineWidth = 1;
ctx.beginPath();
let x = 0;
while(x < cvs.width) {
ctx.moveTo(x, 0);
ctx.lineTo(x, cvs.height);
x += GRID;
}
let y = 0;
while(y < cvs.height) {
ctx.moveTo(0, y);
ctx.lineTo(cvs.width, y);
y += GRID;
}
ctx.stroke();
}
boxes.forEach(box => renderBox(box));
renderToken(token);
renderGrid();
// Utils
// https://errorsandanswers.com/test-if-two-lines-intersect-javascript-function/
function intersects(a,b,c,d,p,q,r,s) {
var det, gamma, lambda;
det = (c - a) * (s - q) - (r - p) * (d - b);
if (det === 0) {
return false;
} else {
lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
return (0 <= lambda && lambda <= 1) && (0 <= gamma && gamma <= 1);
}
}
function pointInBox(x, y, box) {
return (
x > box.x &&
x < box.x + box.w &&
y > box.y &&
y < box.bottom
);
}
canvas { border: 1px solid black; }
Implemented a canvas, Drawing a square there and get the calculated coordinates,
Can see on the following pic the drawing:
I'm calculating and getting the upleft point X and Y coordinates,
And for the down right coordinates that i need, I'm adding the height and width, as follows:
{ upLeft: { x: position.x, y: position.y }, downRight: { x: position.x + position.width, y: position.y + position.height } },
Now i want to get the same dimensions when i'm rotating the canvas clockwise or anti-clockwise.
So i have the angle, And i try to calculate via the following function:
function getRotatedCoordinates(cx, cy, x, y, angle) {
let radians = (Math.PI / 180) * angle,
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = (cos * (x - cx)) - (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) + (sin * (x - cx)) + cy;
return [nx, ny];
}
And i'm calling the function via the following args and using it.
let newCoords = getRotatedCoordinates(0, 0, position.x, position.y, angle);
position.x = newCoords[0];
position.y = newCoords[1];
So firstly, I'm not sure that the cx and cy points are correct, I'm always entering 0 for both of them.
Secondly, I'm not getting the desired results, They are getting changed but i'm pretty sure that something is wrong with the x and y, So i guess that the function is wrong.
Thanks.
Here is how I would do it:
function getRectangeCoordinates(x, y, width, height, angle) {
let points = [ [x, y] ]
let radians = (Math.PI / 180) * angle;
for (let i = 0; i < 3; i++) {
x += Math.cos(radians) * ((i == 1) ? height : width);
y += Math.sin(radians) * ((i == 1) ? height : width);
points.push([x, y])
radians += Math.PI / 2
}
return points
}
let canvas = document.createElement("canvas");
canvas.width = canvas.height = 140
let ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
function draw(coords, radius) {
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.arc(coords[i][0], coords[i][1], radius, 0, 8);
ctx.moveTo(coords[i][0], coords[i][1]);
let next = (i + 1) % 4
ctx.lineTo(coords[next][0], coords[next][1]);
ctx.stroke();
}
}
let coords = getRectangeCoordinates(20, 10, 120, 40, 15)
console.log(JSON.stringify(coords))
draw(coords, 3)
ctx.strokeStyle = "red";
coords = getRectangeCoordinates(60, 40, 40, 50, 65)
draw(coords, 5)
ctx.strokeStyle = "blue";
coords = getRectangeCoordinates(120, 3, 20, 20, 45)
draw(coords, 2)
In the getRectangeCoordinates I'm returning all corners of a rectangle and the paraments of the function are the top left corner (x, y) the height and width of the rectangle and last the angle.
I'm drawing a few rectangles with different shapes and angles to show how it looks like
The calculations in the function are simple trigonometry here is a visual representation that could help you remember it the next time you need it:
I'm having some issues calculating the corners of a rotated rectangle within a rotated container with both having offset x/y co-ords.
The pivot is off but I'm not sure of the solution. The following scenarios work:
(x, y, rotation)
image = 0, 0, 45
container = 100, 100, 45
image = 200, 0, 45
container = 100, 100, 0
however setting the rotation of the container, and the image co-ords messes up the pivot e.g.
image = 200, 0, 45
container = 100, 100, 45
Below is the code for calculating the corners of the image in global co-ordinate space:
public get corners() {
const worldData = this.worldData;
//Get angle of object in radians;
const radAngle = worldData.rotation * Math.PI / 180;
const pivotX = worldData.pivotX;
const pivotY = worldData.pivotY;
const width = this.sourceWidth * worldData.scaleX;
const height = this.sourceHeight * worldData.scaleY;
const x = worldData.x;//this.x;
const y = worldData.y;//this.y;
//Get the corners
const c1 = this.getCorner(pivotX, pivotY, x, y, radAngle);
const c2 = this.getCorner(pivotX, pivotY, x + width, y, radAngle);
const c3 = this.getCorner(pivotX, pivotY, x + width, y + height, radAngle);
const c4 = this.getCorner(pivotX, pivotY, x, y + height, radAngle);
return {c1, c2, c3, c4};
}
public get worldData() {
let x = this.x;
let y = this.y;
let pivotX = this.x;
let pivotY = this.y;
let rotation = this.rotation;
let scaleX = this.scaleX;
let scaleY = this.scaleY;
let parent = this.parent;
while(parent) {
x += parent.x;
y += parent.y;
pivotX += parent.x;
pivotY += parent.y;
rotation += parent.rotation;
scaleX *= parent.scaleX;
scaleY *= parent.scaleY;
parent = parent.parent;
}
return {x, y, scaleX, scaleY, rotation, pivotX, pivotY}
}
protected getCorner(pivotX:number, pivotY:number, cornerX:number, cornerY:number, angle:number) {
let x, y, distance, diffX, diffY;
/// get distance from center to point
diffX = cornerX - pivotX;
diffY = cornerY - pivotY;
distance = Math.sqrt(diffX * diffX + diffY * diffY);
/// find angle from pivot to corner
angle += Math.atan2(diffY, diffX);
/// get new x and y and round it off to integer
x = pivotX + distance * Math.cos(angle);
y = pivotY + distance * Math.sin(angle);
return {x, y};
}
Let's suppose that the scenario is as follows:
where the lower left corner of the image (solid line) has coordinates (x_i, y_i) and the lower left corner of the container (dashed line) has coordinates (X_c, Y_c). Moreover, the image (of width w and height h) is rotated counter-clockwise by angle beta with respect to the laboratory frame, while the container itself is rotated (also counter-clockwise) by angle alpha.
Now, let's focus for example on the upper-right corner P. With respect to the laboratory frame (global canvas), its coordinates can be expressed as:
R(beta) . ( w, h ) + ( x_i, y_i )
where . denotes matrix multiplication, and R is a counter-clockwise rotation matrix
R(beta) = [ cos(beta) -sin(beta) ]
[ sin(beta) cos(beta) ]
Now, we need to transform this into a coordinate frame with respect to the container. Formally, this means that we need first to subtract the offset and then to rotate by -alpha (or alpha clock-wise). Thus with everything together:
R(-alpha).( R(beta) . (w, h) + (x_i, y_i) - (X_c, Y_c) )
The other corners can be handled similarly, just by replacing (w, h) with the proper coordinates...
In terms of code, one might implement these formulae as:
//counter-clock-wise rotation by given angle in degrees
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}
//shift by a multiple fac of an offset {xref, yref}
function offsetBy(fac, {x:xref, y:yref}, {x, y}) {
return {
x: fac*xref + x,
y: fac*yref + y
};
}
const image = {
coords: {x: 200, y: 0}, //lab-frame coordinates
angle: 45, //lab-frame rotation angle
width: 50,
height: 10
};
const container = {
coords: {x: 100, y: 100}, //lab-frame coordinates
angle: 45 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-right corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
EDIT:
In case the y-axis is supposed to point "downwards", the formulas above work as well, one just needs to interpret the angles as clock-wise instead of counter-clockwise (so in principle the function rotateCCWBy should be renamed to rotateCWBy). As an example, let's consider this scenario:
Here, the top-left corner of the container is located at position (2,1) and the container itself is rotated by 15 degrees. The image (black rectangle) of width 4 and height 2 is rotated by 30 degrees and its top-left corner is located at position (3, 3). Now, we want to calculate the coordinates (x, y) of point P with respect to the container.
Using:
const image = {
coords: {x: 3, y: 3}, //lab-frame coordinates
angle: 30, //lab-frame rotation angle
width: 4,
height: 2
};
const container = {
coords: {x: 2, y: 1}, //lab-frame coordinates
angle: 15 //lab-frame rotation angle
};
//calculate the coordinates of the image's top-left corner
//with respect to the container
const corner = rotateCCWBy(-container.angle,
offsetBy(
-1, container.coords,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
console.log(corner);
yields
{ x: 4.8296291314453415, y: 4.640160440463835 }
which can be (approximately) visually verified from the attached figure.
EDIT2:
After additional clarification, the coordinates of the image are not supposed to be "lab-frame" (i.e., with respect to the canvas), but with respect to the already rotated container. Thus the transformation needs to be adapted as:
const corner =
offsetBy(
+1, container.coords,
rotateCCWBy(container.angle,
offsetBy(
+1, image.coords,
rotateCCWBy(image.angle,
{x: image.width, y: image.height}
)
)
)
);
function rotateCCWBy(angle, {x, y}) {
const angle_rad = angle * Math.PI / 180;
const cos_a = Math.cos(angle_rad),
sin_a = Math.sin(angle_rad);
return {
x: cos_a * x - sin_a * y,
y: sin_a * x + cos_a * y
};
}
I am wanting to calculate the 4 offsets from the point of rotation when I rotate a square.
The axis of rotation is initially the top left of the square. When I perform a rotation I would like to know how far the shape will spead in all 4 directions (minX, minY, maxX, maxy).
I currently have the general math:
const rotation = .35 // radians = 20 degrees
const size = 50 // size of original square
const o1 = Math.round(size * Math.sin(rotation))
const o2 = Math.round(size * Math.cos(rotation))
Using these numbers I see how I can use them to create an array of offsets
const offsets = [o1, 0, o2, o1 + o2]
When I rotate my square from 20, 110, 200 and 290 degrees it will rotate around the axis marked by the black dot on image.
For each of the 4 rotations I have the offests array as well as the actual numbers that I desire. As you can see the numbers are sort of there but... I initially thought an array shift was all I needed but its more than that.
// 20 degrees
console.log(offsets) // [17, 0, 47, 64]
// The dimensions I actually need
// minX: -17,
// minY: 0
// maxX: 47
// maxY: -64
// 110 degrees
console.log(offsets) // [47, 0, -17, 30]
// The dimensions I actually need
// minX: -64,
// minY: -17,
// maxX: 0,
// maxY: 47
// 200 degrees
console.log(offsets) // [-17, 0, -47, -64]
// The dimensions I actually need
// minX: -47,
// minY: -64,
// maxX: 17,
// maxY: 0
// 290 degrees
console.log(offsets) // [-47, 0, 17, -30]
// The dimensions I actually need
// minX: 0,
// minY: -47,
// maxX: 64,
// maxY: 17
I can certainly shift the array if needed (say for every 90deg) but how can I get the correct numbers? I'm looking for the magic formula for any angle.
Transforming points
The easiest way to do this is create a simple rotation matrix. This is just the direction of the x and y axis as vectors each with a length the size of a pixel (or unit whatever that may be) and the location of the origin.
To rotate a point
First define the point
var x = ?; // the point to rotate
var y = ?;
Then the origin and rotation
const ox = ?; // location of origin
const oy = ?;
const rotation = ?; // in radians
From the rotation we calculate to vector that is the direction of the x axis
var xAxisX = Math.cos(rotation);
var xAxisY = Math.sin(rotation);
Optionally you could have a scale as well
const scale = ?;
that would change the length of the x and y axis so the x axis calculation is
var xAxisX = Math.cos(rotation) * scale;
var xAxisY = Math.sin(rotation) * scale;
No we can apply the rotation to the point. First move the point relative to the origin.
x -= ox;
y -= oy;
Then move the point x distance along the x axis
var rx = x * xAxisX;
var ry = x * xAxisY;
Then move y distance along the y axis. The y axis is at 90 deg clockwise from the x. To rotate any vector 90deg you swap the x and y and negate the new x. Thus moving along the y axis is as follows
rx -= y * xAxisY; // use x axis y for y axis x and negate
ry += y * xAxisX; // use x axis x for y axis y
Now the point has been rotated but is still relative to the origin, we need to move it back to the world space. To do that just add the origin
rx += ox;
ry += oy;
And rx,ry is the rotated point around the origin, and scaled if you did that.
Match rotation in 2D context
You can get the 2D context to do the same for you
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, ox, oy);
ctx.fillRect(x,y,1,1); // draw the rotated pixel
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default transform
Or you can add the rotation via a function call
ctx.setTransform(1, 0, 0, 1, ox, oy);
ctx.rotate(rotation);
// and if scale then
// ctx.scale(scale,scale)
ctx.fillRect(x,y,1,1); // draw the rotated pixel
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default transform
The various steps above can be compacted, the next part of the answer rotates a rectangle using the above method.
Rotating a rectangle
The following function will return the 4 rotated corners.
// angle is the amount of rotation in radians
// ox,oy is the origin (center of rotation)
// x,y is the top left of the rectangle
// w,h is the width and height of the rectangle
// returns an array of points as arrays [[x,y],[x1,y1],...]
// Order of returned points topLeft, topRight, bottomRight, bottomLeft
function rotateRect(angle,ox,oy,x,y,w,h){
const xAx = Math.cos(angle); // x axis x
const xAy = Math.sin(angle); // x axis y
x -= ox; // move rectangle onto origin
y -= oy;
return [[ // return array holding the resulting points
x * xAx - y * xAy + ox, // Get the top left rotated position
x * xAy + y * xAx + oy, // and move it back to the origin
], [
(x + w) * xAx - y * xAy + ox, // Get the top right rotated position
(x + w) * xAy + y * xAx + oy,
], [
(x + w) * xAx - (y + h) * xAy + ox, // Get the bottom right rotated position
(x + w) * xAy + (y + h) * xAx + oy,
], [
x * xAx - (y + h) * xAy + ox, // Get the bottom left rotated position
x * xAy + (y + h) * xAx + oy,
]
];
}
Finding the offsets
To use the function
var angle = 1; // amount to rotate in radians
var ox = 0; // origin top left of rectangle
var oy = 0;
const rotatedRect = rotateRect(angle,ox,oy,0,0,50,50);
const r = rotatedRect; // alias to make following code more readable
var leftOfOrigin = Math.min(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var rightOfOrigin = Math.max(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var aboveOrigin = Math.min(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
var belowOrigin = Math.max(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
I keep the distance calcs outside the function as that is a little more useful as you may want more information about the rotated points.
DEMO
As an example
const ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 512;
// angle is the amount of rotation in radians
// ox,oy is the origin (center of rotation)
// x,y is the top left of the rectangle
// w,h is the width and height of the rectangle
// returns an array of points as arrays [[x,y],[x1,y1],...]
// Order of returned points topLeft, topRight, bottomRight, bottomLeft
function rotateRect(angle,ox,oy,x,y,w,h){
const xAx = Math.cos(angle); // x axis x
const xAy = Math.sin(angle); // x axis y
x -= ox; // move rectangle onto origin
y -= oy;
return [[ // return array holding the resulting points
x * xAx - y * xAy + ox, // Get the top left rotated position
x * xAy + y * xAx + oy, // and move it back to the origin
], [
(x + w) * xAx - y * xAy + ox, // Get the top right rotated position
(x + w) * xAy + y * xAx + oy,
], [
(x + w) * xAx - (y + h) * xAy + ox, // Get the bottom right rotated position
(x + w) * xAy + (y + h) * xAx + oy,
], [
x * xAx - (y + h) * xAy + ox, // Get the bottom left rotated position
x * xAy + (y + h) * xAx + oy,
]
];
}
function drawRectangle(angle, ox, oy, rect){
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.setTransform(1,0,0,1,ox,oy);
ctx.rotate(angle);
ctx.strokeRect(rect.x - ox, rect.y - oy, rect.w, rect.h);
ctx.setTransform(1,0,0,1,0,0); // restore transform to default
}
function drawBounds(rotatedRect){
const r = rotatedRect; // alias to make following code more readable
const left = Math.min(r[0][0], r[1][0], r[2][0], r[3][0]);
const right = Math.max(r[0][0], r[1][0], r[2][0], r[3][0]);
const top = Math.min(r[0][1], r[1][1], r[2][1], r[3][1]);
const bottom = Math.max(r[0][1], r[1][1], r[2][1], r[3][1]);
ctx.strokeStyle = "#999";
ctx.lineWidth = 2;
ctx.strokeRect(left, top, right - left, bottom - top);
}
function drawDistance(text,x,y,dist,direction,textOverflowDir){
if(dist.toFixed(2) == 0) { return }
function drawArrows(){
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.lineTo(8,-12);
ctx.lineTo(0,-7);
ctx.lineTo(8,-2);
ctx.moveTo(dist - 8, -12);
ctx.lineTo(dist, -7);
ctx.lineTo(dist - 8, -2);
ctx.stroke();
}
ctx.setTransform(1,0,0,1,x,y);
ctx.rotate(direction);
const width = ctx.measureText(text).width;
ctx.fillStyle = "blue";
ctx.fillRect(-1, - 16, 2, 14);
ctx.fillRect(dist -1, - 16, 2, 14);
if(width + 8 > dist){
ctx.fillRect(1, -8, dist - 2, 2);
drawArrows();
ctx.fillStyle = "black";
if(textOverflowDir < 0){
ctx.fillText(text, - width / 2 - 4, - 9);
}else{
ctx.fillText(text,dist + width / 2 + 6, - 9);
}
}else{
ctx.fillRect(-1, - 8, (dist - width) / 2 - 4, 2);
ctx.fillRect(dist - 1 - ((dist - width) / 2 - 4), - 8, (dist - width) / 2 - 4, 2);
drawArrows();
ctx.fillStyle = "black";
ctx.fillText(text, dist / 2, - 9);
}
ctx.setTransform(1,0,0,1,0,0); //restore default transform
}
// set up the font
ctx.font = "16px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
var angle = 3.2; // amount to rotate in radians
var ox = 256; // origin top left of rectangle
var oy = 256;
const rect = {
x : 256,
y : 256,
w : 164,
h : 164,
}
function mainLoop(){
ctx.clearRect(0,0,512,512);
angle += 0.01; // slowly rotate
// draw origin
ctx.fillStyle = "#FA2";
ctx.fillRect(ox-1,0,2,512);
ctx.fillRect(0,oy-1,512,2);
const rotatedRect = rotateRect(angle, ox, oy, rect.x, rect.y, rect.w, rect.h);
drawBounds(rotatedRect);
drawRectangle(angle, ox, oy, rect);
const r = rotatedRect; // alias to make following code more readable
var leftOfOrigin = Math.min(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var rightOfOrigin = Math.max(r[0][0],r[1][0],r[2][0],r[3][0]) - ox;
var aboveOrigin = Math.min(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
var belowOrigin = Math.max(r[0][1],r[1][1],r[2][1],r[3][1]) - oy;
// draw distances
drawDistance(leftOfOrigin.toFixed(2), ox + leftOfOrigin, oy +aboveOrigin, - leftOfOrigin, 0, -1);
drawDistance(rightOfOrigin.toFixed(2), ox, oy + aboveOrigin, rightOfOrigin, 0, 1);
drawDistance(belowOrigin.toFixed(2), ox + leftOfOrigin, oy + belowOrigin, belowOrigin, - Math.PI / 2, -1);
drawDistance(aboveOrigin.toFixed(2), ox + leftOfOrigin, oy, - aboveOrigin, - Math.PI / 2, 1);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
I gave this a try, not claiming it to be efficient or the best way, but I couldn't match your expected values. Either I did something wrong or your first set of expected values is incorrect?
'use strict';
const degToRad = deg => (deg * Math.PI) / 180;
const rotatePoint = (pivot, point, radians) => {
const cosA = Math.cos(radians);
const sinA = Math.sin(radians);
const [x, y] = pivot;
const difX = point[0] - x;
const difY = point[1] - y;
return [
Math.round(((cosA * difX) - (sinA * difY)) + x),
Math.round((sinA * difX) + (cosA * difY) + y),
];
};
const rotateSquare = (square, pivot, angle) => {
const radians = degToRad(angle);
return square.map(point => rotatePoint(pivot, point, radians));
};
const extents = (points, pivot) => points.reduce((acc, point) => {
const [difX, difY] = point.map((value, index) => value - pivot[index]);
return [
Math.min(acc[0], difX),
Math.min(acc[1], difY),
Math.max(acc[2], difX),
Math.max(acc[3], difY),
];
}, [0, 0, 0, 0]);
const createSquare = (x, y, size) => [
[x, y],
[x + size, y],
[x + size, y + size],
[x, y + size],
];
const pivot = [0, 0];
const square = createSquare(...pivot, 50);
const angles = [20, 110, 200, 290];
const rotations = angles.map(angle => rotateSquare(square, pivot, angle));
const offsets = rotations.map(rotation => extents(rotation, pivot));
const expecteds = [
[-17, 0, 47, -64],
[-64, -17, 0, 47],
[-47, -64, 17, 0],
[0, -47, 64, 17],
];
offsets.forEach((offset, index) => {
const actual = JSON.stringify(offset);
const expected = JSON.stringify(expecteds[index]);
console.log(
`Actual:${actual}`,
`Expected:${expected}`,
`Same:${actual === expected}`
);
});
Animating canvas (line) up and down based on the variable value.
Here I have added variable name position. Default my line will be in bottom of the cylinder. When I enter the value as 20 it has move up and vice verca if I enter the value as 15 it has to come down like that 0 - 100. So what I tried was I created a variable inside a new function call
function animatecurve()
{
var position = 0;
alert("position " + position);
var bottomcylin = $('#canvas').position().top + $('#canvas').outerHeight(true);
alert("cylinder bottom" + bottomcylin);
if (position1 > 0) {
alert("inside position" + position1);
$('#myCanvas').animate({top:"-=10px",}, 300);
} else {
$('#myCanvas').animate({top:"0px",}, 300);
}
}
Here I have found outerheight of the cylinder. It should not go beyond the cylinder if i enter the value 30 in the position my curve has to move towards top of the cylinder.
Fiddle link
Please guide me.
Regards
M
Take a look http://jsfiddle.net/BG6p2/, maybe this is what you are looking for
HTML
<canvas id="canvas" width="300" height="500"></canvas>
<input type="text" id="level" placeholder="type value here %" />
Javascript
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
input = document.getElementById('level');
//helpers
ctx.drawEllipse = function(x, y, radius, color, colorBorder){
this.fillStyle = '#000';
this.strokeStyle = '#000';
this.save();
this.scale(2, 1);
this.beginPath();
this.arc(x, y, radius, 0, 2 * Math.PI, false);
this.restore();
this.closePath();
if(color) {
this.fillStyle = color;
this.fill();
if(colorBorder) this.strokeStyle = color;
}
this.stroke();
}
ctx.drawLine = function(startX, startY, endX, endY, color){
if(color) this.strokeStyle = color;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
};
/**
* #params
* #param x - top left x coordinate
* #param y - top left y coordinate
* #param radius - ellipsis radius
* #param height - cylinder height
* #param liquidColor - inner liquid color
* #param liquidSunnySideColor - liquid surface color
* #param liquidLevel - liquid level
**/
ctx.drawCylinder = function(x, y, radius, height, liquidColor, liquidSunnySideColor,liquidLevel){
//do not spill
liquidLevel = Math.min(height, liquidLevel);
//draw liquid inner body
ctx.fillStyle = liquidColor;
ctx.fillRect(x, y + (height - liquidLevel), x + radius * 4, liquidLevel);
//console.log(x, y);
//bottom cap
ctx.drawEllipse(x, y + height, radius, liquidLevel ? liquidColor : false);
//liquid level
ctx.drawEllipse(x, y + (height - liquidLevel), radius, liquidLevel ? liquidSunnySideColor : false, true);
ctx.drawEllipse(x, y + height, radius);
//top cap
ctx.drawEllipse(x, y, radius);
//left border
ctx.drawLine(x, y, x, y + height, '#000');
//right border
ctx.drawLine(x + radius * 4, y, x + radius * 4, y + height, '#000');
}
var renderLoop = window.webkitRequestAnimationFrame,
//workers
_fps = 60,
_speed = 1,
_currentLevel = 0,
_maxLevel = 0,
_renderTime = 0,
_render,
time,
//cylinder
x = 100,
y = 100,
radius = 50,
height = 200;
_render = function(){
time = window.performance.now();
if(time - _renderTime > 1000 / _fps){
ctx.clearRect(0, 0, 300, 500);
//only positive values
_currentLevel = Math.max(0, _currentLevel);
if(_currentLevel > _maxLevel) _currentLevel -= _speed;
if(_currentLevel < _maxLevel) _currentLevel += _speed;
ctx.drawCylinder(x, y, radius, height, '#90C94D', '#BBED80', _currentLevel);
_renderTime = time;
}
renderLoop(_render);
}
_render();
input.addEventListener('blur', function(){
//only positive values
_maxLevel = Math.max(0, height * Math.min(1, this.value / 100));
});