A recently discovered super good article! One year ago, I had to develop a web hand-painted board, if I had seen it! Don’t miss the two super 6 effects at the end of this article! P.S. each example comes with codepen, so click on the original to try it out

Exploring Canvas Drawing Techniques

———- Body split line ———-

I’ve been experimenting with different styles of web sketching – smooth strokes, Bezier curves, ink strokes, pencil strokes, prints, etc. I was so impressed with the results that I decided to put together an interactive Canvas brush tutorial to enjoy the experience. We’ll start from the basics (the very primitive mouse-moving and line-drawing strokes), to the harmonious brushstrokes, to other strokes with complex curves, weird but beautiful. This tutorial also reflects my exploration of Canvas.

I’ll give you a brief introduction to the different implementations of brushes, just know you’re doing free strokes, and then you can have fun.

Before you start, you should at least know something about canvas.

basis

Start with the basics.

Common stroke

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if(isDrawing) { ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }}; el.onmouseup =function() {
  isDrawing = false;
};
Copy the code

Listen for mousedown, Mousemove, and Mouseup events on the canvas. When mousedown, move the starting point to the (ctx.moveto) mouse click coordinates. When mousemove, connect (ctx.lineto) to the new coordinate and draw a line. Finally, on mouseup, finish drawing and set the isDrawing flag to false. It is designed to avoid drawing lines when the mouse simply moves across the canvas out of focus without any clicks. You can also listen for Mousemove events on mousedown and unlisten for Mousemove events on Mouseup, but it’s easier to set a global flag.

Smooth connection

We just started our first step. It is now possible to change the line thickness by changing ctx.lineWidth. However, the thicker the line, the more pronounced the serrated edge. Abrupt line turns can be resolved by setting ctx.lineJoin and ctx.lineCap to ’round’ (in some cases on MDN).

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if(isDrawing) { ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }}; el.onmouseup =function() {
  isDrawing = false;
};
Copy the code

Smooth edges with shadows

The edges of the corners are less jagged now. However, the main line is still serrated. Since canvas does not have a direct API for removing serrations, how can we optimize the edges?

One way is through shadows.

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.shadowBlur = 10;
  ctx.shadowColor = 'rgb(0, 0, 0)';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if(isDrawing) { ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }}; el.onmouseup =function() {
  isDrawing = false;
};
Copy the code

Just add ctx.shadowBlur and ctx.shadowColor. The edges are noticeably smoother, with jagged edges covered in shadow. But there was a small problem. Notice that the beginning of the line is usually lighter and squishy, while the end becomes darker. The effect is unique, but it’s not what we intended. What causes this?

The answer is shadow overlap. The shadow of the current stroke overlays the shadow of the previous stroke, and the more the shadow overlays, the less blur and the darker the line color. How to fix this problem?

Point-based processing

You can get around this problem by only drawing once. Instead of connecting every time we scroll, we can introduce a new approach: store the stroke coordinates in an array and redraw each time.

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 10;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (var i = 1; i < points.length; i++) {
    ctx.lineTo(points[i].x, points[i].y);
  }
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

As you can see, it’s almost the same as the first example, it’s even from beginning to end. Now we can try to shadow it

Point-based processing + shading

Smooth edges with radial gradient

Another way to smooth the edges is to use a radial gradient. Instead of a shadow effect that feels a little “fuzzy” rather than “smooth,” gradients make the color distribution more even.

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    var radgrad = ctx.createRadialGradient(
      e.clientX,e.clientY,10,e.clientX,e.clientY,20);
    
    radgrad.addColorStop(0, '# 000'); Radgrad. AddColorStop (0.5,'rgba (0,0,0,0.5)');
    radgrad.addColorStop(1, 'rgba (0,0,0,0)'); ctx.fillStyle = radgrad; ctx.fillRect(e.clientX-20, e.clientY-20, 40, 40); }}; el.onmouseup =function() {
  isDrawing = false;
};
Copy the code

But as you can see, there is an obvious problem with gradient strokes. We did this by filling the mouse movement area with a circular gradient, but when the mouse moved too fast, it resulted in tracks of disconnected points rather than straight lines with smooth edges.

The solution to this problem can be to automatically fill the space between two finishing points with additional points when the spacing between them is too large.

