Related
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
I have about 120 000 particles (each particle 1px size) that I need to find the best and most important: fastest way to draw to my canvas.
How would you do that?
Right now I'm basically getting my pixels into an Array, and then I loop over these particles, do some x and y calculations and draw them out using fillRect. But the framerate is like 8-9 fps right now.
Any ideas? Please example.
Thank you
LATEST UPDATE (my code)
function init(){
window.addEventListener("mousemove", onMouseMove);
let mouseX, mouseY, ratio = 2;
const canvas = document.getElementById("textCanvas");
const context = canvas.getContext("2d");
canvas.width = window.innerWidth * ratio;
canvas.height = window.innerHeight * ratio;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
context.imageSmoothingEnabled = false;
context.fillStyle = `rgba(255,255,255,1)`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
const width = canvas.width;
const height = canvas.height;
context.font = "normal normal normal 232px EB Garamond";
context.fillText("howdy", 0, 160);
var pixels = context.getImageData(0, 0, width, height).data;
var data32 = new Uint32Array(pixels.buffer);
const particles = new Array();
for(var i = 0; i < data32.length; i++) {
if (data32[i] & 0xffff0000) {
particles.push({
x: (i % width),
y: ((i / width)|0),
ox: (i % width),
oy: ((i / width)|0),
xVelocity: 0,
yVelocity: 0,
a: pixels[i*4 + 3] / 255
});
}
}
/*const particles = Array.from({length: 120000}, () => [
Math.round(Math.random() * (width - 1)),
Math.round(Math.random() * (height - 1))
]);*/
function onMouseMove(e){
mouseX = parseInt((e.clientX-canvas.offsetLeft) * ratio);
mouseY = parseInt((e.clientY-canvas.offsetTop) * ratio);
}
function frame(timestamp) {
context.clearRect(0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
const index = 4 * Math.round((particle.x + particle.y * width));
data[index + 0] = 0;
data[index + 1] = 0;
data[index + 2] = 0;
data[index + 3] = 255;
}
context.putImageData(imageData, 0, 0);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
var homeDX = p.ox - p.x;
var homeDY = p.oy - p.y;
var cursorForce = 0;
var cursorAngle = 0;
if(mouseX && mouseX > 0){
var cursorDX = p.ox - mouseX;
var cursorDY = p.oy - mouseY;
var cursorDistanceSquared = (cursorDX * cursorDX + cursorDY * cursorDY);
cursorForce = Math.min(10/cursorDistanceSquared,10);
cursorAngle = -Math.atan2(cursorDY, cursorDX);
}else{
cursorForce = 0;
cursorAngle = 0;
}
p.xVelocity += 0.2 * homeDX + cursorForce * Math.cos(cursorAngle);
p.yVelocity += 0.2 * homeDY + cursorForce * Math.sin(cursorAngle);
p.xVelocity *= 0.55;
p.yVelocity *= 0.55;
p.x += p.xVelocity;
p.y += p.yVelocity;
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
Moving 7.2Million particles a second
Not using webGL and shaders and you want 120K particles per frame at
60fps you need a throughput of 7.2million points per second. You need a fast machine.
Web workers multi-core CPUs
Quick solutions. On multi core machines web workers give linear performance increase for each hardware core. Eg On a 8 Core i7 you can run 7 workers sharing data via sharedArrayBuffers (shame that its all turned of ATM due to CPU security risk see MDN sharedArrayBuffer) and get slightly lower than 7 times performance improvement. Note benifits are only from actual hardware cores, JS threads tend to run flat out, Running two workers in one core results in an overall decreased throughput.
Even with shared buffers turned of it is still a viable solution if you are in control of what hardware you run on.
Make a movie.
LOL but no it is an option, and there is no upper limit to particle count. Though not as interactive as I think you may want. If you are selling something via the FX you are after a wow, not a how?
Optimize
Easy to say hard to do. You need to go over the code with a fine tooth comb. Remember that removing a single line if running at full speed is 7.2million lines removed per second.
I have gone over the code one more time. I can not test it so it may or may not work. But its to give you ideas. You could even consider using integer only math. JS can do fixed point math. The integer size is 32bits way more than you need for even a 4K display.
Second optimization pass.
// call this just once outside the animation loop.
const imageData = this.context.getImageData(0, 0, this.width * this.ratio, this.height * this.ratio);
// create a 32bit buffer
const data32 = new Uint32Array(imageData.data.buffer);
const pixel = 0xFF000000; // pixel to fill
const width = imageData.width;
// inside render loop
data32.fill(0); // clear the pixel buffer
// this line may be a problem I have no idea what it does. I would
// hope its only passing a reference and not creating a copy
var particles = this.particleTexts[0].getParticles();
var cDX,cDY,mx,my,p,cDistSqr,cForce,i;
mx = this.mouseX | 0; // may not need the floor bitwize or 0
my = this.mouseY | 0; // if mouse coords already integers
if(mX > 0){ // do mouse test outside the loop. Need loop duplication
// But at 60fps thats 7.2million less if statements
for (let i = 0; i < particles.length; i++) {
var p = particles[i];
p.xVelocity += 0.2 * (p.ox - p.x);
p.yVelocity += 0.2 * (p.oy - p.y);
p.xVelocity *= 0.55;
p.yVelocity *= 0.55;
data32[((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width] = pixel;
}
}else{
for (let i = 0; i < particles.length; i++) {
var p = particles[i];
cDX = p.x - mx;
cDY = p.y - my;
cDist = Math.sqrt(cDistSqr = cDX*cDX + cDY*cDY + 1);
cForce = 1000 / (cDistSqr * cDist)
p.xVelocity += cForce * cDx + 0.2 * (p.ox - p.x);
p.yVelocity += cForce * cDY + 0.2 * (p.oy - p.y);
p.xVelocity *= 0.55;
p.yVelocity *= 0.55;
data32[((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width] = pixel;
}
}
// put pixel onto the display.
this.context.putImageData(imageData, 0, 0);
Above is about as much as I can cut it down. (Cant test it so may or may not suit your need) It may give you a few more frames a second.
Interleaving
Another solution may suit you and that is to trick the eye. This increases frame rate but not points processed and requires that the points be randomly distributed or artifacts will be very noticeable.
Each frame you only process half the particles. Each time you process a particle you calculate the pixel index, set that pixel and then add the pixel velocity to the pixel index and particle position.
The effect is that each frame only half the particles are moved under force and the other half coast for a frame..
This may double the frame rate. If your particles are very organised and you get clumping flickering type artifacts, you can randomize the distribution of particles by applying a random shuffle to the particle array on creation. Again this need good random distribution.
The next snippet is just as an example. Each particle needs to hold the pixelIndex into the pixel data32 array. Note that the very first frame needs to be a full frame to setup all indexes etc.
const interleave = 2; // example only setup for 2 frames
// but can be extended to 3 or 4
// create frameCount outside loop
frameCount += 1;
// do half of all particals
for (let i = frameCount % frameCount ; i < particles.length; i += interleave ) {
var p = particles[i];
cDX = p.x - mx;
cDY = p.y - my;
cDist = Math.sqrt(cDistSqr = cDX*cDX + cDY*cDY + 1);
cForce = 1000 / (cDistSqr * cDist)
p.xVelocity += cForce * cDx + 0.2 * (p.ox - p.x);
p.yVelocity += cForce * cDY + 0.2 * (p.oy - p.y);
p.xVelocity *= 0.55;
p.yVelocity *= 0.55;
// add pixel index to particle's property
p.pixelIndex = ((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width;
// write this frames pixel
data32[p.pixelIndex] = pixel;
// speculate the pixel index position in the next frame. This need to be as simple as possible.
p.pixelIndex += (p.xVelocity | 0) + (p.yVelocity | 0) * width;
p.x += p.xVelocity; // as the next frame this particle is coasting
p.y += p.yVelocity; // set its position now
}
// do every other particle. Just gets the pixel index and sets it
// this needs to remain as simple as possible.
for (let i = (frameCount + 1) % frameCount ; i < particles.length; i += interleave)
data32[particles[i].pixelIndex] = pixel;
}
Less particles
Seams obvious, but is often over looked as a viable solution. Less particles does not mean less visual elements/pixels.
If you reduce the particle count by 8 and at setup create a large buffer of offset indexes. These buffers hold animated pixel movements that closely match the behavior of pixels.
This can be very effective and give the illusion that each pixels is in fact independent. But the work is in the pre processing and setting up the offset animations.
eg
// for each particle after updating position
// get index of pixel
p.pixelIndex = (p.x | 0 + p.y | 0) * width;
// add pixel
data32[p.pixelIndex] = pixel;
// now you get 8 more pixels for the price of one particle
var ind = p.offsetArrayIndex;
// offsetArray is an array of pixel offsets both negative and positive
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
data32[p.pixelIndex + offsetArray[ind++]] = pixel;
// offset array arranged as sets of 8, each set of 8 is a frame in
// looping pre calculated offset animation
// offset array length is 65536 or any bit mask able size.
p.offsetArrayIndex = ind & 0xFFFF ; // ind now points at first pixel of next
// set of eight pixels
This and an assortment of other similar tricks can give you the 7.2million pixels per second you want.
Last note.
Remember every device these days has a dedicated GPU. Your best bet is to use it, this type of thing is what they are good at.
Computing those particles within a shader on a webgl context will provide the most performant solution. See e. g. https://www.shadertoy.com/view/MdtGDX for an example.
If you prefer to continue using a 2d context, you could speed up rendering particles by doing so off-screen:
Get the image data array by calling context.getImageData()
Draw pixels by manipulating the data array
Put the data array back with context.putImageData()
A simplified example:
const output = document.getElementById("output");
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const particles = Array.from({length: 120000}, () => [
Math.round(Math.random() * (width - 1)),
Math.round(Math.random() * (height - 1))
]);
let previous = 0;
function frame(timestamp) {
// Print frames per second:
const delta = timestamp - previous;
previous = timestamp;
output.textContent = `${(1000 / delta).toFixed(1)} fps`;
// Draw particles:
context.clearRect(0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
const index = 4 * (particle[0] + particle[1] * width);
data[index + 0] = 0;
data[index + 1] = 0;
data[index + 2] = 0;
data[index + 3] = 255;
}
context.putImageData(imageData, 0, 0);
// Move particles randomly:
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
particle[0] = Math.max(0, Math.min(width - 1, Math.round(particle[0] + Math.random() * 2 - 1)));
particle[1] = Math.max(0, Math.min(height - 1, Math.round(particle[1] + Math.random() * 2 - 1)));
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
<canvas id="canvas" width="500" height="500"></canvas>
<output id="output"></output>
Instead of drawing individual pixels, you might also want to consider drawing and moving a few textures with a lot of particles on each of them. This might come close to a full particle effect at better performance.
So I built some time ago a little Breakout clone, and I wanted to upgrade it a little bit, mostly for the collisions. When I first made it I had a basic "collision" detection between my ball and my brick, which in fact considered the ball as another rectangle. But this created an issue with the edge collisions, so I thought I would change it. The thing is, I found some answers to my problem:
for example this image
and the last comment of this thread : circle/rect collision reaction but i could not find how to compute the final velocity vector.
So far I have :
- Found the closest point on the rectangle,
- created the normal and tangent vectors,
And now what I need is to somehow "divide the velocity vector into a normal component and a tangent component; negate the normal component and add the normal and tangent components to get the new Velocity vector" I'm sorry if this seems terribly easy but I could not get my mind around that ...
code :
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}
Thanks !
EDIT: Also found this but I didn't know if it is applicable at all points of the rectangle or only the corners.
Best explained with a couple of diagrams:
Have angle of incidence = angle of reflection. Call this value θ.
Have θ = normal angle - incoming angle.
atan2 is the function for computing the angle of a vector from the positive x-axis.
Then the code below immediately follows:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
var normal_angle = atan2(dnormal.y, dnormal.x);
var incoming_angle = atan2(circle.vel.y, circle.vel.x);
var theta = normal_angle - incoming_angle;
circle.vel = circle.vel.rotate(2*theta);
}
Another way of doing it is to get the velocity along the tangent and then subtracting twice this value from the circle velocity.
Then the code becomes
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var tangent_vel = dist.normalize().dot(circle.vel);
circle.vel = circle.vel.sub(tangent_vel.mult(2));
}
Both of the code snippets above do basically the same thing in about the same time (probably). Just pick whichever one you best understand.
Also, as #arbuthnott pointed out, there's a copy-paste error in that NearestY should use rect.h instead of rect.w.
Edit: I forgot the positional resolution. This is the process of moving two physics objects apart so that they're no longer intersecting. In this case, since the block is static, we only need to move the ball.
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
//update circle.vel using one of the above methods
}
var penetrationDepth = circle.r - dist.mag();
var penetrationVector = dist.normalise().mult(penetrationDepth);
circle.pos = circle.pos.sub(penetrationVector);
}
Bat and Ball collision
The best way to handle ball and rectangle collision is to exploit the symmetry of the system.
Ball as a point.
First the ball, it has a radius r that defines all the points r distance from the center. But we can turn the ball into a point and add to the rectangle the radius. The ball is now just a single point moving over time, which is a line.
The rectangle has grown on all sides by radius. The diagram shows how this works.
The green rectangle is the original rectangle. The balls A,B are not touching the rectangle, while the balls C,D are touching. The balls A,D represent a special case, but is easy to solve as you will see.
All motion as a line.
So now we have a larger rectangle and a ball as a point moving over time (a line), but the rectangle is also moving, which means over time the edges will sweep out areas which is too complicated for my brain, so once again we can use symmetry, this time in relative movement.
From the bat's point of view it is stationary while the ball is moving, and from the ball, it is still while the bat is moving. They both see each other move in the opposite directions.
As the ball is now a point, making changes to its movement will only change the line it travels along. So we can now fix the bat in space and subtract its movement from the ball. And as the bat is now fixed we can move its center point to the origin, (0,0) and move the ball in the opposite direction.
At this point we make an important assumption. The ball and bat are always in a state that they are not touching, when we move the ball and/or bat then they may touch. If they do make contact we calculate a new trajectory so that they are not touching.
Two possible collisions
There are now two possible collision cases, one where the ball hits the side of the bat, and one where the ball hits the corner of the bat.
The next images show the bat at the origin and the ball relative to the bat in both motion and position. It is travelling along the red line from A to B then bounces off to C
Ball hits edge
Ball hits corner
As there is symmetry here as well which side or corner is hit does not make any difference. In fact we can mirror the whole problem depending on which size the ball is from the center of the bat. So if the ball is left of the bat then mirror its position and motion in the x direction, and the same for the y direction (you must keep track of this mirror via a semaphore so you can reverse it once the solution is found).
Code
The example does what is described above in the function doBatBall(bat, ball) The ball has some gravity and will bounce off of the sides of the canvas. The bat is moved via the mouse. The bats movement will be transferred to the ball, but the bat will not feel any force from the ball.
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 1;
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx,style) => { Object.keys(style).forEach(key=> ctx[key] = style[key] ) };
// the ball
const ball = {
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
maxSpeed : 8,
style : {
lineWidth : 12,
strokeStyle : "green",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.beginPath();
ctx.arc(this.x,this.y,this.r-this.style.lineWidth * 0.45,0,PI2);
ctx.stroke();
},
update(){
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if(y > canvas.height - this.r){
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if(y < this.r){
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if(x > canvas.width - this.r){
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if(x < this.r){
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if(speed > this.maxSpeed){ // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed-this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
}
}
const ballShadow = { // this is used to do calcs that may be dumped
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
}
// Creates the bat
const bat = {
x : 100,
y : 250,
dx : 0,
dy : 0,
width : 140,
height : 10,
style : {
lineWidth : 2,
strokeStyle : "black",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.strokeRect(this.x - this.width / 2,this.y - this.height / 2, this.width, this.height);
},
update(){
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
//=============================================================================
// THE FUNCTION THAT DOES THE BALL BAT sim.
// the ball and bat are at new position
function doBatBall(bat,ball){
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if(s.x < 0){
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if(s.y < 0){
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if(s.dx > 0 && s.dy > 0){ return }// ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy); // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x +(s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y +(s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if(s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR){ // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if(! hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB){ // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if(!hit){ // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy)/(ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if(dist > radSqr){ return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x,cpy - s.y);
// is the distance greater than the ball speed then its a miss
if(d > ballSpeed){ return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed;
s.dy *= ballSpeed;
hit = true;
}
// if the ball hit the bat restore absolute position
if(hit){
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
// main update function
function update(timer){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
// move bat and ball
bat.update();
ball.update();
// check for bal bat contact and change ball position and trajectory if needed
doBatBall(bat,ball);
// draw ball and bat
bat.draw(ctx);
ball.draw(ctx);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
body {font-family : arial; }
Use the mouse to move the bat and hit the ball.
<canvas id="canvas"></canvas>
Flaws with this method.
It is possible to trap the ball with the bat such that there is no valid solution, such as pressing the ball down onto the bottom of the screen. At some point the balls diameter is greater than the space between the wall and the bat. When this happens the solution will fail and the ball will pass through the bat.
In the demo there is every effort made to not loss energy, but over time floating point errors will accumulate, this can lead to a loss of energy if the sim is run without some input.
As the bat has infinite momentum it is easy to transfer a lot of energy to the ball, to prevent the ball accumulating to much momentum I have added a max speed to the ball. if the ball moves quicker than the max speed it is gradually slowed down until at or under the max speed.
On occasion if you move the bat away from the ball at the same speed, the extra acceleration due to gravity can result in the ball not being pushed away from the bat correctly.
Correction of an idea shared above, with adjusting velocity after collision using tangental velocity.
bounciness - constant defined to represent lost force after collision
nv = vector # normalized vector from center of cricle to collision point (normal)
pv = [-vector[1], vector[0]] # normalized vector perpendicular to nv (tangental)
n = dot_product(nv, circle.vel) # normal vector length
t = dot_product(pv, circle.vel) # tangental_vector length
new_v = sum_vectors(multiply_vector(t*bounciness, pv), multiply_vector(-n*self.bounciness, nv)) # new velocity vector
circle.velocity = new_v
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.
I have circle with a line from the center to the edge of the circle. The user can click on and drag the line and get the degrees from wherever they set the line. I'm using the code from the answer here and have adjusted the code accordingly.
Everything works fine, but the farther down the page the element affected by raphael is, the farther outside of the circle the mouse has to be in order to drag the line. jsfiddle
var canvas = Raphael('pivot', 0, 0, 320, 320);
var clock = canvas.circle(200, 150, 100).attr("stroke-width", 2);
canvas.circle(200, 150, 3).attr("fill", "#000");
var angleplus = 360,
rad = Math.PI / 180,
cx = 200,
cy = 150,
r = 90,
startangle = -90,
angle = 90,
x, y, endangle;
for (i = 1; i < 5; i++) {
endangle = startangle + angle;
x = cx + r * Math.sin(endangle * rad);
y = cy - r * Math.cos(endangle * rad);
canvas.text(x, y, endangle);
startangle = endangle;
}
var hand = canvas.path("M200 50L200 150").attr("stroke-width", 5);
hand.drag( move, start, end );
function move (dx,dy,x,y) {
var pt = this.node.ownerSVGElement.createSVGPoint();
pt.x = x;
pt.y = y;
var angle = ( 90 + Math.atan2(pt.y - clock.attr('cy') - 5, pt.x - clock.attr('cx') - 5 ) * 180 / Math.PI + 360) % 360;
this.rotate(angle, 200,150);
this.angle = angle;
}
function start() {
};
function end() {
alert(parseInt(this.angle));
}
I'm wondering why this happens and if it's even fixable?
Here is an updated original one, a bit added and another bit of redundancy removed, and I've added an extra fiddle to the original answer to reflect it.
The key bits I've added are...
var cpt = clock.node.ownerSVGElement.createSVGPoint();
cpt.x = clock.attr('cx');
cpt.y = clock.attr('cy');
cpt = cpt.matrixTransform(clock.node.getScreenCTM());
The centre of the clock has now moved, so using cx of the circle alone, doesn't really make sense relative to the mouse event. We need to take into account any offset and transforms on the screen to the clock.
We can get this with getScreenCTM (get the matrix from element to the screen), and we can use that matrix with the original cx, cy to figure out where it is on the screen.
Then later, instead of cx, cy, we use the new adjusted coordinates cpt.x/y
var angle = ( 90 + Math.atan2(y - cpt.y - 5, x - cpt.x - 5 ) * 180 / Math.PI + 360)
jsfiddle - drag any hand