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!