我們在上一篇文章中講了如何繪制平滑曲線 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>
兩種解決方法對比
這兩種方法都可以實現熒光筆的效果,如下截圖:

第一種方法只繪制上個點和當前點,而第二種需要繪制所有線條,所以從流暢性上對比第一種有優勢。但如果需要實現橡皮擦線擦除的功能第一種就滿足不了了,我的一篇博文中具體介紹了橡皮擦點擦除和線擦除的實現可以參看,具體采用哪種繪制熒光筆點的方法需要根據功能需求來定!
相關文章:
