繁体   English   中英

如何使用 html canvas 和 javascript 用鼠标绘制平滑的连续线

[英]How to draw a smooth continuous line with mouse using html canvas and javascript

我正在尝试使用 html5 画布和普通 javascript 创建一个简单的绘图/绘画程序。 我让它工作正常,但是当绘制和移动鼠标太快时,线会断开连接,我最终会得到一行点 - 我怎样才能使它成为一条平滑的连续线?

建议将不胜感激! 我对 JS 很陌生,所以代码示例会非常有用,提前致谢。

当前的JS是:

 var canvas, ctx var mouseX, mouseY, mouseDown = 0 function draw(ctx,x,y,size) { ctx.fillStyle = "#000000" ctx.beginPath() ctx.arc(x, y, size, 0, Math.PI*2, true) ctx.closePath() ctx.fill() } function clearCanvas(canvas,ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height) } function onMouseDown() { mouseDown = 1 draw(ctx, mouseX, mouseY, 2) } function onMouseUp() { mouseDown = 0 } function onMouseMove(e) { getMousePos(e) if (mouseDown == 1) { draw(ctx, mouseX, mouseY, 2) } } function getMousePos(e) { if (!e) var e = event if (e.offsetX) { mouseX = e.offsetX mouseY = e.offsetY } else if (e.layerX) { mouseX = e.layerX mouseY = e.layerY } } function init() { canvas = document.getElementById('sketchpad') ctx = canvas.getContext('2d') canvas.addEventListener('mousedown', onMouseDown, false) canvas.addEventListener('mousemove', onMouseMove, false) window.addEventListener('mouseup', onMouseUp, false) } init();
 <canvas id="sketchpad" width="500" height="500"></canvas>

用鼠标绘制平滑曲线。

可悲的是,如果您希望忠于艺术家的预期路线,这并不容易。

它涉及记录整个鼠标行程。 笔触完成后,将点数减少到细节限制(由艺术家设置),然后对剩余点应用贝塞尔平滑函数。

它可以在绘制笔划时完成,但对于某些设备,如果线条变得很长,这可能会变得太多。 由于线条细节减少在显示平滑线条时会查看所有点,因此有些人不喜欢随着线条变长而略微改变的方式。

演示

下面的代码演示了我发现有用的解决方案。

  • 使用左键绘制平滑完成一键释放。
  • 使用右键以实时平滑(蓝线)进行绘制。
  • 单击鼠标中键清除。

