Related
I have point c, which represents the center of a circle. After I drag the circle, I want its y coordinate hmm to snap to a line that's drawn between point a and b. How do I solve for hmm?
So far I've been able to find the midway y value between points a and b. However, it's not taking into account point c's x value.:
const snapYToLine = (aY, bY) => {
const yDist = bY - aY;
return aY + (yDist * 0.5);
}
const a = { x: 10, y: 10 };
const b = { x: 50, y: 30 };
const c = { x: 20, y: 0 }; // not doing anything with this yet...
const hmm = snapYToLine(a.y, b.y); // will need to include c.x here...
console.log(hmm);
X coordinate of C depends on where you drag it, but Y coordinate should be so point stays on line between A and B?
The equation of line between 2 points (A and B):
(x - Xa) / (Xa - Xb) = (y - Ya) / (Ya - Yb).
So hmm = (x - Xa) * (Ya - Yb) / (Xa - Xb) + Ya.
Because it's equation of line you can drag point C over A/B X coordinates and still find corresponsive Y value.
Hope I've understood your question right.
I have an array of objects. Each object represents a point has an ID and an array with x y coordinates. , e.g.:
let points = [{id: 1, coords: [1,2]}, {id: 2, coords: [2,3]}]
I also have an array of arrays containing x y coordinates. This array represents a polygon, e.g.:
let polygon = [[0,0], [0,3], [1,4], [0,2]]
The polygon is closed, so the last point of the array is linked to the first.
I use the following algorithm to check if a point is inside a polygon:
pointInPolygon = function (point, polygon) {
// from https://github.com/substack/point-in-polygon
let x = point.coords[0]
let y = point.coords[1]
let inside = false
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
let xi = polygon[i][0]
let yi = polygon[i][1]
let xj = polygon[j][0]
let yj = polygon[j][1]
let intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi)
if (intersect) inside = !inside
}
return inside
}
The user draws the polygon with the mouse, which works like this:
http://bl.ocks.org/bycoffe/5575904
Every time the mouse moves (gets new coordinates), we have to add the current mouse location to the polygon, and then we have to loop through all the points and call the pointInPolygon function on the point on every iteration. I have throttled the event already to improve performance:
handleCurrentMouseLocation = throttle(function (mouseLocation, points, polygon) {
let pointIDsInPolygon = []
polygon.push(mouseLocation)
for (let point in points) {
if (pointInPolygon(point, polygon) {
pointIDsInPolygon.push(point.id)
}
}
return pointIDsInPolygon
}, 100)
This works fine when the number of points is not that high (<200), but in my current project, we have over 4000 points. Iterating through all these points and calling the pointInPolygon function for each point every 100 ms makes the whole thing very laggy.
I am looking for a quicker way to accomplish this. For example: maybe, instead of triggering this function every 100 ms when the mouse is drawing the polygon, we could look up some of the closest points to the mouse location and store this in a closestPoints array. Then, when the mouse x/y gets higher/lower than a certain value, it would only loop through the points in closestPoints and the points already in the polygon. But I don't know what these closestPoints would be, or if this whole approach even makes sense. But I do feel like the solution is in decreasing the number of points we have to loop through every time.
To be clear, the over 4000 points in my project are fixed- they are not generated dynamically, but always have exactly the same coordinates. In fact, the points represent centroids of polygons, which represent boundaries of municipalities on a map. So it is, for example, possible to calculate the closestPoints for every point in advance (in this case we would calculate this for the points, not the mouse location like in the previous paragraph).
Any computational geometry expert who could help me with this?
If I understand you correctly, a new point logged from the mouse will make the polygon one point larger. So if at a certain moment the polygon is defined by n points (0,1,...,n-1) and a new point p is logged, then the polygon becomes (0,1,...,n-1,p).
So this means that one edge is removed from the polygon and two are added to it instead.
For example, let's say we have 9 points on the polygon, numbered 0 to 8, where point 8 was the last point that was added to it:
The grey line is the edge that closes the polygon.
Now the mouse moves to point 9, which is added to the polygon:
The grey edge is removed from the polygon, and the two green ones are added to it. Now observe the following rule:
Points that are in the triangle formed by the grey and two green edges swap in/out of the polygon when compared to where they were before the change. All other points retain their previous in/out state.
So, if you would retain the status of each point in memory, then you only need to check for each point whether it is within the above mentioned triangle, and if so, you need to toggle the status of that point.
As the test for inclusion in a triangle will take less time than to test the same for a potentially complex polygon, this will lead to a more efficient algorithm.
You can further improve the efficiency, if you take the bounding rectangle of the triangle with corners at (x0, y0),(x1, y0),(x1, y1),(x0, y1). Then you can already skip over points that have an x or y coordinate that is out of range:
Any point outside of the blue box will not change state: if it was inside the polygon before the last point 9 was added, it still is now. Only for points within the box you'll need to do the pointInPolygon test, but on the triangle only, not the whole polygon. If that test returns true, then the state of the tested point must be toggled.
Group points in square boxes
To further speed up the process you could divide the plane with a grid into square boxes, where each point belongs to one box, but a box will typically have many points. For determining which points are in the triangle, you could first identify which boxes overlap with the triangle.
For that you don't have to test each box, but can derive the boxes from the coordinates that are on the triangle's edges.
Then only the points in the remaining boxes would need to be tested individually. You could play with the box size and see how it impacts performance.
Here is a working example, implementing those ideas. There are 10000 points, but I have no lagging on my PC:
canvas.width = document.body.clientWidth;
const min = [0, 0],
max = [canvas.width, canvas.height],
points = Array.from(Array(10000), i => {
let x = Math.floor(Math.random() * (max[0]-min[0]) + min[0]);
let y = Math.floor(Math.random() * (max[1]-min[1]) + min[1]);
return [x, y];
}),
polygon = [],
boxSize = Math.ceil((max[0] - min[0]) / 50),
boxes = (function (xBoxes, yBoxes) {
return Array.from(Array(yBoxes), _ =>
Array.from(Array(xBoxes), _ => []));
})(toBox(0, max[0])+1, toBox(1, max[1])+1),
insidePoints = new Set,
ctx = canvas.getContext('2d');
function drawPoint(p) {
ctx.fillRect(p[0], p[1], 1, 1);
}
function drawPolygon(pol) {
ctx.beginPath();
ctx.moveTo(pol[0][0], pol[0][1]);
for (const p of pol) {
ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
}
function segmentMap(a, b, dim, coord) {
// Find the coordinate where ab is intersected by a coaxial line at
// the given coord.
// First some boundary conditions:
const dim2 = 1 - dim;
if (a[dim] === coord) {
if (b[dim] === coord) return [a[dim2], b[dim2]];
return [a[dim2]];
}
if (b[dim] === coord) return [b[dim2]];
// See if there is no intersection:
if ((coord > a[dim]) === (coord > b[dim])) return [];
// There is an intersection point:
const res = (coord - a[dim]) * (b[dim2] - a[dim2]) / (b[dim] - a[dim]) + a[dim2];
return [res];
}
function isLeft(a, b, c) {
// Return true if c lies at the left of ab:
return (b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) > 0;
}
function inTriangle(a, b, c, p) {
// First do a bounding box check:
if (p[0] < Math.min(a[0], b[0], c[0]) ||
p[0] > Math.max(a[0], b[0], c[0]) ||
p[1] < Math.min(a[1], b[1], c[1]) ||
p[1] > Math.max(a[1], b[1], c[1])) return false;
// Then check that the point is on the same side of each of the
// three edges:
const x = isLeft(a, b, p),
y = isLeft(b, c, p),
z = isLeft(c, a, p);
return x ? y && z : !y && !z;
}
function toBox(dim, coord) {
return Math.floor((coord - min[dim]) / boxSize);
}
function toWorld(dim, box) {
return box * boxSize + min[dim];
}
function drawBox(boxX, boxY) {
let x = toWorld(0, boxX);
let y = toWorld(1, boxY);
drawPolygon([[x, y], [x + boxSize, y], [x + boxSize, y + boxSize], [x, y + boxSize], [x, y]]);
}
function triangleTest(a, b, c, points, insidePoints) {
const markedBoxes = new Set(), // collection of boxes that overlap with triangle
box = [];
for (let dim = 0; dim < 2; dim++) {
const dim2 = 1-dim,
// Order triangle points by coordinate
[d, e, f] = [a, b, c].sort( (p, q) => p[dim] - q[dim] ),
lastBox = toBox(dim, f[dim]);
for (box[dim] = toBox(dim, d[dim]); box[dim] <= lastBox; box[dim]++) {
// Calculate intersections of the triangle edges with the row/column of boxes
const coord = toWorld(dim, box[dim]),
intersections =
[...new Set([...segmentMap(a, b, dim, coord),
...segmentMap(b, c, dim, coord),
...segmentMap(a, c, dim, coord)])];
if (!intersections.length) continue;
intersections.sort( (a,b) => a - b );
const lastBox2 = toBox(dim2, intersections.slice(-1)[0]);
// Mark all boxes between the two intersection points
for (box[dim2] = toBox(dim2, intersections[0]); box[dim2] <= lastBox2; box[dim2]++) {
markedBoxes.add(boxes[box[1]][box[0]]);
if (box[dim]) {
markedBoxes.add(boxes[box[1]-dim][box[0]-(dim2)]);
}
}
}
}
// Perform the triangle test for each individual point in the marked boxes
for (const box of markedBoxes) {
for (const p of box) {
if (inTriangle(a, b, c, p)) {
// Toggle in/out state of this point
if (insidePoints.delete(p)) {
ctx.fillStyle = '#000000';
} else {
ctx.fillStyle = '#e0e0e0';
insidePoints.add(p);
}
drawPoint(p);
}
}
}
}
// Draw points
points.forEach(drawPoint);
// Distribute points into boxes
for (const p of points) {
let hor = Math.floor((p[0] - min[0]) / boxSize);
let ver = Math.floor((p[1] - min[1]) / boxSize);
boxes[ver][hor].push(p);
}
canvas.addEventListener('mousemove', (e) => {
if (e.buttons !== 1) return;
polygon.push([Math.max(e.offsetX,0), Math.max(e.offsetY,0)]);
ctx.strokeStyle = '#000000';
drawPolygon(polygon);
const len = polygon.length;
if (len > 2) {
triangleTest(polygon[0], polygon[len-2+len%2], polygon[len-1-len%2], points, insidePoints);
}
});
canvas.addEventListener('mousedown', (e) => {
// Start a new polygon
polygon.length = 0;
});
Drag mouse to draw a shape:
<canvas id="canvas"></canvas>
Keep a background image where you perform polygon filling every time you update the polygon.
Then testing any point for interiorness will take constant time independently of the polygon complexity.
I have a Bezier curve: (0,0), (.25,.1), (.25,1), and (1,1).
This is graphically seen here: http://cubic-bezier.com/#.25,.1,.25,1
We see on the x axis is time.
This is my unknown. This is a unit cell. So I was wondering how can I get x when y is 0.5?
Thanks
I saw this topic: y coordinate for a given x cubic bezier
But it loops, I need to avoid something loops
So I found this topic: Cubic bezier curves - get Y for given X
But I can't figure out how to solve a cubic polynomial in js :(
This is mathematically impossible unless you can guarantee that there will only be one y value per x value, which even on a unit rectangle you can't (for instance, {0,0},{1,0.6},{0,0.4},{1,1} will be rather interesting at the mid point!). The fastest is to simply build a LUT, like for instance:
var LUT_x = [], LUT_y = [], t, a, b, c, d;
for(let i=0; i<100; i++) {
t = i/100;
a = (1-t)*(1-t)*(1-t);
b = (1-t)*(1-t)*t;
c = (1-t)*t*t;
d = t*t*t;
LUT_x.push( a*x1 + 3*b*x2 + 3*c*x3 + d*x4 );
LUT_y.push( a*y1 + 3*b*y2 + 3*c*y3 + d*y4 );
}
Done, now if you want to look up an x value for some y value, just run through LUT_y until you find your y value, or more realistically until you find two values at index i and i+1 such that your y value lies somewhere in between them, and you will immediately know the corresponding x value because it'll be at the same index in LUT_x.
For nonexact matches with 2 indices i and i+1 you simply do a linear interpolation (i.e. y is at distance ... between i and i+1, and this at the same distance between i and i+1 for the x coordinates)
All the solutions that use a look up table can only give you an approximate result. If that is good enough for you, you are set. If you want a more accurate result, then you need to use some sort of numeric method.
For a general Bezier curve of degree N, you do need to loop. Meaning, you need to use bi-section method or Newton Raphson method or something similar to find the x value corresponding to a given y value and such methods (almost) always involve iterations starting with an initial guess. If there are mutiple solutions, then what x value you get will depend on your initial guess.
However, if you only care about cubic Bezier curves, then analytic solution is possible as roots of cubic polynomials can be found using the Cardano formula. In this link (y coordinate for a given x cubic bezier), which was referenced in the OP, there is an answer by Dave Bakker that shows how to solve cubic polynomial using Cardano formula. Source codes in Javascript is provided. I think this will be your good source to start your investigation on.
Thanks again to Mike's help we found the fastest way to do this. I put this function togather, takes 0.28msg on average:
function getValOnCubicBezier_givenXorY(options) {
/*
options = {
cubicBezier: {xs:[x1, x2, x3, x4], ys:[y1, y2, y3, y4]};
x: NUMBER //this is the known x, if provide this must not provide y, a number for x will be returned
y: NUMBER //this is the known y, if provide this must not provide x, a number for y will be returned
}
*/
if ('x' in options && 'y' in options) {
throw new Error('cannot provide known x and known y');
}
if (!('x' in options) && !('y' in options)) {
throw new Error('must provide EITHER a known x OR a known y');
}
var x1 = options.cubicBezier.xs[0];
var x2 = options.cubicBezier.xs[1];
var x3 = options.cubicBezier.xs[2];
var x4 = options.cubicBezier.xs[3];
var y1 = options.cubicBezier.ys[0];
var y2 = options.cubicBezier.ys[1];
var y3 = options.cubicBezier.ys[2];
var y4 = options.cubicBezier.ys[3];
var LUT = {
x: [],
y: []
}
for(var i=0; i<100; i++) {
var t = i/100;
LUT.x.push( (1-t)*(1-t)*(1-t)*x1 + 3*(1-t)*(1-t)*t*x2 + 3*(1-t)*t*t*x3 + t*t*t*x4 );
LUT.y.push( (1-t)*(1-t)*(1-t)*y1 + 3*(1-t)*(1-t)*t*y2 + 3*(1-t)*t*t*y3 + t*t*t*y4 );
}
if ('x' in options) {
var knw = 'x'; //known
var unk = 'y'; //unknown
} else {
var knw = 'y'; //known
var unk = 'x'; //unknown
}
for (var i=1; i<100; i++) {
if (options[knw] >= LUT[knw][i] && options[knw] <= LUT[knw][i+1]) {
var linearInterpolationValue = options[knw] - LUT[knw][i];
return LUT[unk][i] + linearInterpolationValue;
}
}
}
var ease = { //cubic-bezier(0.25, 0.1, 0.25, 1.0)
xs: [0, .25, .25, 1],
ys: [0, .1, 1, 1]
};
var linear = {
xs: [0, 0, 1, 1],
ys: [0, 0, 1, 1]
};
//console.time('calc');
var x = getValOnCubicBezier_givenXorY({y:.5, cubicBezier:linear});
//console.timeEnd('calc');
//console.log('x:', x);
The question title might be a little confusing, imagine this:
In this diagram, I know the postions of points a and b (their x and y values) - the origin though is in the top left corner, so yb for instance is somewhat at 137 and ya 81. Also, I know the degree value of the angle <) a which defines how much the lighter rectangle is rotated (I also know how much the other one is rotated, if that is of use).
So, I want to check if point a is left or right of an imaginary line (axis, the pink line) of point b. How would you do that? I know it's simply maths probably but I'm lost.
var isleft = function(xb,yb, xa, ya, degree) {
if the point at xa, ya is left from the line starting at point xb, yb using d degrees return true;
};
I guess I would have to use a simple linear function to do this, but I dont know how to deal with the degrees, calculate the respective coordinates on that line for a given y value, so help is appreciated.
You can work out the bearing from point b to point a and if it's less than the angle a (the bearing along the side of the rectangle) then the point is "left", otherwise it's "right". The following function returns the bearing from b to a, but using coordinate order (x, y). The coordinate direction is per the screen, i.e. +x to the right and +y downward (a normal cartesian system would be +y up):
// Long version
function calcAngle(a, b) {
var PI = Math.PI;
var points
var dx = b[0] - a[0];
var dy = a[1] - b[1];
var beta = Math.atan(dx / dy);
// 1st quadrant
if (dx >= 0 && dy >=0) return beta;
// 2nd quadrant
if (dx >= 0 && dy < 0) return PI + beta;
// 3rd quadrant
if (dx < 0 && dy < 0) return PI + beta;
// 4th quadrant
if (dx < 0 && dy >= 0) return 2*PI + beta
}
In the above, a and b are coordinate pairs in (x, y) order, where a is point b in the diagram and b is point a in the diagram. Some tests:
// Convert radians to degrees
function rad2deg(rad) {return rad * 180 / Math.PI}
var pair, points = [
// 1st quadrant
[[10, 30], [12, 15]], // 7.6 deg
[[10, 30], [12, 30]], // 90.0 deg
// 2nd quadrant
[[10, 30], [20, 31]], // 95.7 deg
[[10, 30], [10, 40]], // 180.0 deg
// 3rd quadrant
[[10, 30], [ 1, 31]], // 263.7 deg
[[10, 30], [ 5, 30]], // 270.0 deg
// 4th quadrant
[[10, 30], [ 5, 5]], // 348.7 deg
[[10, 30], [10, 5]] // 0.0 deg
];
for (var i=0, iLen=points.length; i<iLen; i++) {
pair = points[i];
console.log(pair[0] + ',' + pair[1] + ' : ' + rad2deg(calcAngle(pair[0], pair[1])));
}
The function can be more concise:
function calcAngle(a, b) {
var PI = Math.PI;
var dx = b[0] - a[0];
var dy = a[1] - b[1];
var beta = Math.atan(dx / dy);
if (dy < 0) return PI + beta;
if (dx < 0 && dy >= 0) return 2*PI + beta
return beta;
}
To work out left or right, use the calcAngle function to work out the second bearing and compare it to the first. If it's less, it's to the left. If it's greater, it's to the right. That sense is always clockwise, which may not suit.
e.g. if a is 5° and the calculated angle is 355°, is that 350° to the right or 10° to the left? You may decide that if the difference it more than 180° to reverse the side (e.g. 181° to the right is seen as 179° to the left).
i want to make a little photoshop javascript. Technically, i just need to know how to compare the color values of pixels af if they were an array with three integer values in each, for example: (pseudocode)
for all pixels x
for all pixels y
if left pixel's green channel is bigger than red channel:
set the blue channel to 25
else
if the blue channel is greater than 50
set the green channel to 0
in the documentation, there's a ton of things like filters, text and layers you can do, but how do you do something as simple as this?
Reading and writing pixel values in Photoshop scripts is indeed not as simple as it could be ... Check out the following script which inverts the blue channel of an image:
var doc = app.open(new File("~/Desktop/test1.bmp"));
var sampler = doc.colorSamplers.add([0, 0]);
for (var x = 0; x < doc.width; ++x) {
for (var y = 0; y < doc.height; ++y) {
sampler.move([x, y]);
var color = sampler.color;
var region = [
[x, y],
[x + 1, y],
[x + 1, y + 1],
[x, y + 1],
[x, y]
];
var newColor = new SolidColor();
newColor.rgb.red = color.rgb.red;
newColor.rgb.green = 255 - color.rgb.green;
newColor.rgb.blue = color.rgb.blue;
doc.selection.select(region);
doc.selection.fill(newColor);
}
}
I'm not sure if there's a prettier way of setting a pixel color than the select + fill trick.
This script runs super slow, so maybe Photoshop scripts are not the best tool for pixel manipulation ...