function distanceBetween(point1, point2) {
  return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
}
function angleBetween(point1, point2) {
  return Math.atan2( point2.x - point1.x, point2.y - point1.y );
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for (var i = 0; i < dist; i+=5) {
    
    x = lastPoint.x + (Math.sin(angle) * i);
    y = lastPoint.y + (Math.cos(angle) * i);
    
    var radgrad = ctx.createRadialGradient(x,y,10,x,y,20);
    
    radgrad.addColorStop(0, '# 000'); Radgrad. AddColorStop (0.5,'rgba (0,0,0,0.5)');
    radgrad.addColorStop(1, 'rgba (0,0,0,0)');
    
    ctx.fillStyle = radgrad;
     ctx.fillRect(x-20, y-20, 40, 40);
  }
  
  lastPoint = currentPoint;
};

el.onmouseup = function() {
  isDrawing = false;
};
Copy the code

Finally, a smooth curve!

You may have noticed a small change to the previous example. We only store the last point of the path, not all points along the entire path. Each time a connection is made, the last point is connected to the latest point in order to achieve the distance between two points. If the spacing is too large, fill it with more. The advantage of this is that you don’t have to store all the points arrays each time!

Bessel curve

Remember, instead of connecting a straight line between two points, you use bezier curves. It makes the path look more natural. To do this, replace the straight line with quadraticCurveTo and use the midpoint between the two points as the control point:

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);
  console.log(points);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};
Copy the code

Now that you have the basics, you know how to draw smooth curves. Now let’s do something even more fun

Brush effect, burr effect, hand-drawn effect

One of the tips of the brush tool is to fill the handwriting with pictures. I know from this article that there are many possibilities that can be created by filling paths.

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for (var i = 0; i < dist; i++) {
    x = lastPoint.x + (Math.sin(angle) * i) - 25;
    y = lastPoint.y + (Math.cos(angle) * i) - 25;
    ctx.drawImage(img, x, y);
  }
  
  lastPoint = currentPoint;
};
Copy the code

According to the fill picture, we can create different characteristics of the brush. This is a thick brush.

Burr effect (reverse stroke)

Each time you fill the path with an image, rotate the image randomly to get an interesting effect, similar to the burr/wreath effect shown below:

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for(var i = 0; i < dist; i++) { x = lastPoint.x + (Math.sin(angle) * i); y = lastPoint.y + (Math.cos(angle) * i); ctx.save(); ctx.translate(x, y); CTX. Scale (0.5, 0.5); ctx.rotate(Math.PI * 180 / getRandomInt(0, 180)); ctx.drawImage(img, 0, 0); ctx.restore(); } lastPoint = currentPoint; };Copy the code

Hand-drawn effect (random width)

To simulate hand-drawn effects, generate variable path widths. We still used the old method of moveTo+lineTo, but changed the line width each time we joined:

.for (var i = 1; i < points.length; i++) {
    ctx.beginPath();
    ctx.moveTo(points[i-1].x, points[i-1].y);
    ctx.lineWidth = points[i].width;
    ctx.lineTo(points[i].x, points[i].y);
    ctx.stroke();
  }
 
Copy the code

But remember, custom line width can not be too wide.

Hand-painted Effect #2(Multiple Lines)

Another implementation of the hand-drawn effect is to simulate multiple lines. We will add two more lines (hereafter called “attached lines”) to the side of the line, but of course the position will be slightly offset. You do this by choosing two random points (blue points) near the origin (green points) and connecting them, so that you get two more side lines near the original line. Is it perfect to simulate the effect of the forked tip!

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = 'purple';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if(! isDrawing)return;

  ctx.beginPath();
  
  ctx.moveTo(lastPoint.x - getRandomInt(0, 2), lastPoint.y - getRandomInt(0, 2));
  ctx.lineTo(e.clientX - getRandomInt(0, 2), e.clientY - getRandomInt(0, 2));
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x, lastPoint.y);
  ctx.lineTo(e.clientX, e.clientY);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + getRandomInt(0, 2), lastPoint.y + getRandomInt(0, 2));
  ctx.lineTo(e.clientX + getRandomInt(0, 2), e.clientY + getRandomInt(0, 2));
  ctx.stroke();
    
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmouseup = function() {
  isDrawing = false;
};
Copy the code

Thick brush effect

You can invent variations using the “multiple strokes” effect. As shown below, we increase the line width and offset the attached line a little bit from the original line to simulate the thick brush effect. The essence is the blank area of the transition section!

Cross section brush effect

