I'm writing a simple computer animation, which is a line that rotates around a fixed point at the center of that line. The amount of rotation is based on a gradient noise algorithm (OpenSimplex noise). The line has an origin [x,y] and a nr of the animation frame. These three values plugged into OpenSimplex noise give a rotation value. This part is working perfectly.
The problem is I want to make the line appear to follow the mouse cursor, depending on how far the mouse cursor is from the line. The cursor has coordinates [mx, my] (which change for every frame of animation). I can easily rotate the line and point straight towards the cursor. But I'm having difficulties factoring in the distance. To clarify; the line is rotation on the gradient noise and the mouse cursor alters that rotation to make the line (at [x, y]) point at [mx, my].
Also, the line has an 180 degree identity, so the closest end should point towards the mouse.
Basically what I'm doing now is taking "rotation line" plus "rotation mouse". If it is between 90 and 270 deg the back of the line is closest to the cursor, otherwise the front (for simplicity this is not included in the example code below). So I then take the difference, factor in the distance and substract or add it to the rotation of the line. And this works fairly well except for some artifacts.
let r = OpenSimplexNoise(x, y, frame); // gives current original rotation
let frame = 68; // whichever frame
let x = 60; // some fixed coordinate of line
let y = 60; // some fixed coordinate of line
let mouseX = 10; // changes when the mouse moves
let mouseY = 10; // changes when the mouse moves
let mouseRadius = 200;
let width = 100;
let height = 1;
function distance (x, y, cx, cy) {
return Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
}
function angle (x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return 360 + (Math.atan2(dy, dx) * 180 / Math.PI);
}
if (distance(x, y, mouseX, mouseY) <= mouseRadius) {
let dist = distance(x, y, mouseX, mouseY);
let mouseR = angle(x, y, mouseX, mouseY) % 360;
let near = (mouseRadius - dist) / mouseRadius;
let far = 1 - near;
r = (r * far + near * mouseR) % 360;
}
// r now includes mouse
A live version:
https://jsfiddle.net/Ruudt/56pk2wd1/1/
The problem lies in the cases where the mouse passes from being left to right of perpendicular to the (original rotation) line. Here the calculation will nominate the other end as "closests", then calculate the distance and apply this to the rotation. This results in the line jumping from pointing slightly left of the cursor to right of the cursor (or vice versa).
Is there a way to fix this?
I've made an image to illustrate the situation.
The red line represents the line using only the rotation of the gradient noise
The black line is the line that also includes mouse position
the blue arc is the mouse rotation value (right end is origin)
line rotation:
Related
I'm building a p5js donut chart, but I'm struggling to show the data labels in the middle. I think I have managed to get the boundaries right for it, but how would match the angle that I'm in? Or is there a way of matching just through the colours?
https://i.stack.imgur.com/enTBo.png
I have started by trying to match the boundaries of the chart to the pointer, which I managed to do using mouseX and mouseY. Any suggestions, please?
if(mouseX >= width / 2 - width * 0.2 && mouseY >= height / 2 - width * 0.2
&& mouseX <= width / 2 + width * 0.2 && mouseY <= height / 2 + width * 0.2)
{
//console.log("YAY!!! I'm inside the pie chart!!!");
}
else
{
textSize(14);
text('Hover over to see the labels', width / 2, height / 2);
}
};
[1]: https://i.stack.imgur.com/enTBo.png
While you could theoretically use the get() function to check the color of the pixel under the mouse cursor and correlate that with one of the entries in your dataset, I think you would be much better off doing the math to determine which segment the mouse is currently over. And conveniently p5.js provides helper functions that make it very easy.
In the example you showed you are only checking if the mouse cursor is in a rectangular region. But in reality you want to check if the mouse cursor is within a circle. To do this you can use the dist(x1, y1, x2, y2) function. Once you've established that the mouse cursor is over your pie chart, you'll want to determine which segment it is over. This can be done by finding the angle between a line draw from the center of the chart to the right (or whichever direction is where you started drawing the wedges), and a line drawn from the center of the chart to the mouse cursor. This can be accomplished using the angleBetween() function of p5.Vector.
Here's a working example:
const colors = ['red', 'green', 'blue'];
const thickness = 40;
let segments = {
foo: 34,
bar: 55,
baz: 89
};
let radius = 80, centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
noFill();
strokeWeight(thickness);
strokeCap(SQUARE);
ellipseMode(RADIUS);
textAlign(CENTER, CENTER);
textSize(20);
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(200);
let keys = Object.keys(segments);
let total = keys.map(k => segments[k]).reduce((v, s) => v + s, 0);
let start = 0;
// Check the mouse distance and angle
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
// Find the angle between a vector pointing to the right, and the vector
// pointing from the center of the window to the current mouse position.
let mouseAngle =
createVector(1, 0).angleBetween(
createVector(mouseX - centerX, mouseY - centerY)
);
// Counter clockwise angles will be negative 0 to PI, switch them to be from
// PI to TWO_PI
if (mouseAngle < 0) {
mouseAngle += TWO_PI;
}
for (let i = 0; i < keys.length; i++) {
stroke(colors[i]);
let angle = segments[keys[i]] / total * TWO_PI;
arc(centerX, centerY, radius, radius, start, start + angle);
// Check mouse pos
if (mouseDist > radius - thickness / 2 &&
mouseDist < radius + thickness / 2) {
if (mouseAngle > start && mouseAngle < start + angle) {
// If the mouse is the correct distance from the center to be hovering over
// our "donut" and the angle to the mouse cursor is in the range for the
// current slice, display the slice information
push();
noStroke();
fill(colors[i]);
text(`${keys[i]}: ${segments[keys[i]]}`, centerX, centerY);
pop();
}
}
start += angle;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I think I know the source of the problem was that #thenewbie experienced: it is the p5 library being used. I was using the p5.min.js and experiencing the same problem. Once I started using the full p5.js library, the issue was resolved and #Paul's script worked.
Here is a link I came across while researching this which put me onto the solution:
https://github.com/processing/p5.js/issues/3973
Thanks Paul for the clear explanations and code above.
I'm making a game in javascript, where an object is supposed to bounce from walls. I really tried to get it to work myself, but it never works correctly.
Let's say theres a ball bouncing inside this cage (blue = 30°, brown = 60°);
The ball's coordinates are known. The angle of movement is known. The point of collision (P) coordinates are known. The angle of the wall is known. The ball's position is updating it's coordinates inside a setInterval function using this function:
function findNewPoint(x, y, angle, distance) {
var result = {};
result.x =(Math.cos(angle * Math.PI / 180) * distance + x);
result.y = (Math.sin(angle * Math.PI / 180) * distance + y);
return result;
So, upon colliding, there should be a function that properly changes the ball's angle. It's a very complicated problem it seems, because even if I know that the wall is 30°, its important to know from what side the ball is colliding into it. I tried using the "Reflection across a line in the plane" formula and also some vectors, but it never worked out for me. I'm not expecting a complete answer with code, if someone could suggest in what way this should be programmed, it would help aswell.
Edit:
Thanks for your tips guys, I realized what was causing the most confustion; if I select an angle on the canvas with my mouse, the starting coordinate(0,0) is in the bottom left corner. But since the canvas' starting coordinate is in the top left corner, this has to be considered.
Basically using this formula for calculating the angle:
function angle(cx, cy, ex, ey) {
var dy = ey - cy;
var dx = ex - cx;
var theta = Math.atan2(dy, dx);
theta *= 180 / Math.PI;
return theta;
}
if the ball moved from (50,50) to (100,100), the angle would be -45.
Now, this angle changes in the following way when hitting walls:
If im honest, I got these out of trial and error, am not really understanding why exactly 60 and 120.
It is not wise to use angle for moving ball and calculate Cos/Sin again and again. Instead use unit velocity direction vector with components vx, vy like this:
new_x = old_x + vx * Velocity_Magnitude * Time_Interval
Note that vx = Cos(angle), vy = Sin(angle), but with direction approach you seldom need to use trigonometric functions.
Tilted wall with angle Fi has normal
nx = -Sin(Fi)
ny = Cos(Fi)
To find reflection , you need to calculate dot product of velocity and normal
dot = vx * nx + vy * ny
Velocity after reflection transforms:
vnewx = v.x - 2 * dot * n.x
vnewy = v.y - 2 * dot * n.y
Use these values for further moving
(note that you can use both internal and external normal direction, because direction flip changes both components, and sign of 2 * dot * n.x remains the same)
Examples:
horizontal moving right
vx=1, vy=0
30 degrees wall has normal
nx=-1/2, ny=Sqrt(3)/2
dot = -1/2
vnewx = 1 - 2 * (-1/2) * (-1/2) = 1/2
vnewy = 0 - 2 * (-1/2) * Sqrt(3)/2 = Sqrt(3)/2
(velocity direction angle becomes 60 degrees)
horizontal moving left
vx=-1, vy=0
330 degrees wall (left bottom corner) has normal
nx=1/2, ny=Sqrt(3)/2
dot = -1/2
vnewx = -1 - 2 * (-1/2) * (1/2) = -1/2
vnewy = 0 - 2 * (-1/2) * (Sqrt(3)/2) = Sqrt(3)/2
(velocity direction angle becomes 120 degrees)
Here is a function that returns the angle of reflection given an angle of incidence and a surface angle (in degrees). It also ensures that the returned angle is between 0 and 359 degrees.
function angleReflect(incidenceAngle, surfaceAngle){
var a = surfaceAngle * 2 - incidenceAngle;
return a >= 360 ? a - 360 : a < 0 ? a + 360 : a;
}
Here's a demonstration, where the blue line is the angle of incidence, the purple line is the angle of reflection, and the black line is the surface.
If you're assuming that the ball behaves like light bouncing off a mirror, then the angle of incidence equals the angle of reflection.
So your board is 30° from 0° (straight up). The means the normal (perpendicular to the board at the point the ball hits ) is 300°. Say the ball arrives from 280°, it must leave at 320° as the difference between the angle of incidence and the normal and the angle of reflection and the normal must be equal.
I'm trying to highlight pixels that fall within a sector of a circle. I'm writing a shader to do this, but I'm implementing the logic in JavaScript until I get it right.
Essentially, each pixel coordinate in a canvas is scaled to be between 0 and 1, and is passed into the following code along with the canvas context:
function isWithinSector(ctx, x, y) {
let startAngle = degToRad(135), endAngle = degToRad(205);
// Distance of pixel from the circle origin (0.5, 0.5).
let dx = scaledX - 0.5;
let dy = scaledY - 0.5;
let angle = Math.atan2(dy, dx);
if (angle >= startAngle && angle <= endAngle) {
ctx.fillStyle = "rgba(255, 255, 0, .5)";
ctx.fillRect(x, y, 1, 1);
}
}
This works fine for some angles, but not for others. Pixels highlighted between 135 and 205 degrees appear like this (i.e. only 135 to 180 degrees are highlighted):
Note that the highlighted pixels don't match my black arc (the source of truth). I've been trying all kinds of things from Google but I'm stuck.
I have a CodePen that shows the issue: https://codepen.io/chrisparton1991/pen/XRpqXb. Can anybody guide me on what I'm doing wrong in my algorithm?
Thanks!
You get the problem if the angle is greater than 180°, as the atan2 function will then return a negative angle that is 360° smaller. This can be corrected by
let angle = Math.atan2(dy, dx);
if (angle<0) angle += 2*Math.PI;
But this is still not sufficient if you want to highlight the sector from 350° to 10°, that is, the small sector containing the 0° ray. Then the following extended normalization procedure helps.
let angle = Math.atan2(dy, dx);
let before = angle-startAngle;
if(before < -Math.PI)
before += 2*Math.PI;
let after = angle-endAngle;
if(after < -Math.PI)
after += 2*Math.PI;
Note that your image is upside-down as the screen origin is top-right, where you put the coordinates (0,1).
I have a program of a ball that follows the cursor on the screen, on a html canvas.
I also have a circle on the screen, I need to make this ball stay within the circle.
here is the current conditional keeping the ball within the canvas.
if(ball.x < 0 || ball.x > canvas.width)
ball.dx = -ball.dx;
if(ball.y < 0 || ball.y > canvas.height)
ball.dy = -ball.dy;
ball.x, ball.y are the x,y coordinates of the "ball" object instance. The ball.dy and ball.dx is the direction of the ball, and the "-" reverses the direction of the ball when it hits the outside of the canvas.
here is the circle code for the arc.
context.arc(canvas.width / 2, canvas.height / 2, 60, 0, 2*Math.PI, false);
format:arc(x,y,radius, start, end, false)
how can I utilize the center point of the circle by not allowing it to move outside of it?
thank you.
I could be completely wrong on this, but can't you just use the Pythagorean Theorem? (a*a)+(b*b)=(c*c)
The center of your canvas is 0,0
The radius of your arc is 60
Ball position is 29,-29
function isInsideCircle(ball,radius){
var a=Math.pow(ball.x,2);
var b=Math.pow(ball.y,2);
var c=Math.sqrt(a+b);
return c<radius;
}
With ball at 29,-29 = distance of ~41 from center (inside the circle)
With ball at 50,50 = distance of ~71 from center (outside circle)
You will have to calculate the x and the y coordinates of the bound each time. The bound basically runs around the entire circle. This point is the intersection of the line between the center of the circle and the current x and y mouse co-ordinates and the circle. The limiting condition is that the length of this line does not exceed the radius of the circle. So if you are ever in a state where the distance between the center of the circle and the current x and y is greater than the radius of the circle, you will have to limit the x and y coordinate to be on the circumference of the circle.
The equation for a circle is:
x^2 + y^2 = r^2
Where r is the radius of the circle.
The equation for a line is:
y = mx + c
Where m is the slope and c is an offset. In our case we're going to assume that the origin is the center of the circle, so we don't have a c which means the equation is:
y = mx
What about the value of m? m is our slope which we can calculate from the rise over the run:
rise = ball.x - yCircleCenter
run = ball.y - xCircleCenter
Which means the equation for our line is:
y = (rise / run) * x
You can substitute values of x and y into the circle equation to figure out the x and y that lie on the circumference of the circle, which gives you:
x_on_circle = (r * run) / sqrt(rise^2 + run^2)
y_on_circle = (r * rise) / sqrt(rise^2 + run^2)
Assuming this was a function called limit, you can do something like this:
var distance = Math.sqrt(((ball.x - xCircleCenter) ^ 2) + ((ball.y - yCircleCenter) ^ 2));
if(distance > radius) {
var limitedCoordinates = limit(ball.x, ball.y);
ball.x = limitedCoordinates.x;
ball.y = limitedCoordinates.y
}
I found this excellent question and answer which starts with x/y (plus the center x/y and degrees/radians) and calculates the rotated-to x'/y'. This calculation works perfectly, but I would like to run it in the opposite direction; starting with x'/y' and degrees/radians, I would like to calculate the originating x/y and the center x/y.
(x', y') = new position
(xc, yc) = center point things rotate around
(x, y) = initial point
theta = counterclockwise rotation in radians (radians = degrees * Pi / 180)
dx = x - xc
dy = y - yc
x' = xc + dx cos(theta) - dy sin(theta)
y' = yc + dx sin(theta) + dy cos(theta)
Or, in JavaScript/jQuery:
XYRotatesTo = function($element, iDegrees, iX, iY, iCenterXPercent, iCenterYPercent) {
var oPos = $element.position(),
iCenterX = ($element.outerWidth() * iCenterXPercent / 100),
iCenterY = ($element.outerHeight() * iCenterYPercent / 100),
iRadians = (iDegrees * Math.PI / 180),
iDX = (oPos.left - iCenterX),
iDY = (oPos.top - iCenterY)
;
return {
x: iCenterX + (iDX * Math.cos(iRadians)) - (iDY * Math.sin(iRadians)),
y: iCenterY + (iDX * Math.sin(iRadians)) + (iDY * Math.cos(iRadians))
};
};
The math/code above solves for the situation in Figure A; it calculates the position of the destination x'/y' (green circle) based on the known values for x/y (red circle), the center x/y (blue star) and the degrees/radians.
But I need math/code to solve for Figure B; where I can find not only the destination x/y (green circle), but also the destination center x/y (green star) from the known values of the starting x/y (grey circle, though probably not needed), the destination x'/y' (red circle) and the degrees/radians.
The code above will solve for the destination x/y (green circle) via iDegrees * -1 (thanks to #andrew cooke's answer which has since been removed by him), but in order to do that I need to feed into it the location of the destination center x/y (green star), and that is the calculations I'm currently missing, as you can see in Diagram C, below:
So... how do I find the coordinates ?/? (green star) given n, A (angle) and x'/y' (red circle)?
You're trying to find an inverse transformation. You start with the composition of two linear transformations, a translation T and a rotation R. You apply R first to a vector x and T second, so the expression is y = TRx. To solve the inverse problem you need the inverse of TR, written (TR)-1, which is equal to R-1T-1. The inverse of the rotation R is just the rotation by the negative of the angle (which you mention). The inverse of the translation is, similarly, the original translation multiplied by -1. So your answer is x = R-1T-1y.
In your present situation, you're given the rotation by means of its angle, but you'll need to compute the translation. You'll need the grey circle, which you didn't think you would need. Apply the rotation R (not its inverse) to the gray circle. Subtract this point from the red circle. This is the original translation T. Reverse the sign to get T-1.