roberto
roberto

Reputation: 195

reshape circle in canvas

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

Answers (1)

obscure
obscure

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:

circle 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>

Upvotes: 1

Related Questions