If we use multiple lines with smaller offsets, we can mimic the cross-sectional brush effect of a marker. This eliminates the need to fill the path with an image and the strokes will naturally have an offset effect

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 3;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if(! isDrawing)return;

  ctx.beginPath();
  
  ctx.globalAlpha = 1;
  ctx.moveTo(lastPoint.x, lastPoint.y);
  ctx.lineTo(e.clientX, e.clientY);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x - 4, lastPoint.y - 4);
  ctx.lineTo(e.clientX - 4, e.clientY - 4);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x - 2, lastPoint.y - 2);
  ctx.lineTo(e.clientX - 2, e.clientY - 2);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + 2, lastPoint.y + 2);
  ctx.lineTo(e.clientX + 2, e.clientY + 2);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + 4, lastPoint.y + 4);
  ctx.lineTo(e.clientX + 4, e.clientY + 4);
  ctx.stroke();
    
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmouseup = function() {
  isDrawing = false;
};
Copy the code

Cross section brush with transparency

If we add more and more opacity to each line, we get an interesting effect like the one below:

Multiple line

Now that we’ve had enough practice with straight lines, can we apply some of the techniques described above to Bessel curves? Of course. Again, just shift each curve a bit from the original:

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
   
  stroke(offsetPoints(-4));
  stroke(offsetPoints(-2));
  stroke(points);
  stroke(offsetPoints(2));
  stroke(offsetPoints(4));
};

function offsetPoints(val) {
  var offsetPoints = [ ];
  for (var i = 0; i < points.length; i++) {
    offsetPoints.push({ 
      x: points[i].x + val,
      y: points[i].y + val
    });
  }
  return offsetPoints;
}

function stroke(points) {
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
}

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Multiple lines with transparency

You can also add transparency to each line in turn, which is quite elegant.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  ctx.strokeStyle = 'rgba (0,0,0,1)';
  stroke(offsetPoints(-4));
  ctx.strokeStyle = 'rgba (0,0,0,0.8)';
  stroke(offsetPoints(-2));
  ctx.strokeStyle = 'rgba (0,0,0,0.6)';
  stroke(points);
  ctx.strokeStyle = 'rgba (0,0,0,0.4)';
  stroke(offsetPoints(2));
  ctx.strokeStyle = 'rgba (0,0,0,0.2)';
  stroke(offsetPoints(4));
};

function offsetPoints(val) {
  var offsetPoints = [ ];
  for (var i = 0; i < points.length; i++) {
    offsetPoints.push({ 
      x: points[i].x + val,
      y: points[i].y + val
    });
  }
  return offsetPoints;
}

function stroke(points) {
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
}

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Printing paper

Effect of basis

Now that we’ve learned how to draw lines and curves, it’s much easier to implement a printing brush! We simply draw some kind of shape over the coordinates of each point on the mouse path, and here is what the red circle looks like:

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    ctx.beginPath();
    ctx.arc(points[i].x, points[i].y, radius, false, Math.PI * 2, false); ctx.fill(); ctx.stroke(); }}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Effect of trajectory

The image above also has several points too far apart, which can also be solved by filling in the middle points. The following will generate interesting tracks or pipe effects. You can control the spacing between the points to control the trajectory density.

Ictqs
@kangax
CodePen

Random radius and transparency

You can also add a little spice to the original recipe and make random changes to each print. For example, randomly change the radius and transparency of a print.

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ 
    x: e.clientX, 
    y: e.clientY,
    radius: getRandomInt(10, 30),
    opacity: Math.random()
  });
};
el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ 
    x: e.clientX, 
    y: e.clientY,
    radius: getRandomInt(5, 20),
    opacity: Math.random()
  });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    ctx.beginPath();
    ctx.globalAlpha = points[i].opacity;
    ctx.arc(
      points[i].x, points[i].y, points[i].radius, 
      false, Math.PI * 2, false); ctx.fill(); }}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

graphics

Since it is a print, the shape of the print can also be arbitrary. Here is a print made from the shape of a pentagram:

function drawStar(x, y) {
  var length = 15;
  ctx.save();
  ctx.translate(x, y);
  ctx.beginPath();
  ctx.rotate((Math.PI * 1 / 10));
  for (var i = 5; i--;) {
    ctx.lineTo(0, length);
    ctx.translate(0, length);
    ctx.rotate((Math.PI * 2 / 10));
    ctx.lineTo(0, -length);
    ctx.translate(0, -length);
    ctx.rotate(-(Math.PI * 6 / 10));
  }
  ctx.lineTo(0, length);
  ctx.closePath();
  ctx.stroke();
  ctx.restore();
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for(var i = 0; i < points.length; i++) { drawStar(points[i].x, points[i].y); }}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Rotating graphics

The same pentacle, if you rotate them randomly, it looks more natural.

Cspre
@kangax
CodePen

All random

If we will… Size, Angle, transparency, color and even thickness are all random, and the results are super gorgeous!

