Correctly calculating point on a bicubic Bézier surface - javascript

I wrote a function that should return a point on a bicubic Bézier surface from a 4x4 matrix of control points for parameters u and v, which are element of [0, 1], using Bernstein polynomials. But either my function doesn't work like it should, or my understanding of the matter is even worse than I thought.
The function to calculate the point looks like this:
var bezierSurface = function (u, v, p) {
var result = [];
var p00 = p[0], p01 = p[1], p02 = p[2], p03 = p[3],
p10 = p[4], p11 = p[5], p12 = p[6], p13 = p[7],
p20 = p[8], p21 = p[9], p22 = p[10], p23 = p[11],
p30 = p[12], p31 = p[13], p32 = p[14], p33 = p[15];
var uin = (1 - u),
vin = (1 - v);
var bu0 = Math.pow(uin, 3),
bu1 = 3 * u * Math.pow(uin, 2),
bu2 = 3 * Math.pow(u, 2) * uin,
bu3 = Math.pow(u, 3);
var bv0 = Math.pow(vin, 3),
bv1 = 3 * v * Math.pow(vin, 2),
bv2 = 3 * Math.pow(v, 2) * vin,
bv3 = Math.pow(v, 3);
for (var i = 0; i < 3; i++) {
result.push(
p00[i] * bu0 * bv0 +
p01[i] * bu0 * bv1 +
p02[i] * bu0 * bv2 +
p03[i] * bu0 * bv3 +
p10[i] * bu1 * bv0 +
p11[i] * bu1 * bv1 +
p12[i] * bu1 * bv2 +
p13[i] * bu1 * bv3 +
p20[i] * bu2 * bv0 +
p21[i] * bu2 * bv1 +
p22[i] * bu2 * bv2 +
p23[i] * bu2 * bv3 +
p30[i] * bu3 * bv0 +
p31[i] * bu3 * bv1 +
p32[i] * bu3 * bv2 +
p33[i] * bu3 * bv3
);
}
return result;
};
Most probably this is not the most efficient way to get the job done, but since I'm just getting started with parametric surfaces, I'm trying to keep things as simple as possible, yet not even thinking about tesselating the surface to get vertices for triangles or something like that.
Now, the problem appeared when I called the function with the following arguments:
var getSurfacePoint = function () {
var u = 0.5,
v = 0.25;
var cp = [
[-1.0, 0.0, -1.0],
[-0.5, 0.3, -0.8],
[ 0.5, 0.3, -0.8],
[ 1.0, 0.0, -1.0],
[-0.8, 0.3, -0.5],
[-0.3, 1.0, -0.4],
[ 0.3, 1.0, -0.4],
[ 0.8, 0.3, -0.5],
[-0.8, 0.3, 0.5],
[-0.3, 1.0, 0.4],
[ 0.3, 1.0, 0.4],
[ 0.8, 0.3, 0.5],
[-1.0, 0.0, 1.0],
[-0.5, 0.3, 0.8],
[ 0.5, 0.3, 0.8],
[ 1.0, 0.0, 1.0]
];
return bezierSurface(u, v, cp);
};
The result of calling bezierSurface via getSurfacePoint is -0.4437500000000001 for x, 0.5625 for y and -4.683753385137379e-17 for z, and that is not what I expected. I mean, at first sight, the return values for x and y seem plausible, but considering the values provided by the matrix of control points, the return value for z just looks completely wrong.
As far as I understand it, the points of a Bézier curve as well as the points of a Bézier surface are always enclosed within the convex hull of the control polygon, that is here represented by the points of the 4x4 matrix. So, when the range of z-values of the control points only goes from -1.0 to 1.0, how can the calculated point of the surface have a z-value < -4.0?
If we suppose the result is wrong, there must be something wrong with my function to calculate the point on the surface, but though alternately staring at bezierSurface and the mathematical definition of the Bézier surface for some time, I wasn't able to spot the error yet. I hope someone else can.

the return value for z just looks completely wrong
-4.683753385137379e-17, the value is (almost) 0. The result looks pretty right.

Related

Making a planar dynamical system with JSXgraph

