JavaScript Point Collision with Regular Hexagon - javascript

I'm making an HTML5 canvas hexagon grid based system and I need to be able to detect what hexagonal tile in a grid has been clicked when the canvas is clicked.
Several hours of searching and trying my own methods led to nothing, and porting implementations from other languages has simply confused me to a point where my brain is sluggish.
The grid consists of flat topped regular hexagons like in this diagram:
Essentially, given a point and the variables specified in this image as the sizing for every hexagon in the grid (R, W, S, H):
I need to be able to determine whether a point is inside a hexagon given.
An example function call would be pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) where hexX and hexY are the coordinates for the top left corner of the bounding box of a hexagonal tile (like the top left corner in the image above).
Is there anyone who has any idea how to do this? Speed isn't much of a concern for the moment.

Simple & fast diagonal rectangle slice.
Looking at the other answers I see that they have all a little over complicated the problem. The following is an order of magnitude quicker than the accepted answer and does not require any complicated data structures, iterators, or generate dead memory and unneeded GC hits. It returns the hex cell row and column for any related set of R, H, S or W. The example uses R = 50.
Part of the problem is finding which side of a rectangle a point is if the rectangle is split diagonally. This is a very simple calculation and is done by normalising the position of the point to test.
Slice any rectangle diagonally
Example a rectangle of width w, and height h split from top left to bottom right. To find if a point is left or right. Assume top left of rectangle is at rx,ry
var x = ?;
var y = ?;
x = ((x - rx) % w) / w;
y = ((y - ry) % h) / h;
if (x > y) {
// point is in the upper right triangle
} else if (x < y) {
// point is in lower left triangle
} else {
// point is on the diagonal
}
If you want to change the direction of the diagonal then just invert one of the normals
x = 1 - x; // invert x or y to change the direction the rectangle is split
if (x > y) {
// point is in the upper left triangle
} else if (x < y) {
// point is in lower right triangle
} else {
// point is on the diagonal
}
Split into sub cells and use %
The rest of the problem is just a matter of splitting the grid into (R / 2) by (H / 2) cells width each hex covering 4 columns and 2 rows. Every 1st column out of 3 will have diagonals. with every second of these column having the diagonal flipped. For every 4th, 5th, and 6th column out of 6 have the row shifted down one cell. By using % you can very quickly determine which hex cell you are on. Using the diagonal split method above make the math easy and quick.
And one extra bit. The return argument retPos is optional. if you call the function as follows
var retPos;
mainLoop(){
retPos = getHex(mouse.x, mouse.y, retPos);
}
the code will not incur a GC hit, further improving the speed.
Pixel to Hex coordinates
From Question diagram returns hex cell x,y pos. Please note that this function only works in the range 0 <= x, 0 <= y if you need negative coordinates subtract the min negative pixel x,y coordinate from the input
// the values as set out in the question image
var r = 50;
var w = r * 2;
var h = Math.sqrt(3) * r;
// returns the hex grid x,y position in the object retPos.
// retPos is created if not supplied;
// argument x,y is pixel coordinate (for mouse or what ever you are looking to find)
function getHex (x, y, retPos){
if(retPos === undefined){
retPos = {};
}
var xa, ya, xpos, xx, yy, r2, h2;
r2 = r / 2;
h2 = h / 2;
xx = Math.floor(x / r2);
yy = Math.floor(y / h2);
xpos = Math.floor(xx / 3);
xx %= 6;
if (xx % 3 === 0) { // column with diagonals
xa = (x % r2) / r2; // to find the diagonals
ya = (y % h2) / h2;
if (yy % 2===0) {
ya = 1 - ya;
}
if (xx === 3) {
xa = 1 - xa;
}
if (xa > ya) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
if (xx < 3) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
Hex to pixel
And a helper function that draws a cell given the cell coordinates.
// Helper function draws a cell at hex coordinates cellx,celly
// fStyle is fill style
// sStyle is strock style;
// fStyle and sStyle are optional. Fill or stroke will only be made if style given
function drawCell1(cellPos, fStyle, sStyle){
var cell = [1,0, 3,0, 4,1, 3,2, 1,2, 0,1];
var r2 = r / 2;
var h2 = h / 2;
function drawCell(x, y){
var i = 0;
ctx.beginPath();
ctx.moveTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
while (i < cell.length) {
ctx.lineTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
}
ctx.closePath();
}
ctx.lineWidth = 2;
var cx = Math.floor(cellPos.x * 3);
var cy = Math.floor(cellPos.y * 2);
if(cellPos.x % 2 === 1){
cy -= 1;
}
drawCell(cx, cy);
if (fStyle !== undefined && fStyle !== null){ // fill hex is fStyle given
ctx.fillStyle = fStyle
ctx.fill();
}
if (sStyle !== undefined ){ // stroke hex is fStyle given
ctx.strokeStyle = sStyle
ctx.stroke();
}
}

I think you need something like this~
EDITED
I did some maths and here you have it. This is not a perfect version but probably will help you...
Ah, you only need a R parameter because based on it you can calculate H, W and S. That is what I understand from your description.
// setup canvas for demo
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height = 275;
var context = canvas.getContext('2d');
var hexPath;
var hex = {
x: 50,
y: 50,
R: 100
}
// Place holders for mouse x,y position
var mouseX = 0;
var mouseY = 0;
// Test for collision between an object and a point
function pointInHexagon(target, pointX, pointY) {
var side = Math.sqrt(target.R*target.R*3/4);
var startX = target.x
var baseX = startX + target.R / 2;
var endX = target.x + 2 * target.R;
var startY = target.y;
var baseY = startY + side;
var endY = startY + 2 * side;
var square = {
x: startX,
y: startY,
side: 2*side
}
hexPath = new Path2D();
hexPath.lineTo(baseX, startY);
hexPath.lineTo(baseX + target.R, startY);
hexPath.lineTo(endX, baseY);
hexPath.lineTo(baseX + target.R, endY);
hexPath.lineTo(baseX, endY);
hexPath.lineTo(startX, baseY);
if (pointX >= square.x && pointX <= (square.x + square.side) && pointY >= square.y && pointY <= (square.y + square.side)) {
var auxX = (pointX < target.R / 2) ? pointX : (pointX > target.R * 3 / 2) ? pointX - target.R * 3 / 2 : target.R / 2;
var auxY = (pointY <= square.side / 2) ? pointY : pointY - square.side / 2;
var dPointX = auxX * auxX;
var dPointY = auxY * auxY;
var hypo = Math.sqrt(dPointX + dPointY);
var cos = pointX / hypo;
if (pointX < (target.x + target.R / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
}
if (pointX > (target.x + target.R * 3 / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
}
return true;
}
return false;
}
// Loop
setInterval(onTimerTick, 33);
// Render Loop
function onTimerTick() {
// Clear the canvas
canvas.width = canvas.width;
// see if a collision happened
var collision = pointInHexagon(hex, mouseX, mouseY);
// render out text
context.fillStyle = "Blue";
context.font = "18px sans-serif";
context.fillText("Collision: " + collision + " | Mouse (" + mouseX + ", " + mouseY + ")", 10, 20);
// render out square
context.fillStyle = collision ? "red" : "green";
context.fill(hexPath);
}
// Update mouse position
canvas.onmousemove = function(e) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
#canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
Just replace your pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) by the var hover = ctx.isPointInPath(hexPath, x, y).
This is for Creating and copying paths
This is about the Collision Detection
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var hexPath = new Path2D();
hexPath.lineTo(25, 0);
hexPath.lineTo(75, 0);
hexPath.lineTo(100, 43);
hexPath.lineTo(75, 86);
hexPath.lineTo(25, 86);
hexPath.lineTo(0, 43);
function draw(hover) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = hover ? 'blue' : 'red';
ctx.fill(hexPath);
}
canvas.onmousemove = function(e) {
var x = e.clientX - canvas.offsetLeft, y = e.clientY - canvas.offsetTop;
var hover = ctx.isPointInPath(hexPath, x, y)
draw(hover)
};
draw();
<canvas id="canvas"></canvas>

I've made a solution for you that demonstrates the point in triangle approach to this problem.
http://codepen.io/spinvector/pen/gLROEp
maths below:
isPointInside(point)
{
// Point in triangle algorithm from http://totologic.blogspot.com.au/2014/01/accurate-point-in-triangle-test.html
function pointInTriangle(x1, y1, x2, y2, x3, y3, x, y)
{
var denominator = ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3));
var a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / denominator;
var b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / denominator;
var c = 1 - a - b;
return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
}
// A Hex is composite of 6 trianges, lets do a point in triangle test for each one.
// Step through our triangles
for (var i = 0; i < 6; i++) {
// check for point inside, if so, return true for this function;
if(pointInTriangle( this.origin.x, this.origin.y,
this.points[i].x, this.points[i].y,
this.points[(i+1)%6].x, this.points[(i+1)%6].y,
point.x, point.y))
return true;
}
// Point must be outside.
return false;
}

Here is a fully mathematical and functional representation of your problem. You will notice that there are no ifs and thens in this code other than the ternary to change the color of the text depending on the mouse position. This whole job is in fact nothing more than pure simple math of just one line;
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
and this code is reusable for all polygons from triangle to circle. So if interested please read on. It's very simple.
In order to display the functionality I had to develop a mimicking model of the problem. I draw a polygon on a canvas by utilizing a simple utility function. So that the overall solution should work for any polygon. The following snippet will take the canvas context c, radius r, number of sides s, and the local center coordinates in the canvas cx and cy as arguments and draw a polygon on the given canvas context at the right position.
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
We have some other utility functions which one can easily understand what exactly they are doing. However the most important part is to check whether the mouse is floating over our polygon or not. It's done by the utility function isMouseIn. It's basically calculating the distance and the angle of the mouse position to the center of the polygon. Then, comparing it with the boundaries of the polygon. The boundaries of the polygon can be expressed by simple trigonometry, just like we have calculated the vertices in the drawPolygon function.
We can think of our polygon as a circle with an oscillating radius at the frequency of number of sides. The oscillation's peak is at the given radius value r (which happens to be at the vertices at angle 2π/s where s is the number of sides) and the minimum m is r*Math.cos(Math.PI/s) (each shows at at angle 2π/s + 2π/2s = 3π/s). I am pretty sure the ideal way to express a polygon could be done by the Fourier transformation but we don't need that here. All we need is a constant radius component which is the average of minimum and maximum, (r+m)/2 and the oscillating component with the frequency of number of sides, s and the amplitude value maximum - minimum)/2 on top of it, Math.cos(a*s)*(r-m)/2. Well of course as per Fourier states we might carry on with smaller oscillating components but with a hexagon you don't really need further iteration while with a triangle you possibly would. So here is our polygon representation in math.
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
Now all we need is to calculate the angle and distance of our mouse position relative to the center of the polygon and compare it with the above mathematical expression which represents our polygon. So all together our magic function is orchestrated as follows;
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s), // the min dist from an edge to the center
d = Math.hypot(mx-cx,my-cy), // the mouse's distance to the center of the polygon
a = Math.atan2(cy-my,mx-cx); // angle of the mouse pointer
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
So the following code demonstrates how you might approach to solve your problem.
// Generic function to draw a polygon on the canvas
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
// To write the mouse position in canvas local coordinates
function writeText(c,x,y,msg,col){
c.clearRect(0, 0, 300, 30);
c.font = "10pt Monospace";
c.fillStyle = col;
c.fillText(msg, x, y);
}
// Getting the mouse position and coverting into canvas local coordinates
function getMousePos(c, e) {
var rect = c.getBoundingClientRect();
return { x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
// To check if mouse is inside the polygone
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s),
d = Math.hypot(mx-cx,my-cy),
a = Math.atan2(cy-my,mx-cx);
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
// the event listener callback
function mouseMoveCB(e){
var mp = getMousePos(cnv, e),
msg = 'Mouse at: ' + mp.x + ',' + mp.y,
col = "black",
inside = isMouseIn(radius,sides,center[0],center[1],mp.x,mp.y);
writeText(ctx, 10, 25, msg, inside ? "turquoise" : "red");
}
// body of the JS code
var cnv = document.getElementById("myCanvas"),
ctx = cnv.getContext("2d"),
sides = 6,
radius = 100,
center = [150,150];
cnv.addEventListener('mousemove', mouseMoveCB, false);
drawPolgon(ctx, radius, sides, center[0], center[1]);
#myCanvas { background: #eee;
width: 300px;
height: 300px;
border: 1px #ccc solid
}
<canvas id="myCanvas" width="300" height="300"></canvas>