function drawStar(options) {
  var length = 15;
  ctx.save();
  ctx.translate(options.x, options.y);
  ctx.beginPath();
  ctx.globalAlpha = options.opacity;
  ctx.rotate(Math.PI / 180 * options.angle);
  ctx.scale(options.scale, options.scale);
  ctx.strokeStyle = options.color;
  ctx.lineWidth = options.width;
  for (var i = 5; i--;) {
    ctx.lineTo(0, length);
    ctx.translate(0, length);
    ctx.rotate((Math.PI * 2 / 10));
    ctx.lineTo(0, -length);
    ctx.translate(0, -length);
    ctx.rotate(-(Math.PI * 6 / 10));
  }
  ctx.lineTo(0, length);
  ctx.closePath();
  ctx.stroke();
  ctx.restore();
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

var isDrawing, points = [ ], radius = 15;

functionaddRandomPoint(e) { points.push({ x: e.clientX, y: e.clientY, angle: getRandomInt(0, 180), width: Opacity: Math. Random (), scale: getRandomInt(1, 20) / 10, color: (opacity: opacity)'rgb('+ + getRandomInt (0255)', '+ + getRandomInt (0255)', '+ + getRandomInt (0255)') ')}); } el.onmousedown =function(e) {
  isDrawing = true;
  addRandomPoint(e);
};
el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  addRandomPoint(e);
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for(var i = 0; i < points.length; i++) { drawStar(points[i]); }}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Color pixel

Don’t get bogged down in shape. Random scattered color pixels near the moving strokes, also very cute yo! Color and positioning can be random!

function drawPixels(x, y) {
  for (var i = -10; i < 10; i+= 4) {
    for (var j = -10; j < 10; j+= 4) {
      if (Math.random() > 0.5) {
        ctx.fillStyle = ['red'.'orange'.'yellow'.'green'.'light-blue'.'blue'.'purple'][getRandomInt(0,6)];
        ctx.fillRect(x+i, y+j, 4, 4);
      }
    }
  }
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};
el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  drawPixels(e.clientX, e.clientY);
  
  lastPoint = { x: e.clientX, y: e.clientY };
};
el.onmouseup = function() {
  isDrawing = false;
};
Copy the code

Design of the brush

We tried the seal effect, now let’s take a look at a different but fun technique – the pattern brush. We can use the Canvas createPatternapi to populate the path. Here is a simple dot brush.

A little
function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      patternCtx = patternCanvas.getContext('2d');

  patternCanvas.width = patternCanvas.height = dotWidth + dotDistance;

  patternCtx.fillStyle = 'red';
  patternCtx.beginPath();
  patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false);
  patternCtx.closePath();
  patternCtx.fill();
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Notice how the pattern is generated here. We first initialized a mini-canvas, drew a circle on it, and then used that canvas as a pattern to draw on the canvas we used to draw. Of course, you can also use a circle image directly, but the beauty of using a circle canvas is that you can change it as you want. We can use dynamic patterns to change the color or radius of the circles.

stripe

Based on the examples above, you can also create your own patterns, such as horizontal stripes.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = patternCanvas.height = 10;
  ctx.strokeStyle = 'green';
  ctx.lineWidth = 5;
  ctx.beginPath();
  ctx.moveTo(0, 5);
  ctx.lineTo(10, 5);
  ctx.closePath();
  ctx.stroke();
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

##### Two-color stripe

… Or a vertical two-color stripe.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = 10; patternCanvas.height = 20;
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, 5, 20);
  ctx.fillStyle = 'gold';
  ctx.fillRect(5, 0, 10, 20);
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

The colours of the rainbow

… Or multiple threads with different colors (I love this pattern!). . Anything is possible!

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = 35; patternCanvas.height = 20;
  ctx.fillStyle = 'red';
  ctx.fillRect(0, 0, 5, 20);
  ctx.fillStyle = 'orange';
  ctx.fillRect(5, 0, 10, 20);
  ctx.fillStyle = 'yellow';
  ctx.fillRect(10, 0, 15, 20);
  ctx.fillStyle = 'green';
  ctx.fillRect(15, 0, 20, 20);
  ctx.fillStyle = 'lightblue';
  ctx.fillRect(20, 0, 25, 20);
  ctx.fillStyle = 'blue';
  ctx.fillRect(25, 0, 30, 20);
  ctx.fillStyle = 'purple';
  ctx.fillRect(30, 0, 35, 20);
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

The picture

Finally, give another example of filling Bessel paths based on images. The only thing that changed was that an image was passed to createPattern.

Spray gun