I'm working on a predator-prey model for a dynamical systems book. I start by creating a dragable point for the initial condition. If I set up a second point whose coordinates are functions of the first point, I can drag one and it moves the other. I'm trying to get a 100 point orbit of the system, and I'm having difficulty. Here's a fiddle that works for a single point - https://jsfiddle.net/jford1906/gx86vbtc/21/
var board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-0.1, 3, 1, -0.1],
axis: true,
grid: true,
showFullscreen: true
});
var p1 = board.create('point', [0.5, 0.5], {
name: 'A'
});
var coords = board.create('text',
[0.3, 2.8, function() {
return "Initial Condition: (" + JXG.toFixed(p1.X(), 2) + "," + JXG.toFixed(p1.Y(), 2) + ")";
}]
);
var p2 = board.create('point', [function() {
return 2 * p1.X() * (1 - p1.X()) - 0.5 * p1.X() * p1.Y()
}, function() {
return 4 * p1.Y() / 5 + 1.5 * p1.X() * p1.Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 3
});
What I've tried so far is to plug each point of the orbit into an array, and have the next point run the same functions for it's coordinates as I did in the working example. It initially shows the whole orbit, but when I move the initial condition, all points except the last one in the orbit vanish. This fiddle shows how I've tried to do it - https://jsfiddle.net/jford1906/jra9g2d3/3/
var i; //indexing variable
var pts = [p1] //Put the initial condition in an array
for (i = 1; i < 100; i++) {
var p2 = board.create('point', [function() {
return 2 * pts[i - 1].X() * (1 - pts[i - 1].X()) - 0.5 * pts[i - 1].X() * pts[i - 1].Y()
}, function() {
return 4 * pts[i - 1].Y() / 5 + 1.5 * pts[i - 1].X() * pts[i - 1].Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 1
});
pts.push(p2);
}
Ideas on why this might happen, or thoughts on a different approach? I've also tried putting the whole loop in a function and having that trigger when the point is dragged, but the same issue occurs.
This is a problem with JavaScript closures. It has been answered in https://groups.google.com/g/jsxgraph/c/Y1y1Mbd23ZQ. A very quick fix would be to define the variable i with let instead of var:
var pts = [p1] //Put the initial condition in an array
for (let i = 1; i < 100; i++) {
var p2 = board.create('point', [function() {
return 2 * pts[i - 1].X() * (1 - pts[i - 1].X()) - 0.5 * pts[i - 1].X() * pts[i - 1].Y()
}, function() {
return 4 * pts[i - 1].Y() / 5 + 1.5 * pts[i - 1].X() * pts[i - 1].Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 1
});
pts.push(p2);
}

how to make a custom easing function that eases between given breakpoints

I need a custom easing function that takes breakpoints as parameters and eases between those breakpoints similar to this but this doesn't work:
function makeEasing(breakpoints) {
return t => {
const iPoint = Math.floor(t / breakpoints.length),
iDest = iPoint + 1;
return (breakpoints[iPoint] - breakpoints[iDest]) * (t);
};
}
Usage:
const easing = makeEasing([0.5, 0.0, 0.5, 1.0, 0.5]);
easing(0) // 0.5
// easing(0 + 1/4 * 0.5) // 0.25
easing(1/4) // 0.0
easing(2/4) // 0.5
easing(3/4) // 1.0
// easing(3/4 + 1/4 * 0.5) // 0.75
easing(4/4) // 0.5
let's say we can use this elastic easing function
const easing = t => {
return .04 * t / (--t) * Math.sin(25 * t);
};
i have two variables in the range [0.0, 1.0]
impact and targetImpact
I want impact to reach targetImpact slowly so I do:
// delta is passed time
impact += (targetImpact - impact) * delta * 0.001;
This works for interpolating between impact and targetImpact.
But I want elastic effect when impact reaches targetImpact, that is
impact goes up to targetImpact + 0.2,
then goes down to targetImpact - 0.2,
then goes up to targetImpact + 0.1,
then goes down to targetImpact - 0.1,
finally reaches targetImpact.
I think the fastest way for you to have this effect is to create 4 impacts in a row with your different targetImpact. Use the previous targetImpact as impact for next one.
Edit:
var easing = 0;
function makeEasing(breakpoints) {
breakpoints.forEach(t => {
let iPoint = Math.floor(t / breakpoints.length),
iDest = iPoint + 1;
easing = (breakpoints[iPoint] - breakpoints[iDest]) * (t);
})
}
makeEasing([0.5, 0.0, 0.5, 1.0, 0.5]);
Thanks #AppyGG for giving the idea this now works:
function makeEasing(breakpoints) {
const spaces = breakpoints.length - 1;
return t => {
const iPoint = Math.floor(t / (1 / spaces)),
iDest = iPoint + 1;
return breakpoints[iPoint] + (breakpoints[iDest] - breakpoints[iPoint]) * smoothstep(0, 1/spaces, (t % (1/spaces)));
};
}
function smoothstep (min, max, value) {
var x = Math.max(0, Math.min(1, (value-min)/(max-min)));
return x*x*(3 - 2*x);
};
function round(v, d = 100) {
return Math.round(v * d) / d;
}
function testEasing(easing) {
for (let i = 0; i< 1; i+= 0.01) {
console.log(round(i), round(easing(i)));
}
}
const easing2 = makeEasing([0.5, 0.0, 0.5, 1.0, 0.5]);
testEasing(easing2);

How to calculate the rotation angle of two constrained segments?

I have two vectors, the Y-aligned is fixed whereby the X-aligned is allowed to rotate. These vectors are connected together through two fixed-length segments. Given the angle between the two vectors (82.74) and the length of all segments, how can I get the angle of the two jointed segments (24.62 and 22.61)?
What is given: the magnitude of the vectors, and the angle between the X-axis and OG:
var magOG = 3,
magOE = 4,
magGH = 3,
magEH = 2,
angleGamma = 90;
This is my starting point: angleGamma = 90 - then, I will have following vectors:
var vOG = new vec2(-3,0),
vOE = new vec2(0,-4);
From here on, I am trying to get angleAlphaand angleBeta for values of angleGamma less than 90 degrees.
MAGNITUDE OF THE CONSTRAINED SEGMENTS:
Segments HG and HE must meet following conditions:
/
| OG*OG+ OE*OE = (HG + HE)*(HG + HE)
>
| OG - HG = OE - HE
\
which will lead to following two solutions (as pointed out in the accepted answer - bilateration):
Solution 1:
========================================================
HG = 0.5*(-Math.sqrt(OG*OG + OE*OE) + OG - OE)
HE = 0.5*(-Math.sqrt(OG*OG + OE*OE) - OG + OE)
Solution 2:
========================================================
HG = 0.5*(Math.sqrt(OG*OG + OE*OE) + OG - OE)
HE = 0.5*(Math.sqrt(OG*OG + OE*OE) - OG + OE)
SCRATCHPAD:
Here is a playground with the complete solution. The visualization library used here is the great JSXGraph. Thanks to the Center for Mobile Learning with Digital Technology of the Bayreuth University.
Credits for the circle intersection function: 01AutoMonkey in the accepted answer to this question: A JavaScript function that returns the x,y points of intersection between two circles?
function deg2rad(deg) {
return deg * Math.PI / 180;
}
function rad2deg(rad) {
return rad * 180 / Math.PI;
}
function lessThanEpsilon(x) {
return (Math.abs(x) < 0.00000000001);
}
function angleBetween(point1, point2) {
var x1 = point1.X(), y1 = point1.Y(), x2 = point2.X(), y2 = point2.Y();
var dy = y2 - y1, dx = x2 - x1;
var t = -Math.atan2(dx, dy); /* range (PI, -PI] */
return rad2deg(t); /* range (180, -180] */
}
function circleIntersection(circle1, circle2) {
var r1 = circle1.radius, cx1 = circle1.center.X(), cy1 = circle1.center.Y();
var r2 = circle2.radius, cx2 = circle2.center.X(), cy2 = circle2.center.Y();
var a, dx, dy, d, h, h2, rx, ry, x2, y2;
/* dx and dy are the vertical and horizontal distances between the circle centers. */
dx = cx2 - cx1;
dy = cy2 - cy1;
/* angle between circle centers */
var theta = Math.atan2(dy,dx);
/* vertical and horizontal components of the line connecting the circle centers */
var xs1 = r1*Math.cos(theta), ys1 = r1*Math.sin(theta), xs2 = r2*Math.cos(theta), ys2 = r2*Math.sin(theta);
/* intersection points of the line connecting the circle centers */
var sxA = cx1 + xs1, syA = cy1 + ys1, sxL = cx2 - xs2, syL = cy2 - ys2;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy*dy) + (dx*dx));
/* Check for solvability. */
if (d > (r1 + r2)) {
/* no solution. circles do not intersect. */
return [[sxA,syA], [sxL,syL]];
}
thetaA = -Math.PI - Math.atan2(cx1,cy1); /* Swap X-Y and re-orient to -Y */
xA = +r1*Math.sin(thetaA);
yA = -r1*Math.cos(thetaA);
ixA = cx1 - xA;
iyA = cy1 - yA;
thetaL = Math.atan(cx2/cy2);
xL = -r2*Math.sin(thetaL);
yL = -r2*Math.cos(thetaL);
ixL = cx2 - xL;
iyL = cy2 - yL;
if(d === 0 && r1 === r2) {
/* infinite solutions. circles are overlapping */
return [[ixA,iyA], [ixL,iyL]];
}
if (d < Math.abs(r1 - r2)) {
/* no solution. one circle is contained in the other */
return [[ixA,iyA], [ixL,iyL]];
}
/* 'point 2' is the point where the line through the circle intersection points crosses the line between the circle centers. */
/* Determine the distance from point 0 to point 2. */
a = ((r1*r1) - (r2*r2) + (d*d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = cx1 + (dx * a/d);
y2 = cy1 + (dy * a/d);
/* Determine the distance from point 2 to either of the intersection points. */
h2 = r1*r1 - a*a;
h = lessThanEpsilon(h2) ? 0 : Math.sqrt(h2);
/* Now determine the offsets of the intersection points from point 2. */
rx = -dy * (h/d);
ry = +dx * (h/d);
/* Determine the absolute intersection points. */
var xi = x2 + rx, yi = y2 + ry;
var xi_prime = x2 - rx, yi_prime = y2 - ry;
return [[xi, yi], [xi_prime, yi_prime]];
}
function plot() {
var cases = [
{a: 1.1, l: 1.9, f: 0.3073},
{a: 1.0, l: 1.7, f: 0.3229}
];
var testCase = 1;
var magA = cases[testCase].a, magL = cases[testCase].l;
var maxS = Math.sqrt(magA*magA+magL*magL), magS1 = maxS * cases[testCase].f, magS2 = maxS - magS1;
var origin = [0,0], board = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-5.0, 5.0, 5.0, -5.0], axis: true});
var drawAs = {dashed: {dash: 3, strokeWidth: 0.5, strokeColor: '#888888'} };
board.suspendUpdate();
var leftArm = board.create('slider', [[-4.5, 3], [-1.5, 3], [0, -64, -180]]);
var leftLeg = board.create('slider', [[-4.5, 2], [-1.5, 2], [0, -12, -30]]);
var rightArm = board.create('slider', [[0.5, 3], [3.5, 3], [0, 64, 180]]);
var rightLeg = board.create('slider', [[0.5, 2], [3.5, 2], [0, 12, 30]]);
var lh = board.create('point', [
function() { return +magA * Math.sin(deg2rad(leftArm.Value())); },
function() { return -magA * Math.cos(deg2rad(leftArm.Value())); }
], {size: 3, name: 'lh'});
var LA = board.create('line', [origin, lh], {straightFirst: false, straightLast: false, lastArrow: true});
var cLS1 = board.create('circle', [function() { return [lh.X(), lh.Y()]; }, function() { return magS1; }], drawAs.dashed);
var lf = board.create('point', [
function() { return +magL * Math.sin(deg2rad(leftLeg.Value())); },
function() { return -magL * Math.cos(deg2rad(leftLeg.Value())); }
], {size: 3, name: 'lf'});
var LL = board.create('line', [origin, lf], {straightFirst: false, straightLast: false, lastArrow: true});
var cLS2 = board.create('circle', [function() { return [lf.X(), lf.Y()]; }, function() { return magS2; }], drawAs.dashed);
var lx1 = board.create('point', [
function() { return circleIntersection(cLS1, cLS2)[0][0]; },
function() { return circleIntersection(cLS1, cLS2)[0][1]; }
], {size: 3, face:'x', name: 'lx1'});
var lx2 = board.create('point', [
function() { return circleIntersection(cLS1, cLS2)[1][0]; },
function() { return circleIntersection(cLS1, cLS2)[1][1]; }
], {size: 3, face:'x', name: 'lx2'});
/* Angle between lh, lx1 shall be between 0 and -180 */
var angleLAJ = board.create('text', [-3.7, 0.5, function(){ return angleBetween(lh, lx1).toFixed(2); }]);
/* Angle between lf, lx1 shall be between 0 and 180 */
var angleLLJ = board.create('text', [-2.7, 0.5, function(){ return angleBetween(lf, lx1).toFixed(2); }]);
var rh = board.create('point', [
function() { return +magA * Math.sin(deg2rad(rightArm.Value())); },
function() { return -magA * Math.cos(deg2rad(rightArm.Value())); }
], {size: 3, name: 'rh'});
var RA = board.create('line', [origin, rh], {straightFirst: false, straightLast: false, lastArrow: true});
var cRS1 = board.create('circle', [function() { return [rh.X(), rh.Y()]; }, function() { return magS1; }], drawAs.dashed);
var rf = board.create('point', [
function() { return +magL * Math.sin(deg2rad(rightLeg.Value())); },
function() { return -magL * Math.cos(deg2rad(rightLeg.Value())); }
], {size: 3, name: 'rf'});
var RL = board.create('line', [origin, rf], {straightFirst: false, straightLast: false, lastArrow: true});
var cRS2 = board.create('circle', [function() { return [rf.X(), rf.Y()]; }, function() { return magS2; }], drawAs.dashed);
var rx1 = board.create('point', [
function() { return circleIntersection(cRS1, cRS2)[1][0]; },
function() { return circleIntersection(cRS1, cRS2)[1][1]; }
], {size: 3, face:'x', name: 'rx1'});
var rx2 = board.create('point', [
function() { return circleIntersection(cRS1, cRS2)[0][0]; },
function() { return circleIntersection(cRS1, cRS2)[0][1]; }
], {size: 3, face:'x', name: 'rx2'});
var angleRAJ = board.create('text', [+1.3, 0.5, function(){ return angleBetween(rh, rx1).toFixed(2); }]);
var angleRLJ = board.create('text', [+2.3, 0.5, function(){ return angleBetween(rf, rx1).toFixed(2); }]);
board.unsuspendUpdate();
}
plot();
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.99.7/jsxgraph.css" />
<link rel="stylesheet" href="style.css">
<script type="text/javascript" charset="UTF-8" src="//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.99.7/jsxgraphcore.js"></script>
</head>
<body>
<div id="jxgbox" class="jxgbox" style="width:580px; height:580px;"></div>
</body>
</html>
According to your sketch, the coordinates of E and G are:
E = (0, -magOE)
G = magOG * ( -sin(gamma), -cos(gamma) )
Then, calculating the position of H is a trilateration problem. Actually, it is just bilateration because you are missing a third distance. Hence, you will get two possible positions for H.
First, let us define a new coordinate system, where E lies at the origin and G lies on the x-axis. The x-axis direction in our original coordinate system is then:
x = (G - E) / ||G - E||
The y-axis is:
y = ( x.y, -x.x )
The coordinates of E and G in this new coordinate system are:
E* = (0, 0)
G* = (0, ||G - E||)
Now, we can easily find the coordinates of H in this coordinate system, up to the ambiguity mentioned earlier. I will abbreviate ||G - E|| = d like in the notation used in the Wikipedia article:
H.x* = (magGH * magGH - magEH * magEH + d * d) / (2 * d)
H.y* = +- sqrt(magGH * magGH - H.x* * H.x*)
Hence, we have two solutions for H.y, one positive and one negative.
Finally, we just need to transform H back into our original coordinate system:
H = x * H.x* + y * H.y* - (0, magOE)
Given the coordinates of H, calculating the angles is pretty straightforward:
alpha = arccos((H.x - G.x) / ||H - G||)
beta = arccos((H.y - E.y) / ||H - E||)
Example
Taking the values from your example
magOG = 3
magOE = 4
magGH = 3
magEH = 2
angleGamma = 82.74°
we first get:
E = (0, -4)
G = 3 * ( -sin(82.74°), -cos(82.74°) )
= (-2.976, -0.379)
Our coordinate system:
x = (-0.635, 0.773)
y = ( 0.773, 0.635)
In this coordinate system:
E* = (0, 0)
G* = (0, 4.687)
Then, the coordinates of H in our auxiliary coordinate system are:
H* = (2.877, +- 0.851)
I will only focus on the positive value for H*.y because this is the point that you marked in your sketch.
Transform back to original coordinate system:
H = (-1.169, -1.237)
And finally calculate the angles:
alpha = 25.41°
beta = 22.94°
The slight differences to your values are probably caused by rounding errors (either in my calculations or in yours).