At the redblog there is a full explanation with math and working examples.
The main idea is that hexagons are horizontally spaced by $3/4$ of hexagons size, vertically it is simply $H$ but the column needs to be taken to take vertical offset into account. The case colored red is determined by comparing x to y at 1/4 W slice.

Related

Simplest way to make a spiral line go through an arbitrary point?

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
let last = 1
let start = 1
let i = 0
let origin = [250, 250]
for (let i2 = 0; i2 < 20; i2++) {
ctx.ellipse(...origin, start, start, Math.PI / 2 * i, 0, Math.PI / 2);
i++
i %= 4
if (i == 1) origin[1] -= last
else if (i == 2) origin[0] += last
else if (i == 3) origin[1] += last
else if (i == 0) origin[0] -= last;
[last, start] = [start, start + last]
}
ctx.stroke();
ctx.beginPath()
ctx.lineCap = 'round'
ctx.lineWidth = 7
ctx.strokeStyle = "red";
ctx.lineTo(400, 400)
ctx.stroke()
<canvas width="500" height="500" style="border:1px solid #000000;"></canvas>
What is the simplest way to make the spiral line go through an arbitrary point in the canvas? For example 400x 400y. I think adjusting the initial start and last values based on some calculation could work. The only difference between the first code snippet and the second one is the initial last and start variables. Other solutions that rewrite the entire thing are welcome too.
const canvas = document.querySelector( 'canvas' );
const ctx = canvas.getContext( '2d' );
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
let last = 0.643
let start = 0.643
let i = 0
let origin = [250,250]
for (let i2=0; i2<20; i2++) {
ctx.ellipse(...origin, start, start, Math.PI/2 *i , 0, Math.PI /2);
i++
i%=4
if (i==1) origin[1] -= last
if (i==2) origin[0] += last
if (i==3) origin[1] += last
if (i==0) origin[0] -= last
;[last, start] = [start, start + last]
}
ctx.stroke();
ctx.beginPath()
ctx.lineCap = 'round'
ctx.lineWidth = 7
ctx.strokeStyle = "red";
ctx.lineTo(400, 400)
ctx.stroke()
<canvas width="500" height="500" style="border:1px solid #000000;"></canvas>
I am not sure how you want the spiral to intercept the point. There are twp options.
Rotate the spiral to intercept point
Scale the spiral to intercept point
This answer solves using method 1. Method 2 has some problems as the number of turns can grow exponentially making the rendering very slow if we don't set limits to where the point of intercept can be.
Not a spiral
The code you provided does not draw a spiral in the mathematical definition but rather is just a set of connected ellipsoids.
This means that there is more than one function that defines a point on these connected curves. To solve for a point will require some complexity as each possible curve must be solved and the solution then vetted to locate the correct curve. On top of that ellipsoids I find to result in some very ugly math.
A spiral function
If we define the curve as just one function where the spiral radius is defined by the angle, it is then very easy to solve.
The function for the radius can be a simplified polynomial in the form Ax^P+C where x is the angle, P is the spiralness (for want of a better term), A is the scale (again for want of a better term) and C is the start angle
C is there if you want to make the step angle of the spiral be a set length eg 1 px would be angle += 1 / (Ax^P+C) If C is 0 then 1/0 would result in an infinite loop.
Drawing the spiral
As defined above there are many types of spirals that can be rendered so there should be one that is close to the spiral you have.
Any point on the spiral is found as follows
x = cos(angle) * f(angle) + origin.x
y = sin(angle) * f(angle) + origin.y
where f is the poly f(x) = Ax^P+C
The following function draws a basic linear spiral f(x) = 1*x^1+0.1
function drawSpiral(origin) {
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
let i = 0;
while (i < 5) {
const r = i + 0.1; // f(x) = 1*x^1+0.1
ctx.lineTo(
Math.cos(i) * r + origin.x,
Math.sin(i) * r + origin.y
);
i += 0.1
}
ctx.stroke();
}
Solve to pass though point
To solve for a point we convert the point to a polar coordinate relative to the origin. See functions pointDist , pointAngle. We then solve for Ax^P+C = dist in terms of x (the angle) and dist the distance from the origin. Then subtract the angle to the point to get the spirals orientation. (NOTE ^ means to power of, rest of answer uses JavaScripts **)
To solve an arbitrary polynomial can become rather complex that is why I used the simplified version.
The function A * x ** P + C = pointDist(point) needs to be rearranged in terms of pointDist(point).
This gives x = ((pointDist(point) - C) / A) ** (1 / P)
And then subtract the polar angle x = ((pointDist(point)- C) / A) ** (1 / P) - pointAngle(point) and we have the angle offset so that the spiral will intercept the point.
Example
A working example in case the above was TLDR or had too much math like jargon.
The example defines a spiral via the coefficients of the radius function A, C, and P.
There are 3 example spirals Black, Blue, and Green.
A spiral is drawn until its radius is greater than the diagonal distance to the canvas corner. The origin is the center of the canvas.
The point to intercept is set by the mouse position over the page.
The spirals are only rendered when the mouse position changes.
The solution for the simplified polynomial is shown in steps in the function startAngle.
While I wrote the code I seam to have lost the orientation and thus needed to add 180 deg to the start angle (Math.PI) or the point ends up midway between spiral arms.
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0}, mouseOld = {x : undefined, y : undefined};
document.addEventListener("mousemove", (e) => { mouse.x = e.pageX; mouse.y = e.pageY });
requestAnimationFrame(loop);
const TURNS = 4 * Math.PI * 2;
let origin = {x: canvas.width / 2, y: canvas.height / 2};
scrollTo(0, origin.y - innerHeight / 2);
const maxRadius = (origin.x ** 2 + origin.y ** 2) ** 0.5; // dist from origin to corner
const pointDist = (p1, p2) => Math.hypot(p1.x - p2.x, p1.y - p2.y);
const pointAngle = (p1, p2) => Math.atan2(p1.y - p2.y, p1.x - p2.x);
const radius = (x, spiral) => spiral.A * x ** spiral.P + spiral.C;
const startAngle = (origin, point, spiral) => {
const dist = pointDist(origin, point);
const ang = pointAngle(origin, point);
// Da math
// from radius function A * x ** P + C
// where x is ang
// A * x ** P + C = dist
// A * x ** P = dist - C
// x ** P = (dist - C) / A
// x = ((dist - C) / A) ** (1 / p)
return ((dist - spiral.C) / spiral.A) ** (1 / spiral.P) - ang;
}
// F for Fibonacci
const startAngleF = (origin, point, spiral) => {
const dist = pointDist(origin, point);
const ang = pointAngle(origin, point);
return (1 / spiral.P) * Math.log(dist / spiral.A) - ang;
}
const radiusF = (x, spiral) => spiral.A * Math.E ** (spiral.P * x);
const spiral = (P, A, C, rFc = radius, aFc = startAngle) => ({P, A, C, rFc, aFc});
const spirals = [
spiral(2, 1, 0.1),
spiral(3, 0.25, 0.1),
spiral(0.3063489,0.2972713047, null, radiusF, startAngleF),
spiral(0.8,4, null, radiusF, startAngleF),
];
function drawSpiral(origin, point, spiral, col) {
const start = spiral.aFc(origin, point, spiral);
ctx.strokeStyle = col;
ctx.beginPath();
let i = 0;
while (i < TURNS) {
const r = spiral.rFc(i, spiral);
const ang = i - start - Math.PI;
ctx.lineTo(
Math.cos(ang) * r + origin.x,
Math.sin(ang) * r + origin.y
);
if (r > maxRadius) { break }
i += 0.1
}
ctx.stroke();
}
loop()
function loop() {
if (mouse.x !== mouseOld.x || mouse.y !== mouseOld.y) {
ctx.clearRect(0, 0, 500, 500);
ctx.lineWidth = 1;
drawSpiral(origin, mouse, spirals[0], "#FFF");
drawSpiral(origin, mouse, spirals[1], "#0FF");
ctx.lineWidth = 4;
drawSpiral(origin, mouse, spirals[2], "#FF0");
drawSpiral(origin, mouse, spirals[3], "#AF0");
ctx.beginPath();
ctx.lineCap = "round";
ctx.lineWidth = 7;
ctx.strokeStyle = "red";
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
Object.assign(mouseOld, mouse);
}
requestAnimationFrame(loop);
}
canvas { position : absolute; top : 0px; left : 0px; background: black }
<canvas id="canvas" width = "500" height = "500"></canvas>
UPDATE
As requested in the comments
I have added the Fibonacci spiral to the example
The radius function is radiusF
The function to find the start angle to intercept a point is startAngleF
The two new Fibonacci spirals are colored limeGreen and Yellow
To use the Fibonacci spiral you must include the functions radiusF and startAngleF when defining the spiral eg spiral(1, 1, 0, radiusF, startAngleF)
Note the 3rd argument is not used and is zero in the eg above. as I don't think you will need it

