我们在上一篇文章中讲了如何绘制平滑曲线 canvas小画板——(1)平滑曲线。
透明度实现荧光笔
现在我们需要加另外一种画笔效果,带透明度的荧光笔。那可能会觉得绘制画笔的时候加上透明度就可以了。我们来在原来代码上设置
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<style>
canvas {
border: 1px solid #ccc
}
body {
margin: 0;
}
</style>
</head>
<body style="overflow: hidden;background-color: rgb(250, 250, 250);touch-action: none;">
<canvas id="c" width="1920" height="1080"></canvas>
<script>
var el = document.getElementById('c');
var ctx = el.getContext('2d');
//设置绘制线条样式
ctx.globalAlpha=0.3;
ctx.strokeStyle = 'red';
ctx.lineWidth = 10;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
var isDrawing;//标记是否要绘制
//存储坐标点
let points = [];
document.body.onpointerdown = function (e) {
console.log('pointerdown');
isDrawing = true;
points.push({ x: e.clientX, y: e.clientY });
};
document.body.onpointermove = function (e) {
console.log('pointermove');
if (isDrawing) {
draw(e.clientX, e.clientY);
}
};
document.body.onpointerup = function (e) {
if (isDrawing) {
draw(e.clientX, e.clientY);
}
points = [];
isDrawing = false;
};
function draw(mousex, mousey) {
points.push({ x: mousex, y: mousey });
ctx.beginPath();
let x = (points[points.length - 2].x + points[points.length - 1].x) / 2,
y = (points[points.length - 2].y + points[points.length - 1].y) / 2;
if (points.length == 2) {
ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
ctx.lineTo(x, y);
} else {
let lastX = (points[points.length - 3].x + points[points.length - 2].x) / 2,
lastY = (points[points.length - 3].y + points[points.length - 2].y) / 2;
ctx.moveTo(lastX, lastY);
ctx.quadraticCurveTo(points[points.length - 2].x, points[points.length - 2].y, x, y);
}
ctx.stroke();
points.slice(0, 1);
}
</script>
</body>
</html>
我们鼠标画线出来的效果如下,可以看到有很多重叠区域:

对canvas有所了解的同学,知道