Instancing PointCloud(ParticleCloud) in THREE.js

i am just a beginner in Threejs so please excuse if its a noobie question. but i haven't worked with particles.
How do i put points(particles) inside a custom geometry of a text geometry?
What i want to achieve is instance points inside a geometry or text then explode it to the world position. if someone direct me to the path, would be much helpful.
i know there's an example https://threejs.org/examples/#webgl_points_dynamic
but i cant understand whats happening in the render loop.
This is not the ultimate solution, but just a starting point.
You can set points from any type of geometry (usual geometry or buffer geometry).
Let's imagine that you have a THREE.TextGeometry(), then you can set points from it as:
textGeo = new THREE.TextGeometry("ABC", {
font: font,
size: 2,
height: 0.25,
curveSegments: 1,
bevelEnabled: false
});
textGeo.computeBoundingBox();
textGeo.computeVertexNormals();
textGeo.center();
fillWithPoints(textGeo, 1000); // fill our text geometry with 1000 random points
textGeo.vertices.forEach(function(vertex){
vertex.startPoint = vertex.clone(); // remember the starting position of a vertex
vertex.direction = vertex.clone().normalize(); // set direction
})
textPoints = new THREE.Points(textGeo, new THREE.PointsMaterial({color: 0x00ff00, size: 0.1})); // all you need is to have a geometry and THREE.PointsMaterial()
scene.add(textPoints);
To determine if a random point is inside our geometry, we can do the trick with projection of all the faces of our text geometry into 2D (x,y) and check if the point (with its x,y coordinates) is inside of one of projected triangles (faces):
function isPointInside(point, geometry) {
var retVal = false;
for (var i = 0; i < geometry.faces.length; i++) { //loop through faces
face = geometry.faces[i];
a = geometry.vertices[face.a];
b = geometry.vertices[face.b];
c = geometry.vertices[face.c];
if (ptInTriangle(point, a, b, c)) {
var retVal = true;
break; // exit the loop if the point is in a projected triangle
}
}
return retVal;
}
where
function ptInTriangle(p, p0, p1, p2) {
// credits: http://jsfiddle.net/PerroAZUL/zdaY8/1/
var A = 1/2 * (-p1.y * p2.x + p0.y * (-p1.x + p2.x) + p0.x * (p1.y - p2.y) + p1.x * p2.y);
var sign = A < 0 ? -1 : 1;
var s = (p0.y * p2.x - p0.x * p2.y + (p2.y - p0.y) * p.x + (p0.x - p2.x) * p.y) * sign;
var t = (p0.x * p1.y - p0.y * p1.x + (p0.y - p1.y) * p.x + (p1.x - p0.x) * p.y) * sign;
return s > 0 && t > 0 && (s + t) < 2 * A * sign;
}
and then in the animation loop we'll use the stuff of a vertex (startPoint and direction):
textGeo.vertices.forEach(function(vertex){
vertex.copy(vertex.startPoint).addScaledVector(vertex.direction, 5 + Math.sin(Date.now() * 0.001) * 5);
});
textGeo.verticesNeedUpdate = true; // this is the most important thing, you have to set it to true after each rendering
jsfiddle example

