How to make a 2D array resembling an ellipse - javascript

I need a function that creates a 2D array resembling an ellipse where each cell is a pixel that can either be on (1) or off (0). For example, if you ran circlearray(5,8), it would return something like:
[[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0]];
I have tried this before, but can't seem to get the rounding (decimal to integer, not making it elliptical) right. The formula that I'm using is: f(x) = h/w * sqrt(w^2 - x^2), where h is the length of the radius at the top of the ellipse and w is the length of the radius at the side of the ellipse. It gives you how many columns you should have for the circle given row, but with circles, I notice that it is different turned on its side that upright, which I know shouldn't be. I can't seem to get the circle right with Math.round(f(x)), Math.floor(f(x)), nor Math.ceil(f(x)). I also don't want to use jQuery.
Here is a the jsFiddle shows my findings: http://jsfiddle.net/r7cH5/
And here is an example of how to use the formula: http://www.desmos.com/calculator/v5qbcd1jkm

I assume you mean an ellipse with major and minor axes aligned horizontally and vertically. (Plotting an ellipse in general orientation is a much more difficult problem.)
You want a generalization of the Bresenham circle algorithm for ellipses. A nice description of the theory and an implementation is described in this paper. Simple code can be found here for using Bresenham's algorithm for lines, circles, ellipses and Bézier curves. From the latter site, here's the code for plotting an ellipse inside a specified rectangle:
void plotEllipseRect(int x0, int y0, int x1, int y1)
{
int a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; /* values of diameter */
long dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */
long err = dx+dy+b1*a*a, e2; /* error of 1.step */
if (x0 > x1) { x0 = x1; x1 += a; } /* if called with swapped points */
if (y0 > y1) y0 = y1; /* .. exchange them */
y0 += (b+1)/2; y1 = y0-b1; /* starting pixel */
a *= 8*a; b1 = 8*b*b;
do {
setPixel(x1, y0); /* I. Quadrant */
setPixel(x0, y0); /* II. Quadrant */
setPixel(x0, y1); /* III. Quadrant */
setPixel(x1, y1); /* IV. Quadrant */
e2 = 2*err;
if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */
if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */
} while (x0 <= x1);
while (y0-y1 < b) { /* too early stop of flat ellipses a=1 */
setPixel(x0-1, y0); /* -> finish tip of ellipse */
setPixel(x1+1, y0++);
setPixel(x0-1, y1);
setPixel(x1+1, y1--);
}
}
This isn't JavaScript, of course, but it should be straightforward to convert. (Since no integer division is involved, you don't need to worry about the fact that JavaScript doesn't have integer division.) Instead of setPixel, of course, you would simply set the array element to 1.
Note that this plots a one-pixel thick ellipse boundary. If you want a filled ellipse, just apply a scan line fill: iterate row by row and fill between the two columns that are set to 1 by the above (or leave the row alone if you don't find two distinct columns set). Alternatively, you could follow up the above with a flood fill algorithm starting at the center of the rectangle.

var width = 100,
height = 160,
span = document.createElement('span'),
output = document.getElementById("output");
function ellipse(x) {
return Math.floor((height / width) * Math.sqrt((width * width) - (x * x))); // Times itself doesn't give NaN as often as squared.
}
for (var i = -1 * width; i <= width; i++) {
var s = span.cloneNode(false);
s.style.height = ellipse(i) + 'px';
output.appendChild(s)
}
Demo

Related

Function that generates a path along other path, from a starting to an end point

const coords = [
{
name: "Rijnstraat vervolg",
points: [
[695, 500],
[680, 480],
[580, 475],
[520, 460],
],
width: 10,
types: [types.car, types.truck, types.pedestrian, types.bike],
oneway: true,
},
...
]
I have an array that looks like the above and I want to make a function that generates a path (along the other paths, which are the black lines in the image) from a black or gray circle to another black or gray circle. So I want the function to take in a start and end point (black or gray circle) and return an array of points that follow the already existings paths. (Which are sort of like roads)
And the function can be described as someone who is trying to get to somewhere.
I already tried a recursive function that looks like this:
function calculatePathToShop(startPoint, shopPoint) {
const targetShopPoint = findClosestPointOnPath(shopPoint);
const targetPathIndex = findPathByPoint(targetShopPoint);
const connectedPaths = calculateConnectedPaths(targetPathIndex);
let startPathIndex = -1;
connectedPaths.forEach(path => {
const pathPoints = coords[path].points;
pathPoints.forEach(pathPoint => {
if (comparePoints(startPoint.point, pathPoint)) startPathIndex = path;
});
});
if (startPathIndex == -1) return false;
let startPathPoints = coords[startPathIndex].points;
let targetPathPoints = coords[targetPathIndex].points;
if (!comparePoints(startPoint.point, startPathPoints[0])) startPathPoints.reverse();
ctx.strokeStyle = "rgba(255, 0, 0, .05)";
}
This one generated a path (along the existing ones) to a shop point, which is almost the same as a gray point. But this worked for some starting points, but the rest would just straight up fail
So does anyone know an algorithm, or has a function/solution that I can use to generate the path that someone can walk along the road (the black lines in the image)
Full coords array, and part of my already existing code is found here: https://raw.githubusercontent.com/CodeFoxDev/people-simulation/main/func/paths.js
(The rest of the code is in the github repo itself)
Fixed step interpolation
To interpolate a line segment you divide the vector from the start pointing to the end by the number of steps.
EG
steps = 100;
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: (end.x - start.x) / steps, y: (end.y - start.y) / steps};
Then loop that number of steps adding the vector to a position initialized to the start point.
points = []; // array of interpolated points
point = {...start} // set start position.
while (steps--) {
points.push({...point});
point.x += vec.x;
point.y += vec.y;
}
points.push({...end}); // last point at end
This will create different spacing for different line lengths.
Fixed distance interpolation
To get a constant spacing between points you will need to use the lines' length to get the number of steps.
pixelsPerStep = 2; // distance between points.
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: end.x - start.x, y: end.y - start.y};
lineSteps = Math.hypot(step.x, step.y) / pixelsPerStep;
points = []; // array of interpolated points
for (i = 0; i < lineSteps ; i += 1) {
u = i / lineSteps;
points.push({x: start.x + step.x * u, y: start.y + step.y * u});
}
// check to add end point
Note that the last point may or may not be at the correct distance. Due to rounding errors in floating point numbers you will need to check if the last point is close to the correct spacing and whether or not to include it.
eg from code above
// add last point if within (0.01 * pixelsPerStep) pixels of correct spacing
if (Math.abs(lineSteps - i) < 0.01) {
points.push({...end});
}
Note Use the overflow lineSteps - i when interpolating many line segments, to carry the correct start offset to each subsequent line segment.
Example
The code below is an example of a constant spaced set of points interpolated from another set of points.
The example draws the new points in black dots. The original points are rendered in red.
Note that the distance between new points is constant and thus may not fall on the original (red) points.
Note that there is a check at the end to test if a last point should be added.
const ctx = canvas.getContext("2d");
const P2 = (x, y) => ({x, y});
const points = [
P2(100,90),
P2(300,210),
P2(350,110),
P2(50,10),
P2(6,219),
];
const interpolatedPoints = interpolatePath(points, 35);
drawPoints(interpolatedPoints, 2);
ctx.fillStyle = "RED";
drawPoints(points);
function drawPoints(points, size = 1) {
ctx.beginPath();
for (const p of points) {
ctx.rect(p.x - size, p.y - size, size * 2 + 1, size * 2 + 1);
}
ctx.fill();
}
function interpolatePath(path, pixelStep) {
const res = [];
var p2, i = 1, overflow = 0;
while (i < path.length) {
const p1 = path[i - 1];
p2 = path[i];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const len = Math.hypot(dx, dy) / pixelStep;
let j = overflow;
while (j < len) {
const u = j / len;
res.push(P2(p1.x + dx * u, p1.y + dy * u));
j++;
}
overflow = j - len;
i++;
}
// add last point if close to correct distance
if (Math.abs(overflow) < 0.01) {
res.push(P2(p2.x, p2.y));
}
return res;
}
<canvas id="canvas" width="400" height="400"></canvas>