解决荧光笔重叠问题
为什么会有这种重叠渲染颜色的问题呢?细细品味代码,你会发现是因为每次move的时候绘制的部分是上个鼠标点和当前鼠标点之前的连线,这样就会导致头部和尾部有重叠部分多次被stroke了。(不同连接设置的头部尾部重叠不同)
为了避免出现上述重叠这种问题下面介绍两种方法。
利用globalCompositeOperation
现在我们需要用上另外一个api方法
globalCompositeOperation,具体介绍可以看我另外一篇博文讲的比较详细(Canvas学习:globalCompositeOperation详解)。这个小画板荧光笔效果我们需要使用globalCompositeOperation=‘xor’,另外注意透明度的只能设置成0.5,其他值的话还是可以看到重叠区域。这个设置也是我不断尝试得出来的,具体为什么可以我也无法给出说法,有待研究或者知道的博友可以在评论给出答案。
1 <!doctype html> 2 <html> 3 4 <head> 5 <meta charset=utf-8> 6 <style> 7 canvas { 8 border: 1px solid #ccc 9 } 10 11 body { 12 margin: 0; 13 } 14 </style> 15 </head> 16 17 <body style="overflow: hidden;background-color: rgb(250, 250, 250);touch-action: none;"> 18 <canvas id="c" width="1920" height="1080"></canvas> 19 <script> 20 var el = document.getElementById('c'); 21 var ctx = el.getContext('2d'); 22 //设置绘制线条样式 23 ctx.strokeStyle = 'rgba(253, 58, 43, 0.5)'; 24 ctx.lineWidth = 10; 25 ctx.lineJoin = 'round'; 26 ctx.lineCap = 'round'; 27 28 var isDrawing;//标记是否要绘制 29 //存储坐标点 30 let points = []; 31 document.body.onpointerdown = function (e) { 32 console.log('pointerdown'); 33 isDrawing = true; 34 points.push({ x: e.clientX, y: e.clientY }); 35 }; 36 document.body.onpointermove = function (e) { 37 console.log('pointermove'); 38 if (isDrawing) { 39 draw(e.clientX, e.clientY); 40 } 41 42 }; 43 document.body.onpointerup = function (e) { 44 if (isDrawing) { 45 draw(e.clientX, e.clientY); 46 } 47 points = []; 48 isDrawing = false; 49 }; 50 51 function draw(mousex, mousey) { 52 points.push({ x: mousex, y: mousey }); 53 ctx.globalCompositeOperation = "xor";//使用异或操作对源图像与目标图像进行组合。 54 ctx.beginPath(); 55 let x = (points[points.length - 2].x + points[points.length - 1].x) / 2, 56 y = (points[points.length - 2].y + points[points.length - 1].y) / 2; 57 if (points.length == 2) { 58 ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y); 59 ctx.lineTo(x, y); 60 } else { 61 let lastX = (points[points.length - 3].x + points[points.length - 2].x) / 2, 62 lastY = (points[points.length - 3].y + points[points.length - 2].y) / 2; 63 ctx.moveTo(lastX, lastY); 64 ctx.quadraticCurveTo(points[points.length - 2].x, points[points.length - 2].y, x, y); 65 } 66 ctx.stroke(); 67 points.slice(0, 1); 68 69 } 70 </script> 71 </body> 72 73 </html>
如果透明度设置成0.8,画出的线条如下:

如果透明度设置成0.2,画出的线条如下:

存储坐标点
另有一种普遍做法是使用数组points存储每个点的坐标值,每次绘制前先清除画布内容,再循环points数组绘制路径,最后进行一次stroke。
这种方法每次只能保留一条线条,因为在不断的清除画布内容,如果需要保留住的话,可以扩展下points为二维数组,保留每一条线条的所有鼠标点。清除画布后遍历points数组重绘所有线条。
1 <!doctype html> 2 <html> 3 4 <head> 5 <meta charset=utf-8> 6 <style> 7 canvas { 8 border: 1px solid #ccc 9 } 10 11 body { 12 margin: 0; 13 } 14 </style> 15 </head> 16 17 <body style="overflow: hidden;background-color: rgb(250, 250, 250);touch-action: none;"> 18 <canvas id="c" width="1920" height="1080"></canvas> 19 <script> 20 var el = document.getElementById('c'); 21 var ctx = el.getContext('2d'); 22 //设置绘制线条样式 23 ctx.globalAlpha = 0.3; 24 ctx.strokeStyle = 'red'; 25 ctx.lineWidth = 10; 26 ctx.lineJoin = 'round'; 27 ctx.lineCap = 'round'; 28 var isDrawing;//标记是否要绘制 29 //存储坐标点 30 let points = []; 31 document.body.onpointerdown = function (e) { 32 console.log('pointerdown'); 33 isDrawing = true; 34 points.push({ x: e.clientX, y: e.clientY }); 35 }; 36 document.body.onpointermove = function (e) { 37 console.log('pointermove'); 38 if (isDrawing) { 39 points.push({ x: e.clientX, y: e.clientY }); 40 draw(e.clientX, e.clientY); 41 } 42 43 }; 44 document.body.onpointerup = function (e) { 45 if (isDrawing) { 46 points.push({ x: e.clientX, y: e.clientY }); 47 draw(e.clientX, e.clientY); 48 } 49 points = []; 50 isDrawing = false; 51 }; 52 53 function draw(mousex, mousey) { 54 ctx.clearRect(0, 0, 1920, 1080); 55 ctx.beginPath(); 56 for (let i = 0; i < points.length; i++) { 57 if (i == 0) 58 ctx.moveTo(points[i].x, points[i].y); 59 else { 60 let p0 = points[i]; 61 let p1 = points[i + 1]; 62 let c, d; 63 if (!p1) { 64 c = p0.x; 65 d = p0.y; 66 }else { 67 c = (p0.x + p1.x) / 2; 68 d = (p0.y + p1.y) / 2; 69 } 70 ctx.quadraticCurveTo(p0.x, p0.y, c, d); //二次贝塞曲线函数 71 } 72 } 73 ctx.stroke(); 74 } 75 </script> 76 </body> 77 78 </html>
两种解决方法对比
这两种方法都可以实现荧光笔的效果,如下截图:

第一种方法只绘制上个点和当前点,而第二种需要绘制所有线条,所以从流畅性上对比第一种有优势。但如果需要实现橡皮擦线擦除的功能第一种就满足不了了,我的一篇博文中具体介绍了橡皮擦点擦除和线擦除的实现可以参看,具体采用哪种绘制荧光笔点的方法需要根据功能需求来定!
相关文章:
