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).
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
I'm making a pie chart and would like my segments to highlight when the cursor moves over them, then to expand when the user clicks on the segment. I've seen many tutorials on how to have a circle or square recognize the cursor is within their space but nothing that I can wrap my head around for an arc that can change in size dependent on the value input.
Here's how I've set my chart up:
chartX = 250;
chartY = 250;
chartW = 250;
chartH = 250;
// Movie Genres
com = 32;
act = 52;
rom = 40;
dra = 18;
sci = 26;
totalMovies = com+act+rom+dra+sci;
function setup() {
createCanvas(500, 500);
background(255);
}
function draw() {
startAngle = 0;
totalRadians = TWO_PI;
// Pie Chart
noFill();
ellipse(chartX, chartY, chartW);
fill(38,70,83);
arc(chartX, chartY, chartW, chartH, startAngle, (totalRadians/(totalMovies/com)),PIE);
startAngle = (totalRadians/(totalMovies/com));
fill(42,157,143);
arc(chartX, chartY, chartW, chartH, startAngle, startAngle + (totalRadians/(totalMovies/act)),PIE);
startAngle+=(totalRadians/(totalMovies/act));
fill(233,196,106);
arc(chartX, chartY, chartW, chartH, startAngle, startAngle + (totalRadians/(totalMovies/rom)),PIE);
startAngle+=(totalRadians/(totalMovies/rom));
fill(244,162,97);
arc(chartX, chartY, chartW, chartH, startAngle, startAngle + (totalRadians/(totalMovies/dra)),PIE);
startAngle+=(totalRadians/(totalMovies/dra));
fill(231,111,81);
arc(chartX, chartY, chartW, chartH, startAngle, startAngle + (totalRadians/(totalMovies/sci)),PIE);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js" integrity="sha512-N4kV7GkNv7QR7RX9YF/olywyIgIwNvfEe2nZtfyj73HdjCUkAfOBDbcuJ/cTaN04JKRnw1YG1wnUyNKMsNgg3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Assuming your pie chart segments are defined as a series of angles and that the pie slices originate from a fix direction, then you can use the horizontal and vertical offset from the center of your pie to the mouse cursor, and then use trigonometric functions to find the angle between the line from the center of your pie to the mouse cursor and the horizontal axis. That angle can then be used to determine which pie segment the mouse is over. The specific trigonometric function you will want to use is the arc tangent (a.k.a atan, inverse tangent, or tan⁻¹), and the specific p5.js function is atan2().
The trigonometric function tangent takes an angle from one of the corners of a right triangle and returns the ratio between the length of the side opposite that angle and the side adjacent to it (that is between the angle and the 90 corner, not the hypotenuse). For the same angle, this ratio will be the same no matter the size of the triangle. The arc tangent performs the reverse operation, taking the ratio and returning the angle. However, because the ratio is the same for some triangles in different orientations, the atan2 function is a helpful variant that instead of taking a ratio, takes the vertical and horizontal sides (signed to indicate direction) and returns the correct angle from 0 to 360° (or 0 to 2π in radians). Obviously the angle in this scenario is not the actual angle of the corner of the triangle, but the angle between the positive horizontal axes and the hypotenuse of the triangle.
const colorNames = ['red', 'green', 'blue'];
const radius = 80;
let segments = [ 34, 55, 89 ];
let angles;
let colors;
let centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
ellipseMode(RADIUS);
angleMode(DEGREES);
noStroke();
let total = segments.reduce((v, s) => v + s, 0);
angles = segments.map(v => v / total * 360);
colors = colorNames.map(n => color(n));
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(255)
let start = 0;
let mouseAngle = atan2(mouseY - centerY, mouseX - centerX);
if (mouseAngle < 0) {
mouseAngle += 360;
}
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
for (let ix = 0; ix < angles.length; ix++) {
let hover = mouseDist < radius && mouseAngle >= start && mouseAngle < start + angles[ix];
fill(red(colors[ix]), green(colors[ix]), blue(colors[ix]), hover ? 255 : 127);
arc(centerX, centerY, radius, radius, start, start + angles[ix]);
start += angles[ix];
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I assume you can do something with angles.
you can get the angle from the middle of the chart with simple cos/sin function, and then you can determine if the mouse is on the line of the arc by the distance from the middle of the chart.
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 have the xy coordinates from before and during a drag event, this.x and this.y```` are the current coordinates,this.lastXandthis.lastY``` are the origin.
What I need to do is given a radian of the source element, determine which mouse coordinate to use, IE if the angle is 0 then the x coordinates is used to give a "distance" if the degrees are 90 then the y coordinates are used
if the radian is 0.785398 then both x and y would need to be used.
I have the following code for one axis, but this only flips the y coordinates
let leftPosition;
if (this.walls[this.dragItem.wall].angle < Math.PI / 2) {
leftPosition = Math.round((-(this.y - this.lastY) / this.scale + this.dragItem.origin.left));
} else {
leftPosition = Math.round(((this.y - this.lastY) / this.scale + this.dragItem.origin.left));
}
I have an example here https://engine.owuk.co.uk
what I need to do is have the radian dictate what x or y coordinate is used to control the drag of the item by calculating the leftPosition, I have been loosing my mind trying to get this to work :(
The Math.sin and Math.cos is what you need, here is an example
<canvas id="c" width=300 height=150></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
function drawShape(size, angle, numPoints, color) {
ctx.beginPath();
for (j = 0; j < numPoints; j++) {
a = angle * Math.PI / 180
x = size * Math.sin(a)
y = size * Math.cos(a)
ctx.lineTo(x, y);
angle += 360 / numPoints
}
ctx.fillStyle = color;
ctx.fill();
}
ctx.translate(80, 80);
drawShape(55, 0, 7, "green");
drawShape(45, 0, 5, "red");
drawShape(35, 0, 3, "blue");
ctx.translate(160, 0);
drawShape(55, 15, 7, "green");
drawShape(45, 35, 5, "red");
drawShape(35, 25, 3, "blue");
</script>
Here is a theoretical answer to your problem.
In the simplest way, you have an object within a segment that has to move relative to the position of the mouse, but constrained by the segment's vector.
Here is a visual representation:
So with the mouse at the red arrow, the blue circle needs to move to the light blue.
(the shortest distance between a line and a point)
How do we do that?
Let's add everything we can to that image:
The segment and the mouse form a triangle and we can calculate the length of all sides of that triangle.
The distance between two points is an easy Pythagorean calculation:
https://ncalculators.com/geometry/length-between-two-points-calculator.htm
Then we need the height of the triangle where the base is our segment:
https://tutors.com/math-tutors/geometry-help/how-to-find-the-height-of-a-triangle
That will give us the distance from our mouse to the segment, and we do know the angle by adding the angle of the segment + 90 degrees (or PI/2 in radians) that is all that we need to calculate the position of our light blue circle.
Of course, we will need to also add some min/max math to not exceed the boundaries of the segment, but if you made it this far that should be easy pickings.
I was able to make the solution to my issue
let position;
const sin = Math.sin(this.walls[this.dragItem.wall].angle);
const cos = Math.cos(this.walls[this.dragItem.wall].angle);
position = Math.round(((this.x - this.lastX) / this.scale * cos + (this.y - this.lastY) / this.scale * sin) + this.dragItem.origin.left);
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:
I need to take a long (max resolution) image and wrap it into a circle. So imagine bending a steel bar so that it is now circular with each end touching.
I have been banging my head against threejs for the last 8 hours and have so far managed to apply the image as a texture on a circle geometry, but can't figure out how to apply the texture to a long mesh and then warp that mesh appropriately. The warping doesn't need to be (and shouldn't be) animated. What we basically have is a 360 panoramic image that we need to "flatten" into a top-down view.
In lieu of sharing my code (as it's not significantly different), I've so far been playing around with this tutorial:
http://www.johannes-raida.de/tutorials/three.js/tutorial06/tutorial06.htm
And I do (I think) understand the broad strokes at this point.
Other things I've tried is to use just canvas to slice the image up into strips and warp each strip... this was horribly slow and I couldn't get that to work properly either!
Any help/suggestions?
Here's also a shader version: Shadertoy - Circle Distortion
This is the actual code:
#define dPI 6.28318530718 // 2*PI
#define sR 0.3 // small radius
#define bR 1.0 // big radius
void main(void)
{
// calc coordinates on the canvas
vec2 uv = gl_FragCoord.xy / iResolution.xy*2.-vec2(1.);
uv.x *= iResolution.x/iResolution.y;
// calc if it's in the ring area
float k = 0.0;
float d = length(uv);
if(d>sR && d<bR)
k = 1.0;
// calc the texture UV
// y coord is easy, but x is tricky, and certain calcs produce artifacts
vec2 tUV = vec2(0.0,0.0);
// 1st version (with artifact)
//tUV.x = atan(uv.y,uv.x)/dPI;
// 2nd version (more readable version of the 3rd version)
//float disp = 0.0;
//if(uv.x<0.0) disp = 0.5;
//tUV.x = atan(uv.y/uv.x)/dPI+disp;
// 3rd version (no branching, ugly)
tUV.x = atan(uv.y/uv.x)/dPI+0.5*(1.-clamp(uv.x,0.0,1.0)/uv.x);
tUV.y = (d-sR)/(bR-sR);
// output pixel
vec3 col = texture2D(iChannel0, tUV).rgb;
gl_FragColor = vec4(col*k,1.);
}
So you could draw rectangle on the canvas and add this shader code.
I hope this helps.
So here's a function using canvas's context2d that does the job.
The idea is to go around all the circle by a small angular step and to draw a thin slice of 'texture' along the circle radius.
To make it faster, only way i see is to compute by hand the transform to do one single setTransform instead of all this stuff.
The step count is optimal with step = atan(1, radius)
(if you do the scheme it's obvious : to go one y up when you're radius far from the center then tan = 1/radius => step angle = atan(1, radius).)
fiddle is here :
http://jsfiddle.net/gamealchemist/hto1s6fy/
A small example with a cloudy landscape :
// draw the part of img defined by the rect (startX, startY, endX, endY) inside
// the circle of center (cx,cy) between radius (innerRadius -> outerRadius)
// - no check performed -
function drawRectInCircle(img, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY) {
var angle = 0;
var step = 1 * Math.atan2(1, outerRadius);
var limit = 2 * Math.PI;
ctx.save();
ctx.translate(cx, cy);
while (angle < limit) {
ctx.save();
ctx.rotate(angle);
ctx.translate(innerRadius, 0);
ctx.rotate(-Math.PI / 2);
var ratio = angle / limit;
var x = startX + ratio * (endX - startX);
ctx.drawImage(img, x, startY, 1, (endY - startY), 0, 0, 1, (outerRadius - innerRadius));
ctx.restore();
angle += step;
}
ctx.restore();
}