使用顶部的两个滑块设置平滑量和细节量。 左键单击拖出一个笔划,显示原始线条。 松开鼠标后,线条会被简化、平滑并添加到背景图像中。

 var canvas = document.getElementById("canV"); var ctx = canvas.getContext("2d"); // mouse stuff var mouse = { x:0, y:0, buttonLastRaw:0, // user modified value buttonRaw:0, buttons:[1,2,4,6,5,3], // masks for setting and clearing button raw bits; }; function mouseMove(event){ mouse.x = event.offsetX; mouse.y = event.offsetY; if(mouse.x === undefined){ mouse.x = event.clientX; mouse.y = event.clientY;} if(event.type === "mousedown"){ mouse.buttonRaw |= mouse.buttons[event.which-1]; }else if(event.type === "mouseup"){mouse.buttonRaw &= mouse.buttons[event.which+2]; }else if(event.type === "mouseout"){ mouse.buttonRaw = 0; mouse.over = false; }else if(event.type === "mouseover"){ mouse.over = true; } event.preventDefault(); } canvas.addEventListener('mousemove',mouseMove); canvas.addEventListener('mousedown',mouseMove); canvas.addEventListener('mouseup' ,mouseMove); canvas.addEventListener('mouseout' ,mouseMove); canvas.addEventListener('mouseover' ,mouseMove); canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false); // Line simplification based on // the Ramer–Douglas–Peucker algorithm // referance https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm // points are and array of arrays consisting of [[x,y],[x,y],...,[x,y]] // length is in pixels and is the square of the actual distance. // returns array of points of the same form as the input argument points. var simplifyLineRDP = function(points, length) { var simplify = function(start, end) { // recursize simplifies points from start to end var maxDist, index, i, xx , yy, dx, dy, ddx, ddy, p1, p2, p, t, dist, dist1; p1 = points[start]; p2 = points[end]; xx = p1[0]; yy = p1[1]; ddx = p2[0] - xx; ddy = p2[1] - yy; dist1 = (ddx * ddx + ddy * ddy); maxDist = length; for (var i = start + 1; i < end; i++) { p = points[i]; if (ddx !== 0 || ddy !== 0) { t = ((p[0] - xx) * ddx + (p[1] - yy) * ddy) / dist1; if (t > 1) { dx = p[0] - p2[0]; dy = p[1] - p2[1]; } else if (t > 0) { dx = p[0] - (xx + ddx * t); dy = p[1] - (yy + ddy * t); } else { dx = p[0] - xx; dy = p[1] - yy; } }else{ dx = p[0] - xx; dy = p[1] - yy; } dist = dx * dx + dy * dy if (dist > maxDist) { index = i; maxDist = dist; } } if (maxDist > length) { // continue simplification while maxDist > length if (index - start > 1){ simplify(start, index); } newLine.push(points[index]); if (end - index > 1){ simplify(index, end); } } } var end = points.length - 1; var newLine = [points[0]]; simplify(0, end); newLine.push(points[end]); return newLine; } // This is my own smoothing method // It creates a set of bezier control points either 2nd order or third order // bezier curves. // points: list of points // cornerThres: when to smooth corners and represents the angle between to lines. // When the angle is smaller than the cornerThres then smooth. // match: if true then the control points will be balanced. // Function will make a copy of the points var smoothLine = function(points,cornerThres,match){ // adds bezier control points at points if lines have angle less than thres var p1, p2, p3, dist1, dist2, x, y, endP, len, angle, i, newPoints, aLen, closed, bal, cont1, nx1, nx2, ny1, ny2, np; function dot(x, y, xx, yy) { // get do product // dist1,dist2,nx1,nx2,ny1,ny2 are the length and normals and used outside function // normalise both vectors dist1 = Math.sqrt(x * x + y * y); // get length if (dist1 > 0) { // normalise nx1 = x / dist1 ; ny1 = y / dist1 ; }else { nx1 = 1; // need to have something so this will do as good as anything ny1 = 0; } dist2 = Math.sqrt(xx * xx + yy * yy); if (dist2 > 0) { nx2 = xx / dist2; ny2 = yy / dist2; }else { nx2 = 1; ny2 = 0; } return Math.acos(nx1 * nx2 + ny1 * ny2 ); // dot product } newPoints = []; // array for new points aLen = points.length; if(aLen <= 2){ // nothing to if line too short for(i = 0; i < aLen; i ++){ // ensure that the points are copied newPoints.push([points[i][0],points[i][1]]); } return newPoints; } p1 = points[0]; endP =points[aLen-1]; i = 0; // start from second poitn if line not closed closed = false; len = Math.hypot(p1[0]- endP[0], p1[1]-endP[1]); if(len < Math.SQRT2){ // end points are the same. Join them in coordinate space endP = p1; i = 0; // start from first point if line closed p1 = points[aLen-2]; closed = true; } newPoints.push([points[i][0],points[i][1]]) for(; i < aLen-1; i++){ p2 = points[i]; p3 = points[i + 1]; angle = Math.abs(dot(p2[0] - p1[0], p2[1] - p1[1], p3[0] - p2[0], p3[1] - p2[1])); if(dist1 !== 0){ // dist1 and dist2 come from dot function if( angle < cornerThres*3.14){ // bend it if angle between lines is small if(match){ dist1 = Math.min(dist1,dist2); dist2 = dist1; } // use the two normalized vectors along the lines to create the tangent vector x = (nx1 + nx2) / 2; y = (ny1 + ny2) / 2; len = Math.sqrt(x * x + y * y); // normalise the tangent if(len === 0){ newPoints.push([p2[0],p2[1]]); }else{ x /= len; y /= len; if(newPoints.length > 0){ var np = newPoints[newPoints.length-1]; np.push(p2[0]-x*dist1*0.25); np.push(p2[1]-y*dist1*0.25); } newPoints.push([ // create the new point with the new bezier control points. p2[0], p2[1], p2[0]+x*dist2*0.25, p2[1]+y*dist2*0.25 ]); } }else{ newPoints.push([p2[0],p2[1]]); } } p1 = p2; } if(closed){ // if closed then copy first point to last. p1 = []; for(i = 0; i < newPoints[0].length; i++){ p1.push(newPoints[0][i]); } newPoints.push(p1); }else{ newPoints.push([points[points.length-1][0],points[points.length-1][1]]); } return newPoints; } // creates a drawable image var createImage = function(w,h){ var image = document.createElement("canvas"); image.width = w; image.height =h; image.ctx = image.getContext("2d"); return image; } // draws the smoothed line with bezier control points. var drawSmoothedLine = function(line){ var i,p; ctx.beginPath() ctx.moveTo(line[0][0],line[0][1]) for(i = 0; i < line.length-1; i++){ p = line[i]; p1 = line[i+1] if(p.length === 2){ // linear ctx.lineTo(p[0],p[1]) }else if(p.length === 4){ // bezier 2nd order ctx.quadraticCurveTo(p[2],p[3],p1[0],p1[1]); }else{ // bezier 3rd order ctx.bezierCurveTo(p[2],p[3],p[4],p[5],p1[0],p1[1]); } } if(p.length === 2){ ctx.lineTo(p1[0],p1[1]) } ctx.stroke(); } // smoothing settings var liveSmooth; var lineSmooth = {}; lineSmooth.lengthMin = 8; // square of the pixel length lineSmooth.angle = 0.8; // angle threshold lineSmooth.match = false; // not working. // back buffer to save the canvas allowing the new line to be erased var backBuffer = createImage(canvas.width,canvas.height); var currentLine = []; mouse.lastButtonRaw = 0; // add mouse last incase not there ctx.lineWidth = 3; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.strokeStyle = "black"; ctx.clearRect(0,0,canvas.width,canvas.height); var drawing = false; // if drawing var input = false; // if menu input var smoothIt = false; // flag to allow feedback that smoothing is happening as it takes some time. function draw(){ // if not drawing test for menu interaction and draw the menus if(!drawing){ if(mouse.x < 203 && mouse.y < 24){ if(mouse.y < 13){ if(mouse.buttonRaw === 1){ ctx.clearRect(3,3,200,10); lineSmooth.angle = (mouse.x-3)/200; input = true; } }else if(mouse.buttonRaw === 1){ ctx.clearRect(3,14,200,10); lineSmooth.lengthMin = (mouse.x-3)/10; input = true; } canvas.style.cursor = "pointer"; }else{ canvas.style.cursor = "crosshair"; } if(mouse.buttonRaw === 0 && input){ input = false; mouse.lastButtonRaw = 0; } ctx.lineWidth = 0.5; ctx.fillStyle = "red"; ctx.clearRect(3,3,200,10); ctx.clearRect(3,14,200,10); ctx.fillRect(3,3,lineSmooth.angle*200,10); ctx.fillRect(3,14,lineSmooth.lengthMin*10,10); ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000" ctx.strokeRect(3,3,200,10); ctx.fillText("Smooth "+(lineSmooth.angle * (180 / Math.PI)).toFixed(0)+"deg",5,2) ctx.strokeRect(3,14,200,10); ctx.fillText("Detail "+lineSmooth.lengthMin.toFixed(0) + "pixels",5,13); }else{ canvas.style.cursor = "crosshair"; } if(!input){ ctx.lineWidth = 3; if(mouse.buttonRaw === 4 && mouse.lastButtonRaw === 0){ currentLine = []; drawing = true; backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height); backBuffer.ctx.drawImage(canvas,0,0); currentLine.push([mouse.x,mouse.y]) }else if(mouse.buttonRaw === 4){ var lp = currentLine[currentLine.length-1]; // get last point // dont record point if no movement if(mouse.x !== lp[0] || mouse.y !== lp[1] ){ currentLine.push([mouse.x,mouse.y]); ctx.beginPath(); ctx.moveTo(lp[0],lp[1]) ctx.lineTo(mouse.x,mouse.y); ctx.stroke(); liveSmooth = smoothLine( simplifyLineRDP( currentLine, lineSmooth.lengthMin ), lineSmooth.angle, lineSmooth.match ); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(backBuffer,0,0); ctx.strokeStyle = "Blue"; drawSmoothedLine(liveSmooth ); ctx.strokeStyle = "black"; } }else if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 4){ ctx.textAlign = "center" ctx.fillStyle = "red" ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5); smoothIt = true; }else if(smoothIt){ smoothIt = false; var newLine = smoothLine( simplifyLineRDP( currentLine, lineSmooth.lengthMin ), lineSmooth.angle, lineSmooth.match ); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(backBuffer,0,0); drawSmoothedLine(newLine); drawing = false; } if(mouse.buttonRaw === 1 && mouse.lastButtonRaw === 0){ currentLine = []; drawing = true; backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height); backBuffer.ctx.drawImage(canvas,0,0); currentLine.push([mouse.x,mouse.y]) }else if(mouse.buttonRaw === 1){ var lp = currentLine[currentLine.length-1]; // get last point // dont record point if no movement if(mouse.x !== lp[0] || mouse.y !== lp[1] ){ currentLine.push([mouse.x,mouse.y]); ctx.beginPath(); ctx.moveTo(lp[0],lp[1]) ctx.lineTo(mouse.x,mouse.y); ctx.stroke(); } }else if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 1){ ctx.textAlign = "center" ctx.fillStyle = "red" ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5); smoothIt = true; }else if(smoothIt){ smoothIt = false; var newLine = smoothLine( simplifyLineRDP( currentLine, lineSmooth.lengthMin ), lineSmooth.angle, lineSmooth.match ); ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(backBuffer,0,0); drawSmoothedLine(newLine); drawing = false; } } // middle button clear if(mouse.buttonRaw === 2){ ctx.clearRect(0,0,canvas.width,canvas.height); } mouse.lastButtonRaw = mouse.buttonRaw; requestAnimationFrame(draw); } draw();
 .canC { width:1000px; height:500px; border:1px black solid;}
 <canvas class="canC" id="canV" width=1000 height=500></canvas>

