I'm trying to recreate this image using Processing. How to make the red saw fins?
My thought process given the image is symmetrical over a few axis was to break it up. I'm struggling with the "red saw fins". It looks like I have to use a bezier to create the lines, then fill them in. If I'm able to create 1 fin, then I can rotate a copy of it on the axis to spin it around. https://processing.org/reference/bezier_.html
Just play with control points of Bezier curves to provide needed form.
This is quick-made example in Delphi.
Points P0-P3 give the first side of tooth (concave), points P3-P6 give the second side of tooth.
Rin and ROut are inner and outer radii of saw, RM is middle radius used for control points' calculation.
Inside loop an is base angle for a tooth, an1 is slightly shifted to provide curvature for concave arc, an2 is ending angle (same as base angle of the next tooth) and an3 is middle angle for control points of convex arc.
var
P: array[0..6] of TPoint;
i, N, CX, CY, Rin, ROut, RM: integer;
an, an1, an2, an3: Double;
begin
Rin := 100;
ROut := 170;
RM := (RIn + ROut) div 2;
N := 8;
CX := 350;
CY := 350;
for i := 0 to 7 do begin
an := i * 2 * Pi / N;
an1 := an + 0.25 * Pi / N;
an2 := an + 2 * Pi / N;
an3 := an + Pi / N;
P[0] := Point(Round(CX + Rin * Cos(an)), Round(CY + Rin * Sin(an)));
P[1] := Point(Round(CX + RM * Cos(an1)), Round(CY + RM * Sin(an1)));
P[2] := Point(Round(CX + RM * Cos(an1)), Round(CY + RM * Sin(an1)));
P[3] := Point(Round(CX + ROut * Cos(an)), Round(CY + ROut * Sin(an)));
P[4] := Point(Round(CX + RM * Cos(an3)), Round(CY + RM * Sin(an3)));
P[5] := Point(Round(CX + RM * Cos(an3)), Round(CY + RM * Sin(an3)));
P[6] := Point(Round(CX + RIn * Cos(an2)), Round(CY + RIn * Sin(an2)));
Canvas.PolyBezier(P);
Canvas.Ellipse(CX - 3 * RIn div 4, CY - 3 * RIn div 4,
CX + 3 * RIn div 4, CY + 3 * RIn div 4);
This is a tiny picture. Enlarge it, and then load it into a tool for drawing bezier curves. You know the three fixed coordinates (the two coordinates at the circle, and the tip) and you know the general tangents along which your control points will need to be:
You also know that the control points and the on-curve points need to form a box that surrounds the actual curve (e.g. the curve cannot go outside that box) as a property of Bezier curves.
So now: you play around in finding the correct points. You can use something like Inkscape or Photoshop or Illustrator to see which curve looks good enough, and then you copy the coordinates over into your own program, or you just draw it out on a grid and find the rough coordinates simply by looking at your grid and then play around with values near to where you think.
And then when you have the missing control points, you draw it in processing as a shape:
void draw() {
// draw the inner circle with a "fat" stroke
strokeWeight(20);
// assuming center mode
ellipse(width/2, height/2, 100, 100);
// reset that stroke fatness
strokeWeight(1);
// make sure we'll be rotating about the center of the sketch
translate(width/2, height/2);
// and then start drawing eight 'teeth'
for (int i=0; i<8; i++) {
beginShape();
// we know where p1, p2, and p3 are.
vertex(p1.x, p1.y);
// and we "guessed" at c1, c2, c3, and c4.
bezierVertex(c1.x, c1.y, c2.y, c2.y, p2.x, p2.y);
bezierVertex(c3.x, c3.y, c4.y, c4.y, p3.x, p3.y);
// We leave the shape "open" in case you want both stroke and fill
endShape();
// we're drawing eight teeth, so we need to rotate by 2*PI/8 each time
rotate(0.25 * PI);
}
}
Related
I am trying to understand how Adobe After Effects performs some of interpolations when animating different transformation properties. Properties like position are straightforward. It uses cubic bezier with start,end positions and 2 control tangents. But now I try to implement interpolation of scale property the same way Adobe does. I have two key frames, each contains same values for scale in 2 dimensions. Key1 (100,100) , Key2 (100,100) (in %). Basically,it could be just linear interpolation, but After Effects allows plotting a curve to allow easing even between two similar values. Here is how the curve looks for the points above:
Now,the data from this spline is as follows:
vec2 P0 = vec2(100.0f,100.0f); //start points
vec2 P1 = vec2(100.0f,100.0f); //end point
vec2 T1 = vec2(0.06f,40.12f); //first tangent
vec3 T2 = vec2(0.95f,40.84f); //second tangent
It is clear to me that X value of the tangent contains time in range [0-1],while Y value contains the value of the scale,which is percentage from the default value,which is 100%. I tried to match different types of spline in order to get same interpolated values across 50 frames as in After Effects. I tried:
Catmull- Rom
template <typename T>
T catmullrom(float t, T p0, T p1, T p2, T p3)
{
return (
(p1 * 2.0f) +
(-p0 + p2) * t +
(p0 * 2.0f - p1 * 5.0f + p2 * 4.0f - p3) * t * t +
(-p0 + p1 * 3.0f - p2 * 3.0f + p3) * t * t * t
) * 0.5f;
}
Hermite
template <typename T>
T hermite(float s, T P1, T P2, T T1, T T2)
{
float h1 = 2 * glm::pow(s, 3) - 3 * glm::pow(s, 2) + 1;
float h2 = -2 * glm::pow(s, 3) + 3 * glm::pow(s, 2);
float h3 = glm::pow(s, 3) - 2 * glm::pow(s, 2) + s;
float h4 = glm::pow(s, 3) - glm::pow(s, 2);
T p = P1 * h1 +
P2 * h2 +
T1 * h3 +
T2 * h4;
return p;
}
And even regular Cubic bezier.
template <typename T>
T cubic(float t, T p0, T p1, T p2, T p3)
{
T p = glm::pow3(1.0f - t) * p0 + 3 * glm::pow2(1.0f - t) * t * (p0 + p2) + 3 * (1.0f - t)*glm::pow2(t)*(p1 + p3) + glm::pow3(t) * p1;
return p;
}
int steps = 50;
for (int t = 0; t <= steps; t++)
{
float s = (float)t / (float)steps; // scale s to go from 0 to 1
auto p = cubic(s, P1, P2, T2, T1);
printf("t:%f | %f , %f\n",s,p.x,p.y);
}
I started with Catmull-Rom and Hermite because those define control points on curve,but the results were completely wrong. Then I thought that these two are actually least fit because the tangents in this case have nothing to do with control points on curve. Cubic bezier actually gives correct interpolation in terms of value magnitude change over time. It gave me values starting from 100,with the peak at 130 (as can be seen on the graph),then descending to 100 towards t = 1. But the interpolation steps are completely wrong. Also,only Y value gets interpolated close to the values I expect. I am not a math expert.I was reading about different spline interpolations, but most of the material is completely theoretical. I would like to understand what I am doing wrong here.
I will try to be very descriptive with this. I'm editing a game right now and the scenario is a 3D area.
I have an initial angle, writen as a direction vector, and another vector which haves different coordinates. As we know, the angle between 2 vectors is given by the formula: Theta = ACos( DotProduct( vec1, vec2 ) / ( VectorLength( vec1 ) * VectorLength( vec2 ) ) )
So let's describe the scenario: I'm currently programming some kind of stationary weapon, a sentry gun, this thing moves slowly his "head", shooting bullets to enemies. That angle rotation thing is my problem.
Let's imagine this: I have my sentry gun on a empty 3D area, and a "enemy" spawns over there. I can currently get the direction vector of my sentry's view angle, and the direction vector between my sentry and the player. Let's guess, using the formula described, his separation angle is 45 degrees. My sentry gun thinks (calls a function) at every 0.1 seconds, and I want to move his head 5 degrees at every thinking function until it reach the the player (ie, both vectors are nearly equal), and that means it will reach the player (if player keeps on its position...) in 0.9 seconds (5 degrees from 45)
How I can move sentry's view angle slowly until it reach a target? In 2D is easily but know I'm fighting with a 3D scenario, and I'm currently lost with this.
Any help would be appreciated, and about coding, I will be grateful with a pseudocode. Thanks! (and sorry for my english)
What you need is called SLERP - spherical linear interpolation
Your starting direction vector is p0 there, goal direction is p1, Omega is your Theta, and t parameter varies in range 0..1 with needed step
Delphi example for 2D case (it is easy to control)
var
p0, p1: TPoint;
i, xx, yy: Integer;
omega, InvSinOmega, t, a0, a1: Double;
begin
P0 := Point(0, 200);
P1 := Point(200, 0);
omega := -Pi / 2;
InvSinOmega := 1.0 / Sin(omega);
Canvas.Brush.Color := clRed;
Canvas.Ellipse(120 + P0.X, 120 + P0.Y, 120 + P0.X + 7, 120 + P0.Y + 7);
Canvas.Ellipse(120 + P1.X, 120 + P1.Y, 120 + P1.X + 7, 120 + P1.Y + 7);
for i := 1 to 9 do begin
t := i / 10;
a0 := sin((1 - t) * omega) * InvSinOmega;
a1 := sin(t * omega) * InvSinOmega;
xx := Round(P0.X * a0 + P1.X * a1);
yy := Round(P0.Y * a0 + P1.Y * a1);
Canvas.Brush.Color := RGB(25 * i, 25 * i, 25 * i);
Canvas.Ellipse(120 + xx, 120 + yy, 120 + xx + 9, 120 + yy + 9);
end;
I'm trying to implement 2D Perlin noise to create Minecraft-like terrain (Minecraft doesn't actually use 2D Perlin noise) without overhangs or caves and stuff.
The way I'm doing it, is by creating a [50][20][50] array of cubes, where [20] will be the maximum height of the array, and its values will be determined with Perlin noise. I will then fill that array with arrays of cube.
I've been reading from this article and I don't understand, how do I compute the 4 gradient vector and use it in my code? Does every adjacent 2D array such as [2][3] and [2][4] have a different 4 gradient vector?
Also, I've read that the general Perlin noise function also takes a numeric value that will be used as seed, where do I put that in this case?
I'm going to explain Perlin noise using working code, and without relying on other explanations. First you need a way to generate a pseudo-random float at a 2D point. Each point should look random relative to the others, but the trick is that the same coordinates should always produce the same float. We can use any hash function to do that - not just the one that Ken Perlin used in his code. Here's one:
static float noise2(int x, int y) {
int n = x + y * 57;
n = (n << 13) ^ n;
return (float) (1.0-((n*(n*n*15731+789221)+1376312589)&0x7fffffff)/1073741824.0);
}
I use this to generate a "landscape" landscape[i][j] = noise2(i,j); (which I then convert to an image) and it always produces the same thing:
...
But that looks too random - like the hills and valleys are too densely packed. We need a way of "stretching" each random point over, say, 5 points. And for the values between those "key" points, you want a smooth gradient:
static float stretchedNoise2(float x_float, float y_float, float stretch) {
// stretch
x_float /= stretch;
y_float /= stretch;
// the whole part of the coordinates
int x = (int) Math.floor(x_float);
int y = (int) Math.floor(y_float);
// the decimal part - how far between the two points yours is
float fractional_X = x_float - x;
float fractional_Y = y_float - y;
// we need to grab the 4x4 nearest points to do cubic interpolation
double[] p = new double[4];
for (int j = 0; j < 4; j++) {
double[] p2 = new double[4];
for (int i = 0; i < 4; i++) {
p2[i] = noise2(x + i - 1, y + j - 1);
}
// interpolate each row
p[j] = cubicInterp(p2, fractional_X);
}
// and interpolate the results each row's interpolation
return (float) cubicInterp(p, fractional_Y);
}
public static double cubicInterp(double[] p, double x) {
return cubicInterp(p[0],p[1],p[2],p[3], x);
}
public static double cubicInterp(double v0, double v1, double v2, double v3, double x) {
double P = (v3 - v2) - (v0 - v1);
double Q = (v0 - v1) - P;
double R = v2 - v0;
double S = v1;
return P * x * x * x + Q * x * x + R * x + S;
}
If you don't understand the details, that's ok - I don't know how Math.cos() is implemented, but I still know what it does. And this function gives us stretched, smooth noise.
->
The stretchedNoise2 function generates a "landscape" at a certain scale (big or small) - a landscape of random points with smooth slopes between them. Now we can generate a sequence of landscapes on top of each other:
public static double perlin2(float xx, float yy) {
double noise = 0;
noise += stretchedNoise2(xx, yy, 5) * 1; // sample 1
noise += stretchedNoise2(xx, yy, 13) * 2; // twice as influential
// you can keep repeating different variants of the above lines
// some interesting variants are included below.
return noise / (1+2); // make sure you sum the multipliers above
}
To put it more accurately, we get the weighed average of the points from each sample.
( + 2 * ) / 3 =
When you stack a bunch of smooth noise together, usually about 5 samples of increasing "stretch", you get Perlin noise. (If you understand the last sentence, you understand Perlin noise.)
There are other implementations that are faster because they do the same thing in different ways, but because it is no longer 1983 and because you are getting started with writing a landscape generator, you don't need to know about all the special tricks and terminology they use to understand Perlin noise or do fun things with it. For example:
1) 2) 3)
// 1
float smearX = interpolatedNoise2(xx, yy, 99) * 99;
float smearY = interpolatedNoise2(xx, yy, 99) * 99;
ret += interpolatedNoise2(xx + smearX, yy + smearY, 13)*1;
// 2
float smearX2 = interpolatedNoise2(xx, yy, 9) * 19;
float smearY2 = interpolatedNoise2(xx, yy, 9) * 19;
ret += interpolatedNoise2(xx + smearX2, yy + smearY2, 13)*1;
// 3
ret += Math.cos( interpolatedNoise2(xx , yy , 5)*4) *1;
About perlin noise
Perlin noise was developed to generate a random continuous surfaces (actually, procedural textures). Its main feature is that the noise is always continuous over space.
From the article:
Perlin noise is function for generating coherent noise over a space. Coherent noise means that for any two points in the space, the value of the noise function changes smoothly as you move from one point to the other -- that is, there are no discontinuities.
Simply, a perlin noise looks like this:
_ _ __
\ __/ \__/ \__
\__/
But this certainly is not a perlin noise, because there are gaps:
_ _
\_ __/
___/ __/
Calculating the noise (or crushing gradients!)
As #markspace said, perlin noise is mathematically hard. Lets simplify by generating 1D noise.
Imagine the following 1D space:
________________
Firstly, we define a grid (or points in 1D space):
1 2 3 4
________________
Then, we randomly chose a noise value to each grid point (This value is equivalent to the gradient in the 2D noise):
1 2 3 4
________________
-1 0 0.5 1 // random noise value
Now, calculating the noise value for a grid point it is easy, just pick the value:
noise(3) => 0.5
But the noise value for a arbitrary point p needs to be calculated based in the closest grid points p1 and p2 using their value and influence:
// in 1D the influence is just the distance between the points
noise(p) => noise(p1) * influence(p1) + noise(p2) * influence(p2)
noise(2.5) => noise(2) * influence(2, 2.5) + noise(3) * influence(3, 2.5)
=> 0 * 0.5 + 0.5 * 0.5 => 0.25
The end! Now we are able to calculate 1D noise, just add one dimension for 2D. :-)
Hope it helps you understand! Now read #mk.'s answer for working code and have happy noises!
Edit:
Follow up question in the comments:
I read in wikipedia article that the gradient vector in 2d perlin should be length of 1 (unit circle) and random direction. since vector has X and Y, how do I do that exactly?
This could be easily lifted and adapted from the original perlin noise code. Find bellow a pseudocode.
gradient.x = random()*2 - 1;
gradient.y = random()*2 - 1;
normalize_2d( gradient );
Where normalize_2d is:
// normalizes a 2d vector
function normalize_2d(v)
size = square_root( v.x * v.x + v.y * v.y );
v.x = v.x / size;
v.y = v.y / size;
Compute Perlin noise at coordinates x, y
function perlin(float x, float y) {
// Determine grid cell coordinates
int x0 = (x > 0.0 ? (int)x : (int)x - 1);
int x1 = x0 + 1;
int y0 = (y > 0.0 ? (int)y : (int)y - 1);
int y1 = y0 + 1;
// Determine interpolation weights
// Could also use higher order polynomial/s-curve here
float sx = x - (double)x0;
float sy = y - (double)y0;
// Interpolate between grid point gradients
float n0, n1, ix0, ix1, value;
n0 = dotGridGradient(x0, y0, x, y);
n1 = dotGridGradient(x1, y0, x, y);
ix0 = lerp(n0, n1, sx);
n0 = dotGridGradient(x0, y1, x, y);
n1 = dotGridGradient(x1, y1, x, y);
ix1 = lerp(n0, n1, sx);
value = lerp(ix0, ix1, sy);
return value;
}
I know that to draw a regular polygon from a center point, you use something along the lines of:
for (int i = 0; i < n; i++) {
p.addPoint((int) (100 + 50 * Math.cos(i * 2 * Math.PI / n)),
(int) (100 + 50 * Math.sin(i * 2 * Math.PI / n))
);
}
However, is there anyway to change this code (without adding rotations ) to make sure that the polygon is always drawn so that the topmost or bottommost edge is parallel to a 180 degree line? For example, normally, the code above for a pentagon or a square (where n = 5 and 4 respectively) would produce something like:
When what I'm looking for is:
Is there any mathematical way to make this happen?
You have to add Pi/2-Pi/n
k[n_] := Pi/2 - Pi/n;
f[n_] := Line[
Table[50 {Cos[(2 i ) Pi/n + k[n]] ,Sin[(2 i) Pi/n + k[n]]}, {i,0,n}]];
GraphicsGrid#Partition[Graphics /# Table[f[i], {i, 3, 8}], 3]
Edit
Answering your comment, I'll explain how I arrived at the formula. Look at the following image:
As you may see, we want the middle point of a side aligned with Pi/2. So ... what is α? It's obvious
2 α = 2 Pi/n (one side) -> α = Pi/n
Edit 2
If you want the bottom side aligned with the x axis, add 3 Pi/2- Pi/n instead ...
Add Math.PI / n to the angles.
While working on SVG implementation for Internet Explorer to be based on its own VML format I came to a problem of translation of an SVG elliptical arc to an VML elliptical arc.
In VML an arc is given by: two angles for two points on ellipse and lengths of radiuses,
In SVG an arc is given by: two pairs of coordinates for two points on ellipse and sizes of ellipse boundary box
So, the question is: How to express angles of two points on ellipse to two pairs of their coordinates.
An intermediate question could be: How to find the center of an ellipse by coordinates of a pair of points on its curve.
Update: Let's have a precondition saying that an ellipse is normally placed (its radiuses are parallel to linear coordinate system axis), thus no rotation is applied.
Update: This question is not related to svg:ellipse element, rather to "a" elliptical arc command in svg:path element (SVG Paths: The elliptical arc curve commands)
So the solution is here:
The parametrized formula of an ellipse:
x = x0 + a * cos(t)
y = y0 + b * sin(t)
Let's put known coordinates of two points to it:
x1 = x0 + a * cos(t1)
x2 = x0 + a * cos(t2)
y1 = y0 + b * sin(t1)
y2 = y0 + b * sin(t2)
Now we have a system of equations with 4 variables: center of ellipse (x0/y0) and two angles t1, t2
Let's subtract equations in order to get rid of center coordinates:
x1 - x2 = a * (cos(t1) - cos(t2))
y1 - y2 = b * (sin(t1) - sin(t2))
This can be rewritten (with product-to-sum identities formulas) as:
(x1 - x2) / (2 * a) = sin((t1 + t2) / 2) * sin((t1 - t2) / 2)
(y2 - y1) / (2 * b) = cos((t1 + t2) / 2) * sin((t1 - t2) / 2)
Let's replace some of the equations:
r1: (x1 - x2) / (2 * a)
r2: (y2 - y1) / (2 * b)
a1: (t1 + t2) / 2
a2: (t1 - t2) / 2
Then we get simple equations system:
r1 = sin(a1) * sin(a2)
r2 = cos(a1) * sin(a2)
Dividing first equation by second produces:
a1 = arctan(r1/r2)
Adding this result to the first equation gives:
a2 = arcsin(r2 / cos(arctan(r1/r2)))
Or, simple (using compositions of trig and inverse trig functions):
a2 = arcsin(r2 / (1 / sqrt(1 + (r1/r2)^2)))
or even more simple:
a2 = arcsin(sqrt(r1^2 + r2^2))
Now the initial four-equations system can be resolved with easy and all angles as well as eclipse center coordinates can be found.
The elliptical curve arc link you posted includes a link to elliptical arc implementation notes.
In there, you will find the equations for conversion from endpoint to centre parameterisation.
Here is my JavaScript implementation of those equations, taken from an interactive demo of elliptical arc paths, using Sylvester.js to perform the matrix and vector calculations.
// Calculate the centre of the ellipse
// Based on http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
var x1 = 150; // Starting x-point of the arc
var y1 = 150; // Starting y-point of the arc
var x2 = 400; // End x-point of the arc
var y2 = 300; // End y-point of the arc
var fA = 1; // Large arc flag
var fS = 1; // Sweep flag
var rx = 100; // Horizontal radius of ellipse
var ry = 50; // Vertical radius of ellipse
var phi = 0; // Angle between co-ord system and ellipse x-axes
var Cx, Cy;
// Step 1: Compute (x1′, y1′)
var M = $M([
[ Math.cos(phi), Math.sin(phi)],
[-Math.sin(phi), Math.cos(phi)]
]);
var V = $V( [ (x1-x2)/2, (y1-y2)/2 ] );
var P = M.multiply(V);
var x1p = P.e(1); // x1 prime
var y1p = P.e(2); // y1 prime
// Ensure radii are large enough
// Based on http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
// Step (a): Ensure radii are non-zero
// Step (b): Ensure radii are positive
rx = Math.abs(rx);
ry = Math.abs(ry);
// Step (c): Ensure radii are large enough
var lambda = ( (x1p * x1p) / (rx * rx) ) + ( (y1p * y1p) / (ry * ry) );
if(lambda > 1)
{
rx = Math.sqrt(lambda) * rx;
ry = Math.sqrt(lambda) * ry;
}
// Step 2: Compute (cx′, cy′)
var sign = (fA == fS)? -1 : 1;
// Bit of a hack, as presumably rounding errors were making his negative inside the square root!
if((( (rx*rx*ry*ry) - (rx*rx*y1p*y1p) - (ry*ry*x1p*x1p) ) / ( (rx*rx*y1p*y1p) + (ry*ry*x1p*x1p) )) < 1e-7)
var co = 0;
else
var co = sign * Math.sqrt( ( (rx*rx*ry*ry) - (rx*rx*y1p*y1p) - (ry*ry*x1p*x1p) ) / ( (rx*rx*y1p*y1p) + (ry*ry*x1p*x1p) ) );
var V = $V( [rx*y1p/ry, -ry*x1p/rx] );
var Cp = V.multiply(co);
// Step 3: Compute (cx, cy) from (cx′, cy′)
var M = $M([
[ Math.cos(phi), -Math.sin(phi)],
[ Math.sin(phi), Math.cos(phi)]
]);
var V = $V( [ (x1+x2)/2, (y1+y2)/2 ] );
var C = M.multiply(Cp).add(V);
Cx = C.e(1);
Cy = C.e(2);
An ellipse cannot be defined by only two points. Even a circle (a special cased ellipse) is defined by three points.
Even with three points, you would have infinite ellipses passing through these three points (think: rotation).
Note that a bounding box suggests a center for the ellipse, and most probably assumes that its major and minor axes are parallel to the x,y (or y,x) axes.
The intermediate question is fairly easy... you don't. You work out the centre of an ellipse from the bounding box (namely, the centre of the box is the centre of the ellipse, as long as the ellipse is centred in the box).
For your first question, I'd look at the polar form of the ellipse equation, which is available on Wikipedia. You would need to work out the eccentricity of the ellipse as well.
Or you could brute force the values from the bounding box... work out if a point lies on the ellipse and matches the angle, and iterate through every point in the bounding box.
TypeScript implementation based on the answer from Rikki.
Default DOMMatrix and DOMPoint are used for the calculations (Tested in the latest Chrome v.80) instead of the external library.
ellipseCenter(
x1: number,
y1: number,
rx: number,
ry: number,
rotateDeg: number,
fa: number,
fs: number,
x2: number,
y2: number
): DOMPoint {
const phi = ((rotateDeg % 360) * Math.PI) / 180;
const m = new DOMMatrix([
Math.cos(phi),
-Math.sin(phi),
Math.sin(phi),
Math.cos(phi),
0,
0,
]);
let v = new DOMPoint((x1 - x2) / 2, (y1 - y2) / 2).matrixTransform(m);
const x1p = v.x;
const y1p = v.y;
rx = Math.abs(rx);
ry = Math.abs(ry);
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if (lambda > 1) {
rx = Math.sqrt(lambda) * rx;
ry = Math.sqrt(lambda) * ry;
}
const sign = fa === fs ? -1 : 1;
const div =
(rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p) /
(rx * rx * y1p * y1p + ry * ry * x1p * x1p);
const co = sign * Math.sqrt(Math.abs(div));
// inverse matrix b and c
m.b *= -1;
m.c *= -1;
v = new DOMPoint(
((rx * y1p) / ry) * co,
((-ry * x1p) / rx) * co
).matrixTransform(m);
v.x += (x1 + x2) / 2;
v.y += (y1 + y2) / 2;
return v;
}
Answering part of the question with code
How to find the center of an ellipse by coordinates of a pair of points on its curve.
This is a TypeScript function which is based on the excellent accepted answer by Sergey Illinsky above (which ends somewhat halfway through, IMHO). It calculates the center of an ellipse with given radii, given the condition that both provided points a and b must lie on the circumference of the ellipse. Since there are (almost) always two solutions to this problem, the code choses the solution that places the ellipse "above" the two points:
(Note that the ellipse must have major and minor axis parallel to the horizontal/vertical)
/**
* We're in 2D, so that's what our vertors look like
*/
export type Point = [number, number];
/**
* Calculates the vector that connects the two points
*/
function deltaXY (from: Point, to: Point): Point {
return [to[0]-from[0], to[1]-from[1]];
}
/**
* Calculates the sum of an arbitrary amount of vertors
*/
function vecAdd (...vectors: Point[]): Point {
return vectors.reduce((acc, curr) => [acc[0]+curr[0], acc[1]+curr[1]], [0, 0]);
}
/**
* Given two points a and b, as well as ellipsis radii rX and rY, this
* function calculates the center-point of the ellipse, so that it
* is "above" the two points and has them on the circumference
*/
function topLeftOfPointsCenter (a: Point, b: Point, rX: number, rY: number): Point {
const delta = deltaXY(a, b);
// Sergey's work leads up to a simple system of liner equations.
// Here, we calculate its general solution for the first of the two angles (t1)
const A = Math.asin(Math.sqrt((delta[0]/(2*rX))**2+(delta[1]/(2*rY))**2));
const B = Math.atan(-delta[0]/delta[1] * rY/rX);
const alpha = A + B;
// This may be the new center, but we don't know to which of the two
// solutions it belongs, yet
let newCenter = vecAdd(a, [
rX * Math.cos(alpha),
rY * Math.sin(alpha)
]);
// Figure out if it is the correct solution, and adjusting if not
const mean = vecAdd(a, [delta[0] * 0.5, delta[1] * 0.5]);
const factor = mean[1] > newCenter[1] ? 1 : -1;
const offMean = deltaXY(mean, newCenter);
newCenter = vecAdd(mean, [offMean[0] * factor, offMean[1] * factor]);
return newCenter;
}
This function does not check if a solution is possible, meaning whether the radii provided are large enough to even connect the two points!