Reputation: 195
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>
Upvotes: 0
Views: 108
Reputation: 12891
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:
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>
Upvotes: 1