效果结尾处可验收。
画线准备
准备一个canvas
<canvas id="canvasId" width="1000" height="800"></canvas>
使用pointer事件监听,落笔,拖拽,收笔。
document.onpointerdown = function (e) { if (e.type == "touchstart") handwriting.down(e.touches[0].pageX, e.touches[0].pageY); else handwriting.down(e.x, e.y); } document.onpointermove = function (e) { if (e.type == "touchmove") handwriting.move(e.touches[0].pageX, e.touches[0].pageY); else handwriting.move(e.x, e.y); } document.onpointerup = function (e) { if (e.type == "touchend") handwriting.up(e.touches[0].pageX, e.touches[0].pageY); else handwriting.up(e.x, e.y); }
主要的逻辑在Handwritinglff 上,存储了当前绘制中的线条的所有点集合,所有绘制过的线条集合pointLines 。
class Handwritinglff { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext("2d") this.line = new Line(); this.pointLines = new Array();//Line数组 this.k = 0.5; this.begin = null; this.middle = null; this.end = null; this.lineWidth = 10; this.isDown = false; }
down事件的时候初始化当前绘制线条line;
move事件的时候将点加入到当前线条line,并开始绘制
up的时候将点加入绘制线条,并绘制完整一条线。
需要注意的点:
加入点的时候,距离太近的点不需要重复添加;
怎么形成笔锋效果呢
很简单!就是在一条线段的最后几个点的lineWidth不断减小,我们这里选用了最后6个点,如果只选用六个阶梯变化,效果是很难看的,会看到一节节明显的线条变细的过程,如下图:
所以我们有个关键的补点过程,我们会再每6个像素之间补一个点,根据线条粗细变化的范围和最后计算出来的点数,就可以知道每两点连线lineWidth的粗细。
这里的补点过程我们用到了在贝塞尔曲线上补点的算法。具体不明白的可以留言哈
bezierCalculate(poss, precision) { //维度,坐标轴数(二维坐标,三维坐标...) let dimersion = 2; //贝塞尔曲线控制点数(阶数) let number = poss.length; //控制点数不小于 2 ,至少为二维坐标系 if (number < 2 || dimersion < 2) return null; let result = new Array(); //计算杨辉三角 let mi = new Array(); mi[0] = mi[1] = 1; for (let i = 3; i <= number; i++) { let t = new Array(); for (let j = 0; j < i - 1; j++) { t[j] = mi[j]; } mi[0] = mi[i - 1] = 1; for (let j = 0; j < i - 2; j++) { mi[j + 1] = t[j] + t[j + 1]; } } //计算坐标点 for (let i = 0; i < precision; i++) { let t = i / precision; let p = new Point(0, 0); result.push(p); for (let j = 0; j < dimersion; j++) { let temp = 0.0; for (let k = 0; k < number; k++) { temp += Math.pow(1 - t, number - k - 1) * (j == 0 ? poss[k].x : poss[k].y) * Math.pow(t, k) * mi[k]; } j == 0 ? p.x = temp : p.y = temp; } p.x = this.toDecimal(p.x); p.y = this.toDecimal(p.y); } return result; }
部分代码如下;
addPoint(p) { if (this.line.points.length >= 1) { let last_point = this.line.points[this.line.points.length - 1] let distance = this.z_distance(p, last_point); if (distance < 10) { return; } } if (this.line.points.length == 0) { this.begin = p; p.isControl = true; this.pushPoint(p); } else { this.middle = p; let controlPs = this.computeControlPoints(this.k, this.begin, this.middle, null); this.pushPoint(controlPs.first); this.pushPoint(p); p.isControl = true; this.begin = this.middle; } }
draw(isUp = false) { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.strokeStyle = "rgba(255,20,87,1)"; //绘制不包含this.line的线条 this.pointLines.forEach((line, index) => { let points = line.points; this.ctx.beginPath(); this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(points[0].x, points[0].y); let lastW = line.lineWidth; this.ctx.lineWidth = line.lineWidth; this.ctx.lineJoin = "round"; this.ctx.lineCap = "round"; let minLineW = line.lineWidth / 4; let isChangeW = false; let changeWidthCount = line.changeWidthCount; for (let i = 1; i <= points.length; i++) { if (i == points.length) { this.ctx.stroke(); break; } if (i > points.length - changeWidthCount) { if (!isChangeW) { this.ctx.stroke();//将之前的线条不变的path绘制完 isChangeW = true; if (i > 1 && points[i - 1].isControl) continue; } let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW; points[i - 1].lineWidth = w; this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍 // this.ctx.strokeStyle = "rgba("+Math.random()*255+","+Math.random()*255+","+Math.random()*255+",1)"; this.ctx.lineWidth = w; this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点 this.ctx.lineTo(points[i].x, points[i].y); this.ctx.stroke();//将之前的线条不变的path绘制完 } else { if (points[i].isControl && points[i + 1]) { this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y); } else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制 } else this.ctx.lineTo(points[i].x, points[i].y); } } }) //绘制this.line线条 let points; if (isUp) points = this.line.points; else points = this.line.points.clone(); //当前绘制的线条最后几个补点 贝塞尔方式增加点 let count = 0; let insertCount = 0; let i = points.length - 1; let endPoint = points[i]; let controlPoint; let startPoint; while (i >= 0) { if (points[i].isControl == true) { controlPoint = points[i]; count++; } else { startPoint = points[i]; } if (startPoint && controlPoint && endPoint) {//使用贝塞尔计算补点 let dis = this.z_distance(startPoint, controlPoint) + this.z_distance(controlPoint, endPoint); let insertPoints = this.BezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1); insertPoints.splice(0, 1); insertCount += insertPoints.length; var index = i;//插入位置 // 把arr2 变成一个适合splice的数组(包含splice前2个参数的数组) insertPoints.unshift(index, 1); Array.prototype.splice.apply(points, insertPoints); //补完点后 endPoint = startPoint; startPoint = null; } if (count >= 6) break; i--; } //确定最后线宽变化的点数 let changeWidthCount = count + insertCount; if (isUp) this.line.changeWidthCount = changeWidthCount; //制造椭圆头 this.ctx.fillStyle = "rgba(255,20,87,1)" this.ctx.beginPath(); this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2); this.ctx.fill(); this.ctx.beginPath(); this.ctx.moveTo(points[0].x, points[0].y); let lastW = this.line.lineWidth; this.ctx.lineWidth = this.line.lineWidth; this.ctx.lineJoin = "round"; this.ctx.lineCap = "round"; let minLineW = this.line.lineWidth / 4; let isChangeW = false; for (let i = 1; i <= points.length; i++) { if (i == points.length) { this.ctx.stroke(); break; } //最后的一些点线宽变细 if (i > points.length - changeWidthCount) { if (!isChangeW) { this.ctx.stroke();//将之前的线条不变的path绘制完 isChangeW = true; if (i > 1 && points[i - 1].isControl) continue; } //计算线宽 let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW; points[i - 1].lineWidth = w; this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍 // this.ctx.strokeStyle = "rgba(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ",0.5)"; this.ctx.lineWidth = w; this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点 this.ctx.lineTo(points[i].x, points[i].y); this.ctx.stroke();//将之前的线条不变的path绘制完 } else { if (points[i].isControl && points[i + 1]) { this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y); } else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制 } else this.ctx.lineTo(points[i].x, points[i].y); } } }
最终效果
动手试试:拖拽写字即可
相关文章: