Nick Briz
Nick Briz

Reputation: 1977

smoother lineWidth changes in canvas lineTo

so i'm trying to create a drawing tool in HTML5 canvas where the weight of the stroke increases the faster you move the mouse and decreases the slower you move. I'm using ctx.lineTo() but on my first attempt noticed that if i move too quickly the change in thickness is registered as obvious square increments ( rather than a smooth increase in weight )

first try

so i changed the ctx.lineJoin and ctx.lineCap to "round" and it got a little better

current state

but this is still not as smooth as i'd like. i'm shooting for something like this

what i'd like

any advice on how to make the change in weight a bit smoother would be great! here's a working demo: http://jsfiddle.net/0fhag522/1/

and here' a preview of my "dot" object ( the pen ) and my draw function:

    var dot = {         
        start: false,
        weight: 1,
        open: function(x,y){
            ctx.lineJoin = "round";
            ctx.lineCap = "round";
            ctx.beginPath();
            ctx.moveTo(x,y);
        },
        connect: function(x,y){
            ctx.lineWidth = this.weight;
            ctx.lineTo(x,y);
            ctx.stroke();
            ctx.closePath();
            ctx.beginPath();
            ctx.moveTo(x,y);
        },
        close: function(){
            ctx.closePath();
        }
    }

    function draw(){
        if(down){
            if(!dot.start){ 
                dot.close();
                prevx = mx;  prevy = my;
                dot.open(mx,my); 
                dot.start=true;  
            }
            else { 
                var dx = (prevx>mx) ? prevx-mx : mx-prevx;
                var dy = (prevy>my) ? prevy-my : my-prevy;
                dot.weight = Math.abs(dx-dy)/2;
                dot.connect( mx,my );
                prevx = mx;  prevy = my;
            }
        }
    }

Upvotes: 0

Views: 1653

Answers (2)

Robbendebiene
Robbendebiene

Reputation: 4879

Here is a simple function to create growing lines with a round line cap:

/* 
 * this function returns a Path2D object
 * the path represents a growing line between two given points
 */
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
  // calculate direction vector of point 1 and 2
  const directionVectorX = x2 - x1,
        directionVectorY = y2 - y1;
  // calculate angle of perpendicular vector
  const perpendicularVectorAngle = Math.atan2(directionVectorY, directionVectorX) + Math.PI/2;
  // construct shape
  const path = new Path2D();
  path.arc(x1, y1, startWidth/2, perpendicularVectorAngle, perpendicularVectorAngle + Math.PI);
  path.arc(x2, y2, endWidth/2, perpendicularVectorAngle + Math.PI, perpendicularVectorAngle);
  path.closePath();
  return path;
}

const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>

Explanation: The function createGrowingLine constructs a shape between two given points by:

  1. calculating the direction vector of the two points
  2. calculating the angle in radians of the perpendicular vector
  3. creating a semi circle path from the calculated angle to the calculated angle + 180 degree with the center and radius of the start point
  4. creating another semi circle path from the calculated angle + 180 degree to the calculated angle with the center and radius of the end point
  5. closing the path by connecting the start point of the first circle with the end point of the second circle

In case you do not want to have the rounded line cap use the following function:

/* 
 * this function returns a Path2D object
 * the path represents a growing line between two given points
 */
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
  const startRadius = startWidth/2;
  const endRadius = endWidth/2;
  // calculate direction vector of point 1 and 2
  let directionVectorX = x2 - x1,
      directionVectorY = y2 - y1;
  // calculate vector length
  const directionVectorLength = Math.hypot(directionVectorX, directionVectorY);
  // normalize direction vector (and therefore also the perpendicular vector)
  directionVectorX = 1/directionVectorLength * directionVectorX;
  directionVectorY = 1/directionVectorLength * directionVectorY;
  // construct perpendicular vector
  const perpendicularVectorX = -directionVectorY,
        perpendicularVectorY = directionVectorX;
  // construct shape
  const path = new Path2D();
  path.moveTo(x1 + perpendicularVectorX * startRadius, y1 + perpendicularVectorY * startRadius);
  path.lineTo(x1 - perpendicularVectorX * startRadius, y1 - perpendicularVectorY * startRadius);
  path.lineTo(x2 - perpendicularVectorX * endRadius, y2 - perpendicularVectorY * endRadius);
  path.lineTo(x2 + perpendicularVectorX * endRadius, y2 + perpendicularVectorY * endRadius);
  path.closePath();
  return path;
}

const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>

Upvotes: 1

markE
markE

Reputation: 105015

Since canvas does not have a variable width line you must draw closed paths between your line points.

However, this leaves a visible butt-joint.

enter image description here

To smooth the butt-joint, you can draw a circle at each joint.

enter image description here

Here is example code and a Demo:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();

var isDown = false;
var startX;
var startY;

var PI = Math.PI;
var halfPI = PI / 2;
var points = [];

$("#canvas").mousedown(function(e) {
  handleMouseDown(e);
});

function handleMouseDown(e) {
  e.preventDefault();
  e.stopPropagation();

  mx = parseInt(e.clientX - offsetX);
  my = parseInt(e.clientY - offsetY);

  var pointsLength = points.length;

  if (pointsLength == 0) {
    points.push({
      x: mx,
      y: my,
      width: Math.random() * 5 + 2
    });
  } else {
    var p0 = points[pointsLength - 1];
    var p1 = {
      x: mx,
      y: my,
      width: Math.random() * 5 + 2
    };
    addAngle(p0, p1);
    p0.angle = p1.angle;
    addEndcap(p0);
    addEndcap(p1);
    points.push(p1);
    extendLine(p0, p1);
  }
}

function addAngle(p0, p1) {
  var dx = p1.x - p0.x;
  var dy = p1.y - p0.y;
  p1.angle = Math.atan2(dy, dx);
}

function addEndcap(p) {
  p.x0 = p.x + p.width * Math.cos(p.angle - halfPI);
  p.y0 = p.y + p.width * Math.sin(p.angle - halfPI);
  p.x1 = p.x + p.width * Math.cos(p.angle + halfPI);
  p.y1 = p.y + p.width * Math.sin(p.angle + halfPI);
}

function extendLine(p0, p1) {
  ctx.beginPath();
  ctx.moveTo(p0.x0, p0.y0);
  ctx.lineTo(p0.x1, p0.y1);
  ctx.lineTo(p1.x1, p1.y1);
  ctx.lineTo(p1.x0, p1.y0);
  ctx.closePath();
  ctx.fillStyle = 'blue';
  ctx.fill();
  // draw a circle to cover the butt-joint
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);
  ctx.arc(p1.x, p1.y, p1.width, 0, Math.PI * 2);
  ctx.closePath();
  ctx.fill();
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click to add line segments.</h4>
<canvas id="canvas" width=300 height=300></canvas>

Upvotes: 0

Related Questions