您可以保存最后一个位置并在最后一个点和实际点之间画一条线。

if (lastX && lastY && (x !== lastX || y !== lastY)) {
    ctx.fillStyle = "#000000";
    ctx.lineWidth = 2 * size;
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.stroke();
    // ...
   lastX = x;
   lastY = y;
}

在 mouseup 事件上将两个变量设置为零。

 var canvas, ctx var mouseX, mouseY, mouseDown = 0, lastX, lastY; function draw(ctx,x,y,size) { if (lastX && lastY && (x !== lastX || y !== lastY)) { ctx.fillStyle = "#000000"; ctx.lineWidth = 2 * size; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(x, y); ctx.stroke(); } ctx.fillStyle = "#000000"; ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI*2, true); ctx.closePath(); ctx.fill(); lastX = x; lastY = y; } function clearCanvas(canvas,ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height) } function onMouseDown() { mouseDown = 1 draw(ctx, mouseX, mouseY, 2) } function onMouseUp() { mouseDown = 0; lastX = 0; lastY = 0; } function onMouseMove(e) { getMousePos(e) if (mouseDown == 1) { draw(ctx, mouseX, mouseY, 2) } } function getMousePos(e) { if (!e) var e = event if (e.offsetX) { mouseX = e.offsetX mouseY = e.offsetY } else if (e.layerX) { mouseX = e.layerX mouseY = e.layerY } } function init() { canvas = document.getElementById('sketchpad') ctx = canvas.getContext('2d') canvas.addEventListener('mousedown', onMouseDown, false) canvas.addEventListener('mousemove', onMouseMove, false) window.addEventListener('mouseup', onMouseUp, false) } init();
 <canvas id="sketchpad" width="600" height="300"></canvas>

