You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Canvas API绘制可随周围线条自适应变形的圆形

问题描述

我正在尝试使用canvas和JavaScript绘制圆形,要求该圆形可根据周围线条自适应调整形状,后续还要添加可拖拽的交互能力,保证圆形与线条发生碰撞时,形状可以自动调整适配。

  • 当前实现效果:
    当前绘制的标准正圆效果
  • 期望达成效果:
    碰撞线条后自适应形变的圆形效果

疑问:使用canvas API是否可以实现上述效果?


实现可行性结论

原生Canvas API完全可以实现该效果。现有代码仅实现了标准正圆的拖拽绘制,缺失两个核心逻辑,因此达不到预期效果:

  • 没有存储已绘制线条的几何数据:当前逻辑是直接将线条像素渲染到画布,没有留存每条线的起点、终点、线宽参数,无法进行碰撞计算
  • 没有圆形形变逻辑:当前调用arc()接口只能绘制标准正圆,碰撞边界后需要自定义路径生成逻辑才能画出适配的形变轮廓

核心改造思路

  1. 新增全局数组存储所有已绘制图形的几何信息,比如线条就存起点坐标、终点坐标、线宽,后续所有碰撞检测都基于这个数组的数据计算,不要直接读画布像素。
  2. 替换原有正圆绘制逻辑:
    • 拖拽生成圆形时,先以拖拽起点为圆心、当前拖拽距离为半径,在圆周上均匀取32~64个采样点
    • 遍历所有已存储的线条,逐点做碰撞校验:如果采样点到某条线条的距离小于「线宽+圆形半径」的安全阈值,就将采样点沿圆心方向回退,直到和线条保持安全间距
    • 所有采样点调整完成后,用二次贝塞尔曲线将点平滑连接为闭合路径,再做填充/描边,就能得到碰撞后自适应形变的圆形效果
  3. 原有快照缓存的逻辑可以直接复用,拖拽过程中先恢复快照,再重绘所有固定元素和当前形变圆形,普通场景下性能够用。

现有实现代码

JavaScript 逻辑

var canvas,
  context,
  dragging = false,
  dragStartLocation,
  snapshot;


function getCanvasCoordinates(event) {
  var x = event.clientX - canvas.getBoundingClientRect().left,
    y = event.clientY - canvas.getBoundingClientRect().top;

  return {
    x: x,
    y: y
  };
}

function takeSnapshot() {
  snapshot = context.getImageData(0, 0, canvas.width, canvas.height);
}

function restoreSnapshot() {
  context.putImageData(snapshot, 0, 0);
}

function drawLine(position) {
  context.beginPath();
  context.moveTo(dragStartLocation.x, dragStartLocation.y);
  context.lineTo(position.x, position.y);
  context.stroke();
}

function drawCircle(position) {
  var radius = Math.sqrt(Math.pow((dragStartLocation.x - position.x), 2) + Math.pow((dragStartLocation.y - position.y), 2));
  context.beginPath();
  context.arc(dragStartLocation.x, dragStartLocation.y, radius, 0, 2 * Math.PI, false);
}

function drawPolygon(position, sides, angle) {
  var coordinates = [],
    radius = Math.sqrt(Math.pow((dragStartLocation.x - position.x), 2) + Math.pow((dragStartLocation.y - position.y), 2)),
    index = 0;

  for (index = 0; index < sides; index++) {
    coordinates.push({
      x: dragStartLocation.x + radius * Math.cos(angle),
      y: dragStartLocation.y - radius * Math.sin(angle)
    });
    angle += (2 * Math.PI) / sides;
  }

  context.beginPath();
  context.moveTo(coordinates[0].x, coordinates[0].y);
  for (index = 1; index < sides; index++) {
    context.lineTo(coordinates[index].x, coordinates[index].y);
  }

  context.closePath();
}

function draw(position) {
  var fillBox = document.getElementById("fillBox"),
    shape = document.querySelector('input[type="radio"][name="shape"]:checked').value,
    polygonSides = document.getElementById("polygonSides").value,
    polygonAngle = document.getElementById('polygonAngle').value,
    linecaps = document.querySelector('input[type="radio"][name="linecaps"]:checked').value,
    globalComposite = document.querySelector('option[type="radio"][name="globalComposite"]:checked').value;

  context.globalCompositeOperation = globalComposite;
  context.lineCap = linecaps;
  if (shape === "circle") {
    drawCircle(position);
  }
  if (shape === "line") {
    drawLine(position);
  }

  if (shape === "polygon") {
    drawPolygon(position, polygonSides, polygonAngle * (Math.PI / 180));
  }
  if (shape !== 'line') {
    if (fillBox.checked) {
      context.fill();
    } else {
      context.stroke();
    }
  }
}

function dragStart(event) {
  dragging = true;
  dragStartLocation = getCanvasCoordinates(event);
  takeSnapshot();
}

