In a p5/processing project i have been working on, i need to create a line that has a triangle in the middle which always faces one of the connection points of the line.
It is pretty easy to create one that stands still, but my endpoints move around and rotate.
I need to find a way to also rotate the little triangle when the line shifts to this "|" from this "---".
My current code goes like this:
let middleX = (fromX + toX)/2;
let middleY = (fromY + toY)/2;
triangle(middleX,middleY+5,middleX+5,middleY,middleX,middleY-5);
line(fromX , fromY, toX, toY);
As you can anticipate, this doesn't work with rotations.
I need help :).
Thanks for your attention.
You can:
use atan2() to calculate the rotation between the two points,
use push() to isolate the coordinate space (rotate locally without affecting the rest of the sketch (e.g. the line)
simply call rotate(): it takes in an angle in radians which is what atan2() returns
Here's an example based on your snippet:
let fromX = 200;
let fromY = 200;
let toX = 300;
let toY = 100;
let triangleSize = 5;
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
// test: change to position
toX = mouseX;
toY = mouseY;
let middleX = (fromX + toX) / 2;
let middleY = (fromY + toY) / 2;
// calculate the angle between from -> to points
let angle = atan2(toY - fromY, toX - fromX);
// isolate coordinate system (indenting is purely visual, not required)
push();
// move to central position
translate(middleX, middleY);
// rotate from translated position
rotate(angle);
// render triangle
triangle(0, triangleSize, triangleSize, 0, 0, -triangleSize);
pop();
line(fromX, fromY, toX, toY);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.0.0/p5.min.js"></script>
Note that the order of transformations (translation, rotation, scale) is important.
(e.g. if rotate, then translate the triangle will land in a different location)
Also you draw the triangle as pointing to the right by default which aligns nicely with 0 radians 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.
Have a class with methods that draws rectangular shapes with random lengths.
However, is unable to only do rotate() on the shapes without translating ( translate() ), which translate will make the shapes draw off the canvas.
So are there anyways to make it so no translation occurs while rotating?
The code:
class rect {
constructor(range) {
this.boundary = 100;
this.x = random(this.boundary, width - this.boundary);
this.y = random(this.boundary, height - this.boundary);
this.xu = this.x + random(50, 200);
this.yu = this.y + random(50, 200);
this.range = range;
this.limit = random(-range, range);
this.rand_color1 = random(255);
this.rand_color2 = random(255);
this.rand_color3 = random(255);
}
custom_shapes() {
// how to make no translations occur while only perform rotation on shapes?
translate(this.x-this.margin,this.y-this.margin);
rotate(30);
fill(this.rand_color1, this.rand_color2, this.rand_color3)
quad(this.x, this.y, this.xu + this.limit, this.y, this.xu, this.yu, this.x, this.yu + this.limit);
}
}
If you mean that your rectangular is going of the screen when rotating, it's rotating around x = 0, y= 0 point, so i guess you could do something like:
push() //push and pop acts as a way to "seperate" any style and translate and so on...
rectMode(CENTER) // basically the middle of the rect = x , y
translate(this.x,this.y) // **OR** translate(this.x - this.rectSizeX / 2, this.y - this.rectSizeY / 2)
//quad() // if you're not using the rectMode()
pop() // also you'll have to fill() and so on in here i believe, not too sure
also if you know it's allways going to be a long or tall square, you can just use rect(x,y,xSize,ySize) // if think it's the size anyways
If you just want to separate translate() in general, just put push() and pop() around it...
Oh yeah and translate() basically just makes whatever x and y you give it into 0,0... Dunno if i said that already i'm just editing this the next day.
I'm trying to join two separate bezier curves into one continuous curve. Currently, what I have looks like this:
The problem is that they aren't joined, so the points at which they meet look pointy/sharp instead of curvy and smooth. I've looked into documentation for joining bezier curves in P5.js, but am unsure of how to translate this into HTML5 Canvas. How do I join these two bezier curves so that they look like one smooth and continuous curve?
This is my code:
const canvas = document.getElementById('canvas');
const c = canvas.getContext("2d");
width = 800;
height = 500;
canvas.width = width;
canvas.height = height;
let face;
let centerX = width / 2;
let centerY = height / 3;
setup();
function setup() {
c.clearRect(0, 0, canvas.width, canvas.height);
face = new Face();
draw();
};
function draw() {
setBackground(`rgba(250, 250, 250, 1)`);
c.beginPath();
c.moveTo(centerX - face.hsx, centerY + face.hsy);
c.bezierCurveTo(centerX - face.hcp1x / 10, centerY - face.hsy2,
centerX + face.hcp1x / 10, centerY - face.hsy2,
centerX + face.hsx, centerY + face.hsy);
c.moveTo(centerX - face.hsx, centerY + face.hsy);
c.bezierCurveTo(centerX - face.hcp1x, centerY + face.hcp1y,
centerX + face.hcp1x, centerY + face.hcp1y,
centerX + face.hsx, centerY + face.hsy);
c.stroke();
c.fillStyle = (`rgba(25, 250, 211, 0)`);
c.fill();
}
function setBackground(color) {
c.fillStyle = color;
c.fillRect(0, 0, width, height);
}
function Face() {
this.hsx = 150;
this.hsy = 0;
this.hsy2 = 120;
this.hcp1x = 120;
this.hcp1y = 250;
}
Common tangent
To join two beziers smoothly you need to make the lines from the common point parallel thus defining the tangent at the end and start of the two beziers to be the same.
The following image illustrates this
The line that is defined by the two control points (C2, C1) and the common point (P) is the tangent of the curve at P. The length of the line segments have no constraints.
How?
There are dozens of ways to do this and how you do it is dependent on the requirements of the curve, the type of curve, and much more.
Example
I am not going to give a full example as it requires an understanding of vector maths and a cover all solution on the assumption you are not familiar with vector maths would be huge.
Thus the most basic pseudo code example uses the previous control and end points to calculate the next control point. ? represents unknowns which are not bound by constraints required to keep the lines parallel
// From illustration in answer
corner = ? // Distance to next control point as fraction of distance
// from previous control point
C2 = {x:?, y:?} // Last control point of previous bezier
P = {x:?, y:?} // Start of next bezier
C1 = { // Next control point along line from previous and scaled
x: P.x + (P.x - C2.x) * corner,
y: P.y + (P.y - C2.y) * corner,
}
// two beziers with common point P
ctx.bezierCurveTo(?,?, C2.x, C2.y, P.x, P.y)
ctx.bezierCurveTo(C1.x, C1.y, ?, ?, ?, ?)
In the below page:
https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_canvas_beziercurveto
You change the width and height of the canvas to 1000.
Then you replace the two lines between beginpath and stroke with the below code.
points=[
{x:0, y:300},//0
{x:100,y:500},//1
{x:200,y:300},//2
{x:300,y:100},//3
{x:400,y:300},//4
{x:100,y:500},//5
{x:100,y:300},//6
];
ctx.rect(points[0].x-5, points[0].y-5, 10,10);
var smoother={};
smoother.x=((points[1].x-points[0].x)/10)+points[0].x;
smoother.y=((points[1].y-points[0].y)/10)+points[0].y;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[1].x-5, points[1].y-5, 10,10);
ctx.rect(points[2].x-5, points[2].y-5, 10,10);
ctx.moveTo(points[0].x,points[0].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[1].x, points[1].y,
points[2].x, points[2].y
);
var smoother={};
var dx=(points[2].x-points[1].x);
var dy=(points[2].y-points[1].y);
var yperx=(dy/dx);
travel_x=dx;
travel_y=(dx*yperx);
smoother.x=points[2].x+travel_x/3;
smoother.y=points[2].y+travel_y/3;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[3].x-5, points[3].y-5, 10,10);
ctx.rect(points[4].x-5, points[4].y-5, 10,10);
ctx.moveTo(points[2].x,points[2].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[3].x, points[3].y,
points[4].x, points[4].y
);
var smoother={};
var dx=(points[4].x-points[3].x);
var dy=(points[4].y-points[3].y);
var yperx=(dy/dx);
travel_x=dx;
travel_y=(dx*yperx);
smoother.x=points[4].x+travel_x/3;
smoother.y=points[4].y+travel_y/3;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[5].x-5, points[5].y-5, 10,10);
ctx.rect(points[6].x-5, points[6].y-5, 10,10);
ctx.moveTo(points[4].x,points[4].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[5].x, points[5].y,
points[6].x, points[6].y
);
You can also run it here by pressing the run button:
https://www.w3schools.com/code/tryit.asp?filename=GSP1RKBFHGGK
At that you can manipulate the pixels in points[], and notice that the bezier curves always connect kinda smoothly.
That's because in each new bezier curve, the system automatically makes the first bezier point, which only serves the role of smoothing the line. Which is basically just a point that continues in whatever direction the previous bezier was heading, for a little bit. The next pixel in the bezier is then an actual destination, which the given bezier curve then takes care of smoothing.
There is the number 3 in there, it represents how quickly you want to start going in the actual direction. If it's too large we start to too quickly head in the needed direction and the smoothness suffers.
If it's too small we are ignoring too much where the line needs to be going, in favor of smoothness.
I've been trying to draw an arc on the canvas, using p5.js. I got start & end points, the chord length i calculate using pythagoras using the two points, the height & width values are also given.
In order to draw an arc, i need to use the following function;
arc(x, y, w, h, start, stop, [mode], [detail]) for docs refer to here
The start & stop parameters refer to the start&stop angles specified in radians. I can't draw the arc without those angles and i'm unable to calculate them using what i got.
I searched for lots of examples similar to my question, but it is suggested to calculate the center angle, which i'm also unable to do so. Even though i was able to calculate the center angle, how i'm supposed to get the start&stop angles afterwards?
I have drawn some example illustrations on GeoGebra;
The angle of a vector can be calculated by atan2().
Note, that:
tan(alpha) = sin(alpha) / cos(alpha)
If you've a vector (x, y), then than angle (alpha) of the vector relative to the x-axis is:
alpha = atan2(y, x);
The start_angle and stop_angle of an arc, where the center of the arc is (cpt_x, cpt_y), the start point is (spt_x, spt_y) and the end point is (ept_x, ept_y), can be calculated by:
start_angle = atan2(spt_y-cpt_y, spt_x-cpt_x);
stop_angle = atan2(ept_y-cpt_y, ept_x-cpt_x);
See the example, where the stop angle depends on the mouse position:
var sketch = function( p ) {
p.setup = function() {
let sketchCanvas = p.createCanvas(p.windowWidth, p.windowHeight);
sketchCanvas.parent('p5js_canvas')
}
p.windowResized = function() {
p.resizeCanvas(p.windowWidth, p.windowHeight);
}
p.draw = function() {
let cpt = new p5.Vector(p.width/2, p.height/2);
let rad = p.min(p.width/2, p.height/2) * 0.9;
let stop_angle = p.atan2(p.mouseY-cpt.y, p.mouseX-cpt.x);
p.background(192);
p.stroke(255, 64, 64);
p.strokeWeight(3);
p.noFill();
p.arc(cpt.x, cpt.y, rad*2, rad*2, 0, stop_angle);
}
};
var circle = new p5(sketch);
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js"></script>
<div id="p5js_canvas"></div>
I was trying to get the green triangle to rotate about its center and orient itself towards the mouse position. I was able to accomplish this, and you can view the full code and result here:
https://codepen.io/Carpetfizz/project/editor/DQbEVe
Consider the following lines of code:
r = Math.atan2(mouseY - centerY, mouseX - centerX)
ctx.rotate(r + Math.PI/2)
I arbitrarily added Math.PI/2 to my angle calculation because without it, the rotations seemed to be 90 degrees off (by inspection). I want a better understanding of the coordinate system which atan2 is being calculated with respect to so I can justify the reason for offsetting the angle by 90 degrees (and hopefully simplify the code).
EDIT:
To my understanding, Math.atan2 is measuring the angle illustrated in blue. Shouldn't rotating both triangles that blue angle orient it towards the mouse mouse pointer (orange dot) ? Well - obviously not since it's the same angle and they are two different orientations, but I cannot seem to prove this to myself.
This is because of how the Math.atan2 works.
From MDN:
This is the counterclockwise angle, measured in radians, between the positive X axis, and the point (x, y).
In above figure, the positive X axis is the horizontal segment going from the junction to the right-most position.
To make it clearer, here is an interactive version of this diagram, where x, y values are converted to [-1 ~ 1] values.
const ctx = canvas.getContext('2d'),
w = canvas.width,
h = canvas.height,
radius = 0.3;
ctx.textAlign = 'center';
canvas.onmousemove = canvas.onclick = e => {
// offset mouse values so they are relative to the center of our canvas
draw(as(e.offsetX), as(e.offsetY));
}
draw(0, 0);
function draw(x, y) {
clear();
drawCross();
drawLineToPoint(x, y);
drawPoint(x, y);
const angle = Math.atan2(y, x);
drawAngle(angle);
writeAngle(angle);
}
function clear() {
ctx.clearRect(0, 0, w, h);
}
function drawCross() {
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(s(0), s(-1));
ctx.lineTo(s(0), s(1));
ctx.moveTo(s(-1), s(0));
ctx.lineTo(s(0), s(0));
ctx.strokeStyle = ctx.fillStyle = '#2e404f';
ctx.stroke();
// positive X axis
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(s(0), s(0));
ctx.lineTo(s(1), s(0));
ctx.stroke();
ctx.lineWidth = 1;
ctx.font = '20px/1 sans-serif';
ctx.fillText('+X', s(1) - 20, s(0) - 10);
}
function drawPoint(x, y) {
ctx.beginPath();
ctx.arc(s(x), s(y), 10, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.font = '12px/1 sans-serif';
ctx.fillText(`x: ${x.toFixed(2)} y: ${y.toFixed(2)}`, s(x), s(y) - 15);
}
function drawLineToPoint(x, y) {
ctx.beginPath();
ctx.moveTo(s(0), s(0));
ctx.lineTo(s(x), s(y));
ctx.strokeStyle = 'red';
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([0]);
}
function drawAngle(angle) {
ctx.beginPath();
ctx.moveTo(s(radius), s(0));
ctx.arc(s(0), s(0), radius * w / 2,
0, // 'arc' method also starts from positive X axis (3 o'clock)
angle,
true // Math.atan2 returns the anti-clockwise angle
);
ctx.strokeStyle = ctx.fillStyle = 'blue';
ctx.stroke();
ctx.font = '20px/1 sans-serif';
ctx.fillText('∂: ' + angle.toFixed(2), s(0), s(0));
}
// below methods will add the w / 2 offset
// because canvas coords set 0, 0 at top-left corner
// converts from [-1 ~ 1] to px
function s(value) {
return value * w / 2 + (w / 2);
}
// converts from px to [-1 ~ 1]
function as(value) {
return (value - w / 2) / (w / 2);
}
<canvas id="canvas" width="500" height="500"></canvas>
So now, if we go back to your image, it currently points to the top (positive Y axis), while the angle you just measured is realtive to the x axis, so it doesn't point where you intended.
Now we know the problem, the solution is quite easy:
either apply the + Math.PI / 2 offset to your angle like you did,
either modify your original image so that it points to the positive X axis directly.
The coordinate system on canvas works with 0° pointing right. This means anything you want to point "up" must be initially drawn right.
All you need to do in this case is to change this drawing:
to
pointing "up" 0°
and you can strip the math back to what you'd expect it to be.
var ctx = c.getContext("2d"), img = new Image;
img.onload = go; img.src = "https://i.stack.imgur.com/Yj9DU.jpg";
function draw(pos) {
var cx = c.width>>1,
cy = c.height>>1,
angle = Math.atan2(pos.y - cy, pos.x - cx);
ctx.setTransform(1,0,0,1,cx, cy);
ctx.rotate(angle);
ctx.drawImage(img, -img.width>>1, -img.height>>1);
}
function go() {
ctx.globalCompositeOperation = "copy";
window.onmousemove = function(e) {draw({x: e.clientX, y: e.clientY})}
}
html, body {margin:0;background:#ccc}
#c {background:#fff}
<canvas id=c width=600 height=600></canvas>
When you do arctangents in math class, you're generally dealing with an y-axis that increases going upwards. In most computer graphics systems, however, including canvas graphics, y increases going downward. [erroneous statement deleted]
Edit: I have to admit what I wrote before was wrong for two reasons:
A change in the direction of the axis would be compensated for by adding π, not π/2.
The canvas context rotate function rotates clockwise for positive angles, and that alone should compensate for the flip of the y-axis.
I played around with a copy of your code in Plunker, and now I realize the 90° rotation simply compensates for the starting orientation of the graphic image you're drawing. If the arrowhead pointed right to start with, instead of straight up, you wouldn't need to add π/2.
I encountered the same problem and was able to achieve the desired result with a following axis 'trick':
// Default usage (works fine if your image / shape points to the RIGHT)
let angle = Math.atan2(delta_y, delta_x);
// 'Tricky' usage (works fine if your image / shape points to the LEFT)
let angle = Math.atan2(delta_y, -delta_x);
// 'Tricky' usage (works fine if your image / shape points to the BOTTOM)
let angle = Math.atan2(delta_x, delta_y);
// 'Tricky' usage (works fine if your image / shape points to the TOP)
let angle = Math.atan2(delta_x, -delta_y);