Curve intersecting points in JavaScript

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.

Convert (x, y) coordinate into (r, g, b) values

Hello wizards of Stack Overflow,
I've been tasked with plotting some data onto a scatter chart in Javascript, but with a twist! These plotted objects need to follow a strict colour code. I've got the plotting part right, but the colour generation has me stumped. The graph follows a maximum value x and y of 100 and a minimum value of zero (I'm dealing with percentages).
The bottom left corner of the graph should be pure green and the diagonal top right should be pure red with a hazy yellow-orange in the middle. E.g. point (0, 0) should be (red:0 green:255 blue:0), point (100, 100) should be (red:255 green:0 blue:0) and point (50, 50) should be (red:132 green:132 blue:20).
So basically there's a diagonal gradient of green to red running from point (0, 0) to point (100, 100).
| red
| /
| /
| green
Has anyone dealt with a similar situation and perhaps has some sort of algorithm to figure this out?
Regards,
JP
I don't think I can envision what you want to plot exactly, but I think you can solve a lot of things when you split your r, g and b values into different functions.
So instead of func_rgb(x, y) {...} you should make three different functions - one for each color channel - that you can manipulate individually and add the results up afterwards.
func_r(x,y) {
return x/100 * 256;
}
func_g(x,y) {
return (1 - x/100) * 256;
}
func_b(x,y) {
return (1 - (0.5 - x/100)^2) * 20;
}
I know these functions only contain the X-value, however, I think you can figure out the rest of the math on your own.
From the information I have so far, the easiest way to deal with this would be:
Green = 255 - ((255/100)*((x+y)/2))
Red = ((255/100)*((x+y)/2))
This way, if you were at (0, 0) you'd have:
Green = 255 - ((255/100)*((0+0)/2)) = 255
Red = ((255/100)*((0+0)/2)) = 0
Or if you were at (13, 13):
Green = 255 - ((255/100)*((13+13)/2)) = 222
Red = ((255/100)*((13+13)/2)) = 33
It's important to mention that blue seems not to be relevant, and I don't know what should happen if x and y are very different, so I just calculated the mean.
If the lower left corner is completely green (in rgb (0, 255, 0)) and the upper right corner is red ((255, 0, 0)), that means that the equation for red is 255 / 100 * y and the equation for green is 255 - 255 / 100 * x. This way the upper left corner will be (255, 255, 0) and the lower right will be (0, 0, 0)
<html>
<body>
<canvas id="canvas"></canvas>
<script type="application/javascript">
// Colours that you want each corner to have
var topLeft = {r: 0,g: 0,b: 0};
var topRight = {r: 255,g: 0,b: 0};
var bottomLeft = {r: 0,g: 255,b: 0};
var bottomRight = {r: 0,g: 0,b: 0};
var output = {r: 0,g: 0,b: 0};
// Perform bilinear interpolation on both axis
// This just means to do linear interpolation for y & x, then combine the results
// Provide the XY you need the colour for and the size of your graph
function getSpectrumColour(x,y,width,height) {
var div = 1.0 / (width*height);
output.r = div * (bottomLeft.r * (width - x) * (height - y) + bottomRight.r * x * y
+ topLeft.r * (width - x) * (height - y) + topRight.r * x * y);
output.g = div * (bottomLeft.g * (width - x) * (height - y) + bottomRight.g * x * y
+ topLeft.g * (width - x) * (height - y) + topRight.g * x * y);
output.b = div * (bottomLeft.b * (width - x) * (height - y) + bottomRight.b * x * y
+ topLeft.b * (width - x) * (height - y) + topRight.b * x * y);
return output;
}
var canvas = null;
var ctx = null;
var graphWidth = 100;
var graphHeight = 100;
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = graphWidth;
canvas.height = graphHeight;
ctx = canvas.getContext("2d");
var colour = null;
for (var x = 0; x < graphWidth; ++x) {
for (var y = 0; y < graphHeight; ++y) {
colour = getSpectrumColour(x,y,graphWidth,graphHeight);
ctx.fillStyle = "rgba("+colour.r+","+colour.g+","+colour.b+",1.0)";
ctx.fillRect(x,graphHeight - y,1,1);
}
}
}
</script>
</body>
</html>