How did you miss the airbrush effect? There are several ways to do it. For example, fill pixels next to the stroke point. The larger the filling radius, the thicker the effect. The more pixels you fill, the denser it becomes.

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;
var density = 50;

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    for(var i = density; i--; ) { var radius = 20; var offsetX = getRandomInt(-radius, radius); var offsetY = getRandomInt(-radius, radius); ctx.fillRect(e.clientX + offsetX, e.clientY + offsetY, 1, 1); }}}; el.onmouseup =function() {
  isDrawing = false;
};
Copy the code

Continuous spray gun

You may notice that there is a slight gap between the above method and the real airbrush effect. The real spray gun is constantly sprayed, not just when the mouse/brush is sliding. An area can be ink-jet painted at specific intervals when the mouse is pressed. In this way, the gun stays in the area longer and gets a heavier inkjet.

Craxn
@kangax
CodePen

Circular area continuous spray gun

There is room for improvement in the airbrush shown above. The real airbrush effect draws a circle instead of a rectangle, so we can also change the allocation to a circle.

Neighboring points are connected

The concept of connecting adjacent dots was created by Zefrank’s Scribble and Mr Doob’s Harmony. Spread. The idea is to connect close points along the drawing path. This creates a sketching or mesh folding effect. .

All the points are connected

You can start by adding extra strokes to the first plain line example. For each point on the path, connect it to the previous point:

el.onmousemove = function(e) {
  if(! isDrawing)return;

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (var i = 1; i < points.length; i++) {
    ctx.lineTo(points[i].x, points[i].y);
    var nearPoint = points[i-5];
    if (nearPoint) {
      ctx.moveTo(nearPoint.x, nearPoint.y);
      ctx.lineTo(points[i].x, points[i].y);
    }
  }
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Add a bit of transparency or shadow to the extra connected lines to give them a more realistic look.

Adjacent points connected

EjivI
@kangax
CodePen

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  points = [ ];
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;

  //ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
  ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
  ctx.stroke();
  
  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - points[points.length-1].x;
    dy = points[i].y - points[points.length-1].y;
    d = dx * dx + dy * dy;

    if (d < 1000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba (0,0,0,0.3)'; Ctx.moveto (points[point.length-1].x + (dx * 0.2), points[point.length-1].y + (dy * 0.2)); ctx.moveto (points[point.length-1].x + (dx * 0.2), points[point.length-1]. CTX. LineTo (points [I]. X - (dx * 0.2), points [I] y - dy * (0.2)); ctx.stroke(); }}}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

The key code for this section is:

var lastPoint = points[points.length-1];

  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - lastPoint.x;
    dy = points[i].y - lastPoint.y;
    d = dx * dx + dy * dy;

    if (d < 1000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba (0,0,0,0.3)'; Ctx.moveto (lastPoint. X + (dx * 0.2), lastPoint. Y + (dy * 0.2)); CTX. LineTo (points [I]. X - (dx * 0.2), points [I] y - dy * (0.2)); ctx.stroke(); }}Copy the code

What’s going on here?! It looks complicated, but the truth is very simple

When drawing a line, we compare the distance between the current point and all points. If the distance is less than a certain number (such as 1000 in the example) that is the adjacent point, then we connect the current point to that adjacent point. Offset the line a little by dx*0.2 and dy*0.2.

That’s it. Simple algorithms create amazing results.

Burr edge effect

Make a drop change to the above formula, so that the connection is reversed (i.e. from the current point to the adjacent point relative to the current point of the opposite adjacent point, ah is a bit of a drag!). . Add a little offset to create a burr edge effect

tmIuD
@kangax
CodePen

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  points = [ ];
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if(! isDrawing)return;

  //ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
  ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
  ctx.stroke();
  
  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - points[points.length-1].x;
    dy = points[i].y - points[points.length-1].y;
    d = dx * dx + dy * dy;

    if (d < 2000 && Math.random() > d / 2000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba (0,0,0,0.3)'; Ctx.moveto (points[point.length-1].x + (dx * 0.5), points[point.length-1].y + (dy * 0.5)); ctx.moveto (points[point.length-1].x + (dx * 0.5), points[point.length-1].y + (dy * 0.5)); CTX. LineTo (points[points.length-1].x - (dx * 0.5), points[points.length-1].y - (dy * 0.5)); ctx.stroke(); }}}; el.onmouseup =function() {
  isDrawing = false;
  points.length = 0;
};
Copy the code

Lukas has an excellent article on connecting adjacent points, if you’re interested.

So now you have the skills to draw basic graphics as well as high-end graphics. However, we have only scratched the surface in this article, canvas painting has infinite possibilities, changing color and changing transparency is a completely different style. Welcome everyone to practice separately, create more cool effect!