Interpolate values in irregular 2d data in JavaScript

I need to find a library that allows me to get interpolated values from irregular 2d data. Imagine having something like this:
var data = [{{x: 0, y: 0, value: 0},
{x: 0.5, y: 1, value: 1},
{x: 1, y: 0, value: 2}}, .... Many more elements]
var value = interpolate(data, 0.24, 0.3); // 0.24 = x, 0.3 = y
What the interpolate method does is that it finds the element, in this case a triangle, that the coordinate is inside. Then it interpolates the value between the corners of the element it is contained in.
I do realize that there are lots of aspects in it to optimize performance like building up a tree that allows fast narrowing of elements by having preprocessed bounding boxes. All of this would be great as well, but I am just trying to get started.
There must be some library out there that I can use for it instead of writing my own.
Since search results for barycentric interpolation in javascript were inconclusive, here's some code that might help you get started.
This code takes as input a data set of 2D points, each with a "value", and a "new point" with an unknown value. It first finds the smallest triangle in the data set that contains the "new point", then performs barycentric interpolation using that triangle to find a value for the "new point".
This runs reasonably quickly with a data set of a few hundred points. There are many opportunities for testing, error checking, and optimization - for example, don't look at every possible triangle in the data set. N choose 3 grows with the cube of N, so optimizing to look at triangles made with only points "close to" the "new point" could show significant performance gains.
// Calculate the area of a triangle
function triangle_area(vertexA, vertexB, vertexC) {
return Math.abs(((vertexA.x - vertexC.x) * (vertexB.y - vertexA.y) - (
vertexA.x - vertexB.x) * (vertexC.y - vertexA.y)) * 0.5)
}
// Given a number N, return a list of all possible triples from the list [1..N]
// credit: http://stackoverflow.com/a/5752056/1612562
function list_triples(N) {
var fn = function(n, src, got, all) {
if (n == 0) {
if (got.length > 0) {
all[all.length] = got;
}
return;
}
for (var j = 0; j < src.length; j++) {
fn(n - 1, src.slice(j + 1), got.concat([ src[j] ]), all);
}
return;
}
var triples = [];
// Generates the list [0, ..., N]
// credit: http://stackoverflow.com/a/20066663/1612562
var indices =
Array.apply(null, {length: N}).map(Number.call, Number);
fn(3, indices, [], triples);
return triples;
}
// Given three vertices of a triangle and a point, determine if
// the point falls in the triangle
// credit: https://koozdra.wordpress.com/2012/06/27/javascript-is-point-in-triangle/
// credit: http://www.blackpawn.com/texts/pointinpoly/default.html
function is_in_triangle(newPoint, vertexA, vertexB, vertexC) {
var v0 = [vertexC.x - vertexA.x, vertexC.y - vertexA.y];
var v1 = [vertexB.x - vertexA.x, vertexB.y - vertexA.y];
var v2 = [newPoint.x - vertexA.x, newPoint.y - vertexA.y];
var dot00 = (v0[0] * v0[0]) + (v0[1] * v0[1]);
var dot01 = (v0[0] * v1[0]) + (v0[1] * v1[1]);
var dot02 = (v0[0] * v2[0]) + (v0[1] * v2[1]);
var dot11 = (v1[0] * v1[0]) + (v1[1] * v1[1]);
var dot12 = (v1[0] * v2[0]) + (v1[1] * v2[1]);
var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return ((u >= 0) && (v >= 0) && (u + v < 1));
}
// Perform barycentric interpolation on a point in a triangle
function barycentric_interpolate(newPoint, vertexA, vertexB, vertexC) {
var area = triangle_area(vertexA, vertexB, vertexC);
var sub_area_1 = triangle_area(newPoint, vertexB, vertexC);
var sub_area_2 = triangle_area(vertexA, newPoint, vertexC);
var sub_area_3 = triangle_area(vertexA, vertexB, newPoint);
return ((sub_area_1 * vertexA.v) + (sub_area_2 * vertexB.v) + (sub_area_3 *
vertexC.v)) / area;
}
// Find the smallest triangle in the data set containing the new
// point, and perform barycentric interpolation using that triangle
function interpolate(newPoint, data) {
var triangles = list_triples(data.length);
var smallest_triangle_area = Number.MAX_VALUE;
var smallest_triangle;
for (t in triangles) {
var vertexA = data[triangles[t][0]];
var vertexB = data[triangles[t][1]];
var vertexC = data[triangles[t][2]];
var in_triangle = is_in_triangle(newPoint, vertexA, vertexB, vertexC);
if (in_triangle) {
if (triangle_area(vertexA, vertexB, vertexC) < smallest_triangle_area) {
smallest_triangle = [vertexA, vertexB, vertexC];
}
}
}
return smallest_triangle
? barycentric_interpolate(newPoint, smallest_triangle[0], smallest_triangle[1], smallest_triangle[2])
: "Interpolation failed: newPoint isn't in a triangle";
}
var newPoint = {'x': 0.24, 'y': 0.3};
var data = [
{'x': 0, 'y': 0, 'v': 0},
{'x': 0.5, 'y': 1, 'v': 1},
{'x': 1, 'y': 0, 'v': 2},
{'x': 1.5, 'y': 2.5, 'v': 1.5},
{'x': 2, 'y': 1, 'v': 0.5}
];
console.log(interpolate(newPoint, data));
There are also other kinds of spatial interpolation, e.g. kriging, which does have at least one ready-made .js library.

Categories

Resources