Stretch image to fit polygon html5 canvas

I have a square image like this:
I am trying to stretch this image into a polygon like this:
So far I have been able to create a polygon on the canvas as the above image using the following javascript:
function drawCanvas() {
var c2 = document.getElementById('myCanvas6').getContext('2d');
var img = document.getElementById("scream");
c2.fillStyle = '#000';
c2.beginPath();
c2.moveTo(20, 20);
c2.lineTo(320, 50);
c2.lineTo(320, 170);
c2.lineTo(20, 200);
//c2.drawImage(img, 150, 10, img.width, img.height);
c2.closePath();
c2.fill();
}
I tried using drawImage() method, but it does not stretch the points A, B, C, D to the new positions. Is there anyway this can be achieved?
The 2D canvas is called 2D for a very good reason. You can not transform a square such that any of its side converge (are not parallel) hence 2D
But where there is a need there is always a way..
You can do it by cutting the image into slices and then draw each slice slightly smaller than the last.
We humans don't like to see an image distort when it converges, so you need to add the distortion we expect, perspective. The further away the object the smaller the distance between points appears to the eye.
So the function below draws an image with the top and bottom edges converging..
It is not true 3D but it does make the image appear as distorted as jus converging the top and bottom without decreasing the y step. The animation introduced a bit of an optical illusion. the second render shortens the image to make it appear a little less fake.
See the code on how to use the function.
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
/** CreateImage.js end **/
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;
const textToDisplay = "Perspective"
const textSize = 80;
ctx.font = textSize+"px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,textSize + 32);
text.ctx.fillStyle = "#08F";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = textSize+"px arial";
text.ctx.fillStyle = "#F80";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,textSize + 8);
text.ctx.fillText(textToDisplay,38,textSize + 8);
// Not quite 3D
// ctx is the context to draw to
// image is the image to draw
// x1,x2 left and right edges of the image
// zz1,zz2 top offset for left and right
// image top edge has a slops from zz1 to zz2
// yy if the position to draw top. This is where the top would be if z = 0
function drawPerspective(ctx, image, x1, zz1, x2, zz2, yy){
var x, w, h, h2,slop, topLeft, botLeft, zDistR, zDistL, lines, ty;
w = image.width; // image size
h = image.height;
h2 = h /2; // half height
slop = (zz2 - zz1) / (x2 - x1); // Slope of top edge
z1 = h2 - zz1; // Distance (z) to first line
z2 = (z1 / (h2 - zz2)) * z1 - z1; // distance (z) between first and last line
if(z2 === 0){ // if no differance in z then is square to camera
topLeft = - x1 * slop + zz1; // get scan line top left edge
ctx.drawImage(image,0, 0, w, h,x1, topLeft + yy ,x2-x1, h - topLeft * 2) // render to desination
return;
}
// render each display line getting all pixels that will be on that line
for (x = x1; x < x2; x++) { // for each line horizontal line
topLeft = (x - x1) * slop + zz1; // get scan line top left edge
botLeft = ((x + 1) - x1) * slop + zz1; // get scan line bottom left edge
zDistL = (z1 / (h2 - topLeft)) * z1; // get Z distance to Left of this line
zDistR = (z1 / (h2 - botLeft)) * z1; // get Z distance to right of this line
ty = ((zDistL - z1) / z2) * w; // get y bitmap coord
lines = ((zDistR - z1) / z2) * w - ty;// get number of lines to copy
ctx.drawImage(image,
ty % w, 0, lines, h, // get the source location of pixel
x, topLeft + yy,1 , h - topLeft * 2 // render to desination
);
}
}
var animTick = 0;
var animRate = 0.01;
var pos = 0;
var short = 0;
function update1(){
animTick += animRate;
pos = Math.sin(animTick) * 20 + 20;
short = Math.cos((pos / 40) * Math.PI) * text.width * 0.12 - text.width * 0.12;
ctx.clearRect(0,0,can.width,can.height)
drawPerspective(ctx,text,0,0,text.width,pos,20)
drawPerspective(ctx,text,0,0,text.width+short,pos,textSize + 32 + 30)
requestAnimationFrame(update1);
}
update1();
I think this is a good solution for you: http://jsfiddle.net/fQk4h/
Here is the magic:
for (i = 0; i < w; i++) {
dy = (leftTop * (w - i)) / w;
dh = (leftBot * (w - i) + h * i) / w;
ctx.drawImage(tmpCtx.canvas,
i, 0, 1, h,
i, dy, 1, dh);
}
ctx.restore();

