阿最近發現的一篇超好文!前一年自己曾有開發網頁手繪板,如果當時有看見它就好啦!文末的兩個超6效果千萬不要錯過喔!p.s. 原文每個例子都附帶codepen,感興趣的話可以點進原文挨個進行試驗~
原文地址: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 Pen Ictqs 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 Pen Cspre 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 Pen Craxn 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 Pen EjivI 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 Pen tmIuD 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作畫有無限的可能性,換個顏色換個透明度又是截然不同的風格。歡迎大家各自實踐,開創更酷的效果!