I've been playing around with how to render wireframe perspective-correct spheres using only canvas2d and ellipse math.
It's been fun, but I've soon come to realize that the ellipse() function has a very strange implementation with regards to the spec.
Indeed, the ellipse function takes 7 (or 8) arguments:
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle)
The startAngle is described as such:
The angle at which the ellipse starts, measured clockwise from the positive x-axis and expressed in radians.
Given a parameter 0 <= t <= 2 * PI, we can compute the position of the associated point on the ellipse like so:
let dx = radiusX * cos(t)
let dy = radiusY * sin(t)
let px = x + dx * cos(rotation) - dy * sin(rotation)
let py = y + dx * sin(rotation) + dy * cos(rotation)
And if we use startAngle = t, our ellipse will begin its arc at our point. But. But. t is NOT an angle, and definitely not the angle of our point from the x-axis of the ellipse. Apparently some people still call it the eccentric angle, but my point still stands.
(See here)
And indeed, if we try to make the arc of an ellipse start at a specific angle, we can see that the result is not what we expect, unless the ellipse is a circle (radiusX = radiusY) or startAngle is a multiple of PI / 2.
Here is an interactive demo I've put up so that you can witness the strange default behaviour.
My claim is that the function should always behave like it does in the corrected case with the current specification.
Either that or change the spec to talk about parameters t for startAngle and endAngle, and avoid saying they are angles, because currently they definitely are not.
Does anyone know how this implementation/spec came to be, if anyone reported this before and if not where to lead such a discussion?
Any other insight appreciated!
I found this related question but it's rather unsatisfactory as replies merely show how to correct the function, but don't discuss whether the spec or implementation should be corrected.
Related
I'm developing a program which tracks many small objects. They are very closely grouped for the most part.
My desire is to smoothly track the center of mass. The following code runs 60 times per second in all cases and calculates the center of mass:
let cX = 0;
let cY = 0;
for (let c of entities) {
cX += c.x;
cY += c.y;
}
prevCenterX = centerX;
prevCenterY = centerY;
centerX = cX / entities.length;
centerY = cY / entities.length;
That is, I simply add up all the positions of each entity and then divide by the entity count in order to get the x and y coordinate of the center of the mass of objects.
Then in the rendering code, I simply do this:
camera.x = lerp(prevCenterX, centerX, alpha);
camera.y = lerp(prevCenterY, centerY, alpha);
Where alpha is the percentage (from 0.0 to 1.0) of the tick that we are through (because the simulation only updates 60 times per second, but the user's monitor may have any sort of refresh rate). The rendering code in my case runs 165 times per second.
Anyways, this works perfectly in 99% of cases. The issue is that when an entity is either gained or lost, then the centerX and centerY very quickly change, which causes the camera to do a jarring jump the next frame, which is very visually unpleasant.
Is there a way to counteract this? My desire is for the camera to always move smoothly. I have been trying to figure out a solution to this for weeks now, but every one of my attempted solutions fails. Is this more tricky than I think, or am I missing something obvious?
You can try simple smoothing of center position, for example, with IIR filter:
centerX = centerX * (1-t) + t * cX / entities.length;
centerY = centerY * (1-t) + t * cY / entities.length;
where t is coefficient like 0.1 or 0.5 (depending on needed smoothing degree).
If result looks better, but is still unpleasant, consider using of Kalman filter. (More complex, but is widely used in object tracking. While math background looks very tangled, you can find consize implementations).
I've noticed that when rotating vectors, the rotation is done in radians even after I have called angleMode with DEGREES. This behaviour only appears with vectors declared using the new p5.Vector syntax, while using createVector avoids it.
Minimal reproducible example: (paste into p5 web editor or OpenProcessing)
function setup() {
createCanvas(400, 400)
translate(100, 100) // To see the effect more clearly
angleMode(DEGREES)
let v = new p5.Vector(1, 0)
line(0, 0, v.x * 50, v.y * 50)
v.rotate(HALF_PI)
line(0, 0, v.x * 50, v.y * 50)
}
Notice that the rotation is still done in radians. If you replace new p5.Vector with createVector then the problem disappears. Why is this?
Since mathematically functions like sin() are effected, there is no good reason why p5.Vector.rotate should not be effected.
However it is not intended to construct a vector like you did. You should use createVector() to create a vector. This function not only creates the object, but sets all internal attributes so that the objects behave as intended.
This behavior is not explained directly for rotate(), however it is documented for heading():
Calculate the angle of rotation for this vector(only 2D vectors). p5.Vectors created using createVector() will take the current angleMode into consideration, and give the angle in radians or degree accordingly.
I'm trying to shape my edges similarly to this:
static mock of desired edges/curves
I'm able to create "S" shaped curves, but i would like them to invert when going downwards from the root node (similarly to the picture). I haven't noticed anything in the docs that describes settings to do this.
I have a demo here: https://codesandbox.io/s/l5m6mnlqrz
What could also work is if I were able to smooth out the 90 degree curve with the "taxi" curve-style, although this doesn't seem possible.
Any suggestions appreciated. Thanks.
It is not possible to create desired form using single Bezier curve, because central range should be vertical. But two conjugated curves might provide appropriate result.
For points A (left one), and B (it is not important - whether B point is lower or higher than A):
The first curve have starting point P0=(XA, YA) and ending point P3=((XA + XB)/2, ((YA + YB)/2)
The first control point must lie at the same horizontal as starting point, the second one - at the same vertical as ending point
X1, Y1 = X0 + DX, Y0
X2, Y2 = X3, Y3 - DY
Parameters DX and DY define rounding of right angle.
Try to set them as DX = (X3 - X0) / 3 and DY = (Y3 - X0) / 3m then vary denominator to get desired curve form
The second part is mirrored curve with points
(X3, Y3), (X3, Y3 + DY), (XB - DX, YB), (XB, YB)
I have been making a mod for a game called Minecraft PE and I'm using it to learn. Before I show my code I want you to know that Y is the vertical axis and X and Z is horizontal. Here is some code I used:
Math.asin(Math.sin((fPosXBeforeMoved - sPosX) /
Math.sqrt(Math.pow(fPosXBeforeMoved - sPosX, 2) +
Math.pow(fPosZBeforeMoved - sPosZ, 2))));
I didn't use tan because sometimes it returns something like NaN at a certain angle. This code gives us the sine of the angle when I clearly used Math.asin. angle is a value between -1 and 1, and it works! I know it works, because when I go past the Z axis I was expecting and it did switch from negative to positive. However, I thought it's supposed to return radians? I read somewhere that the input is radians, but my input is not radians. I really want the answer to how my own code works and how I should have done it! I spent all day learning about trigonometry, but I'm really frustrated so now I ask the question from where I get all my answers from!
Can someone please explain how my own code works and how I should modify it to get the angle in radians? Is what I've done right? Am I actually giving it radians and just turned it into some sort of sine degrees type thing?
OK, let's give a quick refresher as to what sin and asin are. Take a look at this right-angle triangle in the diagram below:
Source: Wikipedia
By taking a look at point A of this right-angle triangle, we see that there is an angle formed between the line segment AC and AB. The relationship between this angle and sin is that sin is the ratio of the length of the opposite side over the hypotenuse. In other words:
sin A = opposite / hypotenuse = a / h
This means that if we took a / h, this is equal to the sin of the angle located at A. As such, to find the actual angle, we would need to apply the inverse sine operator on both sides of this equation. As such:
A = asin(a / h)
For example, if a = 1 and h = 2 in our triangle, the sine of the angle that this right triangle makes between AC and AB is:
sin A = 1 / 2
To find the actual angle that is here, we do:
A = asin(1 / 2)
Putting this in your calculator, we get 30 degrees. Radians are another way of representing angle, where the following relationship holds:
angle_in_radians = (angle_in_degrees) * (Math.PI / 180.0)
I'm actually a bit confused with your code, because you are doing asin and then sin after. A property between asin and sin is:
arcsin is the same as asin. The above equation states that as long as x >= -Math.PI / 2, x <= Math.PI / 2 or x >= -90, x <= 90 degrees, then this relationship holds. In your code, the argument inside the sin will definitely be between -1 to 1, and so this actually simplifies to:
(fPosXBeforeMoved - sPosX) / Math.sqrt(Math.pow(fPosXBeforeMoved - sPosX, 2) +
Math.pow(fPosZBeforeMoved - sPosZ, 2));
If you want to find the angle between the points that are moved, then you're not using the right sides of the triangle. I'll cover this more later.
Alright, so how does this relate to your question? Take a look at the equation that you have in your code. We have four points we need to take a look at:
fPosXBeforeMoved - The X position of your point before we moved
sPosX - The X position of your point after we moved
fPosZBeforeMoved - The Z position of your point before we moved
sPosZ - The Z position of your point after we moved.
We can actually represent this in a right-angle triangle like so (excuse the bad diagram):
We can represent the point before you moved as (fPosXBeforeMoved,fPosZBeforeMoved) on the XZ plane, and the point (sPosX,sPosZ) is when you moved after. In this diagram X would be the horizontal component, while Z would be the vertical component. Imagine that you are holding a picture up in front of you. X would be the axis going from left to right, Z would be the axis going up and down and Y would be the axis coming out towards you and going inside the picture.
We can find the length of the adjacent (AC) segment by taking the difference between the X co-ordinates and the length of the opposite (AB) segment by taking the difference between the Z co-ordinates. All we need left is to find the length of the hypotenuse (h). If you recall from school, this is simply done by using the Pythagorean theorem:
h^2 = a^2 + b^2
h = sqrt(a^2 + b^2)
Therefore, if you refer to the diagram, our hypotenuse is thus (in JavaScript):
Math.sqrt(Math.pow(fPosXBeforeMoved - sPosX, 2) + Math.pow(fPosZBeforeMoved - sPosZ, 2));
You'll recognize this as part of your code. We covered sin, but let's take a look at cos. cos is the ratio of the length of the adjacent side over the hypotenuse. In other words:
cos A = adjacent / hypotenuse = b / h
This explains this part:
(sPosX - fPosXBeforeMoved) / Math.sqrt(Math.pow(sPosX - fPosXBeforeMoved, 2) +
Math.pow(sPosZ - fPosZBeforeMoved, 2));
Take note that I swapped the subtraction of sPosX and fPosXBeforeMoved in comparison to what you had in your code from before. The reason why is because when you are examining the point before and the point after, the point after always comes first, then the point before comes second. In the bottom when you're calculating the hypotenuse, this doesn't matter because no matter which order the values are subtracted from, we take the square of the subtraction, so you will get the same number anyway regardless of the order. I decided to swap the orders here in the hypotenuse in order to be consistent. The order does matter at the top, as the value being positive or negative when you're subtracting will make a difference when you're finding the angle in the end.
Note that this division will always be between -1 to 1 so we can certainly use the inverse trigonometric functions here. Finally, if you want to find the angle, you would apply the inverse cosine. In other words:
Math.acos((sPosX - fPosXBeforeMoved) / Math.sqrt(Math.pow(sPosX - fPosXBeforeMoved, 2)
+ Math.pow(sPosZ - fPosZBeforeMoved, 2)));
This is what I believe you should be programming. Take note that this will return the angle in radians. If you'd like this in degrees, then use the equation that I showed you above, but re-arrange it so that you are solving for degrees instead of radians. As such:
angle_in_degrees = angle_in_radians * (180.0 / Math.PI)
As for what you have now, I suspect that you are simply measuring the ratio of the adjacent and the hypotenuse, which is totally fine if you want to detect where you are crossing over each axis. If you want to find the actual angle, I would use the above code instead.
Good luck and have fun!
I am using Raphaël for the first time with little svg experience and I need someone who is really knowledgeable with these two to help me.
I have created a pie chart with dynamic sectors. The sectors can be resized by dragging on the round buttons. See this fiddle. I have only tested in Chrome and Safari which are the only required browsers.
The pie chart is not yet complete. The sectors can overlap. Please ignore this for now.
I was faced with problems, when the starting angle of a sector was greater than the ending angle. This is the case when the ending angle goes past the 0/360° mark. To solve this I made use of the path-rotation-parameter. I moved the sector forward while moving the angles back, until the end angle is at 360. You can see this in the fiddle in this function:
function sector_update(cx, cy, r, startAngle, endAngle, sec) {
var x1 = cx + r * Math.cos(-startAngle * rad),
x2 = cx + r * Math.cos(-endAngle * rad),
y1 = cy + r * Math.sin(-startAngle * rad),
y2 = cy + r * Math.sin(-endAngle * rad);
var rotation = 0;
// This is the part that I have the feeling could be improved.
// Remove the entire if-clause and let "rotation" equal 0 to see what happens
if (startAngle > endAngle) {
rotation = endAngle;
startAngle = startAngle - endAngle;
endAngle = 360;
}
sec.attr('path', ["M", cx, cy, "L", x1, y1, "A", r, r, rotation,
+(endAngle - startAngle > 180), 0, x2, y2, "z"]);
}
Although it works nicely, I'm a bit skeptical. Can this be solved without the rotation of the path? I appreciate any help or pointers.
Can this be solved without the rotation of the path?
Answer: Yes, it can. You don't have to change the rotation of the path at all. Unless I'm missing something, the following code seems to work the same as what you have in the fiddle:
function sector_update(cx, cy, r, startAngle, endAngle, sec) {
var x1 = cx + r * Math.cos(-startAngle * rad),
x2 = cx + r * Math.cos(-endAngle * rad),
y1 = cy + r * Math.sin(-startAngle * rad),
y2 = cy + r * Math.sin(-endAngle * rad);
//notice there is no "roation" variable
if (startAngle > endAngle) {
startAngle -= endAngle;
endAngle = 360;
}
sec.attr('path', ["M", cx, cy, "L", x1, y1, "A", r, r, 0,
+(endAngle - startAngle > 180), 0, x2, y2, "z"]);
}
Explanation: For my explanation, I will use the SVG terminology in the
W3 Spec and Raphael Reference Library. That is, while you use cx, cy, and rotation, these use rx, ry, and x-axis-rotation respectively.
In short, whenever rx equals ry, then x-axis-rotation is meaningless.
Look at this SVG. Use your browser's development tools, or save the SVG to your computer and use a file editor to edit it. Specifically, look at the last path element, which has four arcs in it. Try modifying the x-axis-rotation value on each arc. You will notice that the first arc (where rx and ry are both "25") never changes when you update x-axis-rotation value.
Why? This is because you have a circular arc. No matter how much you rotate a circle, it will still be the same circle. For example, hold up a glass in front of you so that the glass is horizontal to the ground, and you are looking directly down the glass. Now rotate/twist the glass with your wrist. Do you see how the circular shape you see stays in the same circular shape? Now set the glass on the table normally (so it is vertical and could hold a liquid). Now tip the glass over. You can see the obvious perspective change; it was pointing up, but now it is laying flat. That is what x-axis-rotation does.
Perhaps a better example is to just play around with the aforementioned SVG file. Play with x-axis-rotation on the arcs in the final path element. You will see the arcs being rotated around. That is what x-axis-rotation does.
Back to your code: Because you are dealing only with circular objects, the x-axis-rotation will make no difference on the final output. So long as you are only dealing with circular objects, you can hard-code it's value to zero without any worries. All you really needed to do is modify the angles, which you had done correctly.
Performance: I tried using JavaScript to time your sector_update function both with and without modifying the x-axis-rotation variable. The result? I saw no difference in performance. The majority of the time spent is on actually drawing the SVG, not on the math that determines it's values. In fact, all you are really doing in JavaScript is updating the code to set the value in the path element. At that point in time, the browser takes over with it's rendering engine to actually draw the SVG object. I suppose then it's a per-browser issue, as each browser has different rendering performance. But as for whether or not the x-axis-rotation value has any effect, my guess is no. If there is a performance hit (because the browser may have to do an additional floating-point operation), it is so incredibly moot because the overwhelming majority of the time is spent drawing the object, not calculating it's values. So I would say not to worry about it.
I hope that helps, let me know if I missed something or didn't explain something well enough.