Hi I'm facing a problem with canvas.
I try to make a circle that can be reshape like this.
In the demo the circle can be reshape
the problem is to drag and drop the circle point to reshape it.
I know how to drag and drop point in the javascript canvas but how to reshape the cirle line to follow the point.
const DEBUG = true;
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
const MIN_DIMENSION = WIDTH < HEIGHT ? WIDTH : HEIGHT;
const DEFAULT_RADIUS = MIN_DIMENSION * 0.45;
let canvas, ctx;
let cos = Math.cos;
let sin = Math.sin;
let pi = Math.PI;
let pi2 = pi * 2;
class Point {
constructor(x,y) {
this.x = x;
this.y = y;
}
}
function block(c, cb) {
c.save();
c.beginPath();
cb(c);
c.closePath();
c.restore();
}
function circle(c,r) {
c.arc(0, 0, r, 0, pi2);
}
function debugPoints(c, points) {
points.forEach((p,i) => {
if(i % 2 === 0) {
c.fillStyle = 'red';
} else {
c.fillStyle = 'black';
}
c.beginPath();
c.arc(p.x, p.y, 2, 0, pi2);
c.fill();
c.closePath();
})
}
function bezierCirclePoints(r, n) {
let a = pi2/(2*n);
let R = r/cos(a);
let points = new Array(2 * n);
console.log('n:', n);
console.log('a:', a);
console.log('r:', r);
console.log('R:', R);
// calculate even bezier points
for(let i = 0; i < n; i++) {
let i2 = 2*i;
let x = r * sin(i2 * a);
let y = -r * cos(i2 * a);
points[i2] = new Point(x, y);
}
// calculate odd bezier points
for(let i = 0; i < n; i++) {
let i2 = 2*i + 1;
let x = R * sin(i2 * a);
let y = -R * cos(i2 * a);
points[i2] = new Point(x, y);
}
points.push(points[0]);
return points;
}
function bezierCircle(c, r = DEFAULT_RADIUS, n = 7) {
let points = bezierCirclePoints(r,n);
c.translate(WIDTH * 0.5,HEIGHT * 0.5);
if(DEBUG) {
debugPoints(c, points);
}
c.fillStyle = 'red';
c.strokeStyle = 'red';
// draw circle
c.beginPath();
let p = points[0];
c.moveTo(p.x, p.y);
for(let i = 1; i < points.length; i+=2){
let p1 = points[i];
let i2 = i + 1;
if(i2 >= points.length) {
i2 = 0;
}
let p2 = points[i2];
c.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y);
}
c.stroke();
c.closePath();
}
function redCircle(c) {
c.fillStyle = 'red';
c.translate(200,200);
circle(c, 100);
c.fill();
}
canvas = document.getElementById('circle');
canvas.width = WIDTH;
canvas.height = HEIGHT;
ctx = canvas.getContext('2d');
block(ctx, bezierCircle)
<canvas id="circle"></canvas>
As you already realized a circle can be composed out of four bézier curves. I'm going to use a cubic instead of a quadratic though since it offers two control points.
Let's start by looking at the following illustration:
As we can see the red curve consists of start point A, end point B and two control points c1 & c2 respectively.
So if we want to have a circle at x, y with a radius of r we can say:
Ax = x ; Ay = y - r
Bx = x + r ; By = y
c1x = x + r / 2 ; c1y = y - r
c2x = x + r ; c2y = y - r / 2
Of course the missing three curves can be constructed in the same way.
What we can also see from the above illustration is that the start point for the red segment is also the end point for the orange segment. Likewise the orange segment's control point c8 is connected to the start point of the red segment.
So if we're about to move point A we need to move the orange segment's end point, the red segment's start point AND the two control points c8 and c1.
To do this I'd write a general Arc class which consists of the start point, the end point, the two control points and additionally to which arc the start point is connected to. Then it goes a little something like this:
if someone clicks on point A, B, C or D store the current mouse position
store the position of the arc's control point as well as the connected arc's control point
if the mouse is moved, move the start point, it's control point and the connected arc's end point and it's control point relative to the mouse movement
repaint the circle
Here's an example:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Arc {
constructor(pointA, pointB, controlPointA, controlPointB) {
this.pointA = pointA;
this.pointB = pointB;
this.controlPointA = controlPointA;
this.controlPointB = controlPointB;
this.controlPointOldA = null;
this.controlPointOldB = null;
}
update(x, y, x2, y2) {
this.pointA.x = x;
this.pointA.y = y;
this.connectedArc.pointB.x = x;
this.connectedArc.pointB.y = y;
this.controlPointA.x = this.controlPointOldA.x + x2;
this.controlPointA.y = this.controlPointOldA.y + y2;
this.connectedArc.controlPointB.x = this.controlPointOldB.x + x2;
this.connectedArc.controlPointB.y = this.controlPointOldB.y + y2;
}
connect(connectedArc) {
this.connectedArc = connectedArc;
}
saveControlPoints() {
this.controlPointOldA = new Point(this.controlPointA.x, this.controlPointA.y);
this.controlPointOldB = new Point(this.connectedArc.controlPointB.x, this.connectedArc.controlPointB.y);
}
}
class Circle {
constructor(x, y, radius) {
this.arcA = new Arc(new Point(x, y - radius), new Point(x + radius, y), new Point(x + radius / 2, y - radius), new Point(x + radius, y - radius / 2));
this.arcB = new Arc(new Point(x + radius, y), new Point(x, y + radius), new Point(x + radius, y + radius / 2), new Point(x + radius / 2, y + radius));
this.arcC = new Arc(new Point(x, y + radius), new Point(x - radius, y), new Point(x - radius / 2, y + radius), new Point(x - radius, y + radius / 2));
this.arcD = new Arc(new Point(x - radius, y), new Point(x, y - radius), new Point(x - radius, y - radius / 2), new Point(x - radius / 2, y - radius));
this.arcA.connect(this.arcD);
this.arcB.connect(this.arcA);
this.arcC.connect(this.arcB);
this.arcD.connect(this.arcC);
}
}
var circle = new Circle(150, 150, 75);
var mouseX, mouseY, selectedArc;
var width = 5;
var height = 5;
var dragging = false;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var arcs = [circle.arcA, circle.arcB, circle.arcC, circle.arcD];
var points = document.getElementsByClassName("point");
var arc;
for (var a = 0; a < points.length; a++) {
arc = arcs[a];
points[a].setAttribute('data-linkedID', a);
points[a].style.left = (arc.pointA.x - width) + "px";
points[a].style.top = (arc.pointA.y - height) + "px";
points[a].addEventListener("mousedown", dragStarted);
}
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragStop);
function dragStarted(e) {
mouseX = e.pageX;
mouseY = e.pageY;
selectedArc = arcs[e.target.parentElement.getAttribute("data-linkedID")];
selectedArc.saveControlPoints();
dragging = true;
}
function drag(e) {
if (dragging) {
selectedArc.update(e.pageX - width, e.pageY - height, e.pageX - mouseX, e.pageY - mouseY);
update();
var arc;
for (var a = 0; a < points.length; a++) {
arc = arcs[a];
points[a].style.left = (arc.pointA.x - width) + "px";
points[a].style.top = (arc.pointA.y - height) + "px";
}
}
}
function dragStop(e) {
dragging = false;
}
function update() {
ctx.fillStyle = "#eeeeee";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
arcs.forEach(function(arc) {
ctx.moveTo(arc.pointA.x, arc.pointA.y);
ctx.bezierCurveTo(arc.controlPointA.x, arc.controlPointA.y, arc.controlPointB.x, arc.controlPointB.y, arc.pointB.x, arc.pointB.y);
});
ctx.stroke();
}
update();
#container {
position: absolute;
}
#canvas {
position: absolute;
top: 0px;
left: 0px;
}
.point {
position: absolute;
width: 10px;
height: 10px;
}
<div id="container">
<canvas id="canvas" width=300 height=300></canvas>
<svg class="point" id="pointA">
<circle cx="5" cy="5" r="5" fill="red" />
</svg>
<svg class="point" id="pointB">
<circle cx="5" cy="5" r="5" fill="red" />
</svg>
<svg class="point" id="pointC">
<circle cx="5" cy="5" r="5" fill="red" />
</svg>
<svg class="point" id="pointD">
<circle cx="5" cy="5" r="5" fill="red" />
</svg>
</div>
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 found this function on Stackoverflow some time ago and only now am I realizing that it sometimes seems to give false-positives. Here it is:
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
let xDelta = p1X - p2X;
let yDelta = p1Y - p2Y;
let distance = Math.sqrt(xDelta * xDelta + yDelta * yDelta)
let a = (cY - p1Y) * (p2X - p1X);
let b = (cX - p1X) * (p2Y - p1Y);
return Math.abs(a - b) / distance <= r;
}
Here's a full code demo reproduction showing the issue here:
let ctx = document.querySelector("canvas").getContext("2d");
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
let xDelta = p1X - p2X;
let yDelta = p1Y - p2Y;
let distance = Math.sqrt(xDelta * xDelta + yDelta * yDelta)
let a = (cY - p1Y) * (p2X - p1X);
let b = (cX - p1X) * (p2Y - p1Y);
return Math.abs(a - b) / distance <= r;
}
let circleX = 250;
let circleY = 250;
let circleR = 50;
let lineX1 = 50;
let lineY1 = 350;
let lineX2 = 185;
let lineY2 = 250;
draw = () => {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#ffffff";
drawCircle(circleX, circleY, circleR);
drawLine(lineX1, lineY1, lineX2, lineY2);
}
console.log(lineIntersectsCircle(lineX1, lineY1, lineX2, lineY2, circleX, circleY, circleR))
draw();
canvas { display: flex; margin: 0 auto; }
<canvas width="400" height="400"></canvas>
As you can see, the line doesn't intersect the circle, yet it console logs a true statement. Does anyone know why this would be? If this function is incorrect, what is the proper function for only determining if a line and circle intersect? I do not need the intersection points, only whether or not they intersect at all.
Mathematically, a line is different from a line segment; a line is infinitely long.
You use the formula for finding the distance between a point (centre of the circle) and a line. While the formula uses a line defined by two points, it is not terminated by those points, so that doesn't apply to a line segment.
If you extend that line segment out, you can see that it intersects with the circle.
Three steps to determine whether a line segment intersects with a circle:
transform the coordinates to make (xCenter, yCenter) of the circle as the zero point, and switch the X,Y coordinates (because the Y axis of the canvas points downward, which makes linear functions incorrect);
find the point nearest to the circle in the (infinite) line, if the point is not inside the line segment, get one of the end points which is closer to the point;
if the point is inside the circle, and the 2 end points are not both inside the circle, there is at least one intersection.
let ctx = document.querySelector("canvas").getContext("2d");
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
// to calculate the new position based on the new zero point at the circle center,
//where X, Y coordinates have to be switched because in Canvas Y coordinate increases downwards
let newp1y = p1X - cX, newp1x = p1Y - cY, newp2y = p2X - cX, newp2x = p2Y - cY;
// when the 2 end points are all inside the circle, there is no intersection
if((newp1x*newp1x + newp1y*newp1y < r*r) && (newp2x*newp2x + newp2y*newp2y < r*r)) {
return false;
}
// slope of the line and the slope of the perpendicular line from the circle center
let slopeL = (newp2y - newp1y) / (newp2x - newp1x), slopeC;
if(slopeL != 0){
slopeC = -1/slopeL;
}
else{
slopeC = 65535; // for a vertical line, this slope number is big enough
}
// calculate the nearest point at the straight line from the circle center
let closeX = (newp1y - slopeL*newp1x)/(slopeC - slopeL);
let closeY = closeX * slopeC;
// in this condition, the nearest point is not inside the line segment, so the end point
// which is closer to this point will be picked as the real nearest point to the circle center
if((closeX - newp1x)*(closeX - newp2x) >=0 && (closeY - newp1y)*(closeY - newp2y) >=0){
if((closeX - newp1x)*(closeX - newp2x) > 0){
if(Math.abs(closeX - newp1x) > Math.abs(closeX - newp2x)){
closeX = newp2x;
closeY = newp2y;
}
else{
closeX = newp1x;
closeY = newp1y;
}
}
else {
if(Math.abs(closeY - newp1y) > Math.abs(closeY - newp2y)){
closeX = newp2x;
closeY = newp2y;
}
else{
closeX = newp1x;
closeY = newp1y;
}
}
}
//check if the picked nearest point is inside the circle
return (closeX*closeX + closeY*closeY) < r*r;
}
let circleX = 250;
let circleY = 250;
let circleR = 50;
let lineX1 = 50;
let lineY1 = 350;
let lineX2 = 185;
let lineY2 = 250;
draw = () => {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#ffffff";
drawCircle(circleX, circleY, circleR);
drawLine(lineX1, lineY1, lineX2, lineY2);
}
console.log(lineIntersectsCircle(lineX1, lineY1, lineX2, lineY2, circleX, circleY, circleR))
draw();
canvas { display: flex; margin: 0 auto; }
<canvas width="400" height="400"></canvas>
I want to draw circular balls on an HTML canvas in a pyramid pattern.
Like this:
Fiddle where you can show me the algorithm:
https://jsfiddle.net/ofxmr17c/3/
var canvas = document.getElementById('canvas');
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext('2d');
var balls = [];
var ballsLength = 15;
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
init();
function init() {
for (var i = 0; i < ballsLength; i++) {
balls.push(new Ball());
}
render();
}
function render() {
for (var i = 1; i <= ballsLength; i++) {
if (i >= 1 && i <= 5) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius);
}
if (i >= 6 && i <= 9) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 2);
}
if (i >= 10 && i <= 12) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 3);
}
if (i >= 13 && i <= 14) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 4);
}
if (i == 15) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 5);
}
}
window.requestAnimationFrame(render);
}
canvas {
border: 1px solid #333;
}
<canvas id="canvas"></canvas>
I have Ball class with x, y and radius variables:
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Then I have method of the Ball class which draws the balls on the canvas:
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
I want to create method which will place any number of balls into a pyramid.
The live demo below shows how to pack an arbitrary number of balls into a pyramid using a bit of trigonometry. To change the amount of layers in the pyramid (and thus the number of balls), edit the NUM_ROWS variable.
This is how it looks when it's done:
Live Demo:
var canvas = document.getElementById('canvas');
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext('2d');
var balls = [];
var ballsLength = 15;
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
init();
function init() {
for (var i = 0; i < ballsLength; i++) {
balls.push(new Ball());
}
render();
}
function render() {
var NUM_ROWS = 5;
for (var i = 1; i <= NUM_ROWS; i++) {
for (var j = 0; j < i; j++) {
balls[i].draw(j * balls[0].radius * 2 + 150 - i * balls[0].radius, -(i * balls[0].radius * 2 * Math.sin(Math.PI / 3)) + 150);
}
}
//window.requestAnimationFrame(render);
}
canvas {
border: 1px solid #333;
}
<canvas id="canvas"></canvas>
JSFiddle Version: https://jsfiddle.net/ofxmr17c/6/
A billiard pyramid like this is always made with some known facts:
Each row always contains one more ball than the previous
It's an equilateral equal angled (sp? in english?) triangle which means next row always starts offset 60°
So we can make a vector (everything else in a billiard game would very much involve vectors so why not! :) ) for the direction of the next row's start point like so:
var deg60 = -60 / 180 * Math.PI; // -60°, up-right direction
var v = {
x: radius * Math.cos(deg60),
y: radius * Math.sin(deg60)
}
Then the algorithm would be (driven by total number of balls):
Start with a max limit of 1 for first row
Plot balls until max limit for row is reached
Then, add one to the max limit
Reset row count
Move position to beginning of last row + vector
Repeat until number of balls is reached
Result:
Example
var ctx = c.getContext("2d"),
radius = 9, // ball radius
deg = -60 / 180 * Math.PI, // direction of row start -60°
balls = 15, // number of balls to draw
drawn = 0, // count balls drawn on current row
rowLen = 1, // max length of current row (first=1)
x = 150, // start point
y = 140,
cx = 150, cy =140, // replicates start point + offsets
v = { // vector
x: radius * 2 * Math.cos(deg),
y: radius * 2 * Math.sin(deg)
},
i;
for(i = 0; i < balls; i++) {
drawBall(cx, cy); // draw ball
cx -= radius * 2; // move diameter of ball to left (in this case)
drawn++; // increase balls on row count
if (drawn === rowLen) { // reached max balls for row?
cx = x + v.x * rowLen; // increase one row
cy = y + v.y * rowLen;
drawn = 0; // reset ball count for row
rowLen++; // increase row limit
}
}
ctx.fillStyle = "#D70000";
ctx.fill();
function drawBall(x, y) {
ctx.moveTo(x + radius, y); ctx.arc(x, y, radius, 0, 6.28);
ctx.closePath();
}
<canvas id=c height=300></canvas>
If you want more flexibility in terms of rotation you can simply swap this line:
cx -= radius * 2;
with a vector perpendicular (calculation not shown) to the first vector so:
cx += pv.x;
cy += pv.y;
I want to calculate the radius of an inverted circle.
I managed to implement everything but, after hours of struggle, I could not find a formula to calculate the correct inverted radius.
More info about circle inversion:
http://en.wikipedia.org/wiki/Inversive_geometry
https://www.youtube.com/watch?v=sG_6nlMZ8f4
My code so far: http://codepen.io/rafaelcastrocouto/pen/Mwjdga
It seems to be working but you can easily tell it's totally wrong.
var c = $('#c'),
b = $('body'),
canvas = c[0],
ctx = canvas.getContext('2d'),
pi = Math.PI,
r = 100,
mr = 30,
width, height, hw, hh;
var setup = function() {
width = b.width();
height = b.height();
hw = width/2;
hh = height/2;
canvas.width = width;
canvas.height = height;
mid();
};
var mid = function() {
circle(hw,hh,0.25);
circle(hw,hh,r);
}
var circle = function(x,y,r) {
ctx.beginPath();
ctx.arc(x,y,r,0,pi*2);
ctx.stroke();
ctx.closePath();
};
var move = function(evt) {
var x = evt.clientX,
y = evt.clientY;
ctx.clearRect(0,0,width,height);
mid();
circle(x,y,mr);
var dx = x-hw,
dy = y-hh,
d = dist(dx,dy),
nd = r*r/d,
nx = dx*nd/d,
ny = dy*nd/d,
nr = mr*mr*pi/d; // whats the correct formula?
console.log(nr);
circle(nx+hw, ny+hh, nr);
};
var dist = function(x,y) {
return Math.pow(x*x + y*y, 1/2);
};
$(setup);
$(window).resize(setup);
$(window).mousemove(move);
Need help from the math experts!
As you said, inverting the centre of a circle doesn't give you the centre of the other one. Likewise if we invert two oposite points of one circle, it doesn't mean they'll be opposing points on the inverted circle.
Since three points describe a unique circle we can use these to find the equation for the inverse circle. That gives us the centre of the inverse circle. We can then find the distance from the centre to one of the inverted points, that's the radius.
The following c++ code gives the centre. (I don't know javascript). The function v.norm2() gives the squared norm of the vector v.
Vector2D getcircle(Vector2D p1, Vector2D p2, Vector2D p3){
Vector2D result;
long double div = 2*(p1.x*(p2.y-p3.y)-p1.y*(p2.x-p3.x)+p2.x*p3.y-p3.x*p2.y);
result.x = (p1.norm2()*(p2.y-p3.y)+p2.norm2()*(p3.y-p1.y)+p3.norm2()*(p1.y-p2.y))/div;
result.y = (p1.norm2()*(p3.x-p2.x)+p2.norm2()*(p1.x-p3.x)+p3.norm2()*(p2.x-p1.x))/div;
return result;
}
So if you have a circle c of radius r, and you are inverting respect to another circle C and radius R, you could do something like
float getRadius(Vector2D C, float R, Vector2D c, float r){
Vector2D p1 = Vector2D(c.x + r, c.y).invert(C, R);
Vector2D p2 = Vector2D(c.x - r, c.y).invert(C, R);
Vector2D p3 = Vector2D(c.x, c.y + r).invert(C, R);
return (getcircle(p1, p2, p3) - p1).norm();
}
Here is an image of a circle with centre (130, -130) and radius 128, and it's inversion respect to another circle (not shown) of centre (0, 0) and radius 40.
The red points on the big circle are polar opposites. They are then inverted and shown on the little circle where you can see they are not polar opposites.
My error was that I was assuming that the center of the inverted circle also respected OP x OP' = r2, but as the image below shows, it clearly does not. The solution was to calculate two points on the circle and reflect each one, then use half the distance between this points to find the radius.
So this is the correct code:
var c = $('#c'),
b = $('body'),
canvas = c[0],
ctx = canvas.getContext('2d'),
fixedRadius = 100,
saved = [],
width, height,
half = {
w: 0,
h: 0
},
mouse = {
r: 31,
x: 0,
y: 0
},
reflect = {
x: 0,
y: 0,
r: 0
};
var setup = function() {
width = b.width();
height = b.height();
half.w = width/2;
half.h = height/2;
canvas.width = width;
canvas.height = height;
move();
};
var mid = function() {
circle(half.w,half.h,1.5);
circle(half.w,half.h,fixedRadius);
};
var circle = function(x,y,r,c) {
ctx.strokeStyle = c || 'black';
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI*2);
ctx.stroke();
ctx.closePath();
};
var line = function(x1,y1,x2,y2,c) {
ctx.strokeStyle = c || 'black';
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
ctx.closePath();
};
var axis = function () {
line(half.w,0,half.w,height,'#ccc');
line(0,half.h,width,half.h,'#ccc');
};
var move = function(evt) {
mouse.x = evt ? evt.clientX : half.w;
mouse.y = evt ? evt.clientY : half.h + 11;
ctx.clearRect(0,0,width,height);
axis();
mid();
circle(mouse.x,mouse.y,mouse.r);
circle(mouse.x,mouse.y,1,'grey');
var di = {
x: mouse.x - half.w, // orange
y: mouse.y - half.h // green
}
di.v = dist(di.x,di.y);
var a = Math.atan2(di.y,di.x); // angle
line(mouse.x - di.x,mouse.y,mouse.x,mouse.y,'orange');
line(mouse.x,mouse.y - di.y,mouse.x,mouse.y,'green');
var p1 = {
v: di.v + mouse.r // cyan
};
p1.x = half.w + (Math.cos(a) * p1.v);
p1.y = half.h + (Math.sin(a) * p1.v);
circle(p1.x,p1.y,1.5,'cyan');
var p2 = {
v: di.v - mouse.r // red
};
p2.x = half.w+Math.cos(a)*p2.v;
p2.y = half.h+Math.sin(a)*p2.v;
circle(p2.x,p2.y,1.5,'red');
var rp1 = {
v: Math.pow(fixedRadius,2) / p1.v // cyan
};
rp1.x = Math.cos(a) * rp1.v,
rp1.y = Math.sin(a) * rp1.v;
circle(rp1.x+half.w,rp1.y+half.h,1.5,'cyan');
var rp2 = {
v: Math.pow(fixedRadius,2) / p2.v // red
};
rp2.x = Math.cos(a) * rp2.v,
rp2.y = Math.sin(a) * rp2.v;
circle(rp2.x+half.w,rp2.y+half.h,1.5,'red');
var newDi = {
v: dist(rp1.x - rp2.x, rp1.y - rp2.y)
};
newDi.r = newDi.v/2,
newDi.x = rp1.x + (Math.cos(a) * newDi.r), // yellow
newDi.y = rp1.y + (Math.sin(a) * newDi.r); // purple
if (p2.v < 0) {
newDi.x = rp1.x - (Math.cos(a) * newDi.r),
newDi.y = rp1.y - (Math.sin(a) * newDi.r);
}
reflect.x = half.w+newDi.x;
reflect.y = half.h+newDi.y
// reflected lines
if (di.v<fixedRadius) line(rp1.x+half.w,rp1.y+half.h,p1.x,p1.y,'cyan');
else line(rp2.x+half.w,rp2.y+half.h,p2.x,p2.y,'red');
line(p1.x,p1.y,half.w,half.h,'#ccc');
line(rp2.x+half.w,rp2.y+half.h,half.w,half.h,'#ccc');
line(reflect.x-newDi.x,reflect.y,reflect.x,reflect.y,'yellow');
line(reflect.x,reflect.y-newDi.y,reflect.x,reflect.y,'purple');
// reflected circle
circle(reflect.x, reflect.y, newDi.r);
circle(reflect.x,reflect.y,1,'grey');
circles(); // saved circles
reflect.r = newDi.r;
};
var dist = function(x,y) {
return Math.pow(x*x + y*y, 1/2);
};
var scroll = function(evt) {
if(evt.originalEvent.wheelDelta > 0) {
mouse.r++;
} else {
mouse.r--;
}
move(evt);
};
var click = function(evt) {
saved.push(['c',mouse.x,mouse.y,mouse.r]);
saved.push(['c',reflect.x,reflect.y,reflect.r]);
saved.push(['l',mouse.x,mouse.y,reflect.x,reflect.y]);
};
var circles = function() {
for(var i = 0; i < saved.length; i++) {
var s = saved[i];
if (s[0]=='c') circle(s[1],s[2],s[3],'grey');
if (s[0]=='l') line(s[1],s[2],s[3],s[4],'grey');
}
};
$(setup);
$(window)
.on('resize', setup)
.on('mousemove', move)
.on('mousewheel', scroll)
.on('click', click);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c"></canvas>
so i'm trying to create a drawing tool in HTML5 canvas where the weight of the stroke increases the faster you move the mouse and decreases the slower you move. I'm using ctx.lineTo() but on my first attempt noticed that if i move too quickly the change in thickness is registered as obvious square increments ( rather than a smooth increase in weight )
so i changed the ctx.lineJoin and ctx.lineCap to "round" and it got a little better
but this is still not as smooth as i'd like. i'm shooting for something like this
any advice on how to make the change in weight a bit smoother would be great! here's a working demo: http://jsfiddle.net/0fhag522/1/
and here' a preview of my "dot" object ( the pen ) and my draw function:
var dot = {
start: false,
weight: 1,
open: function(x,y){
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x,y);
},
connect: function(x,y){
ctx.lineWidth = this.weight;
ctx.lineTo(x,y);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(x,y);
},
close: function(){
ctx.closePath();
}
}
function draw(){
if(down){
if(!dot.start){
dot.close();
prevx = mx; prevy = my;
dot.open(mx,my);
dot.start=true;
}
else {
var dx = (prevx>mx) ? prevx-mx : mx-prevx;
var dy = (prevy>my) ? prevy-my : my-prevy;
dot.weight = Math.abs(dx-dy)/2;
dot.connect( mx,my );
prevx = mx; prevy = my;
}
}
}
Here is a simple function to create growing lines with a round line cap:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
// calculate direction vector of point 1 and 2
const directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate angle of perpendicular vector
const perpendicularVectorAngle = Math.atan2(directionVectorY, directionVectorX) + Math.PI/2;
// construct shape
const path = new Path2D();
path.arc(x1, y1, startWidth/2, perpendicularVectorAngle, perpendicularVectorAngle + Math.PI);
path.arc(x2, y2, endWidth/2, perpendicularVectorAngle + Math.PI, perpendicularVectorAngle);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Explanation:
The function createGrowingLine constructs a shape between two given points by:
calculating the direction vector of the two points
calculating the angle in radians of the perpendicular vector
creating a semi circle path from the calculated angle to the calculated angle + 180 degree with the center and radius of the start point
creating another semi circle path from the calculated angle + 180 degree to the calculated angle with the center and radius of the end point
closing the path by connecting the start point of the first circle with the end point of the second circle
In case you do not want to have the rounded line cap use the following function:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
const startRadius = startWidth/2;
const endRadius = endWidth/2;
// calculate direction vector of point 1 and 2
let directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate vector length
const directionVectorLength = Math.hypot(directionVectorX, directionVectorY);
// normalize direction vector (and therefore also the perpendicular vector)
directionVectorX = 1/directionVectorLength * directionVectorX;
directionVectorY = 1/directionVectorLength * directionVectorY;
// construct perpendicular vector
const perpendicularVectorX = -directionVectorY,
perpendicularVectorY = directionVectorX;
// construct shape
const path = new Path2D();
path.moveTo(x1 + perpendicularVectorX * startRadius, y1 + perpendicularVectorY * startRadius);
path.lineTo(x1 - perpendicularVectorX * startRadius, y1 - perpendicularVectorY * startRadius);
path.lineTo(x2 - perpendicularVectorX * endRadius, y2 - perpendicularVectorY * endRadius);
path.lineTo(x2 + perpendicularVectorX * endRadius, y2 + perpendicularVectorY * endRadius);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Since canvas does not have a variable width line you must draw closed paths between your line points.
However, this leaves a visible butt-joint.
To smooth the butt-joint, you can draw a circle at each joint.
Here is example code and a Demo:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
var isDown = false;
var startX;
var startY;
var PI = Math.PI;
var halfPI = PI / 2;
var points = [];
$("#canvas").mousedown(function(e) {
handleMouseDown(e);
});
function handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
mx = parseInt(e.clientX - offsetX);
my = parseInt(e.clientY - offsetY);
var pointsLength = points.length;
if (pointsLength == 0) {
points.push({
x: mx,
y: my,
width: Math.random() * 5 + 2
});
} else {
var p0 = points[pointsLength - 1];
var p1 = {
x: mx,
y: my,
width: Math.random() * 5 + 2
};
addAngle(p0, p1);
p0.angle = p1.angle;
addEndcap(p0);
addEndcap(p1);
points.push(p1);
extendLine(p0, p1);
}
}
function addAngle(p0, p1) {
var dx = p1.x - p0.x;
var dy = p1.y - p0.y;
p1.angle = Math.atan2(dy, dx);
}
function addEndcap(p) {
p.x0 = p.x + p.width * Math.cos(p.angle - halfPI);
p.y0 = p.y + p.width * Math.sin(p.angle - halfPI);
p.x1 = p.x + p.width * Math.cos(p.angle + halfPI);
p.y1 = p.y + p.width * Math.sin(p.angle + halfPI);
}
function extendLine(p0, p1) {
ctx.beginPath();
ctx.moveTo(p0.x0, p0.y0);
ctx.lineTo(p0.x1, p0.y1);
ctx.lineTo(p1.x1, p1.y1);
ctx.lineTo(p1.x0, p1.y0);
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill();
// draw a circle to cover the butt-joint
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.arc(p1.x, p1.y, p1.width, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click to add line segments.</h4>
<canvas id="canvas" width=300 height=300></canvas>