Fill an Unknown Asymmetric Polygon with Pixel Manipulation (WebImage) in JS

Recently, I have been trying to create code to fill a polygon of any shape with color. I have gotten as far as being able to fill a shape that has lines of only one border size correctly, though I have found myself unable to do anything more than that. The problem is that the code does not know when to consider a line of pixels greater than that which it expects as a vertical or horizontal border of the shape. I am going through each pixel of the shape from left to right and checking if any of the pixels have any form of color by checking if the alpha value is 0 or not. Once it finds a pixel that does have an alpha value of anything other than 0, it moves forward a single pixel and then uses the even/odd technique to determine whether the point is inside part of the polygon or not (it makes an infinite line to the right and determines if the number of collisions with colored lines is odd, and if it is, the point is inside the polygon). In general, we consider a single, lone pixel to count as a single line, and we consider a horizontal line of more than one pixel to be two lines because of how often horizontal lines will be part of a border or not. Take the following scenario:
Here, the red dot is the point (pixel) we begin testing from. If we did not consider that horizontal line in the middle to be two points (as is shown by the red lines and x's), we would only have two points of intersection and therefore would not fill the pixel despite the fact that we most definitely do want to fill that pixel. As stated earlier, however, this brings up another problem with a different scenario:
In this case, if we do count a horizontal line of more than one pixel to be two separate lines, we end up not filling any areas with borders that are thicker than the expected thickness. For your reference, the function to handle this is as follows:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
Where...
imgData is the collection of all of the pixels in the given area (essentially a WebImage object)
IMG_SCALE is the integer value by which the image has been scaled up (which gives us the scale of the pixels as well). In this example, it is equal to 4 because the image is scaled up to 192x256 (from 48x64). This means that every "pixel" you see in the image is actually comprised of a 4x4 block of identically-colored pixels.
So, what I'm really looking for here is a way to determine whether a given colored pixel that comes after another is part of a horizontal border or if it is just another piece comprising the thickness of a vertical border. In addition, if I have the wrong approach to this problem in general, I would greatly appreciate any suggestions as to how to do this more efficiently. Thank you.
I understand the problem and I think you would do better if you would switch your strategy here. We know the following:
the point of start is inside the shape
the color should be filled for every pixel inside the shape
So, we could always push the neighbors of the current point into a queue to be processed and be careful to avoid processing the same points twice, this way traversing all the useful pixels and including them into the coloring plan. The function below is untested.
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}
As far as I understood you cannot "consider a horizontal line of more than one pixel to be two lines". I don't think you need to count black pixels the way you do, rather count groups of 1 or more pixels.
I would also tidy the code by avoiding using the "doColor" boolean variable. You could rather move the coloring code to a new function color(x,y) and call it straight away.
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
function floodFill(ctx, x, y, fillColor, range = 1) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// flags for if we visited a pixel already
const visited = new Uint8Array(imageData.width, imageData.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const rangeSq = range * range;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (!visited[y * imageData.width + x] &&
colorsMatch(currentColor, targetColor, rangeSq)) {
setPixel(imageData, x, y, fillColor);
visited[y * imageData.width + x] = 1; // mark we were here already
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
<canvas></canvas>
This is based on other answers
note:"LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,
But it works well in simple HTML,JS website

Generate Radial/Ellipse/Oval/Stadium Gradient Mask (Nested For Loop)

What I'm attempting to do
Loop through two axes and generating a shape with a width and height, either less or equal to the length of the nested for-loops, and calculate the distance from all positions to the center of that shape.
Main Issue(s)
How do I specify the width and height of an ellipse shape to draw using a nested for-loop with different dimensions to that ellipse?
For example a nested for-loop which goes for 0 to 45 in the X axis, and 0 to 100 in the Y axis but draws an ellipse with a width of 39 and a height of 90 - with the remaining difference used as padding (3 on either side, and 5 on top and bottom).
I have this half working using the EdgeOrInBounds function below, however I'm having trouble understanding why the values I'm using are giving the results they are.
Using a nested for-loop the same as above, but specifying an ellipse with a width of 30 and a height of 70 doesn't have the expected padding, it instead draws an ellipse with only one extra sprite surrounding all sides.
How do I calculate the distance from the center of the ellipse to the positions generated by the nested for-loop as a value between zero and one?
For example, any position outside the ellipse returns a value of zero and any position within the ellipse returns the distance scaled between zero and one from the center of the ellipse.
Similar to above, I have this half working as I can return a value of zero for all posiitons outside of the ellipse, but I do not understand how scale the distances for positions within the ellipse.
Bonus Issue(s)
I'm doing this on a platform where code isn't easily shareable and there are few built in functions, so I've had to create my own versions stolen from based on examples from the Nvidia developer site.
I have a basic understanding of some C# and JavaScript, but zero understanding of mathematical formulas.
Ellipse Function(s)
bool EdgeOrInBounds (Vector2 position) {
int x = ((int) Math.Pow (position.x - center.x, 2) / (int) Math.Pow (radius.x, 2));
int y = ((int) Math.Pow (position.y - center.y, 2) / (int) Math.Pow (radius.y, 2));
return (x + y <= 1);
}
Distance Function(s)
float distance (Vector2 position) {
return (sqrt (dot (centerPosition - position, centerPosition - position));
}
float dot (Vector2 a, Vector2 b) {
return (a.x * b.x + a.y * b.y);
}
float sqrt (float a) {
return (1.0 / pow (a, -0.5));
}
Variables
int mapWidth = 45;
int mapHeight = 100;
Vector2 radius = new Vector2 (mapWidth - 8, mapHeight - 4);
Vector2 center = new Vector2 (mapWidth / 2, mapHeight / 2);
Nested For Loops
for (int x = 0; x < width; x ++) {
for (int y = 0; y < height; y ++) {
// Store current position to reference in a minute
Vector2 position = new Vector2 (x, y);
// Check if position is within bounds or lies on the edge of the ellipse
if (EdgeOrInBounds (position)) {
// Calculate distance from center to current position
float dist = distance (position);
}
}
}
Example Image:
Closing Remarks
I know I haven't done a good job of explaining what I'm tring to achieve, so I'd like to apologize in advance, and I'd also like to thank anyone who reads this as any help would be very much appreciated.
Cheers.
To get color shade better under control, you could use an elliptic spiral, instead of a square grid traverse. Start out with the two radii, use X=R1 * Cos(angle) and Y=R2 * Sin(angle), where you gradually decrease R1 and R2 to zero. Your loop will use polar coordinates (angle,r), see below. You are then sure of the size of your "plot" and you won't need to test distances underways. It can probably do without any distance function for color scaling, but I'm not sure how to do that properly.. I have included a few options.
// The image is 440x240, I want ellipse in the center, margins 20 pix
// Parameters, dependent on size and shape of elllipse
Point pc = new Point(220,120); // pixel center
double r1=200; // radius 1 margin 2x20 on 440
double r2=100; // radius 2 margin 2x20 on 240
// Covering all pixels
int rmax = (int)Math.Max(r1,r2);
// scaling for color
var ravgmax = (r1+r2)/2.0;
// Find suitable loop counts
var nr = rmax; // number of radius steps in loop
var nh = 2*nr*Math.PI); // number of angles in loop
// Prepare initial loop displacements
var h=0.0;
var dr1 = r1/(nr*nh);
var dr2 = r2/(nr*nh);
var dh=(Math.PI*2.0)/nh;
// The loop
for (int i=0; i<nr; i++)
{
for (int j=0; j<(int)nh; j++)
{
var p = new PointF((float)(pc.X+r1*Math.Cos(h)),(float)(pc.Y+r2*Math.Sin(h)));
// vanilla shading
// int grayscale = 255 - (int)(255 * ((r1+r2)/2.0)/ravgmax );
// elliptical option without using distance, scale along axes
// grayscale = 255 - (int)(Math.Abs(p.X-pc.X)*255/200+Math.Abs((p.Y-pc.Y)*255/100)/2;
// "Distance grayscale" which is circular, not elliptical
int grayscale = (int)(255 * floatFDistance(p,pc)/rmax);
PlotF(p,grayscale); // you provide: plotpixel(PointF, int)
r1-=dr1; r2-=dr2;
h+=dh;
}
}
}
float floatFDistance(PointF p1, PointF p2)
{
double d1 = (p1.X - p2.X);
double d2 = (p1.Y - p2.Y);
return (float)(Math.Sqrt(d1 * d1 + d2 * d2));
}

Draw Map in Browser out of 2 Dimensional Array of Distances

I'm receiving all distances between a random number of points in a 2 dimensional coordinate system.
How can I visualize this as coordinates on a map in my browser?
In case there are many solutions I just want to see the first possible one that my algorithm can come up with.
So here's an extremely easy example:
PointCount = 3
Distances:
0-1 = 2
0-2 = 4
1-2 = 2
Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement?
I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points.
The above example is simplified -
Real distance values could look like this:
(0) (1) (2) (3)
(0) 0 2344 3333 10000
(1) 0 3566 10333
(2) 0 12520
I'm not sure this is relevant for SO, but anyway...
The way to do this is quite simply to place the points one by one using the data:
Pick a random location for the first point (let's say it's 0,0).
The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0).
The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions.
The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution.
Continue like this until all points have been placed.
Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference.
A quick and dirty implementation (not handling quite a few cases, and tested very little):
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}
// adapted from https://stackoverflow.com/a/12221389/3527940
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* '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 = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* 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;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [
[xi, yi],
[xi_prime, yi_prime]
];
}
function generateData(nbPoints) {
var i, j, k;
var originalPoints = [];
for (i = 0; i < nbPoints; i++) {
originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]);
}
var data = [];
var distances;
for (i = 0; i < nbPoints; i++) {
distances = [];
for (j = 0; j < i; j++) {
distances.push(distance(originalPoints[i], originalPoints[j]));
}
data.push(distances);
}
//console.log("original points", originalPoints);
//console.log("distance data", data);
return data;
}
function findPointsForDistances(data, threshold) {
var points = [];
var solutions;
var solutions1, solutions2;
var point;
var i, j, k;
if (!threshold)
threshold = 0.01;
// First point, arbitrarily set at 0,0
points.push([0, 0]);
// Second point, arbitrarily set at d(0,1),0
points.push([data[1][0], 0]);
// Third point, intersection of two circles, pick any solution
solutions = intersection(
points[0][0], points[0][1], data[2][0],
points[1][0], points[1][1], data[2][1]);
//console.log("possible solutions for point 3", solutions);
points.push(solutions[0]);
//console.log("solution for points 1, 2 and 3", points);
found = true;
// Subsequent points, intersections of n-1 circles, use first two to find 2 solutions,
// the 3rd to pick one of the two
// then use others to check it's valid
for (i = 3; i < data.length; i++) {
// distances to points 1 and 2 give two circles and two possible solutions
solutions = intersection(
points[0][0], points[0][1], data[i][0],
points[1][0], points[1][1], data[i][1]);
//console.log("possible solutions for point " + (i + 1), solutions);
// try to find which solution is compatible with distance to point 3
found = false;
for (j = 0; j < 2; j++) {
if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) {
point = solutions[j];
found = true;
break;
}
}
if (!found) {
console.log("could not find solution for point " + (i + 1));
console.log("distance data", data);
console.log("solution for points 1, 2 and 3", points);
console.log("possible solutions for point " + (i + 1), solutions);
console.log("distances to point 3",
distance(solutions[0], points[2]),
distance(solutions[1], points[2]),
data[i][2]
);
break;
}
// We have found a solution, we need to check it's valid
for (j = 3; j < i; j++) {
if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) {
console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1));
found = false;
break;
}
}
if (!found) {
console.log("stopping");
break;
}
points.push(point);
}
if (found) {
//console.log("complete solution", points);
return points;
}
}
console.log(findPointsForDistances([
[],
[2344],
[3333, 3566],
[10000, 10333, 12520],
]));
console.log(findPointsForDistances([
[],
[2],
[4, 2],
]));
console.log(findPointsForDistances([
[],
[4000],
[5000, 3000],
[3000, 5000, 4000]
]));
console.log(findPointsForDistances([
[],
[2928],
[4938, 3437],
[10557, 10726, 13535]
]));
var nbPoints, i;
for (nbPoints = 4; nbPoints < 8; nbPoints++) {
for (i = 0; i < 10; i++) {
console.log(findPointsForDistances(generateData(nbPoints)));
}
}
Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like:
y = canvasHeight - y
If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let scale = 10;
let radius = 10;
function point(x, y) {
ctx.fillRect(x*scale, y*scale, radius, radius);
}
// test
point(10, 15);
point(20, 8);
<html>
<body>
<canvas id="canvas" width=1000 height=1000></canvas>
</body>
</html>
There are plenty of libraries out there.
chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes.
chartJS is another better looking library.
And you can use html5 canvas it's easy and fun but it will take time especially in scaling.
To scale and position, you should use the minimum and maximum values for x and y.
Good luck

