javascript - 在 Canvas 上 reshape 圆圈

标签 javascript html html5-canvas

您好,我遇到了 Canvas 问题。

我尝试制作一个可以 reshape 的圆圈,如 this 。 在演示中,可以 reshape 圆圈的形状

问题是拖放圆点来 reshape 它的形状。

我知道如何在 JavaScript Canvas 中拖放点,但如何 reshape 圆线以跟随该点。

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>

最佳答案

正如您已经意识到的那样,一个圆可以由四个贝塞尔曲线组成。我将使用三次而不是二次,因为它提供了两个控制点。

让我们首先看下面的插图:

circle illustration

我们可以看到红色曲线由起点A、终点B和两个控制点c1c2组成分别。

因此,如果我们想要在 x、y 处有一个半径为 r 的圆,我们可以这样说:

Ax = x ; Ay = y - r

Bx = x + r ; By = y

c1x = x + r/2 ; c1y = y - r

c2x = x + r ; c2y = y - r/2

当然可以用同样的方式构造缺失的三条曲线。

从上图中我们还可以看到,红色线段的起点也是橙色线段的终点。同样,橙色线段的控制点 c8 连接到红色线段的起点。

因此,如果我们要移动点 A,我们需要移动橙色线段的终点、红色线段的起点以及两个控制点 c8 和 <强>c1。

为此,我将编写一个通用的 Arc 类,其中包含起点、终点、两个控制点以及起点所连接的弧。然后事情是这样的:

  • 如果有人点击 A、B、C 或 D 点,则存储当前鼠标位置
  • 存储圆弧控制点的位置以及连接的圆弧控制点
  • 如果鼠标移动,则相对于鼠标移动移动起点及其控制点以及所连接圆弧的终点及其控制点
  • 重新绘制圆圈

这是一个例子:

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>

关于javascript - 在 Canvas 上 reshape 圆圈,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60309267/

相关文章:

html - 如何使用 html/css 和半彩色边框在矩形中仅显示垂直侧边栏

javascript - 如何将嵌入式 YouTube 视频的帧(通过 iframe)写入 Canvas ?

javascript - 如何关闭手机屏幕的AOS动画?

java - 如何在Struts中使用actionform设置文本框值

javascript - Selenium-webdriver (Java Script) 等待元素消失

javascript - Highcharts donut 派/图例位置

javascript - jQuery 将复选框选择转换为数组(未选中时将其设为空)

javascript - 没有为 React 按钮设置背景图像

javascript - 使用 html5 canvas 混合两个图像

javascript - 如何使用 Javascript 从纹理图集中获取图像?