canvas "random" curved shapes

I want to draw random-looking curved blobs on a canvas, but I can't seem to come up with an algorithm to do it. I've tried creating random Bezier curves like this:
context.beginPath();
// Each shape should be made up of between three and six curves
var i = random(3, 6);
var startPos = {
x : random(0, canvas.width),
y : random(0, canvas.height)
};
context.moveTo(startPos.x, startPos.y);
while (i--) {
angle = random(0, 360);
// each line shouldn't be too long
length = random(0, canvas.width / 5);
endPos = getLineEndPoint(startPos, length, angle);
bezier1Angle = random(angle - 90, angle + 90) % 360;
bezier2Angle = (180 + random(angle - 90, angle + 90)) % 360;
bezier1Length = random(0, length / 2);
bezier2Length = random(0, length / 2);
bezier1Pos = getLineEndPoint(startPos, bezier1Length, bezier1Angle);
bezier2Pos = getLineEndPoint(endPos, bezier2Length, bezier2Angle);
context.bezierCurveTo(
bezier1Pos.x, bezier1Pos.y
bezier2Pos.x, bezier2Pos.y
endPos.x, endPos.y
);
startPos = endPos;
}
(This is a simplification... I added bits constraining the lines to within the canvas, etc.)
The problem with this is getting it to head back to the starting point, and also not just making loads of awkward corners. Does anyone know of a better algorithm to do this, or can think one up?
Edit: I've made some progress. I've started again, working with straight lines (I think I know what to do to make them into smooth Beziers once I've worked this bit out). I've set it so that before drawing each point, it works out the distance and angle to the start from the previous point. If the distance is less than a certain amount, it closes the curve. Otherwise the possible angle narrows based on the number of iterations, and the maximum line length is the distance to the start. So here's some code.
start = {
// start somewhere within the canvas element
x: random(canvas.width),
y: random(canvas.height)
};
context.moveTo(start.x, start.y);
prev = {};
prev.length = random(minLineLength, maxLineLength);
prev.angle = random(360);
prev.x = start.x + prev.length * Math.cos(prev.angle);
prev.y = start.y + prev.length * Math.sin(prev.angle);
j = 1;
keepGoing = true;
while (keepGoing) {
j++;
distanceBackToStart = Math.round(
Math.sqrt(Math.pow(prev.x - start.x, 2) + Math.pow(prev.y - start.y, 2)));
angleBackToStart = (Math.atan((prev.y - start.y) / (prev.x - start.x)) * 180 / Math.pi) % 360;
if (isNaN(angleBackToStart)) {
angleBackToStart = random(360);
}
current = {};
if (distanceBackToStart > minLineLength) {
current.length = random(minLineLength, distanceBackToStart);
current.angle = random(angleBackToStart - 90 / j, angleBackToStart + 90 / j) % 360;
current.x = prev.x + current.length * Math.cos(current.angle);
current.y = prev.y + current.length * Math.sin(current.angle);
prev = current;
} else {
// if there's only a short distance back to the start, join up the curve
current.length = distanceBackToStart;
current.angle = angleBackToStart;
current.x = start.x;
current.y = start.y;
keepGoing = false;
}
context.lineTo(current.x, current.y);
}
console.log('Shape complexity: ' + j);
context.closePath();
context.fillStyle = 'black';
context.shadowColor = 'black';
context.shadowOffsetX = -xOffset;
context.shadowOffsetY = -yOffset;
context.shadowBlur = 50;
context.fill();
The problem I've got now is that the shape's outline often crosses over itself, which looks wrong. The only way I can think of to solve this is to keep track of a bounding box, and each new point should always head out of the bounding box. That's tricky because calculating the available angle adds a whole level of complexity.
One possibility would be to use polar coordinates, and have the radius be a function of the angle. For smooth blobs you want the radius to be smooth, and have the same value at 0 and 2*pi, which can be done using a trigonometric polynomial :
radius(theta) = a_0 + a_1*sin(theta) + a_2*sin(2*theta) + ... + b_1*cos(theta) + ...
where the coefficients are "random". To control how big and small the radius gets you could search for the max and min of the radius function, and then shift and scale the coefficients appropriately (ie if you want rlo<=r<=rhi, and have found min and max, then replace each coefficient a + b*original, where b = (rhi-rlo)/(max-min) and a = rlo-b*min).

Categories

Resources