如何使用Canvas API绘制可随周围线条自适应变形的圆形
问题描述
我正在尝试使用canvas和JavaScript绘制圆形,要求该圆形可根据周围线条自适应调整形状,后续还要添加可拖拽的交互能力,保证圆形与线条发生碰撞时,形状可以自动调整适配。
- 当前实现效果:

- 期望达成效果:

疑问:使用
canvas API是否可以实现上述效果?
实现可行性结论
原生Canvas API完全可以实现该效果。现有代码仅实现了标准正圆的拖拽绘制,缺失两个核心逻辑,因此达不到预期效果:
- 没有存储已绘制线条的几何数据:当前逻辑是直接将线条像素渲染到画布,没有留存每条线的起点、终点、线宽参数,无法进行碰撞计算
- 没有圆形形变逻辑:当前调用
arc()接口只能绘制标准正圆,碰撞边界后需要自定义路径生成逻辑才能画出适配的形变轮廓
核心改造思路
- 新增全局数组存储所有已绘制图形的几何信息,比如线条就存起点坐标、终点坐标、线宽,后续所有碰撞检测都基于这个数组的数据计算,不要直接读画布像素。
- 替换原有正圆绘制逻辑:
- 拖拽生成圆形时,先以拖拽起点为圆心、当前拖拽距离为半径,在圆周上均匀取32~64个采样点
- 遍历所有已存储的线条,逐点做碰撞校验:如果采样点到某条线条的距离小于「线宽+圆形半径」的安全阈值,就将采样点沿圆心方向回退,直到和线条保持安全间距
- 所有采样点调整完成后,用二次贝塞尔曲线将点平滑连接为闭合路径,再做填充/描边,就能得到碰撞后自适应形变的圆形效果
- 原有快照缓存的逻辑可以直接复用,拖拽过程中先恢复快照,再重绘所有固定元素和当前形变圆形,普通场景下性能够用。
现有实现代码
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
相关产品推荐
相关产品推荐