Find column, row on 2D isometric grid from x,y screen space coords (Convert equation to function)

I'm trying to find the row, column in a 2d isometric grid of a screen space point (x, y)
Now I pretty much know what I need to do which is find the length of the vectors in red in the pictures above and then compare it to the length of the vector that represent the bounds of the grid (which is represented by the black vectors)
Now I asked for help over at mathematics stack exchange to get the equation for figuring out what the parallel vectors are of a point x,y compared to the black boundary vectors. Link here Length of Perpendicular/Parallel Vectors
but im having trouble converting this to a function
Ideally i need enough of a function to get the length of both red vectors from three sets of points, the x,y of the end of the 2 black vectors and the point at the end of the red vectors.
Any language is fine but ideally javascript
What you need is a base transformation:
Suppose the coordinates of the first black vector are (x1, x2) and the coordinates of the second vector are (y1, y2).
Therefore, finding the red vectors that get at a point (z1, z2) is equivalent to solving the following linear system:
x1*r1 + y1*r2 = z1
x2*r1 + y2*r2 = z2
or in matrix form:
A x = b
/x1 y1\ |r1| = |z1|
\x2 y2/ |r2| |z2|
x = inverse(A)*b
For example, lets have the black vector be (2, 1) and (2, -1). The corresponding matrix A will be
2 2
1 -1
and its inverse will be
1/4 1/2
1/4 -1/2
So a point (x, y) in the original coordinates will be able to be represened in the alternate base, bia the following formula:
(x, y) = (1/4 * x + 1/2 * y)*(2,1) + (1/4 * x -1/2 * y)*(2, -1)
What exactly is the point of doing it like this? Any isometric grid you display usually contains cells of equal size, so you can skip all the vector math and simply do something like:
var xStep = 50,
yStep = 30, // roughly matches your image
pointX = 2*xStep,
pointY = 0;
Basically the points on any isometric grid fall onto the intersections of a non-isometric grid. Isometric grid controller:
screenPositionToIsoXY : function(o, w, h){
var sX = ((((o.x - this.canvas.xPosition) - this.screenOffsetX) / this.unitWidth ) * 2) >> 0,
sY = ((((o.y - this.canvas.yPosition) - this.screenOffsetY) / this.unitHeight) * 2) >> 0,
isoX = ((sX + sY - this.cols) / 2) >> 0,
isoY = (((-1 + this.cols) - (sX - sY)) / 2) >> 0;
// isoX = ((sX + sY) / isoGrid.width) - 1
// isoY = ((-2 + isoGrid.width) - sX - sY) / 2
return $.extend(o, {
isoX : Math.constrain(isoX, 0, this.cols - (w||0)),
isoY : Math.constrain(isoY, 0, this.rows - (h||0))
});
},
// ...
isoToUnitGrid : function(isoX, isoY){
var offset = this.grid.offset(),
isoX = $.uD(isoX) ? this.isoX : isoX,
isoY = $.uD(isoY) ? this.isoY : isoY;
return {
x : (offset.x + (this.grid.unitWidth / 2) * (this.grid.rows - this.isoWidth + isoX - isoY)) >> 0,
y : (offset.y + (this.grid.unitHeight / 2) * (isoX + isoY)) >> 0
};
},
Okay so with the help of other answers (sorry guys neither quite provided the answer i was after)
I present my function for finding the grid position on an iso 2d grid using a world x,y coordinate where the world x,y is an offset screen space coord.
WorldPosToGridPos: function(iPosX, iPosY){
var d = (this.mcBoundaryVectors.upper.x * this.mcBoundaryVectors.lower.y) - (this.mcBoundaryVectors.upper.y * this.mcBoundaryVectors.lower.x);
var a = ((iPosX * this.mcBoundaryVectors.lower.y) - (this.mcBoundaryVectors.lower.x * iPosY)) / d;
var b = ((this.mcBoundaryVectors.upper.x * iPosY) - (iPosX * this.mcBoundaryVectors.upper.y)) / d;
var cParaUpperVec = new Vector2(a * this.mcBoundaryVectors.upper.x, a * this.mcBoundaryVectors.upper.y);
var cParaLowerVec = new Vector2(b * this.mcBoundaryVectors.lower.x, b * this.mcBoundaryVectors.lower.y);
var iGridWidth = 40;
var iGridHeight = 40;
var iGridX = Math.floor((cParaLowerVec.length() / this.mcBoundaryVectors.lower.length()) * iGridWidth);
var iGridY = Math.floor((cParaUpperVec.length() / this.mcBoundaryVectors.upper.length()) * iGridHeight);
return {gridX: iGridX, gridY: iGridY};
},
The first line is best done once in an init function or similar to save doing the same calculation over and over, I just included it for completeness.
The mcBoundaryVectors are two vectors defining the outer limits of the x and y axis of the isometric grid (The black vectors shown in the picture above).
Hope this helps anyone else in the future

Categories