Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm attempting to create a simple draw/paint programme using html5 canvas and plain javascript. I've got it working ok, but when drawing and moving the mouse too fast the line disconnects and I just end up with a line of dots - how can I make this a smooth continuous line?

Advice would be much appreciated! I'm quite new to JS so code examples would be really useful, thanks in advance.

Current JS is:

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>
like image 573
mmb Avatar asked Nov 17 '16 08:11

mmb


People also ask

How do you draw a line on a mouse with a canvas?

To draw a line on the canvas, you should create a path using beginPath() method in context API. After that, you can use moveTo(x,y) method to move the drawing cursor based on x and y parameters. // To draw a static line.

How do I draw a straight line in canvas HTML?

To draw a line on a canvas, you use the following steps: First, create a new line by calling the beginPath() method. Second, move the drawing cursor to the point (x,y) without drawing a line by calling the moveTo(x, y) . Finally, draw a line from the previous point to the point (x,y) by calling the lineTo(x,y) method.


3 Answers

Drawing a smooth curve with the mouse.

Sadly it is not that easy if you wish to stay true to the artists intended line.

It involves recording the whole mouse stroke. When the stroke is complete, reduce the number of points to the detail limit (set by artist) then apply a bezier smoothing function on the remaining points.

It can be done as the stroke is drawn but for some devices this can become too much if the line becomes very long. As the line detail reduction looks at all points when showing the smoothed line live some people dont like the way it slightly changes as the line gets longer.

Demo

The code below demonstrates a solution I have found useful.

  • Use the left button to draw with smoothing done one button release.
  • Use the right button to draw with live smoothing (blue line).
  • Middle mouse button click to clear.

Use the two sliders at the top to set the amount of smoothing, and the amount of detail. Left click to drag out a stroke, the raw line is shown. When the mouse is released the line is then simplified, smoothed, and added to the background image.

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>
like image 144
Blindman67 Avatar answered Oct 03 '22 12:10

Blindman67


You could save the last position and draw a line between the last point and the actual point.

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;
}

On mouseup event set the two variables to zero.

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>
like image 45
Nina Scholz Avatar answered Oct 03 '22 14:10

Nina Scholz


Good question! And I recommend you a site https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API to learn more canvas API.

I think using lineTo is better than arc.So I hope this code will help you.

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();
like image 42
ywDing Avatar answered Oct 03 '22 13:10

ywDing