阿最近发现的一篇超好文!前一年自己曾有开发网页手绘板,如果当时有看见它就好啦!文末的两个超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.lineJoinctx.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.shadowBlurctx.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 pointvar 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 pointctx.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 pointvar 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 pointctx.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 pointvar 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 pointctx.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的createPatternapi来填充路径。以下就是一个简单的点点图案笔刷。

点点
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.2dy*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作画有无限的可能性,换个颜色换个透明度又是截然不同的风格。欢迎大家各自实践,开创更酷的效果!

【译】canvas笔触魔法师相关推荐

  1. Canvas笔触调整-8

    js部分 // 得到绘制源var c = document.getElementById('canvas');// 创建画布,建立二维视角var ctx = c.getContext('2d');// ...

  2. HTML5 Canvas和EaselJS入门(译)

    HTML5中最受开发者期待的一项新特性莫过于Canvas(画布)元素了.Canvas元素提供了一个可以动态渲染图形和位图的位图画布.它非常类似于Flash中的Bitmap和BitmapData两个类. ...

  3. [译]怎样用HTML5 Canvas制作一个简单的游戏

    这是我翻译自LostDecadeGames主页的一篇文章,原文地址:How To Make A Simple HTML5 Canvas Game. 下面是正文: 自从我制作了一些HTML5游戏(例如C ...

  4. canvas 两个圆相交重叠区域颜色填充_「译」Canvas中的环绕规则 -Winding rules in Canvas...

    前言 已经确定了未来一段时间会在Canvas相关领域深耕了,最近刚开始读fabric.js的源码并完成了3w行左右代码的首轮阅读,后续会深入了解背后的原理.在源码的阅读过程中遇到了不少问题,也解决了不 ...

  5. canvas画笔自定义笔触

    1.先上图: 2.核心代码: //<image src="./images/penStyle.ico" id="penStyle"></ima ...

  6. canvas快速入门(三)canvas实现笔触绘画案例

    实现代码 <!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8& ...

  7. 自绘动画android,(译)android利用Canvas和几何学绘制几何动画

    1 创建圆形动画 首先需要画一些同心圆,并添加动画将同心圆的半径逐渐增加,即从同心圆中心向四周扩散的动画. 需要定义一些属性包括:同心圆间隔.圆线颜色.圆线宽度: 1dp @color/black 1 ...

  8. JQ实现移动端笔触canvas电子签名

    本文主要是通过jq实现电子签名,其中ios有一个坑,已修复.基于mui+vue框架实现的,如果使用此框架,稍稍改动代码即可. 1.相关代码 1.1引入jq <script src="j ...

  9. 常见的canvas优化——模糊问题、旋转效果

    canvas常见优化方案--模糊问题.旋转效果.离屏.自定义图片尺寸 实践demo--"canvas离屏.旋转效果实践--旋转的雪花" 2017-12-18 16:27:35更新关 ...

最新文章

  1. 辽宁大连花灯闹新春 逾万民众赏灯迎新年
  2. C#调用C++写的Dll时的运行时错误解决
  3. Asp.net中的两种刷新父窗体方法
  4. 企业数据中心和互联网数据中心有何不同?
  5. ubuntu tail、history|grep 、alias命令
  6. linux应用之----进程控制理论
  7. Win11系统显示你的账户已被停用怎么办
  8. Python 动态获取对象的属性和方法(内含inspect)
  9. Android录音并输出为Mp4文件
  10. Moss 2007 升级到 Moss2010 成功但界面仍然保持07?
  11. python的文件操作os_​Python:目录和文件的操作模块os.path
  12. BackgroundWorker 简单使用教程 多个线程的创建
  13. CSDN如何修改id号
  14. 关于linux驱动管理笔记
  15. MATLAB 官方文档
  16. oppo X907刷机包 COLOROS 1.0 正式版公布 安卓4.2.2
  17. 远程软件工程师的10个最佳实践
  18. 手机远程服务器rd,手机远程连接服务器工具:RD client远程桌面使用教程
  19. termite:从零开始的go语言学习生活
  20. BeanDefinition BeanFactory Bean的关系

热门文章

  1. 内存不能为读写的解决方法
  2. md5会重复吗_自媒体平台视频重复审查机制,如何避免自己做的视频和别人的重复...
  3. mysql将大表定时转储_mysql数据库数据定时封装转储
  4. 为什么c语言要定义变量,C语言为什么要规定对所用到的变量要“先定义,后使用”...
  5. python输出指定字符串_Python输出指定字符串的方法
  6. php while for 性能,php的foreach,while,for的性能比较
  7. petapoco mysql_PetaPocoEfCoreMvc[持续更新]欢迎在github上star
  8. 网络电缆 计算机电缆,计算机电缆的技术参数
  9. cpc卡内计费信息异常包括_妈妈网广告投放怎么做、妈妈网广告|妈妈网信息流广告投放有哪些计费方式?...
  10. c swap方法在哪个库里面_swap