我们在上一篇文章中讲了如何绘制平滑曲线 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’,另外注意透明度的设置不要使用context.globalAlpha,在设置strokeStyle的时候用rgba设置透明度颜色。这个设置也是我不断尝试得出来的,具体为什么可以我也无法给出说法,有待研究或者知道的博友可以在评论给出答案。
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>
存储坐标点
另有一种普遍做法是使用数组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>
两种解决方法对比
这两种方法都可以实现荧光笔的效果,如下截图:
第一种方法只绘制上个点和当前点,而第二种需要绘制所有线条,所以从流畅性上对比第一种有优势。但如果需要实现橡皮擦的功能第一种就满足不了了,我的一篇博文中具体介绍了橡皮擦的实现可以参看