内容简介:阿最近发现的一篇超好文!前一年自己曾有开发网页手绘板,如果当时有看见它就好啦!文末的两个超6效果千万不要错过喔!p.s. 原文每个例子都附带codepen,感兴趣的话可以点进原文挨个进行试验~原文地址:----------正文分割线----------
阿最近发现的一篇超好文!前一年自己曾有开发网页手绘板,如果当时有看见它就好啦!文末的两个超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 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作画有无限的可能性,换个颜色换个透明度又是截然不同的风格。欢迎大家各自实践,开创更酷的效果!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。