簡體   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