Goal: I'm trying to create a triangle given two angles (a0,b0). To do so, I'm trying to rotate a vector r0 by some angle a0 around one of the vertices of r0. Here's a diagram illustrating my idea.
Problem: However, when I rotate the line, it seems to rotate around the origin. I've read many, many articles on how to fix this, but none of the suggested solutions (i.e., translate then rotate, push(), pop()) don't seem to work, perhaps because I'm dealing with a line segment here. Below is my code.
MWE Code:
let angle = 0;
function setup() {
createCanvas(600, 400);
angleMode(DEGREES);
}
function draw() {
let v1 = createVector(width / 2 - 50, height / 2);
let v2 = createVector(width / 2 + 50, height / 2);
background(255);
stroke(0);
strokeWeight(4);
let r0 = line(v1.x, v1.y, v2.x, v2.y);
rotate(20);
let r1 = line(v1.x, v1.y, v2.x, v2.y);
strokeWeight(10);
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.min.js"></script>
Any help is appreciated.
rotate rotates around the origin of the coordinate system. To rotate around a pivot point, you must:
move the pivot to the origin
rotate the object
move the pivot back to its place in the scene
translate(v1.x, v1.y);
rotate(-1 * mouseX);
translate(-v1.x, -v1.y);
let angle = 0;
function setup() {
createCanvas(600, 300);
angleMode(DEGREES);
}
function draw() {
let v1 = createVector(width / 2 - 50, height / 2);
let v2 = createVector(width / 2 + 50, height / 2);
background(255);
stroke(0);
strokeWeight(4);
push();
translate(v1.x, v1.y);
rotate(-1 * mouseX);
translate(-v1.x, -v1.y);
let r0 = line(v1.x, v1.y, v2.x, v2.y);
strokeWeight(10);
let p1 = point(v1.x, v1.y);
let p2 = point(v2.x, v2.y);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.min.js"></script>
For individual model transformations, you must push() the current matrix before specifying the transformations and pop() the matrix after drawing the objects. So you can draw different objects with different transformations.
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.
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.
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);
I've been recently adding shadows to a project. I've ended up with something that I like, but the shadows are a solid transparent color throughout. I would prefer them to be a fading gradient as they go further.
What I currently have:
What I'd like to achieve:
Right now I'm using paths to draw my shadows on a 2D Canvas. The code that is currently in place is the following:
// Check if edge is invisible from the perspective of origin
var a = points[points.length - 1];
for (var i = 0; i < points.length; ++i, a = b)
{
var b = points[i];
var originToA = _vec2(origin, a);
var normalAtoB = _normal(a, b);
var normalDotOriginToA = _dot(normalAtoB, originToA);
// If the edge is invisible from the perspective of origin it casts
// a shadow.
if (normalDotOriginToA < 0)
{
// dot(a, b) == cos(phi) * |a| * |b|
// thus, dot(a, b) < 0 => cos(phi) < 0 => 90° < phi < 270°
var originToB = _vec2(origin, b);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(a.x + scale * originToA.x,
a.y + scale * originToA.y);
ctx.lineTo(b.x + scale * originToB.x,
b.y + scale * originToB.y);
ctx.lineTo(b.x, b.y);
ctx.closePath();
ctx.globalAlpha = _shadowIntensity / 2;
ctx.fillStyle = 'black';
ctx.fillRect(_innerX, _innerY, _innerWidth, _innerHeight);
ctx.globalAlpha = _shadowIntensity;
ctx.fill();
ctx.globalAlpha = 1;
}
}
Suggestions on how I could go about achieving this? Any and all help is highly appreciated.
You can use composition + the new filter property on the context which takes CSS filters, in this case blur.
You will have to do it in several steps - normally this falls under the 3D domain, but we can "fake" it in 2D as well by rendering a shadow-map.
Here we render a circle shape along a line represented by length and angle, number of iterations, where each iteration increasing the blur radius. The strength of the shadow is defined by its color and opacity.
If the filter property is not available in the browser it can be replaced by a manual blur (there are many out there such as StackBoxBlur and my own rtblur), or simply use a radial gradient.
For multiple use and speed increase, "cache" or render to an off-screen canvas and when done composite back to the main canvas. This will require you to calculate the size based on max blur radius as well as initial radius, then render it centered at angle 0°. To draw use drawImage() with a local transform transformed based on start of shadow, then rotate and scale (not shown below as being a bit too broad).
In the example below it is assumed that the main object is drawn on top after the shadow has been rendered.
The main function takes the following arguments:
renderShadow(ctx, x, y, radius, angle, length, blur, iterations)
// ctx - context to use
// x/y - start of shadow
// radius - shadow radius (assuming circle shaped)
// angle - angle in radians. 0° = right
// length - core-length in pixels (radius/blur adds to real length)
// blur - blur radius in pixels. End blur is radius * iterations
// iterations - line "resolution"/quality, also affects total end blur
Play around with shape, shadow color, blur radius etc. to find the optimal result for your scene.
Demo
Result if browser supports filter:
var ctx = c.getContext("2d");
// render shadow
renderShadow(ctx, 30, 30, 30, Math.PI*0.25, 300, 2.5, 20);
// show main shape
ctx.beginPath();
ctx.moveTo(60, 30);
ctx.arc(30, 30, 30, 0, 6.28);
ctx.fillStyle = "rgb(0,140,200)";
ctx.fill();
function renderShadow(ctx, x, y, radius, angle, length, blur, iterations) {
var step = length / iterations, // calc number of steps
stepX = step * Math.cos(angle), // calc angle step for x based on steps
stepY = step * Math.sin(angle); // calc angle step for y based on steps
for(var i = iterations; i > 0; i--) { // run number of iterations
ctx.beginPath(); // create some shape, here circle
ctx.moveTo(x + radius + i * stepX, y + i * stepY); // move to x/y based on step*ite.
ctx.arc(x + i * stepX, y + i * stepY, radius, 0, 6.28);
ctx.filter = "blur(" + (blur * i) + "px)"; // set filter property
ctx.fillStyle = "rgba(0,0,0,0.5)"; // shadow color
ctx.fill();
}
ctx.filter = "none"; // reset filter
}
<canvas id=c width=450 height=350></canvas>
I am drawing oval using bezierCurveTo method of canvas. I have to highlight the points on boundary of oval(same as oval shape in powerpoint). I want the exact position of all eight coordinates to place them on canvas. Please refer to attached screenshot
I would recommend not to use a Bezier to create an oval/ellipse - it's mathematical inaccurate and will just cause headache when you want to use points such as in this case.
I would suggest to create your own ellipse function - it's easy; this creates an ellipse as a path which you can fill and stroke etc.:
function drawEllipse(cxt, cx, cy, rx, ry) {
ctx.beginPath();
ctx.moveTo(cx + rx, cy);
for(var a = 0, step = 0.02, max = Math.PI * 2; a < max; a += step)
ctx.lineTo(cx + rx * Math.cos(a), cy+ ry * Math.sin(a));
}
Now, to get those edge points all you need to do is have a similar function (or modify the previous) doing the same but with less granular steps as well as returning the calculated points - count is number of points you want. The resulting array here in this example will return points arranges as [x1, y1, x2, y2, ...] - this is something you can adjust as you need:
function getEllipsePoints(cxt, cx, cy, rx, ry, count) {
var points = [],
a = 0, max = Math.PI * 2,
step = max / count
for(; a < max; a += step)
points.push(cx + rx * Math.cos(a), cy+ ry * Math.sin(a));
return points;
}
Now you can plot the points at the edges as you want (and have a mathematical correct ellipse as well as hit points for mouse).
Live demo here
Result: