【譯】canvas筆觸魔法師
阿最近發現的一篇超好文!前一年自己曾有開發網頁手繪板,如果當時有看見它就好啦!文末的兩個超6效果千萬不要錯過喔!p.s. 原文每個例子都附帶codepen,感興趣的話可以點進原文挨個進行試驗~
原文地址: ofollow,noindex">Exploring canvas drawing techniques
----------正文分割線----------
我最近在試驗網頁手繪的不同風格—比如順滑筆觸,貝塞爾曲線筆觸,墨水筆觸,鉛筆筆觸,印花筆觸等等。結果十分讓我驚喜~於是,我決心要整理一份互動式canvas筆觸教程以饗這次經歷。我們會從基礎開始(非常原始的邊移滑鼠邊劃線的筆觸),到和諧的筆刷式筆觸,到曲線複雜,怪異但優美的其他筆觸。這篇教程也折射了我對於canvas的探索之路。
我會簡要介紹關於筆刷的不同實現方式,只要知道自己實現自由筆觸,然後就可以愉快的玩耍啦。
在開始之前,你當然至少要對canvas有所瞭解喔。
基礎
先從最基礎的方式開始。
普通筆劃
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; }; 複製程式碼

在canvas上監聽mousedown, mousemove和mouseup事件。mousedown時,將起點移至( ctx.moveTo
)滑鼠點選的座標。mousemove時,連線( ctx.lineTo
)到新座標,畫一條線。最後在mouseup時,結束繪製,並將 isDrawing
標誌設為false。它是為了避免當滑鼠沒有任何點選操作,只是單純在畫布上失焦移動時,不會劃線。你也可以在mousedown事件時監聽mousemove事件,在mouseup事件時取消監聽mousemove事件,不過設個全域性標誌的做法要來得更方便。
順滑連線
剛剛我們開始了第一步。現在則可以通過改變 ctx.lineWidth
的值來改變線條粗細啦。但是,線條越粗,鋸齒邊緣也更明顯。突兀的線條轉折處可以通過設定 ctx.lineJoin
和 ctx.lineCap
為'round'來解決(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; }; 複製程式碼

帶陰影的順滑邊緣
現在拐角處的線條鋸齒沒那麼嚴重啦。但是線條主幹部分還是有鋸齒,由於canvas並沒有直接的去除鋸齒api,所以我們要如何優化邊緣呢?
一種方式是藉助陰影。
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; }; 複製程式碼

只需加上 ctx.shadowBlur
和 ctx.shadowColor
。邊緣明顯更為順滑,鋸齒邊緣都被陰影包裹住了。但是卻有個小問題。注意到線條的開頭部分通常較淡也較糊,尾部顏色卻會變得更深。效果獨特,不過並不是我們的本意。這是由什麼引起的呢?
答案是陰影重疊。當前筆觸的陰影覆蓋了上條筆觸的陰影,陰影覆蓋得越厲害,模糊效果越弱,線條顏色也更深。該如何修正這個問題嘞?
基於點的處理
可以通過 只畫一次 來規避這類問題。與其每次在滑鼠滾動時都連線,我們可以引進一種新方式:將筆觸座標點儲存在數組裡,每次都重繪一次。
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; }; 複製程式碼

可以看到,它和第一個例子幾乎一樣,從頭到尾粗細都是均勻的。現在我們可以嘗試給它加上陰影啦~
基於點的處理+陰影

帶徑向漸變的順滑邊緣
使邊緣變得順滑的另一種處理辦法是使用徑向漸變。不像陰影效果有點“模糊”大過“順滑”的感覺,漸變讓色彩分配更加均勻。
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; }; 複製程式碼

但是如圖所示,漸變筆觸有個很明顯的問題。我們的做法是給滑鼠移動區域填充圓形漸變,但當滑鼠滑動過快時,會出現不連貫點的軌跡,而不是邊緣光滑的直線。
解決這個問題的辦法可以是當兩個落筆點間距過大時,自動用額外的點去填充之間的間距。
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; }; 複製程式碼

終於得到一條順滑的曲線啦!
你也許留意到了上例的一個小改動。我們只存了路徑的最後一個點,而不是整條路徑上的所有點。每次連線時,會從上一個點連到當前的最新點,以此來取得兩點間距。如果間距過大,則在其中填充更多點。這樣做的好處是可以不用每次都存下所有points陣列!
貝塞爾曲線
請銘記這個概念,與其在兩點間連直線,不如用貝塞爾曲線。它會讓路徑顯得更為自然。做法是將直線替換為 quadraticCurveTo
,並將兩點間的中點作為控制點:
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(); }; 複製程式碼

目前為止,你已有繪製基礎,知道如何畫順滑流暢的曲線了。接下來我們做點更好玩的~
筆刷效果,毛邊效果,手繪效果
筆刷工具的小訣竅之一是用圖片填充筆跡。我是通過這篇文章知道的,通過填充路徑的方式,能製造出多種可能性。
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; }; 複製程式碼

根據填充圖片,我們可以製造不同特色的筆刷。如上圖就是一個厚筆刷。
毛邊效果(反轉筆畫)
每次用圖片填充路徑的時候,都隨機旋轉圖片,可以得到很有趣的效果,類似下圖的毛邊/花環效果:
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; }; 複製程式碼

手繪效果(隨機寬度)
要想模擬手繪效果,那麼生成不定的路徑寬度就行了。我們依然使用 moveTo+lineTo
的老辦法,只不過每次連線時都改變線條寬度:
... 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(); } 複製程式碼

不過要記得,自定義的線條寬度可不能差距太大喔。
手繪效果#2(多線條)
手繪效果的另一種實現是模擬多線條。我們會在連線旁邊多加兩條線(下文命名為“附線”),不過位置當然會有點偏移啦。做法是在原點(綠色點)附近選兩個隨機點(藍點)並連線,這樣就在原線條附近得到另外兩條附線。是不是完美模擬了筆尖分叉的效果!

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; }; 複製程式碼

厚筆刷效果
你可以利用“多筆觸”效果發明多種變體。如下圖,我們我們增加線條寬度,並且讓附線在原線條基礎上偏移一點點,就能模擬厚筆刷效果。精髓是轉折部分的空白區域!

橫截面筆刷效果
如果我們使用多條附線,並偏移小一點,就能模擬到類似記號筆的橫截面筆刷效果。這樣無需使用圖片填充路徑,筆劃會天然有偏移的效果~
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; }; 複製程式碼

帶透明度的橫截面筆刷
如果我們在上個效果的基礎上給每條附線越來越重的透明度,我們就能得到下圖的有趣效果:

多重線
直線練習得夠多的啦,我們能否將上文介紹的幾種技巧應用於貝塞爾曲線上呢?當然。同樣只需將每條曲線在原線的基礎上偏移一點:
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; }; 複製程式碼

帶透明度的多重線
亦可以給每條線依次增加透明度,頗為優雅。
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; }; 複製程式碼

印花篇
基礎效果
既然我們已經學會了如何畫線和曲線,實現印花筆刷就更容易啦!我們只需在滑鼠路徑上每個點的座標上畫出某種圖形,以下就是紅色圈圈的效果:
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; }; 複製程式碼

軌跡效果
上圖也有幾個點間隔得太遠的問題,同樣可以通過填充中間點來解決。以下會生成有趣的軌跡或管道效果。你可以控制點間間隔,從而控制軌跡密度。
See the PenIctqs by Juriy Zaytsev (@kangax) on CodePen.

隨機半徑和透明度
還可以在原來的配方上加點料,給每個印花隨機做點修改。比方說,隨機改改印花的半徑和透明度。
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; }; 複製程式碼

圖形
既然是印花,那印花的形狀也可以隨心所欲。下圖就是由五角星形狀形成的印花:
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; }; 複製程式碼

旋轉圖形
同樣是五角星,如果讓它們隨機旋轉起來,就更顯自然。
See the PenCspre by Juriy Zaytsev (@kangax) on CodePen.

隨機一切
如果我們將…大小,角度,透明度,顏色甚至粗細都隨機起來,結果也超級絢爛!
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; function addRandomPoint(e) { points.push({ x: e.clientX, y: e.clientY, angle: getRandomInt(0, 180), width: getRandomInt(1,10), opacity: Math.random(), scale: getRandomInt(1, 20) / 10, color: ('rgb('+getRandomInt(0,255)+','+getRandomInt(0,255)+','+getRandomInt(0,255)+')') }); } 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; }; 複製程式碼

彩色畫素點
不必拘泥於形狀。就在移動筆觸附近隨機散落彩色畫素點,也很可愛喲!顏色和定位都可以是隨機的!
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; }; 複製程式碼

圖案筆刷
我們嘗試了印章效果,現在來看看另一種截然不同但也妙趣橫生的技巧—圖案筆刷。我們可以利用canvas的 createPattern
api來填充路徑。以下就是一個簡單的點點圖案筆刷。
點點
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; }; 複製程式碼

留意這裡的圖案生成方式。我們先初始化了一張迷你canvas,在上邊畫了圈圈,然後把那張canvas當成圖案繪製到真正被我們用來畫的canvas上。當然也可以直接用圈圈圖片,但是使用圈圈canvas的美妙之處就在於可以隨心所欲的改造它呀。我們可以使用動態圖案,改變圈圈的顏色或是半徑。
條紋
基於上述例子,你也可以創造點自己的圖案啦,比如橫向條紋。
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; }; 複製程式碼

#####雙色條紋
…或者是縱向雙色條紋。
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; }; 複製程式碼

彩虹
…或者是有不同顏色的多重線(我喜歡這個圖案!)。一切皆有可能!
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; }; 複製程式碼

圖片
最後,再給張基於圖片填充貝塞爾路徑的例子。唯一改變的是傳給 createPattern
的是張圖片。

噴槍
怎麼能漏了噴槍效果呢?也有幾種實現它的方式。比如在筆觸點落點旁邊填充畫素點。填充半徑越大,效果更厚重。填充畫素點越多,則更密集。
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; }; 複製程式碼

連續噴槍
你可能留意到上述方法和真實噴槍效果間還是有點差距的。真實噴槍是持續不斷的噴,而不是隻有在滑鼠/筆刷滑動的時候才噴。我們可以在滑鼠按壓某個區域時,通過特定間隔時間給該區域進行噴墨繪製。這樣,”噴槍“在某區域停留時間更長,得到的噴墨也重。
See the PenCraxn by Juriy Zaytsev (@kangax) on CodePen.

圓形區域連續噴槍
其實上圖的噴槍還有提升空間。真實噴槍效果的繪製區域是圓形而不是矩形,所以我們也可以將分配區域改為圓形區域。

鄰點相連
將毗鄰的點連起來的概念由zefrank的Scribble和doob先生的Harmony(注: 這兩連結近乎丟失在歷史的長河裡了…)普及開來。其理念是,將繪製路徑上的相近點連起來。這會創造出一種素描塗抹或是網狀摺疊效果(注:也是我覺得最6的效果了!)。
所有點相連
初始做法可以是在第一個普通連線例子的基礎上增添額外筆劃。針對路徑上的每個點,再將其和前某個點連起來:
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; }; 複製程式碼

給額外連起來的線加點透明度或是陰影,可以使它們變得更具現實風格。
相鄰點相連
See the PenEjivI by Juriy Zaytsev (@kangax) on 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[points.length-1].x + (dx * 0.2), points[points.length-1].y + (dy * 0.2)); ctx.lineTo( points[i].x - (dx * 0.2), points[i].y - (dy * 0.2)); ctx.stroke(); } } }; el.onmouseup = function() { isDrawing = false; points.length = 0; }; 複製程式碼

這部分的關鍵程式碼是:
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(); } } 複製程式碼
這裡發生了些什麼!看起來很複雜,其實道理是很簡單的喔~
當畫一條線時,我們會比較當前點與所有點的距離。如果距離小於某個數值(比如例子中的1000)即相鄰點,那麼我們就會將當前點和那一相鄰點連起來。通過 dx*0.2
和 dy*0.2
給連線加一點偏移。

就是這樣,簡單的演算法製造出驚歎的效果。
毛刺邊效果
給上式做一丟丟修改,使連線 反向 (也就是從當前點連到相鄰點相對當前點的反向相鄰點,阿有點拗口!)。再加點偏移,就能製造出毛刺邊的效果~
See the PentmIuD by Juriy Zaytsev (@kangax) on 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[points.length-1].x + (dx * 0.5), points[points.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; }; 複製程式碼

Lukas有一篇文章對實現相鄰點相連的效果做了優秀的剖析,感興趣的話可以一讀。
所以現在你已掌握畫基本圖形和高階圖形的技巧。不過我們在本文中也僅僅只是介紹了皮毛而已,使用canvas作畫有無限的可能性,換個顏色換個透明度又是截然不同的風格。歡迎大家各自實踐,開創更酷的效果!