Related
I have a polygon that has circles on its vertices.
What I expect to accomplish is that every circle will be moving to the circle on its right. This is a physics concept which proves that if every circle is moving to the one on its right with a constant speed, soon they will reach the center. I'm trying to accomplish this animation, however I am able to move circles but not in the direction to the one next to it.
Here's my current code that draws the polygon with circles:
function particleGenerator(n){
const ctx = document.getElementById('poly').getContext('2d');
ctx.reset();
drawPolygon(ctx, 154, 71.25 , n, 50, 0, 5, 7.5);
}
const drawPolygon = (ctx, x, y, points, radius, rotation = 0, nodeSize = 0, nodeInset = 0) => {
ctx.beginPath();
ctx.moveTo(
x + radius * Math.cos(rotation),
y + radius * Math.sin(rotation)
);
for (let i = 1; i <= points; i += 1) {
const angle = (i * (2 * Math.PI / points)) + rotation;
ctx.lineTo(
x + radius * Math.cos(angle),
y + radius * Math.sin(angle)
);
}
ctx.fillStyle = "#00818A";
ctx.fill();
if (!nodeSize) return;
const dist = radius - nodeInset;
for (let i = 1; i <= points; i += 1) {
const angle = (i * (2 * Math.PI / points)) + rotation;
let x1 = x + dist * Math.cos(angle);
let y1 = y + dist * Math.sin(angle);
ctx.beginPath();
ctx.arc(x1, y1, nodeSize, 0, 2 * Math.PI);
ctx.fillStyle = "#DBEDF3"
ctx.fill();
}
};
<button onclick="particleGenerator(4)">Click Me!</button>
<canvas id="poly">
You can keep track of a list of corners. You generate them in order, so to get a corner's next neighbor you can do corners[i + 1] || corners[0].
To move the corner in the direction of the next one, you can calculate their differences in x and y coordinates and add a percentage of that difference to a corner's current location.
Here's a running example (I did remove some of the code so I could focus on just the updating problem:
function particleGenerator(n) {
const ctx = document.getElementById('poly').getContext('2d');
ctx.reset();
const originalCorners = createCorners(150, 70, n, 50);
const corners = createCorners(150, 70, n, 50);
const next = () => {
corners.forEach(([x0, y0], i) => {
const [x1, y1] = corners[i + 1] || corners[0];
const dx = x1 - x0;
const dy = y1 - y0;
const SPEED = 0.05;
corners[i][0] = x0 + dx * SPEED;
corners[i][1] = y0 + dy * SPEED;
});
}
const frame = () => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
drawPolygon(ctx, originalCorners, "grey");
drawPolygon(ctx, corners);
drawDots(ctx, corners);
next();
requestAnimationFrame(frame);
};
frame();
}
const createCorners = (x, y, n, radius) => {
const corners = [];
for (let i = 1; i <= n; i += 1) {
const angle = (i * (2 * Math.PI / n));
corners.push([
x + radius * Math.cos(angle),
y + radius * Math.sin(angle)
]);
}
return corners;
}
const drawPolygon = (
ctx,
corners,
color = "#00818A"
) => {
// Draw fill
ctx.beginPath();
corners.forEach((c, i) => {
if (i === 0) ctx.moveTo(...c);
else ctx.lineTo(...c);
});
ctx.fillStyle = color
ctx.fill();
};
const drawDots = (
ctx,
corners,
) => {
// Draw dots
corners.forEach(([x, y], i, all) => {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = "red"
ctx.fill();
});
};
<input type="number" value="6" min="3" max="100">
<button onclick="particleGenerator(document.querySelector('input').valueAsNumber)">Click Me!</button>
<canvas id="poly">
I have a 2D board made with KonvaJS and tokens that can move on a square grid. I can already add fog of war and remove it manually. However, I would like to make it so, when each token moves, it reveals a certain around it, taking into account walls. Most of the work is done, however it's not entirely accurate.
Basically for each wall, I'm checking if the token is on the top/right/bottom/left of it. And then depending on which one it is, I reduce the width/height of the revealing area so it doesn't go beyond the wall. Here is an image explaining what I have and what I need
Legend:
Gray is fog of war
Red area is the wall/obstacle
Token is the movable token
Blue area is the revealed area
Blue lines inside red area is where it intersects
Purple lines are squares that should be revealed (aka, it should be blue)
Basically, in this case, an intersection was detected and the token is on the right side of the obstacle. So I got the right side of the wall (the x coordinate), and made the blue area starting point be that x coordinate and removed from the total width of the blue area the intersection width(the blue lines, so 1 square of width was removed).
However, because of that, the purple lines don't get filled in. Unfortunately, I can't just check the intersection points between blue and red and only remove those, because if the blue area is bigger than the red area, it would reveal the other side of the obstacle(which I don't want).
Here is the code I'm using to iterate the walls, checking if there is an intersection, checking where the token is, and then removing the width or height according to the intersection.
const tokenPosition = { x: 10, y: 10 };
const haveIntersection = (r1, r2) => !(
r2.x > r1.x + r1.width || // Compares top left with top right
r2.x + r2.width < r1.x || // Compares top right with top left
r2.y > r1.y + r1.height || // Compare bottom left with bottom right
r2.y + r2.height < r1.y // Compare bottom right with bottom left
);
walls.forEach(wall => {
const redArea = { x: wall.x, y: wall.y, width: wall.width, height: wall.height };
// blueArea has the same properties as redArea
if (haveIntersection(blueArea, redArea)) {
const tokenToTheRight = tokenPosition.x > wall.x + wall.width;
const tokenToTheLeft = tokenPosition.x < wall.x;
const tokenToTheTop = tokenPosition.y < wall.y;
const tokenToTheBottom = tokenPosition.y > wall.y + wall.height;
if (tokenToTheRight) {
let diff = wall.x + wall.width - blueArea.x;
blueArea.x = wall.x + wall.width;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheLeft) {
let diff = blueArea.x + blueArea.width - wall.x;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheTop) {
let diff = blueArea.y + blueArea.height - wall.y;
blueArea.height = blueArea.height - diff;
}
if (tokenToTheBottom) {
let diff = wall.y + wall.height - blueArea.y;
blueArea.y = wall.y + wall.height;
blueArea.height = blueArea.height - diff;
}
}
});
Any idea on how to fix this or if I should be taking a different approach?
You'll have to do something ray-tracing like to get this to work.
In the snippet below, I:
Loop over each cell in your token's field-of-view
Check for that cell center whether
it is in a box, or
a line between the token and the cell center intersects with a wall of a box
Color the cell based on whether it intersects
Note: the occlusion from the boxes is quite aggressive because we only check the center for quite a large grid cell. You can play around with some of the settings to see if it matches your requirements. Let me know if it doesn't.
Legend:
Red: box
Light blue: in field of view
Orange: blocked field of view because box-overlap
Yellow: blocked field of view because behind box
// Setup
const cvs = document.createElement("canvas");
cvs.width = 480;
cvs.height = 360;
const ctx = cvs.getContext("2d");
document.body.appendChild(cvs);
// Game state
const GRID = 40;
const H_GRID = GRID / 2;
const token = { x: 7.5, y: 3.5, fow: 2 };
const boxes = [
{ x: 2, y: 3, w: 4, h: 4 },
{ x: 8, y: 4, w: 1, h: 1 },
];
const getBoxSides = ({ x, y, w, h }) => [
[ [x + 0, y + 0], [x + w, y + 0]],
[ [x + w, y + 0], [x + w, y + h]],
[ [x + w, y + h], [x + 0, y + h]],
[ [x + 0, y + h], [x + 0, y + 0]],
];
const renderToken = ({ x, y, fow }) => {
const cx = x * GRID;
const cy = y * GRID;
// Render FOV
for (let ix = x - fow; ix <= x + fow; ix += 1) {
for (let iy = y - fow; iy <= y + fow; iy += 1) {
let intersectionFound = false;
for (const box of boxes) {
if (
// Check within boxes
pointInBox(ix, iy, box) ||
// Check walls
// Warning: SLOW
getBoxSides(box).some(
([[ x1, y1], [x2, y2]]) => intersects(x, y, ix, iy, x1, y1, x2, y2)
)
) {
intersectionFound = true;
break;
}
}
if (!intersectionFound) {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(0, 255, 255, 0.5)", 0);
ctx.fillStyle = "lime";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
} else {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(255, 255, 0, 0.5)", 0);
ctx.fillStyle = "red";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
}
}
}
ctx.lineWidth = 5;
ctx.fillStyle = "#efefef";
ctx.beginPath();
ctx.arc(cx, cy, GRID / 2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
const renderBox = ({ x, y, w, h }, color = "red", strokeWidth = 5) => {
ctx.fillStyle = color;
ctx.strokeWidth = strokeWidth;
ctx.beginPath();
ctx.rect(x * GRID, y * GRID, w * GRID, h * GRID);
ctx.closePath();
ctx.fill();
if (strokeWidth) ctx.stroke();
}
const renderGrid = () => {
ctx.lineWidth = 1;
ctx.beginPath();
let x = 0;
while(x < cvs.width) {
ctx.moveTo(x, 0);
ctx.lineTo(x, cvs.height);
x += GRID;
}
let y = 0;
while(y < cvs.height) {
ctx.moveTo(0, y);
ctx.lineTo(cvs.width, y);
y += GRID;
}
ctx.stroke();
}
boxes.forEach(box => renderBox(box));
renderToken(token);
renderGrid();
// Utils
// https://errorsandanswers.com/test-if-two-lines-intersect-javascript-function/
function intersects(a,b,c,d,p,q,r,s) {
var det, gamma, lambda;
det = (c - a) * (s - q) - (r - p) * (d - b);
if (det === 0) {
return false;
} else {
lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
return (0 <= lambda && lambda <= 1) && (0 <= gamma && gamma <= 1);
}
}
function pointInBox(x, y, box) {
return (
x > box.x &&
x < box.x + box.w &&
y > box.y &&
y < box.bottom
);
}
canvas { border: 1px solid black; }
I am making a drawing application. I have created a class Polygon. Its constructor will receive three arguments and these will be its properties:
points(Number): Number of points the polygon will have.
rotation(Number): The angle the whole polygon will be rotated.
angles(Array Of number): The angles between two lines of the polygon.
I have been trying for the whole day, but I couldn't figure out the correct solution.
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = rotation;
//if angles are given then convert them to radian
if(angles){
this.angles = angles.map(x => x * Math.PI/ 180);
}
//if angles array is not given
else{
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * Math.PI/ this.points;
//fill the angles array with the same angle
this.angles = Array(points).fill(angle)
}
let sum = 0;
this.angles = this.angles.map(x => {
sum += x;
return sum;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
c.strokeText(0, cx + r, cy);
for(let i = 1; i < this.points; i++){
//console.log(this.angles[i])
let dx = cx + r * Math.cos(this.angles[i] + this.rotation);
let dy = cy + r * Math.sin(this.angles[i] + this.rotation);
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let pol = new Polygon(5, 0);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<canvas></canvas>
In the above code I am trying to make a pentagon but it doesn't work.
Unit polygon
The following snippet contains a function polygonFromSidesOrAngles that returns the set of points defining a unit polygon as defined by the input arguments. sides, or angles
Both arguments are optional but must have one argument
If only sides given then angles are calculated to make the complete polygon with all side lengths equal
If only angles given then the number of sides is assumed to be the number of angles. Angles are in degrees 0-360
If the arguments can not define a polygon then there are several exceptions throw.
The return is a set of points on a unit circle that define the points of the polygon. The first point is at coordinate {x : 1, y: 0} from the origin.
The returned points are not rotated as that is assumed to be a function of the rendering function.
All points on the polygon are 1 unit distance from the origin (0,0)
Points are in the form of an object containing x and y properties as defined by the function point and polarPoint
Method used
I did not lookup an algorithm, rather I worked it out from the assumption that a line from (1,0) on the unit circle at the desired angle will intercept the circle at the correct distance from (1,0). The intercept point is used to calculate the angle in radians from the origin. That angle is then used to calculate the ratio of the total angles that angle represents.
The function that does this is calcRatioOfAngle(angle, sides) returning the angle as a ratio (0-1) of Math.PI * 2
It is a rather long handed method and likely can be significantly reduced
As it is unclear in your question what should be done with invalid arguments the function will throw a range error if it can not proceed.
Polygon function
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromSidesOrAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const v1 = point(Math.cos(ang) - 1, Math.sin(ang));
const len2 = v1.x * v1.x + v1.y * v1.y;
const u = -v1.x / len2;
const v2 = point(v1.x * u + 1, v1.y * u);
const d = (1 - (v2.y * v2.y + v2.x * v2.x)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(v2.y + v1.y * d, v2.x + 1 + v1.x * d) / (Math.PI * (sides - 2) / 2);
}
const vetAngles = angles => angles.reduce((sum, ang) => sum += ang, 0) === (angles.length - 2) * 180;
var ratios = [];
if(angles === undefined) {
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
if (sides === undefined) { sides = angles.length }
else if (sides !== angles.length) { throw new RangeError("Numbers of sides does not match number of angles") }
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
if (!vetAngles(angles)) { throw new RangeError("Set of angles can not create a "+sides+" sided polygon") }
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, sides));
ratios.unshift(ratios.pop()); // rotate right to get first angle at start
}
var ang = 0;
const points = [];
for (const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
Render function
Function to render the polygon. It includes the rotation so you don't need to create a separate set of points for each angle you want to render the polygon at.
The radius is the distance from the center point x,y to any of the polygons vertices.
function drawPolygon(ctx, poly, x, y, radius, rotate) {
ctx.setTransform(radius, 0, 0, radius, x, y);
ctx.rotate(rotate);
ctx.beginPath();
for(const p of poly.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
Example
The following renders a set of test polygons to ensure that the code is working as expected.
Polygons are rotated to start at the top and then rendered clock wise.
The example has had the vetting of input arguments removed.
const ctx = can.getContext("2d");
can.height = can.width = 512;
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const x = Math.cos(ang) - 1, y = Math.sin(ang);
const len2 = x * x + y * y;
const u = -x / len2;
const x1 = x * u + 1, y1 = y * u;
const d = (1 - (y1 * y1 + x1 * x1)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(y1 + y * d, x1 + 1 + x * d) / (Math.PI * (sides - 2) / 2);
}
var ratios = [];
if (angles === undefined) {
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, angles.length));
ratios.unshift(ratios.pop());
}
var ang = 0;
const points = [];
for(const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
function drawPolygon(poly, x, y, radius, rot) {
const xdx = Math.cos(rot) * radius;
const xdy = Math.sin(rot) * radius;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.beginPath();
for (const p of poly) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
const segs = 4;
const tests = [
[3], [, [45, 90, 45]], [, [90, 10, 80]], [, [60, 50, 70]], [, [40, 90, 50]],
[4], [, [90, 90, 90, 90]], [, [90, 60, 90, 120]],
[5], [, [108, 108, 108, 108, 108]], [, [58, 100, 166, 100, 116]],
[6], [, [120, 120, 120, 120, 120, 120]], [, [140, 100, 180, 100, 100, 100]],
[7], [8],
];
var angOffset = -Math.PI / 2; // rotation of poly
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const wStep = w / segs;
const hStep = h / segs;
const radius = Math.min(w / segs, h / segs) / 2.2;
var x,y, idx = 0;
for (y = 0; y < segs && idx < tests.length; y ++) {
for (x = 0; x < segs && idx < tests.length; x ++) {
drawPolygon(polygonFromAngles(...tests[idx++]), (x + 0.5) * wStep , (y + 0.5) * hStep, radius, angOffset);
}
}
canvas {
border: 1px solid black;
}
<canvas id="can"></canvas>
I do just a few modification.
Constructor take angles on degree
When map angles to radian complement 180 because canvas use angles like counterclockwise. We wan to be clockwise
First point start using the passed rotation
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI;
const toRadian = val => val * Math.PI / 180;
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = toRadian(rotation);
//if angles array is not given
if(!angles){
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * 180 / this.points;
//fill the angles array with the same angle
angles = Array(points).fill(angle);
}
this.angles = angles;
let sum = 0;
console.clear();
// To radians
this.angles = this.angles.map(x => {
x = 180 - x;
x = toRadian(x);
return x;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
let sumAngle = 0;
let dx = cx + r * Math.cos(this.rotation);
let dy = cy + r * Math.sin(this.rotation);
c.moveTo(dx, dy);
for(let i = 0; i < this.points; i++){
sumAngle += this.angles[i];
dx = dx + r * Math.cos((sumAngle + this.rotation));
dy = dy + r * Math.sin((sumAngle + this.rotation));
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let elRotation = document.getElementById("elRotation").value;
let rotation = elRotation.length == 0 ? 0 : parseInt(elRotation);
let elPoints = document.getElementById("elPoints").value;
let points = elPoints.length == 0 ? 3 : parseInt(elPoints);
let elAngles = document.getElementById("elAngles").value;
let angles = elAngles.length == 0 ? null : JSON.parse(elAngles);
let pol = new Polygon(points, rotation, angles);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<!DOCTYPE html>
<html lang="en">
<body>
Points: <input id="elPoints" style="width:30px" type="text" value="3" />
Rotation: <input id="elRotation" style="width:30px" type="text" value="0" />
Angles: <input id="elAngles" style="width:100px" type="text" value="[45, 45, 90]" />
<canvas></canvas>
</body>
</html>
I need to draw an arc having initial point, radius and final point.
I am using the HTML5 canvas arc function (x, y, radius, startAngle, endAngle, anticlockwise) in JavaScript.
context.arc(x, y, radius, startAngle, endAngle, anticlockwise)
Have:
var initialPoint = { x: 20, y: 40 };
var radius = 40;
var finalPoint = { x: 180, y: 40 };
The expected results:
please some help
You have to do a little math to find the center of a circle that matches your 3 constraints :
• intersects with initialPoints
• intersects with finalPoint
• has provided radius
Notice that there might be no result : if the points are further from twice the radius from one another no circle can match.
If points are < 2 * radius away, we have two results in fact, i don't know how you'd like your user to choose.
The math uses a few properties :
• the circles center are on the line perpendicular to p1, p2.
• pm, the middle point of (p1, p2) is also the middle point of (c1, c2).
• the triangles (p1, pm, c1) and (p1, pm, c2) have a 90° angle in pm (called 'triangle rectangle' in french, donno in english).
Here's a screenshot with the two possible arcs in green/red :
http://jsbin.com/jutidigepeta/1/edit?js,output
var initialPoint = { x: 100, y: 160 };
var radius = 90;
var finalPoint = { x: 240, y: 190 };
var centers = findCenters(radius,initialPoint, finalPoint );
Core function :
//
function findCenters(r, p1, p2) {
// pm is middle point of (p1, p2)
var pm = { x : 0.5 * (p1.x + p2.x) , y: 0.5*(p1.y+p2.y) } ;
drawPoint(pm, 'PM (middle)');
// compute leading vector of the perpendicular to p1 p2 == C1C2 line
var perpABdx= - ( p2.y - p1.y );
var perpABdy = p2.x - p1.x;
// normalize vector
var norm = Math.sqrt(sq(perpABdx) + sq(perpABdy));
perpABdx/=norm;
perpABdy/=norm;
// compute distance from pm to p1
var dpmp1 = Math.sqrt(sq(pm.x-p1.x) + sq(pm.y-p1.y));
// sin of the angle between { circle center, middle , p1 }
var sin = dpmp1 / r ;
// is such a circle possible ?
if (sin<-1 || sin >1) return null; // no, return null
// yes, compute the two centers
var cos = Math.sqrt(1-sq(sin)); // build cos out of sin
var d = r*cos;
var res1 = { x : pm.x + perpABdx*d, y: pm.y + perpABdy*d };
var res2 = { x : pm.x - perpABdx*d, y: pm.y - perpABdy*d };
return { c1 : res1, c2 : res2} ;
}
utilities :
function sq(x) { return x*x ; }
function drawPoint(p, name) {
ctx.fillRect(p.x - 1,p.y - 1,2, 2);
ctx.textAlign = 'center';
ctx.fillText(name, p.x, p.y+10);
}
function drawCircle(c, r) {
ctx.beginPath();
ctx.arc(c.x, c.y, r, 0, 6.28);
ctx.strokeStyle='#000';
ctx.stroke();
}
function drawCircleArc(c, r, p1, p2, col) {
var ang1 = Math.atan2(p1.y-c.y, p1.x-c.x);
var ang2 = Math.atan2(p2.y-c.y, p2.x-c.x);
ctx.beginPath();
var clockwise = ( ang1 > ang2);
ctx.arc(c.x, c.y, r, ang1, ang2, clockwise);
ctx.strokeStyle=col;
ctx.stroke();
}
Edit :
Here a fiddle using 'side', a boolean that states which side of the arc we should choose.
http://jsbin.com/jutidigepeta/3/edit
If anyone is looking for the equivalent in SVG (using D3.js), using the answer from opsb:
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y
].join(" ");
return d;
}
function sq(x) { return x*x ; }
function drawCircleArcSVG(c, r, p1, p2, col) {
var ang1 = Math.atan2(p1.y-c.y, p1.x-c.x)*180/Math.PI+90;
var ang2 = Math.atan2(p2.y-c.y, p2.x-c.x)*180/Math.PI+90;
var clockwise = side;
var path = describeArc(c.x, c.y, r, ang1, ang2)
console.log(path)
svg.append("path").attr("d", path).attr("fill", "none").attr("stroke-width", 3).attr("stroke", col)
}
function drawPointSVG(p, name) {
svg.append("circle").attr("cx", p.x).attr("cy", p.y).attr("r", 2)
svg.append("text").attr("x", p.x).attr("y", p.y+10).style("font-size", 10).text(name).style("font-family", "Arial")
}
function drawCircleSVG(c, r) {
svg.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", r).style("fill", "none").attr("stroke", "#000")
}
Working example: https://jsbin.com/qawenekesi/edit
I'm trying to draw a smooth curved arc between two points in canvas, I have set up the points as sutch note these are dynamic and can change.
var p1 = {
x=100, y=100
}
var p2 = {
x=255, y=255
}
The curve would look something like this
Here my started code, I can't get my head around the math/logic of this function:
function curveA2B(a,b){
var mindpoint = {
x: (a.x+b.x)/2,
y: (a.y+b.y)/2,
d: Math.sqrt(Math.pow(b.x-a.x,2) + Math.pow(b.y-a.y,2))
};
context.beginPath();
context.arc(
a.x,
a.y,
mindpoint.d/2,
1.5*Math.PI,
0,
false
);
context.arc(
b.x,
b.y,
mindpoint.d/2,
1*Math.PI,
0.5*Math.PI,
true
);
context.context.stroke();
}
The dynamic examples is here: http://jsfiddle.net/CezarisLT/JDdjp/6/
I created a function that would be easily modifiable to many needs called plot_curve that gives you an idea of the breakdown of your problem.
A quick DEMO: http://jsfiddle.net/LVFat/
function plot_curve(x,y,xx,yy, target,color)
{
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var bezierX=x; // x1
var bezierY=yy; // y2
console.log("bx:"+bezierX);
console.log("by:"+bezierY);
var cx,cy, t;
for(t=0.0; t<=1; t+=0.01)
{
cx = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
cy = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
// change this part to whatever you are trying to manipulate to the curve
plot_pixel( Math.round(cx), Math.round(cy), target, color);
}
}
example... (works with a divCanvas function I made.. check out jsfiddle link...)
plot_curve(25,25,5,5, ".divCanvas","blue");
if you just want the coords for the curve between the two points, try this:
function plot_curve(x,y,xx,yy)
{
// returns an array of x,y coordinates to graph a perfect curve between 2 points.
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var xy = [];
var xy_count = -1;
var bezierX=x; // x1
var bezierY=yy; // y2
var t;
for(t=0.0; t<=1; t+=0.01)
{
xy_count++;
xy[xy_count] = {};
xy[xy_count].x = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
xy[xy_count].y = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
}
return xy; // returns array of coordinates
}
You can use the mid of the two points as two radius settings for the x and y axis.
The following example is simplified but it shows one approach to create smooth curves inside the boxes as in your example.
The boxes will always scale so that the curves goes through the mid point between the two points (alter the end point for example).
DEMO
/// set up some values
var ctx = demo.getContext('2d'),
p1 = {x:100, y:100}, /// point 1
p2 = {x:355, y:255}, /// point 2
mx = (p2.x - p1.x) * 0.5, /// mid-point between point 1 and 2
my = (p2.y - p1.y) * 0.5,
c1 = {x: p1.x, y: p1.y + my}, /// create center point objects
c2 = {x: p2.x, y: p2.y - my},
steps = 0.05; /// curve resolution
/// mark the points and the boxes which represent the center of those
ctx.fillStyle = '#ff6e6e';
ctx.fillRect(p1.x, p1.y, mx, my);
ctx.fillStyle = '#6e93ff';
ctx.fillRect(p1.x + mx, p1.y + my, mx, my);
Then we render the quarter ellipse for each "box":
/// render the smooth curves using 1/4 ellipses
ctx.beginPath();
for(var isFirst = true, /// first point is moveTo, rest lineTo
angle = 1.5 * Math.PI, /// start angle in radians
goal = 2 * Math.PI, /// goal angle
x, y; angle < goal; angle += steps) {
/// calculate x and y using cos/sin
x = c1.x + mx * Math.cos(angle);
y = c1.y + my * Math.sin(angle);
/// move or draw line
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
/// second box
for(var isFirst = true,
angle = Math.PI,
goal = 0.5 * Math.PI,
x, y;angle > goal; angle -= steps) {
x = c2.x + mx * Math.cos(angle);
y = c2.y + my * Math.sin(angle);
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
ctx.stroke();
I'll leave it to you to put this into re-usable functions. Hope this helps!
If this doesn't cut it I would recommend you to take a look at my cardinal spline implementation.