Rough.js是一个手绘格调的图形库,提供了一些根本图形的绘制能力,比方:
尽管笔者是个糙汉子,然而对这种可恶的货色都没啥抵抗力,这个库的应用自身很简略,没什么好说的,然而它只有绘制能力,没有交互能力,所以应用场景无限,先来用它画个示例图形:
import rough from 'roughjs/bundled/rough.esm.js'
this.rc = rough.canvas(this.$refs.canvas)
this.rc.rectangle(100, 150, 300, 200, {
fillweight: 0,
roughness: 3
})
this.rc.circle(195, 220, 40, {
fill: 'red'
})
this.rc.circle(325, 220, 40, {
fill: 'red'
})
this.rc.rectangle(225, 270, 80, 30, {
fill: 'red',
fillweight: 5
})
this.rc.line(200, 150, 150, 80, { roughness: 5 })
this.rc.line(300, 150, 350, 80, { roughness: 2 })
成果如下:
是不是有点蠢萌,本文的次要内容是带大家手动实现下面的图形,最终成果预览:http://lxqnsys.com/#/demo/handPaintedStyle。话不多说,代码见。
线段
万物基于线段,所以先来看线段怎么画,认真看上图会发现手绘版线段其实是用两根蜿蜒的线段组成的,曲线能够应用贝塞尔曲线来画,这里应用三次贝塞尔曲线,那么剩下的问题就是求终点、起点、两个控制点的坐标了。
贝塞尔曲线能够在这个网站上尝试:https://cubic-bezier.com/。
首先一条线段的终点和起点咱们都给它加一点随机值,随机值比方就在[-2,2]之间,也能够把这个范畴和线段的长度关联起来,比方线段越长,随机值就越大。
// 直线变曲线
_line (x1, y1, x2, y2) {
let result = []
// 起始点
result[0] = x1 + this.random(-this.offset, this.offset)
result[1] = y1 + this.random(-this.offset, this.offset)
// 起点
result[2] = x2 + this.random(-this.offset, this.offset)
result[3] = y2 + this.random(-this.offset, this.offset)
}
接下来就是两个控制点,咱们把控制点限定在线段所在的矩形内:
_line (x1, y1, x2, y2) {
let result = []
// 起始点
// ...
// 起点
// ...
// 两个控制点
let xo = x2 - x1
let yo = y2 - y1
let randomFn = (x) => {
return x > 0 ? this.random(0, x) : this.random(x, 0)
}
result[4] = x1 + randomFn(xo)
result[5] = y1 + randomFn(yo)
result[6] = x1 + randomFn(xo)
result[7] = y1 + randomFn(yo)
return result
}
而后把下面生成的曲线绘制进去:
// 绘制手绘线段
line (x1, y1, x2, y2) {
this.drawDoubleLine(x1, y1, x2, y2)
}
// 绘制两条曲线
drawDoubleLine (x1, y1, x2, y2) {
// 绘制生成的两条曲线
let line1 = this._line(x1, y1, x2, y2)
let line2 = this._line(x1, y1, x2, y2)
this.drawLine(line1)
this.drawLine(line2)
}
// 绘制单条曲线
drawLine (line) {
this.ctx.beginPath()
this.ctx.moveTo(line[0], line[1])
// bezierCurveTo办法前两个点为控制点,第三个点为完结点
this.ctx.bezierCurveTo(line[4], line[5], line[6], line[7], line[2], line[3])
this.ctx.strokeStyle = '#000'
this.ctx.stroke()
}
成果如下:
然而多试几次就会发现偏离太远、蜿蜒水平过大:
齐全不像一个手失常的人能画进去的,去下面的贝塞尔曲线网站上试几次会发现两个控制点离线段越近,曲线蜿蜒水平越小:
所以咱们要找线段左近的点作为控制点,首先随机一个横坐标点,而后能够计算出线段上该横坐标对应的纵坐标点,把该纵坐标点加减一点随机值即可。
_line (x1, y1, x2, y2) {
let result = []
// ...
// 两个控制点
let c1 = this.getNearRandomPoint(x1, y1, x2, y2)
let c2 = this.getNearRandomPoint(x1, y1, x2, y2)
result[4] = c1[0]
result[5] = c1[1]
result[6] = c2[0]
result[7] = c2[1]
return result
}
// 计算两个点连成的线段上左近的一个随机点
getNearRandomPoint (x1, y1, x2, y2) {
let xo, yo, rx, ry
// 垂直x轴的线段非凡解决
if (x1 === x2) {
yo = y2 - y1
rx = x1 + this.random(-2, 2)// 在横坐标左近找一个随机点
ry = y1 + yo * this.random(0, 1)// 在线段上找一个随机点
return [rx, ry]
}
xo = x2 - x1
rx = x1 + xo * this.random(0, 1)// 找一个随机的横坐标
ry = ((rx - x1) * (y2 - y1)) / (x2 - x1) + y1// 通过两点式求出直线方程
ry += this.random(-2, 2)// 纵坐标加一点随机值
return [rx, ry]
}
看一下成果:
当然和Rough.js
比起来还是不够好,有趣味的能够自行去看一下源码,反正笔者是看不懂,控制变量太多,还没有正文。
多边形&矩形
多边形就是把多个点首尾相连起来,遍历顶点调用绘制线段的办法即可:
// 绘制手绘多边形
polygon (points = [], opt = {}) {
if (points.length < 3) {
return
}
let len = points.length
for (let i = 0; i < len - 1; i++) {
this.line(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1])
}
// 首尾相连
this.line(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1])
}
矩形是多边形的一种非凡状况,四个角都是直角,个别传参为左上角顶点的x坐标、y坐标、矩形的宽、矩形的高:
// 绘制手绘矩形
rectangle (x, y, width, height, opt = {}) {
let points = [
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height]
]
this.polygon(points, opt)
}
圆
圆要怎么解决呢,首先大家都晓得圆是能够应用多边形来近似失去的,只有多边形的边足够多,那么看起来就足够圆,既然不想要太圆,那就把它复原成多边形好了,多边形下面曾经讲过了。复原成多边形很简略,比方咱们要把一个圆变成十边形(具体还原成几边形你也能够和圆的周长关联起来),那么每个边对应的弧度就是2*Math.PI/10
,而后应用Math.cos
和Math.sin
来计算顶点的地位,最初再调用绘制多边形的办法进行绘制:
// 绘制手绘圆
circle (x, y, r) {
let stepCount = 10
let step = (2 * Math.PI) / stepCount
let points = []
for (let angle = 0; angle < 2 * Math.PI; angle += step) {
let p = [
x + r * Math.cos(angle),
y + r * Math.sin(angle)
]
points.push(p)
}
this.polygon(points)
}
成果如下:
能够看到成果很个别,就算边的数量再多一点看起来也不像:
如果间接用失常的线段连起来,那齐全就是个正经多边形了,必定也不行,所以外围是把线段变成随机弧形,首先为了减少随机性,咱们把圆的半径和各个顶点都加一点随机增量:
circle (x, y, r) {
let stepCount = 10
let step = (2 * Math.PI) / stepCount
let points = []
let rx = r + this.random(-r * 0.05, r * 0.05)
let ry = r + this.random(-r * 0.05, r * 0.05)
for (let angle = 0; angle < 2 * Math.PI; angle += step) {
let p = [
x + rx * Math.cos(angle) + this.random(-2, 2),
y + ry * Math.sin(angle) + this.random(-2, 2)
]
points.push(p)
}
}
接下来的问题又变成了计算贝塞尔曲线的两个控制点,首先因为弧线必定是要往多边形外凸的,依据贝塞尔曲线的性质,两个控制点肯定是在线段的里面,间接用线段自身的两个端点来计算的话我试了一下,比拟难解决,不同的角度可能都须要非凡解决,所以咱们参考Rough.js
距离一个点:
比方上图的多边形咱们轻易找一个线段bc
,对于点b
来说上一个点是a
,下一个点是c
,b
点别离加上c
减a
的横坐标纵坐标之差,失去了控制点c1
,其余点也是一样,最初算进去的控制点都会在里面,当初还差一个控制点,咱们不要让点c
闲着,也给它加上前后两点之差:
能够看到点c
的控制点c2
和c1
都在同一侧,这样画进去的曲线显然是朝一个方向的:
咱们让它对称一下,让点c
的前一个点减后一个点:
这样画进去的曲线依然不行:
起因很简略,控制点离的太远了,所以咱们少加一点差值,最初代码如下:
circle (x, y, r) {
// ...
let len = points.length
this.ctx.beginPath()
// 门路的终点移到第一个点
this.ctx.moveTo(points[0][0], points[0][1])
this.ctx.strokeStyle = '#000'
for (let i = 1; i + 2 < len; i++) {
let c1, c2, c3
let point = points[i]
// 控制点1
c1 = [
point[0] + (points[i + 1][0] - points[i - 1][0]) / 5,
point[1] + (points[i + 1][1] - points[i - 1][1]) / 5
]
// 控制点2
c2 = [
points[i + 1][0] + (point[0] - points[i + 2][0]) / 5,
points[i + 1][1] + (point[1] - points[i + 2][1]) / 5
]
c3 = [points[i + 1][0], points[i + 1][1]]
this.ctx.bezierCurveTo(
c1[0],
c1[1],
c2[0],
c2[1],
c3[0],
c3[1]
)
}
this.ctx.stroke()
}
咱们只加差值的五分之一,我试了一下,5-7
之间最天然,Rough.js
加的是六分之一。
事件到这里并没有完结,首先这个圆还有个缺口,起因很简略,i + 2 < len
的循环条件导致最初一个点没连上,另外首尾也没有相连,此外结尾一段很不天然,太直了,起因是咱们门路的终点是从第一个点开始的,然而咱们的第一段曲线的完结点曾经是第三个点了,所以先把门路的终点移到第二个点:
this.ctx.moveTo(points[1][0], points[1][1])
这样缺口就更大了:
红色的代表前两个点,蓝色的是最初一个点,为了要连到第二个点咱们须要把顶点列表里的前三个点追加到列表最初:
// 把前三个点追加到列表最初
points.push([points[0][0], points[0][1]], [points[1][0], points[1][1]], [points[2][0], points[2][1]])
let len = points.length
this.ctx.beginPath()
// ...
成果如下:
问题又来了,应该没有人能徒手把圆的首尾白璧无瑕的连上,所以加的第二个点咱们不能让它和原来的点截然不同,得加点偏移:
let end = [] // 解决最初一个连线点,让它和本来的点来点随机偏移
let radRandom = step * this.random(0.1, 0.5)// 让该点超前一点,代表画过头了,也能够来点正数,代表差一点才连上,然而比拟丑
end[0] = x + rx * Math.cos(step + radRandom)// 要连的最初一个点实际上是列表里的第二个点,所以角度是step而不是0
end[1] = y + ry * Math.sin(step + radRandom)
points.push(
[points[0][0], points[0][1]],
[end[0], end[1]],
[points[2][0], points[2][1]]
)
let len = points.length
this.ctx.beginPath()
//...
最初一个要优化的点是终点或者说起点地位,一般来说咱们徒手画圆都是从下面开始画,因为0度是在x轴正轴方向,所以咱们减去Math.PI/2
左右就能把终点移到上方,最初残缺的代码如下:
drawCircle (x, y, r) {
// 圆变多边形
let stepCount = 10
let step = (2 * Math.PI) / stepCount// 多边形的一条边对应的角度
let startOffset = -Math.PI / 2 + this.random(-Math.PI / 4, Math.PI / 4)// 终点偏移角度
let points = []
let rx = r + this.random(-r * 0.05, r * 0.05)
let ry = r + this.random(-r * 0.05, r * 0.05)
for (let angle = startOffset; angle < (2 * Math.PI + startOffset); angle += step) {
let p = [
x + rx * Math.cos(angle) + this.random(-2, 2),
y + ry * Math.sin(angle) + this.random(-2, 2)
]
points.push(p)
}
// 线段变曲线
let end = [] // 解决最初一个连线点,让它和本来的点来点随机偏移
let radRandom = step * this.random(0.1, 0.5)
end[0] = x + rx * Math.cos(startOffset + step + radRandom)
end[1] = y + ry * Math.sin(startOffset + step + radRandom)
points.push(
[points[0][0], points[0][1]],
[end[0], end[1]],
[points[2][0], points[2][1]]
)
let len = points.length
this.ctx.beginPath()
this.ctx.moveTo(points[1][0], points[1][1])
this.ctx.strokeStyle = '#000'
for (let i = 1; i + 2 < len; i++) {
let c1, c2, c3
let point = points[i]
let num = 6
c1 = [
point[0] + (points[i + 1][0] - points[i - 1][0]) / num,
point[1] + (points[i + 1][1] - points[i - 1][1]) / num
]
c2 = [
points[i + 1][0] + (point[0] - points[i + 2][0]) / num,
points[i + 1][1] + (point[1] - points[i + 2][1]) / num
]
c3 = [points[i + 1][0], points[i + 1][1]]
this.ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], c3[0], c3[1])
}
this.ctx.stroke()
}
最初的最初,也能够和下面的线段一样画两次,综合成果如下:
圆搞定了,椭圆也相似,毕竟圆是椭圆的一种非凡状况,顺带提一下,椭圆的近似周长公式如下:
填充
款式1
先来看一种比较简单的填充:
下面咱们绘制的矩形四条边是断开的,门路不闭合不能间接调用canvas
的fill
办法,所以须要把这四段曲线首尾连起来:
// 绘制手绘多边形
polygon (points = [], opt = {}) {
if (points.length < 3) {
return
}
// 加上填充办法
let lines = this.closeLines(points)
this.fillLines(lines, opt)
// 描边
let len = points.length
// ...
}
closeLines
办法用来把顶点闭合成曲线:
// 把多边形的顶点转换成首尾相连的闭合线段
closeLines (points) {
let len = points.length
let lines = []
let lastPoint = null
for (let i = 0; i < len - 1; i++) {
// _line办法上文曾经实现了,把直线段转换成曲线
let arr = this._line(
points[i][0],
points[i][1],
points[i + 1][0],
points[i + 1][1]
)
lines.push([
lastPoint ? lastPoint[2] : arr[0], // 上一个点存在则应用上一个点的起点来作为该点的终点
lastPoint ? lastPoint[3] : arr[1],
arr[2],
arr[3],
arr[4],
arr[5],
arr[6],
arr[7]
])
lastPoint = arr
}
// 首尾闭合
let arr = this._line(
points[len - 1][0],
points[len - 1][1],
points[0][0],
points[0][1]
)
lines.push([
lastPoint ? lastPoint[2] : arr[0],
lastPoint ? lastPoint[3] : arr[1],
lines[0][0], // 起点是第一条线段的终点
lines[0][1],
arr[4],
arr[5],
arr[6],
arr[7]
])
return lines
}
线段有了,只有遍历线段绘制进去最初调用fill
办法即可:
// 填充多边形
fillLines (lines, opt) {
this.ctx.beginPath()
this.ctx.fillStyle = opt.fillStyle
for (let i = 0; i + 1 < lines.length; i++) {
let line = lines[i]
if (i === 0) {
this.ctx.moveTo(line[0], line[1])
}
this.ctx.bezierCurveTo(
line[4],
line[5],
line[6],
line[7],
line[2],
line[3]
)
}
this.ctx.fill()
}
成果如下:
圆就更简略了,自身差不多就是闭合的,只有咱们把最初一个点的非凡解决逻辑给去掉就行了:
// 上面几行代码都给去掉,应用本来的点即可
let end = []
let radRandom = step * this.random(0.1, 0.5)
end[0] = x + rx * Math.cos(startOffset + step + radRandom)
end[1] = y + ry * Math.sin(startOffset + step + radRandom)
款式2
第二种填充会略微简单一点,比方上面这种最简略的填充,其实就是一些歪斜的线段,但问题是这些线段的端点怎么确定,矩形当然能够暴力的算进去,然而不规则的多边形怎么办,所以须要找到一个通用的办法。
填充最暴力的办法就是判断每个点是否在多边形外部,然而这样的计算量太大,我查了一下多边形填充的思路,大略有两种算法:扫描线填充和种子填充,扫描线填充更风行,Rough.js
用的也是这种办法,所以接下来介绍一下这个算法。
扫描线填充很简略,就是一条扫描线(水平线)从多边形的底部开始往上扫描,那么每条扫描线都会和多边形有交点,同一条扫描线和多边形的各个交点之间的区域就是咱们要填充的,那么问题来了,怎么确定交点,以及怎么判断两个交点之间属于多边形外部。
对于交点的计算,首先咱们交点的y
坐标是已知的,就是扫描线的y
坐标,那么只要求出x
,晓得线段的两个端点坐标,那么能够求出直线方程,而后再计算,然而有一种更简略的办法,就是利用边的相关性,也就是晓得了线段上的某一点,其相邻的点能够轻松的依据该点求出,上面是推导过程:
// 设直线方程
y = kx + b
// 设两点:c(x3, y3),d点的y坐标为c点y坐标+1,d(x4, y3 + 1),那么要求出x4
y3 = kx3 + b// 1
y3 + 1 = kX4 + b// 2
// 1式代入2式
kx3 + b + 1 = kX4 + b
kx3 + 1 = kX4// 约去b
X4 = x3 + 1 / k// 两边同时除k
// 所以y坐标+1,x坐标为上一个点的x坐标加上直线斜率的倒数
// 多边形的线段是已知两个点的,假如为a(x1, y1)、b(x2, y2),那么斜率k如下:
k = (y2 - y1) /
// 斜率的倒数也就是
1/k = (x2 - x1) / (y2 - y1)
这样咱们从线段的一个端点开始,能够挨个计算出线段上的所有点。
具体的算法介绍和推导过程能够看一下这个PPT:https://wenku.baidu.com/view/4ee141347c1cfad6195fa7c9.html,接下来间接来看算法的实现过程。
先简略介绍一下几个名词:
1.边表ET
边表ET,一个数组,外面保留了多边形所有边的信息,每条边保留的信息有:该边y的最大值ymax
和最小值ymin
、该边最低点的x值xi
、该边斜率的倒数dx
。边按ymin
递增排序,ymin
雷同则按xi
递增,xi
也雷同则只能看ymax
,如果ymax
还雷同,阐明两条边重合了,如果不重合,则按yamx
递增排序。
2.流动边表AET
也是一个数组,外面保留着与以后扫描线相交的边信息,随着扫描线的扫描会发生变化,删除不相交的,增加新相交的。该表里的边按xi
递增排序。
比方上面的多边形ET
表程序为:
// ET
[p1p5, p1p2, p5p4, p2p3, p4p3]
上面是具体的算法步骤:
1.依据多边形的顶点数据创立ET
表edgeTable
,按上述程序排序;
2.创立一个空的AET
表activeEdgeTable
;
3.开始扫描,扫描线的y
=多边形的最低点的y
值,也就是activeEdgeTable[0].ymin
;
4.反复上面步骤,直到ET
表和AET
表都为空:
(1)从ET
表里取出与以后扫描线相交的边,增加到AET
表里,同样按下面提到的程序排序
(2)成对取出AET
表里的边信息的xi
值,在每对之间进行填充
(3)从AET
表里删除以后曾经扫描到最初的边,即y >= ymax
(4)更新AET
表里剩下的边信息的xi
,即xi = xi + dx
(5)更新扫描线的y
,即y = y + 1
看着并不难,接下来转化成代码,先创立一下边表ET
:
// 创立排序边表ET
createEdgeTable (points) {
// 边表ET
let edgeTable = []
// 将第一个点复制一份到队尾,用来闭合多边形
let _points = points.concat([[points[0][0], points[0][1]]])
let len = _points.length
for (let i = 0; i < len - 1; i++) {
let p1 = _points[i]
let p2 = _points[i + 1]
// 过滤掉平行于x轴的线段,详见上述PPT链接
if (p1[1] !== p2[1]) {
let ymin = Math.min(p1[1], p2[1])
edgeTable.push({
ymin,
ymax: Math.max(p1[1], p2[1]),
xi: ymin === p1[1] ? p1[0] : p2[0], // 最低顶点的x值
dx: (p2[0] - p1[0]) / (p2[1] - p1[1]) // 线段的斜率的倒数
})
}
}
// 对边表进行排序
edgeTable.sort((e1, e2) => {
// 按ymin递增排序
if (e1.ymin < e2.ymin) {
return -1
}
if (e1.ymin > e2.ymin) {
return 1
}
// ymin雷同则按xi递增
if (e1.xi < e2.xi) {
return -1
}
if (e1.xi > e2.xi) {
return 1
}
// xi也雷同则只能看ymax
// ymax还雷同,阐明两条边重合
if (e1.ymax === e2.ymax) {
return 0
}
// 如果不重合,则按yamx递增排序
if (e1.ymax < e2.ymax) {
return -1
}
if (e1.ymax > e2.ymax) {
return 1
}
})
return edgeTable
}
接下来进行扫描操作:
scanLines (points) {
if (points.length < 3) {
return []
}
let lines = []
// 创立排序边表ET
let edgeTable = this.createEdgeTable(points)
// 流动边表AET
let activeEdgeTable = []
// 开始扫描,从多边形的最低点开始
let y = edgeTable[0].ymin
// 循环的起点是两个表都为空
while (edgeTable.length > 0 || activeEdgeTable.length > 0) {
// 从ET表里把以后扫描线的边增加到AET表里
if (edgeTable.length > 0) {
// 将以后ET表里和扫描线相交的边增加到AET表里
for (let i = 0; i < edgeTable.length; i++) {
// 如果扫描线的距离加大,可能高下差比拟小的线段会被整个间接跳过,导致死循环,须要思考到这种状况
if (edgeTable[i].ymin <= y && edgeTable[i].ymax >= y || edgeTable[i].ymax < y) {
let removed = edgeTable.splice(i, 1)
activeEdgeTable.push(...removed)
i--
}
}
}
// 从AET表里删除y=ymax的记录
activeEdgeTable = activeEdgeTable.filter((item) => {
return y < item.ymax
})
// 按xi从小到大排序
activeEdgeTable.sort((e1, e2) => {
if (e1.xi < e2.xi) {
return -1
} else if (e1.xi > e2.xi) {
return 1
} else {
return 0
}
})
// 如果存在流动边,则填充流动边之间的区域
if (activeEdgeTable.length > 1) {
// 每次取两个边进去进行填充
for (let i = 0; i + 1 < activeEdgeTable.length; i += 2) {
lines.push([
[Math.round(activeEdgeTable[i].xi), y],
[Math.round(activeEdgeTable[i + 1].xi), y]
])
}
}
// 更新流动边的xi
activeEdgeTable.forEach((item) => {
item.xi += item.dx
})
// 更新扫描线y
y += 1
}
return lines
}
代码其实就是上述算法过程的翻译,了解了算法代码并不难理解,在多边形办法里调用一下该办法:
// 绘制手绘多边形
polygon (points = [], opt = {}) {
if (points.length < 3) {
return
}
// 加上填充办法
let lines = this.scanLines(points)
lines.forEach((line) => {
this.drawDoubleLine(line[0][0], line[0][1], line[1][0], line[1][1], {
color: opt.fillStyle
})
})
// 描边
let len = points.length
// ...
}
看一下最初的填充成果:
成果曾经进去了,然而太密了,因为咱们的扫描线每次加的是1,咱们多加点试试:
scanLines (points) {
// ...
// 咱们让扫描线每次加10
let gap = 10
// 更新流动边的xi
activeEdgeTable.forEach((item) => {
item.xi += item.dx * gap// 斜率的倒数为什么也要乘10能够去看下面的推导过程
})
// 更新扫描线y
y += gap
// ...
}
顺便也加粗一下线段的宽度,成果如下:
也能够把线段的首尾交替相连变成一笔画的成果:
具体实现能够去源码里看,接下来咱们看最初一个问题,就是让填充线歪斜一点角度,目前都是程度的。填充线想要歪斜首先咱们能够让图形先旋转肯定角度,这样扫描进去的线还是程度的,而后再让图形和填充线一起再旋转回去就失去歪斜的线了。
上图示意图形逆时针旋转后进行扫描,下图示意图形和填充线顺时针旋转回去。
图形旋转也就是各个顶点旋转,所以问题就变成了求一个点旋转指定角度后的地位,上面来推导一下。
上图里点(x,y)
本来的角度为a
,线段长为r
,求旋转角度b
后的坐标(x1,y1)
:
x = Math.cos(a) * r// 1
y = Math.sin(a) * r// 2
x1 = Math.cos(a + b) * r
y1 = Math.sin(a + b) * r
// 把cos(a+b)、sin(a+b)开展
x1 = (Math.cos(a) * Math.cos(b) - Math.sin(a) * Math.sin(b)) * r// 3
y1 = (Math.sin(a) * Math.cos(b) + Math.cos(a) * Math.sin(b)) * r// 4
// 把1式和2式代入3式和4式
Math.cos(a) = x / r
Math.sin(a) = y / r
x1 = ((x / r) * Math.cos(b) - (y / r) * Math.sin(b)) * r
y1 = ((y / r) * Math.cos(b) + (x / r) * Math.sin(b)) * r
// 约去r
x1 = x * Math.cos(b) - y * Math.sin(b)
y1 = y * Math.cos(b) + x * Math.sin(b)
由此能够失去求一个点旋转指定角度后的坐标的函数:
getRotatedPos (x, y, rad) {
return [
x: x * Math.cos(rad) - y * Math.sin(rad),
y: y * Math.cos(rad) + x * Math.sin(rad)
]
}
有了该函数咱们就能够来旋转多边形了:
// 绘制手绘多边形
polygon (points = [], opt = {}) {
if (points.length < 3) {
return
}
// 扫描前先旋转多边形
let _points = this.rotatePoints(points, opt.rotate)
let lines = this.scanLines(_points)
// 扫描完失去的线段咱们再旋转相同的角度
lines = this.rotateLines(lines, -opt.rotate)
lines.forEach((line) => {
this.drawDoubleLine(line[0][0], line[0][1], line[1][0], line[1][1], {
color: opt.fillStyle
})
})
// 描边
let len = points.length
// ...
}
// 旋转顶点列表
rotatePoints (points, rotate) {
return points.map((item) => {
return this.getRotatedPos(item[0], item[1], rotate)
})
}
// 旋转线段列表
rotateLines (lines, rotate) {
return lines.map((line) => {
return [
this.getRotatedPos(line[0][0], line[0][1], rotate),
this.getRotatedPos(line[1][0], line[1][1], rotate)
]
})
}
成果如下:
圆形也是一样,转换成多边形后先旋转,而后扫描再旋转回去:
总结
本文介绍了几种简略图形的手绘格调实现办法,其中波及到了简略的数学知识及区域填充算法,如果有不合理或更好的实现形式请在留言区探讨吧,残缺的示例代码在:https://github.com/wanglin2/handPaintedStyle。感激浏览,下次再会~
参考文章:
- https://github.com/rough-stuff/rough
- https://wenku.baidu.com/view/4ee141347c1cfad6195fa7c9.html
- https://blog.csdn.net/orbit/article/details/7368996
- https://blog.csdn.net/wodownload2/article/details/52154207
- https://blog.csdn.net/keneyr/article/details/83747501
- http://www.twinklingstar.cn/2013/325/region-polygon-fill-scan-line/
发表回复