function drag(event) {
  var position;
  if (dragging === true) {
    restoreSnapshot();
    position = getCanvasCoordinates(event);
    draw(position, "polygon");
  }
}

function dragStop(event) {
  dragging = false;
  restoreSnapshot();
  var position = getCanvasCoordinates(event);
  draw(position, "polygon");
}

function changeWidth(event) {
  context.lineWidth = this.value;
  event.stopPropogation();

}

function changeColor(event) {
  context.fillStyle = this.value;

  event.stopPropogation();
}

function changeStrokeColor(event) {
  context.strokeStyle = this.value;

  event.stopPropogation();
}

function changeCanvasColor(event) {
  context.save();
  context.fillStyle = document.querySelector('#canvasColor').value;
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.restore();
}

function myFunction() {
  context.clearRect(0, 0, canvas.width, canvas.height);
}

function init() {
  canvas = document.getElementById("cv0");
  context = canvas.getContext('2d');
  var strokeColor = document.getElementById('strokeColor');
  context.strokeStyle = strokeColor.value;
  var fillColor = document.getElementById('fillColor');
  context.fillStyle = fillColor.value;
  var lineWidth = document.getElementById('lineWidth');
  context.lineWidth = lineWidth.value;
  var canvasColor = document.getElementById('canvasColor');

  canvas.addEventListener('mousedown', dragStart, false);
  canvas.addEventListener('mousemove', drag, false);
  canvas.addEventListener('mouseup', dragStop, false);
  lineWidth.addEventListener('input', changeWidth, false);
  fillColor.addEventListener('input', changeColor, false);
  strokeColor.addEventListener('input', changeStrokeColor, false);
  canvasColor.addEventListener('input', changeCanvasColor, false);
  clearCanvas.addEventListener('click', myFunction, false);
}

window.addEventListener('load', init, false);

CSS 样式

#cv0 {
  border: solid gray;
}

#controls {
  margin-top: -14.45rem;
  margin-left: 25rem;
  font-family: Brooklyn, 'sans-sherif';
  font-size: 0.75rem;
}

#controls2 {
  margin-top: -15.45rem;
  margin-left: 40rem;
  font-family: Brooklyn, 'sans-sherif';
  font-size: 0.75rem;
}

#controls3 {
  margin-top: 0.01rem;
  margin-left: 0rem;
  background: hsl(42, 63%, 99%);
  color: black;
  font-family: Brooklyn, 'sans-sherif';
  font-size: 0.75rem;
}

HTML 结构

<canvas id='cv0' width=400 height=300></canvas>

<div id="controls">
  <p><label>Fill: <input id="fillBox" type="checkbox" checked="checked"></label></p>

  <div class="lightBorder">
    <p><input type="radio" name="shape" value="line" checked="checked">Line</p>
    <p><input type="radio" name="shape" value="circle">Circle</p>
    <p><input type="radio" name="shape" value="polygon">Polygon</p>
  </div>

  <p><label>Line With: <input id="lineWidth" type="range" step="1" min="3" max="20"></label></p>

  <p><label>Polygon Sides: <input id="polygonSides" type="range" step="1" min="3" max="10"></label></p>

  <p><label>Polygon Angle: <input id="polygonAngle" type="range" step="22.5" min="0" max="90"></label></p>

  <p><label>Fill Color: <input id="fillColor" type="color" value='#c84341'></label></p>
  <p><label>Stroke Color: <input id="strokeColor" type="color" value='#624c6c'></label></p>
</div>

<div id='controls2'>
  <p> LineCap Style: </p>
  <p><input type='radio' value='round' name='linecaps' id='roundshape' checked="checked">Round</p>
  <p><input type='radio' value='square' name='linecaps' id='sqhape'>Square</p>
  <p><input type='radio' value='butt' name='linecaps' id='buttshape'>Bevel</p>
  <label>Global Composite Control:</label>

  <select id='controls3'>
    <option type='radio' name='globalComposite' value='source-over' checked='checked'>source-over
      <option type='radio' name='globalComposite' value='source-in'>source-in</option>

      <option type='radio' name='globalComposite' value='source-out'>source-out</option>
      <option type='radio' name='globalComposite' value='destination-out'>destination-out</option>

      <option type='radio' name='globalComposite' value='destination-in'>destination-in</option>
      <option type='radio' name='globalComposite' value='destination-over'>destination-over</option>

      <option type='radio' name='globalComposite' value='source-atop'>source-atop</option>
      <option type='radio' name='globalComposite' value='destination-atop'>destination-atop</option>

      <option type='radio' name='globalComposite' value='xor'>xor</option>
      <option type='radio' name='globalComposite' value='lighter'>lighter</option>
      <option type='radio' name='globalComposite' value='color-burn'>color-burn</option>
  </select>

  <p><label>Canvas Color: <input id="canvasColor" type="color" value='#f7f5ec'></label>
  </p>

  <button id='clearCanvas' onclick='myFunction()'>Clear Canvas</button>

内容的提问来源于stack exchange,提问作者C Clarke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:57:13