Given two points (P1 and P2) in XYZ space, create a tube with a given radius. In order to do this, I need to calculate points for a circle around each of the two points, such that the circles are perpendicular to P1→P2 (and parallel to each other). The dx/dy/dz for one circle can be used to make other circles. The form of the code would look like:
function circle(radius, segments, P1, P2) {
// 3D circle around the origin, perpendicular to P1P2
var circle = [];
var Q = [P2[0] - P1[0], P2[1] - P1[1], P2[2] - P1[2]];
for (var i = 0; i < segments; i++) {
var theta = 2*Math.PI*segment/i;
var dx = mysteryFunctionX(Q, theta, radius);
var dy = mysteryFunctionY(Q, theta, radius);
var dz = mysteryFunctionZ(Q, theta, radius);
circle.push([dx, dy, dz]);
}
return circle;
}
What is the calculation needed for each mystery function?
As pointed out in the link in Ed's post, if you have vectors u and v that are perpendicular to your axis Q, and to each other, and each of length 1 then the points
P + cos(theta)*u + sin(theta)*v
are, as theta goes between 0 and 2pi, the points on a circle with centre P on a plane perpendicular to Q.
It can be a bit tricky, given Q, to figure out what u and v should be. One way is to use Householder reflectors. It is straightforward to find a reflector that maps (1,0,0) say to a multiple of Q. If we apply this reflector to (0,1,0) and (0,0,1) we will get vectors u and v as required above. The algebra is a little tedious but the following C code does the job:
static void make_basis( const double* Q, double* u, double* v)
{
double L = hypot( Q[0], hypot( Q[1], Q[2])); // length of Q
double sigma = (Q[0]>0.0) ? L : -L; // copysign( l, Q[0]) if you have it
double h = Q[0] + sigma; // first component of householder vector
double beta = -1.0/(sigma*h); // householder scale
// apply to (0,1,0)'
double f = beta*Q[1];
u[0] = f*h;
u[1] = 1.0+f*Q[1];
u[2] = f*Q[2];
// apply to (0,0,1)'
double g = beta*Q[2];
v[0] = g*h;
v[1] = g*Q[1];
v[2] = 1.0+g*Q[2];
}
Thank you Forward Ed and dmuir - that helped. Here is the code I made that seems to work:
function addTube(radius, segments, P1, P2) {
// Q = P1→P2 moved to origin
var Qx = P2[0] - P1[0];
var Qy = P2[1] - P1[1];
var Qz = P2[2] - P1[2];
// Create vectors U and V that are (1) mutually perpendicular and (2) perpendicular to Q
if (Qx != 0) { // create a perpendicular vector on the XY plane
// there are an infinite number of potential vectors; arbitrarily select y = 1
var Ux = -Qy/Qx;
var Uy = 1;
var Uz = 0;
// to prove U is perpendicular:
// (Qx, Qy, Qz)·(Ux, Uy, Uz) = Qx·Ux + Qy·Uy + Qz·Uz = Qx·-Qy/Qx + Qy·1 + Qz·0 = -Qy + Qy + 0 = 0
}
else if (Qy != 0) { // create a perpendicular vector on the YZ plane
var Ux = 0;
var Uy = -Qz/Qy;
var Uz = 1;
}
else { // assume Qz != 0; create a perpendicular vector on the XZ plane
var Ux = 1;
var Uy = 0;
var Uz = -Qx/Qz;
}
// The cross product of two vectors is perpendicular to both, so to find V:
// (Vx, Vy, Vz) = (Qx, Qy, Qz)×(Ux, Uy, Uz) = (Qy×Uz - Qz×Uy, Qz×Ux - Qx×Uz, Qx×Uy - Qy×Ux)
var Vx = Qy*Uz - Qz*Uy;
var Vy = Qz*Ux - Qx*Uz;
var Vz = Qx*Uy - Qy*Ux;
// normalize U and V:
var Ulength = Math.sqrt(Math.pow(Ux, 2) + Math.pow(Uy, 2) + Math.pow(Uz, 2));
var Vlength = Math.sqrt(Math.pow(Vx, 2) + Math.pow(Vy, 2) + Math.pow(Vz, 2));
Ux /= Ulength;
Uy /= Ulength;
Uz /= Ulength;
Vx /= Vlength;
Vy /= Vlength;
Vz /= Vlength;
for (var i = 0; i < segments; i++) {
var θ = 2*Math.PI*i/segments; // theta
var dx = radius*(Math.cos(θ)*Ux + Math.sin(θ)*Vx);
var dy = radius*(Math.cos(θ)*Uy + Math.sin(θ)*Vy);
var dz = radius*(Math.cos(θ)*Uz + Math.sin(θ)*Vz);
drawLine(P1[0] + dx, P1[1] + dy, P1[2] + dz, // point on circle around P1
P2[0] + dx, P2[1] + dy, P2[2] + dz) // point on circle around P2
}
}
I'm sure there are many ways to shorten the code and make it more efficient. I created a short visual demo online using Three.JS, at http://mvjantzen.com/tools/webgl/cylinder.html
Related
I would like to find the intersection point of an point + direction and a Line.
The point is located at the center of a polygon and the lines are the polygon edges.
The direction is rotation in radians of the longest segment of the polygon.
To illustrate the problem I have I made a screenshot:
image of the current state
You can see the center point, the longest segment of the polygon on the right + the beginning point of the longest segment, all in black color.
The purple and red points are the intersection points my algorithm has found.
The bottom and right intersection are right but on the left it found 2 intersection points, one of them is correct.
The top intersection did not reached the edge of the polygon.
The purple points should be the top and bottom intersection
The smaller red point should be on the left and right of the polygon, as you can see they are partially mixed together.
My code:
First loop calculates the longest segment and the rotation of that segment. Second loop checks for intersection in the vertical and horizontal direction based on the rotation of the longest segment
let sqLongestSide = 0;
let longestSideXDiff = 0;
let longestSideYDiff = 0;
for (let i = 1; i < coordinates.length; i++) {
let pointFrom = coordinates[i - 1];
let pointTo = coordinates[i];
const xDiff = pointFrom[0] - pointTo[0];
const yDiff = pointFrom[1] - pointTo[1];
const sqDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
const roundedDistance = Math.round(sqDistance * 100) / 100;
const roundedLongestSide = Math.round(sqLongestSide * 100) / 100;
if (roundedDistance > roundedLongestSide) {
sqLongestSide = sqDistance;
longestSideXDiff = xDiff;
longestSideYDiff = yDiff;
}
}
const rotation = Math.atan(longestSideYDiff / longestSideXDiff);
for (let i = 1; i < coordinates.length; i++) {
let pointFrom = coordinates[i - 1];
let pointTo = coordinates[i];
const intersectionTopAndBottom = intersectionPoint(anchor, rotation, [pointFrom, pointTo]);
if (intersectionTopAndBottom) {
setPointStyle(drawContext, "#FF00FF", 20);
drawContext.drawPoint(new Point(intersectionTopAndBottom));
drawContext.drawLineString(new LineString([anchor, intersectionTopAndBottom]));
}
const intersectionLeftAndRight = intersectionPoint(anchor, (rotation + Math.PI / 2), [pointFrom, pointTo]);
if (intersectionLeftAndRight) {
setPointStyle(drawContext, "#FF0000", 10);
drawContext.drawPoint(new Point(intersectionLeftAndRight));
setLineStyle(drawContext, "#FF0000", 5);
drawContext.drawLineString(new LineString([anchor, intersectionLeftAndRight]));
}
The function to find the intersection looks like this:
function intersectionPoint(
point: Coordinate,
theta: number,
line: Coordinate[]
): Coordinate {
const x0 = Math.round(point[0] * 100) / 100;
const y0 = Math.round(point[1] * 100) / 100;
const x1 = Math.round(line[0][0] * 100) / 100;
const y1 = Math.round(line[0][1] * 100) / 100;
const x2 = Math.round(line[1][0] * 100) / 100;
const y2 = Math.round(line[1][1] * 100) / 100;
// Check if the line is vertical or horizontal
if (x1 === x2) {
// The line is vertical, so the intersection point is simply the point with the same x-coordinate as the line
return [x1, y0];
} else if (y1 === y2) {
// The line is horizontal, so the intersection point is simply the point with the same y-coordinate as the line
return [x0, y1];
}
// Convert the line to slope-intercept form
const slope = (y2 - y1) / (x2 - x1);
const intercept = y1 - slope * x1;
// Convert the point and direction to slope-intercept form
const slope2 = Math.tan(theta);
const intercept2 = y0 - slope2 * x0;
// Find the intersection point of the two lines
const x = (intercept2 - intercept) / (slope - slope2);
const y = slope * x + intercept;
return [x, y];
}
I need just 4 intersection points but get more for this specfic polygon but for simple rectangle I get the right result.
EDIT:
Just to make it clear. I need to apply a rotation to the ray. This is the rotation of the longest segment, calculated in the first loop.
#Blindman67 your solution gave me the correct results for the first polygon, I just need that intersection "cross" to be rotated. Like in the following image:
correct
the algorithm give me the following result:
not correct
Clarification
Clarifying my understanding of your problem.
If given a set of points P1-6 and a point A find the points that intercept the projected point A along the x and y axis, points B1-7
Your function gave points B1, B2, B3, B4, B5. However points B4, B5 are incorrect
You say you only want 4 points, B1, B2, B3, B6
Solution
It looks like the points of interest are only the points that are on the line segments created by the points P1-6
We can use a function the finds the intercept of a line (infinite length) and a line segment (finite length has a start and end) See example code below.
Example code
The function interceptLineSeg will find the point on the line segment (including the start point and excluding the end point). If you include both the start and end points then you will (may due to floating point error) find the same point twice.
Note That floating point error may result in a point found twice. You may have to check if two found points are too close.
Note That if the point A is on a line (defined by poly edge segment) aligned to the x, or y axis then the point of intercept will depend on the direction of the points that make up the polygon (clockwise or anticlockwise)
const TAU = Math.PI * 2;
const ctx = canvas.getContext("2d");
const P2 = (x, y) => ({x, y}); // 2d point
const L2 = (p1, p2) => ({p1, p2}); // 2d line
const A = P2(100, 115);
const points = [P2(140,20), P2(140, 140), P2(40, 140), P2(40, 115), P2(80, 80), P2(80, 50)];
function addCircle(p, r) {ctx.moveTo(p.x + r, p.y); ctx.arc(p.x, p.y, r, 0, TAU) }
function addLine(l) { ctx.moveTo(l.p1.x, l.p1.y); ctx.lineTo(l.p2.x, l.p2.y) }
function strokePath(points, close = false) {
var i = 0;
ctx.beginPath();
while (i < points.length - 1) { addLine(L2(points[i ++], points[i])); }
close && addLine(L2(points[0], points[points.length - 1]));
ctx.stroke();
}
function markPoints(points, r) {
var i = 0;
ctx.beginPath();
while (i < points.length) { addCircle(points[i++], r); }
ctx.fill();
}
ctx.lineWidth = 3;
ctx.strokeStyle = "#0088ff";
ctx.fillStyle = "#0088ff";
strokePath(points, true);
markPoints(points, 5);
ctx.lineWidth = 1.5;
ctx.strokeStyle = "#ff0000";
ctx.fillStyle = "#ff0000";
strokePath([P2(A.x - 100, A.y), P2(A.x + 100, A.y)]);
strokePath([P2(A.x, A.y - 100), P2(A.x, A.y + 100)]);
markPoints([A], 5);
const iPoints = intercepts(A, points);
ctx.fillStyle = "#000";
markPoints(iPoints, 5);
console.log("Points found: " + iPoints.length);
function interceptLineSeg(l, s){ // l is line, s is seg
const v1x = l.p2.x - l.p1.x;
const v1y = l.p2.y - l.p1.y;
const v2x = s.p2.x - s.p1.x;
const v2y = s.p2.y - s.p1.y;
const c = v1x * v2y - v1y * v2x; // cross product of line vectors
if (c !== 0) { // only if not parallel
// get unit pos of intercept on line segment
const u = (v1x * (l.p1.y - s.p1.y) - v1y * (l.p1.x - s.p1.x)) / c;
if (u >= 0 && u < 1) { // is intercept on segment (include only start)
return P2(s.p1.x + v2x * u, s.p1.y + v2y * u);
}
}
return; // returns undefined if no intercept
}
// a is point
// p is array of points
function intercepts(a, p) {
var i = 0;
const hLine = L2(a, P2(a.x + 100, a.y));
const vLine = L2(a, P2(a.x, a.y + 100));
const wLine = L2();
const intercepts = [];
ahy = a.y;
avx = a.x;
avy = a.y + 100;
while (i < p.length) {
wLine.p1 = p[i];
wLine.p2 = p[(i + 1) % p.length];
const hl = interceptLineSeg(hLine, wLine);
const vl = interceptLineSeg(vLine, wLine);
if (hl) {
intercepts.push(hl);
} else if (vl) {
intercepts.push(vl);
}
i ++;
}
return intercepts;
}
<canvas id="canvas" width="300" height="300"></canvas>
I wish to modify an image by moving columns of pixels up or down such that each column offset follows a curve.
I wish for the curve to intersect 6 or so points in some smooth way. I Imagine looping over image x co-ordinates and calling a curve function that returns the y co-ordinate for the curve at that offset, thus telling me how much to move each column of pixels.
I have investigated various types of curves but frankly I am a bit lost, I was hoping there would be a ready made solution that would allow me to plug in my point co-ords and spit out the data that I need. I'm not too fussed what kind of curve is used, as long as it looks "smooth".
Can anyone help me with this?
I am using HTML5 and canvas. The answer given here looks like the sort of thing I am after, but it refers to an R library (I guess) which is Greek to me!
Sigmoid curve
A very simple solution if you only want the curve in the y direction is to use a sigmoid curve to interpolate the y pos between control points
// where 0 <= x <= 1 and p > 1
// return value between 0 and 1 inclusive.
// p is power and determines the amount of curve
function sigmoidCurve(x, p){
x = x < 0 ? 0 : x > 1 ? 1 : x;
var xx = Math.pow(x, p);
return xx / (xx + Math.pow(1 - x, p))
}
If you want the y pos at x coordinate px that is between two control points x1,y1 and x2, y2
First find the normalized position of px between x1,x2
var nx = (px - x1) / (x2 - x1); // normalised dist between points
Plug the value into sigmoidCurve
var c = sigmoidCurve(nx, 2); // curve power 2
The use that value to calculate y
var py = (y2 - y1) * c + y1;
And you have a point on the curve between the points.
As a single expression
var py = (y2 - y1) *sigmoidCurve((px - x1) / (x2 - x1), 2) + y1;
If you set the power for the sigmoid curve to 1.5 then it is almost a perfect match for a cubic bezier curve
Example
This example shows the curve animated. The function getPointOnCurve will get the y pos of any point on the curve at position x
const ctx = canvas.getContext("2d");
const curve = [[10, 0], [120, 100], [200, 50], [290, 150]];
const pos = {};
function cubicInterpolation(x, p0, p1, p2, p3){
x = x < 0 ? 0 : x > 1 ? 1 : x;
return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));
}
function sigmoidCurve(x, p){
x = x < 0 ? 0 : x > 1 ? 1 : x;
var xx = Math.pow(x, p);
return xx / (xx + Math.pow(1 - x, p))
}
// functional for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
// functional iterator
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
// find index for x in curve
// returns pos{ index, y }
// if x is at a control point then return the y value and index set to -1
// if not at control point the index is of the point befor x
function getPosOnCurve(x,curve, pos = {}){
var len = curve.length;
var i;
pos.index = -1;
pos.y = null;
if(x <= curve[0][0]) { return (pos.y = curve[0][1], pos) }
if(x >= curve[len - 1][0]) { return (pos.y = curve[len - 1][1], pos) }
i = 0;
var found = false;
while(!found){ // some JS optimisers will mark "Do not optimise"
// code that do not have an exit clause.
if(curve[i++][0] <x && curve[i][0] >= x) { break }
}
i -= 1;
if(x === curve[i][0]) { return (pos.y = curve[i][1], pos) }
pos.index =i
return pos;
}
// Using Cubic interpolation to create the curve
function getPointOnCubicCurve(x, curve, power){
getPosOnCurve(x, curve, pos);
if(pos.index === -1) { return pos.y };
var i = pos.index;
// get interpolation values for points around x
var p0,p1,p2,p3;
p1 = curve[i][1];
p2 = curve[i+1][1];
p0 = i === 0 ? p1 : curve[i-1][1];
p3 = i === curve.length - 2 ? p2 : curve[i+2][1];
// get unit distance of x between curve i, i+1
var ux = (x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]);
return cubicInterpolation(ux, p0, p1, p2, p3);
}
// Using Sigmoid function to get curve.
// power changes curve power = 1 is line power > 1 tangents become longer
// With the power set to 1.5 this is almost a perfect match for
// cubic bezier solution.
function getPointOnCurve(x, curve, power){
getPosOnCurve(x, curve, pos);
if(pos.index === -1) { return pos.y };
var i = pos.index;
var p = sigmoidCurve((x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]) ,power);
return curve[i][1] + (curve[i + 1][1] - curve[i][1]) * p;
}
const step = 2;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center width and height
var ch = h / 2;
function update(timer){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
eachOf(curve, (point) => {
point[1] = Math.sin(timer / (((point[0] + 10) % 71) * 100) ) * ch * 0.8 + ch;
});
ctx.strokeStyle = "black";
ctx.beginPath();
doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCurve(x * step, curve, 1.5) - 10)});
ctx.stroke();
ctx.strokeStyle = "blue";
ctx.beginPath();
doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCubicCurve(x * step, curve, 1.5) + 10)});
ctx.stroke();
ctx.strokeStyle = "black";
eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 - 10, 4, 4) );
eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 + 10, 4, 4) );
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
Update
I have added a second curve type to the above demo as the blue curve offset from the original sigmoid curve in black.
Cubic polynomial
The above function can be adapted to a variety of interpolation methods. I have added the function
function cubicInterpolation(x, p0, p1, p2, p3){
x = x < 0 ? 0 : x > 1 ? 1 : x;
return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));
}
Which produces a curve based on the slope of the line at two points either side of x. This method is intended for evenly spaced points but still works if you have uneven spacing (such as this example). If the spacing gets too uneven you can notice a bit of a kink in the curve at that point.
Also the curve over and under shoot may be an issue.
For more on the Maths of cubic interpolation.
I was going to stop at intervals on the ray and check if they were within the radius of the sphere. I found much more efficient mathematical ways to do this but they are all written in C++.
Something like this should work (inefficient version but without any dependency and easy to follow):
function dotProduct(v1, v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
function squaredLength(v) {
return dotProduct(v, v);
}
// Returns whether the ray intersects the sphere
// #param[in] center center point of the sphere (C)
// #param[in] radius radius of the sphere (R)
// #param[in] origin origin point of the ray (O)
// #param[in] direction direction vector of the ray (D)
// #param[out] intersection closest intersection point of the ray with the sphere, if any
function intersectRayWithSphere(center, radius,
origin, direction,
intersection) {
// Solve |O + t D - C|^2 = R^2
// t^2 |D|^2 + 2 t < D, O - C > + |O - C|^2 - R^2 = 0
var OC = intersection; // Use the output parameter as temporary workspace
OC.x = origin.x - center.x;
OC.y = origin.y - center.y;
OC.z = origin.z - center.z;
// Solve the quadratic equation a t^2 + 2 t b + c = 0
var a = squaredLength(direction);
var b = dotProduct(direction, OC);
var c = squaredLength(OC) - radius * radius;
var delta = b * b - a * c;
if (delta < 0) // No solution
return false;
// One or two solutions, take the closest (positive) intersection
var sqrtDelta = Math.sqrt(delta);
// a >= 0
var tMin = (-b - sqrtDelta) / a;
var tMax = (-b + sqrtDelta) / a;
if (tMax < 0) // All intersection points are behind the origin of the ray
return false;
// tMax >= 0
var t = tMin >= 0 ? tMin : tMax;
intersection.x = origin.x + t * direction.x;
intersection.y = origin.y + t * direction.y;
intersection.z = origin.z + t * direction.z;
return true;
}
The quadratic/cubic bézier curve code I find via google mostly works by subdividing the line into a series of points and connects them with straight lines. The rasterization happens in the line algorithm, not in the bézier one. Algorithms like Bresenham's work pixel-by-pixel to rasterize a line, and can be optimized (see Po-Han Lin's solution).
What is a quadratic bézier curve algorithm that works pixel-by-pixel like line algorithms instead of by plotting a series of points?
A variation of Bresenham's Algorithm works with quadratic functions like circles, ellipses, and parabolas, so it should work with quadratic Bezier curves too.
I was going to attempt an implementation, but then I found one on the web: http://members.chello.at/~easyfilter/bresenham.html.
If you want more detail or additional examples, the page mentioned above has a link to a 100 page PDF elaborating on the method: http://members.chello.at/~easyfilter/Bresenham.pdf.
Here's the code from Alois Zingl's site for plotting any quadratic Bezier curve. The first routine subdivides the curve at horizontal and vertical gradient changes:
void plotQuadBezier(int x0, int y0, int x1, int y1, int x2, int y2)
{ /* plot any quadratic Bezier curve */
int x = x0-x1, y = y0-y1;
double t = x0-2*x1+x2, r;
if ((long)x*(x2-x1) > 0) { /* horizontal cut at P4? */
if ((long)y*(y2-y1) > 0) /* vertical cut at P6 too? */
if (fabs((y0-2*y1+y2)/t*x) > abs(y)) { /* which first? */
x0 = x2; x2 = x+x1; y0 = y2; y2 = y+y1; /* swap points */
} /* now horizontal cut at P4 comes first */
t = (x0-x1)/t;
r = (1-t)*((1-t)*y0+2.0*t*y1)+t*t*y2; /* By(t=P4) */
t = (x0*x2-x1*x1)*t/(x0-x1); /* gradient dP4/dx=0 */
x = floor(t+0.5); y = floor(r+0.5);
r = (y1-y0)*(t-x0)/(x1-x0)+y0; /* intersect P3 | P0 P1 */
plotQuadBezierSeg(x0,y0, x,floor(r+0.5), x,y);
r = (y1-y2)*(t-x2)/(x1-x2)+y2; /* intersect P4 | P1 P2 */
x0 = x1 = x; y0 = y; y1 = floor(r+0.5); /* P0 = P4, P1 = P8 */
}
if ((long)(y0-y1)*(y2-y1) > 0) { /* vertical cut at P6? */
t = y0-2*y1+y2; t = (y0-y1)/t;
r = (1-t)*((1-t)*x0+2.0*t*x1)+t*t*x2; /* Bx(t=P6) */
t = (y0*y2-y1*y1)*t/(y0-y1); /* gradient dP6/dy=0 */
x = floor(r+0.5); y = floor(t+0.5);
r = (x1-x0)*(t-y0)/(y1-y0)+x0; /* intersect P6 | P0 P1 */
plotQuadBezierSeg(x0,y0, floor(r+0.5),y, x,y);
r = (x1-x2)*(t-y2)/(y1-y2)+x2; /* intersect P7 | P1 P2 */
x0 = x; x1 = floor(r+0.5); y0 = y1 = y; /* P0 = P6, P1 = P7 */
}
plotQuadBezierSeg(x0,y0, x1,y1, x2,y2); /* remaining part */
}
The second routine actually plots a Bezier curve segment (one without gradient changes):
void plotQuadBezierSeg(int x0, int y0, int x1, int y1, int x2, int y2)
{ /* plot a limited quadratic Bezier segment */
int sx = x2-x1, sy = y2-y1;
long xx = x0-x1, yy = y0-y1, xy; /* relative values for checks */
double dx, dy, err, cur = xx*sy-yy*sx; /* curvature */
assert(xx*sx <= 0 && yy*sy <= 0); /* sign of gradient must not change */
if (sx*(long)sx+sy*(long)sy > xx*xx+yy*yy) { /* begin with longer part */
x2 = x0; x0 = sx+x1; y2 = y0; y0 = sy+y1; cur = -cur; /* swap P0 P2 */
}
if (cur != 0) { /* no straight line */
xx += sx; xx *= sx = x0 < x2 ? 1 : -1; /* x step direction */
yy += sy; yy *= sy = y0 < y2 ? 1 : -1; /* y step direction */
xy = 2*xx*yy; xx *= xx; yy *= yy; /* differences 2nd degree */
if (cur*sx*sy < 0) { /* negated curvature? */
xx = -xx; yy = -yy; xy = -xy; cur = -cur;
}
dx = 4.0*sy*cur*(x1-x0)+xx-xy; /* differences 1st degree */
dy = 4.0*sx*cur*(y0-y1)+yy-xy;
xx += xx; yy += yy; err = dx+dy+xy; /* error 1st step */
do {
setPixel(x0,y0); /* plot curve */
if (x0 == x2 && y0 == y2) return; /* last pixel -> curve finished */
y1 = 2*err < dx; /* save value for test of y step */
if (2*err > dy) { x0 += sx; dx -= xy; err += dy += yy; } /* x step */
if ( y1 ) { y0 += sy; dy -= xy; err += dx += xx; } /* y step */
} while (dy < 0 && dx > 0); /* gradient negates -> algorithm fails */
}
plotLine(x0,y0, x2,y2); /* plot remaining part to end */
}
Code for antialiasing is also available on the site.
The corresponding functions from Zingl's site for cubic Bezier curves are
void plotCubicBezier(int x0, int y0, int x1, int y1,
int x2, int y2, int x3, int y3)
{ /* plot any cubic Bezier curve */
int n = 0, i = 0;
long xc = x0+x1-x2-x3, xa = xc-4*(x1-x2);
long xb = x0-x1-x2+x3, xd = xb+4*(x1+x2);
long yc = y0+y1-y2-y3, ya = yc-4*(y1-y2);
long yb = y0-y1-y2+y3, yd = yb+4*(y1+y2);
float fx0 = x0, fx1, fx2, fx3, fy0 = y0, fy1, fy2, fy3;
double t1 = xb*xb-xa*xc, t2, t[5];
/* sub-divide curve at gradient sign changes */
if (xa == 0) { /* horizontal */
if (abs(xc) < 2*abs(xb)) t[n++] = xc/(2.0*xb); /* one change */
} else if (t1 > 0.0) { /* two changes */
t2 = sqrt(t1);
t1 = (xb-t2)/xa; if (fabs(t1) < 1.0) t[n++] = t1;
t1 = (xb+t2)/xa; if (fabs(t1) < 1.0) t[n++] = t1;
}
t1 = yb*yb-ya*yc;
if (ya == 0) { /* vertical */
if (abs(yc) < 2*abs(yb)) t[n++] = yc/(2.0*yb); /* one change */
} else if (t1 > 0.0) { /* two changes */
t2 = sqrt(t1);
t1 = (yb-t2)/ya; if (fabs(t1) < 1.0) t[n++] = t1;
t1 = (yb+t2)/ya; if (fabs(t1) < 1.0) t[n++] = t1;
}
for (i = 1; i < n; i++) /* bubble sort of 4 points */
if ((t1 = t[i-1]) > t[i]) { t[i-1] = t[i]; t[i] = t1; i = 0; }
t1 = -1.0; t[n] = 1.0; /* begin / end point */
for (i = 0; i <= n; i++) { /* plot each segment separately */
t2 = t[i]; /* sub-divide at t[i-1], t[i] */
fx1 = (t1*(t1*xb-2*xc)-t2*(t1*(t1*xa-2*xb)+xc)+xd)/8-fx0;
fy1 = (t1*(t1*yb-2*yc)-t2*(t1*(t1*ya-2*yb)+yc)+yd)/8-fy0;
fx2 = (t2*(t2*xb-2*xc)-t1*(t2*(t2*xa-2*xb)+xc)+xd)/8-fx0;
fy2 = (t2*(t2*yb-2*yc)-t1*(t2*(t2*ya-2*yb)+yc)+yd)/8-fy0;
fx0 -= fx3 = (t2*(t2*(3*xb-t2*xa)-3*xc)+xd)/8;
fy0 -= fy3 = (t2*(t2*(3*yb-t2*ya)-3*yc)+yd)/8;
x3 = floor(fx3+0.5); y3 = floor(fy3+0.5); /* scale bounds to int */
if (fx0 != 0.0) { fx1 *= fx0 = (x0-x3)/fx0; fx2 *= fx0; }
if (fy0 != 0.0) { fy1 *= fy0 = (y0-y3)/fy0; fy2 *= fy0; }
if (x0 != x3 || y0 != y3) /* segment t1 - t2 */
plotCubicBezierSeg(x0,y0, x0+fx1,y0+fy1, x0+fx2,y0+fy2, x3,y3);
x0 = x3; y0 = y3; fx0 = fx3; fy0 = fy3; t1 = t2;
}
}
and
void plotCubicBezierSeg(int x0, int y0, float x1, float y1,
float x2, float y2, int x3, int y3)
{ /* plot limited cubic Bezier segment */
int f, fx, fy, leg = 1;
int sx = x0 < x3 ? 1 : -1, sy = y0 < y3 ? 1 : -1; /* step direction */
float xc = -fabs(x0+x1-x2-x3), xa = xc-4*sx*(x1-x2), xb = sx*(x0-x1-x2+x3);
float yc = -fabs(y0+y1-y2-y3), ya = yc-4*sy*(y1-y2), yb = sy*(y0-y1-y2+y3);
double ab, ac, bc, cb, xx, xy, yy, dx, dy, ex, *pxy, EP = 0.01;
/* check for curve restrains */
/* slope P0-P1 == P2-P3 and (P0-P3 == P1-P2 or no slope change) */
assert((x1-x0)*(x2-x3) < EP && ((x3-x0)*(x1-x2) < EP || xb*xb < xa*xc+EP));
assert((y1-y0)*(y2-y3) < EP && ((y3-y0)*(y1-y2) < EP || yb*yb < ya*yc+EP));
if (xa == 0 && ya == 0) { /* quadratic Bezier */
sx = floor((3*x1-x0+1)/2); sy = floor((3*y1-y0+1)/2); /* new midpoint */
return plotQuadBezierSeg(x0,y0, sx,sy, x3,y3);
}
x1 = (x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)+1; /* line lengths */
x2 = (x2-x3)*(x2-x3)+(y2-y3)*(y2-y3)+1;
do { /* loop over both ends */
ab = xa*yb-xb*ya; ac = xa*yc-xc*ya; bc = xb*yc-xc*yb;
ex = ab*(ab+ac-3*bc)+ac*ac; /* P0 part of self-intersection loop? */
f = ex > 0 ? 1 : sqrt(1+1024/x1); /* calculate resolution */
ab *= f; ac *= f; bc *= f; ex *= f*f; /* increase resolution */
xy = 9*(ab+ac+bc)/8; cb = 8*(xa-ya);/* init differences of 1st degree */
dx = 27*(8*ab*(yb*yb-ya*yc)+ex*(ya+2*yb+yc))/64-ya*ya*(xy-ya);
dy = 27*(8*ab*(xb*xb-xa*xc)-ex*(xa+2*xb+xc))/64-xa*xa*(xy+xa);
/* init differences of 2nd degree */
xx = 3*(3*ab*(3*yb*yb-ya*ya-2*ya*yc)-ya*(3*ac*(ya+yb)+ya*cb))/4;
yy = 3*(3*ab*(3*xb*xb-xa*xa-2*xa*xc)-xa*(3*ac*(xa+xb)+xa*cb))/4;
xy = xa*ya*(6*ab+6*ac-3*bc+cb); ac = ya*ya; cb = xa*xa;
xy = 3*(xy+9*f*(cb*yb*yc-xb*xc*ac)-18*xb*yb*ab)/8;
if (ex < 0) { /* negate values if inside self-intersection loop */
dx = -dx; dy = -dy; xx = -xx; yy = -yy; xy = -xy; ac = -ac; cb = -cb;
} /* init differences of 3rd degree */
ab = 6*ya*ac; ac = -6*xa*ac; bc = 6*ya*cb; cb = -6*xa*cb;
dx += xy; ex = dx+dy; dy += xy; /* error of 1st step */
for (pxy = &xy, fx = fy = f; x0 != x3 && y0 != y3; ) {
setPixel(x0,y0); /* plot curve */
do { /* move sub-steps of one pixel */
if (dx > *pxy || dy < *pxy) goto exit; /* confusing values */
y1 = 2*ex-dy; /* save value for test of y step */
if (2*ex >= dx) { /* x sub-step */
fx--; ex += dx += xx; dy += xy += ac; yy += bc; xx += ab;
}
if (y1 <= 0) { /* y sub-step */
fy--; ex += dy += yy; dx += xy += bc; xx += ac; yy += cb;
}
} while (fx > 0 && fy > 0); /* pixel complete? */
if (2*fx <= f) { x0 += sx; fx += f; } /* x step */
if (2*fy <= f) { y0 += sy; fy += f; } /* y step */
if (pxy == &xy && dx < 0 && dy > 0) pxy = &EP;/* pixel ahead valid */
}
exit: xx = x0; x0 = x3; x3 = xx; sx = -sx; xb = -xb; /* swap legs */
yy = y0; y0 = y3; y3 = yy; sy = -sy; yb = -yb; x1 = x2;
} while (leg--); /* try other end */
plotLine(x0,y0, x3,y3); /* remaining part in case of cusp or crunode */
}
As Mike 'Pomax' Kamermans has noted, the solution for cubic Bezier curves on the site is not complete; in particular, there are issues with antialiasing cubic Bezier curves, and the discussion of rational cubic Bezier curves is incomplete.
You can use De Casteljau's algorithm to subdivide a curve into enough pieces that each subsection is a pixel.
This is the equation for finding the [x,y] point on a Quadratic Curve at interval T:
// Given 3 control points defining the Quadratic curve
// and given T which is an interval between 0.00 and 1.00 along the curve.
// Note:
// At the curve's starting control point T==0.00.
// At the curve's ending control point T==1.00.
var x = Math.pow(1-T,2)*startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2)*startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
To make practical use of this equation, you can input about 1000 T values between 0.00 and 1.00. This results in a set of 1000 points guaranteed to be along the Quadratic Curve.
Calculating 1000 points along the curve is probably over-sampling (some calculated points will be at the same pixel coordinate) so you will want to de-duplicate the 1000 points until the set represents unique pixel coordinates along the curve.
There is a similar equation for Cubic Bezier curves.
Here's example code that plots a Quadratic Curve as a set of calculated pixels:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var points=[];
var lastX=-100;
var lastY=-100;
var startPt={x:50,y:200};
var controlPt={x:150,y:25};
var endPt={x:250,y:100};
for(var t=0;t<1000;t++){
var xyAtT=getQuadraticBezierXYatT(startPt,controlPt,endPt,t/1000);
var x=parseInt(xyAtT.x);
var y=parseInt(xyAtT.y);
if(!(x==lastX && y==lastY)){
points.push(xyAtT);
lastX=x;
lastY=y;
}
}
$('#curve').text('Quadratic Curve made up of '+points.length+' individual points');
ctx.fillStyle='red';
for(var i=0;i<points.length;i++){
var x=points[i].x;
var y=points[i].y;
ctx.fillRect(x,y,1,1);
}
function getQuadraticBezierXYatT(startPt,controlPt,endPt,T) {
var x = Math.pow(1-T,2) * startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2) * startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
return( {x:x,y:y} );
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id='curve'>Q</h4>
<canvas id="canvas" width=350 height=300></canvas>
The thing to realise here is that "line segments", when created small enough, are equivalent to pixels. Bezier curves are not linearly traversible curves, so we can't easily "skip ahead to the next pixel" in a single step, like we can for lines or circular arcs.
You could, of course, take the tangent at any point for a t you already have, and then guess which next value t' will lie a pixel further. However, what typically happens is that you guess, and guess wrong because the curve does not behave linearly, then you check to see how "off" your guess was, correct your guess, and then check again. Repeat until you've converged on the next pixel: this is far, far slower than just flattening the curve to a high number of line segments instead, which is a fast operation.
If you pick the number of segments such that they're appropriate to the curve's length, given the display it's rendered to, no one will be able to tell you flattened the curve.
There are ways to reparameterize Bezier curves, but they're expensive, and different canonical curves require different reparameterization, so that's really not faster either. What tends to be the most useful for discrete displays is to build a LUT (lookup table) for your curve, with a length that works for the size the curve is on the display, and then using that LUT as your base data for drawing, intersection detection, etc. etc.
First of all, I'd like to say that the fastest and the most reliable way to render bezier curves is to approximate them by polyline via adaptive subdivision, then render the polyline. Approach by #markE with drawing many points sampled on the curve is rather fast, but it can skip pixels. Here I describe another approach, which is closest to line rasterization (though it is slow and hard to implement robustly).
I'll treat usually curve parameter as time. Here is the pseudocode:
Put your cursor at the first control point, find the surrounding pixel.
For each side of the pixel (four total), check when your bezier curves intersects its line by solving quadratic equations.
Among all the calculated side intersection times, choose the one which will happen strictly in future, but as early as possible.
Move to neighboring pixel depending on which side was best.
Set current time to time of that best side intersection.
Repeat from step 2.
This algorithm works until time parameter exceeds one. Also note that it has severe issues with curves exactly touching a side of a pixel. I suppose it is solvable with a special check.
Here is the main code:
double WhenEquals(double p0, double p1, double p2, double val, double minp) {
//p0 * (1-t)^2 + p1 * 2t(1 - t) + p2 * t^2 = val
double qa = p0 + p2 - 2 * p1;
double qb = p1 - p0;
double qc = p0 - val;
assert(fabs(qa) > EPS); //singular case must be handled separately
double qd = qb * qb - qa * qc;
if (qd < -EPS)
return INF;
qd = sqrt(max(qd, 0.0));
double t1 = (-qb - qd) / qa;
double t2 = (-qb + qd) / qa;
if (t2 < t1) swap(t1, t2);
if (t1 > minp + EPS)
return t1;
else if (t2 > minp + EPS)
return t2;
return INF;
}
void DrawCurve(const Bezier &curve) {
int cell[2];
for (int c = 0; c < 2; c++)
cell[c] = int(floor(curve.pts[0].a[c]));
DrawPixel(cell[0], cell[1]);
double param = 0.0;
while (1) {
int bc = -1, bs = -1;
double bestTime = 1.0;
for (int c = 0; c < 2; c++)
for (int s = 0; s < 2; s++) {
double crit = WhenEquals(
curve.pts[0].a[c],
curve.pts[1].a[c],
curve.pts[2].a[c],
cell[c] + s, param
);
if (crit < bestTime) {
bestTime = crit;
bc = c, bs = s;
}
}
if (bc < 0)
break;
param = bestTime;
cell[bc] += (2*bs - 1);
DrawPixel(cell[0], cell[1]);
}
}
Full code is available here.
It uses loadbmp.h, here it is.
How can you find the centroid of a concave irregular polygon given its vertices in JavaScript?
I want to pass a set of x,y points to a JavaScript function and be given an x,y point.
var my_points = [{x:3,y:1},{x:5,y:8},{x:2,y:9}];
function get_polygon_centroid(points){
// answer
}
var my_centroid = get_polygon_centroid(my_points);
The my_points variable is only supposed to represent the format of the points to be given, not to represent the specific count of points to be given.
The centroid returned will be a point somewhere inside the polygon.
The end goal would be to add a marker at the centroid of a polygon in a Google Maps V3 application.
For the centroid of the 2D surface (which is likely to be what you need),
The best is to start with a little bit of maths.
I adapted it here to your own notation :
function get_polygon_centroid(pts) {
var first = pts[0], last = pts[pts.length-1];
if (first.x != last.x || first.y != last.y) pts.push(first);
var twicearea=0,
x=0, y=0,
nPts = pts.length,
p1, p2, f;
for ( var i=0, j=nPts-1 ; i<nPts ; j=i++ ) {
p1 = pts[i]; p2 = pts[j];
f = p1.x*p2.y - p2.x*p1.y;
twicearea += f;
x += ( p1.x + p2.x ) * f;
y += ( p1.y + p2.y ) * f;
}
f = twicearea * 3;
return { x:x/f, y:y/f };
}
The accepted answer has an issue which becomes prominent as the polygon's area becomes smaller. It would not be visible in most cases, but can result in some weird results at very small dimensions. Here's an update to that solution to account for this issue.
function get_polygon_centroid(pts) {
var first = pts[0], last = pts[pts.length-1];
if (first.x != last.x || first.y != last.y) pts.push(first);
var twicearea=0,
x=0, y=0,
nPts = pts.length,
p1, p2, f;
for ( var i=0, j=nPts-1 ; i<nPts ; j=i++ ) {
p1 = pts[i]; p2 = pts[j];
f = (p1.y - first.y) * (p2.x - first.x) - (p2.y - first.y) * (p1.x - first.x);
twicearea += f;
x += (p1.x + p2.x - 2 * first.x) * f;
y += (p1.y + p2.y - 2 * first.y) * f;
}
f = twicearea * 3;
return { x:x/f + first.x, y:y/f + first.y };
}
Here's a case of a centroid ending up outside of a small polygon for anyone curious as to what I'm talking about:
var points = [
{x:78.0001462, y: 40.0008827},
{x:78.0000228, y: 40.0008940},
{x:78.0000242, y: 40.0009264},
{x:78.0001462, y: 40.0008827},
];
// original get_polygon_centroid(points)
// results in { x: 77.99957948181007, y: 40.00065236005001 }
console.log(get_polygon_centroid(points))
// result is { x: 78.0000644, y: 40.000901033333335 }
This is fairly simple to do. The centroid of a finite set of k points x1, x2, ... xk is described by the formula
(x1 + x2 + ... + xk) / k
That means we can just add all the points up and then divide by the number of points, like this:
function getPolygonCentroid(points){
var centroid = {x: 0, y: 0};
for(var i = 0; i < points.length; i++) {
var point = points[i];
centroid.x += point.x;
centroid.y += point.y;
}
centroid.x /= points.length;
centroid.y /= points.length;
return centroid;
}
If you want the label to be positioned within the concave irregular polygon, then the above answers do not work very well. The centroid of a concave polygon could be very likely outside the polygon.
There's this Polylabel library that addresses the problem very well. This blog article gives the details of the algorithm. If you want to use the library in browser and don't want to package it yourself, you can download the library from this page.
Code example to use the library:
function get_polygon_centroid(points) {
var pts = points.map(p => [p.x, p.y]); // convert {x:x, y:y} into [x, y]
var centroid = polylabel([pts]);
return {x:centroid[0], y:centroid[1]};
}
var my_points = [{x:3,y:1},{x:5,y:8},{x:2,y:9}];
console.log('centroid', get_polygon_centroid(my_points));
If you are not casual about the definition of 'centroid', this is the formula for the centroid of a polygon. As you can see, it's sufficiently more complicated than the centroid of a set of points. If you can do with the centroid of the points, that's fine, but if you want the centroid of the polygon, you'd have to implement this formula, which btw is not very difficult. Please remember that in the general case of an irregular polygon, which is your case, these two centroids will be different (otherwise this formula wouldn't exist).
Building on Myobis' and pragmar's answers, it's not necessary to alter the array by duplicating the first element.
In fact, in their implementation the first iteration of the for loop doesn't do anything because pts[i] and pts[j] are identical.
Here is the same algorithm with some optimizations (originally ported from Finding the centroid of a polygon?):
function get_polygon_centroid(points) {
//Correction for very small polygons:
const x0 = points[0].x , y0 = points[0].y;
let x = 0, y = 0, twiceArea = 0;
let prev = points[points.length - 1];
for (const next of points)
{
const x1 = prev.x - x0, y1 = prev.y - y0,
x2 = next.x - x0, y2 = next.y - y0,
a = x1 * y2 - x2 * y1;
twiceArea += a;
x += (x1 + x2) * a;
y += (y1 + y2) * a;
prev = next;
}
const factor = 3 * twiceArea; // 6 * twiceArea/2
x /= factor;
y /= factor;
return { x: x + x0, y: y + y0 };
}
const points = [
{ x: 78.0001462, y: 40.0008827 },
{ x: 78.0000228, y: 40.0008940 },
{ x: 78.0000242, y: 40.0009264 },
];
console.log(get_polygon_centroid(points));