好问题! 我向您推荐一个站点https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API以了解更多画布 API。

我认为使用lineToarc更好。所以我希望这段代码能帮助你。

var canvas, ctx;
var mouseDown = 0, lastX, lastY;



function draw(ctx,x,y) {
  ctx.beginPath();
  ctx.moveTo(lastX,lastY);
  ctx.lineTo(x,y);
  ctx.closePath();
  ctx.stroke();
}

function clearCanvas(canvas,ctx) {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
}

function onMouseDown(e) {
  var xy = getMousePos(e);
  lastX = xy.mouseX;
  lastY = xy.mouseY;
  mouseDown = 1;
}

function onMouseUp() {
  mouseDown = 0
}

function onMouseMove(e) {
  if (mouseDown == 1) {
      var xy = getMousePos(e);
      draw(ctx, xy.mouseX, xy.mouseY);
      lastX = xy.mouseX, lastY = xy.mouseY;
  }
}

function getMousePos(e) {
    var o = {};
  if (!e)
      var e = event
  if (e.offsetX) {
      o.mouseX = e.offsetX
      o.mouseY = e.offsetY
  }
  else if (e.layerX) {
      o.mouseX = e.layerX
      o.mouseY = e.layerY
  }
  return o;
 }

function init() {
    canvas = document.getElementById('sketchpad')
    ctx = canvas.getContext('2d')
    canvas.addEventListener('mousedown', onMouseDown, false)
    canvas.addEventListener('mousemove', onMouseMove, false)
    canvas.addEventListener('mouseup', onMouseUp, false)
}
init();

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM