阿最近发现的一篇超好文!前一年自己曾有开发网页手绘板,如果当时有看见它就好啦!文末的两个超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作画有无限的可能性,换个颜色换个透明度又是截然不同的风格。欢迎大家各自实践,开创更酷的效果!