画线准备

准备一个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;
}
}

addPoint

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);
}
}
}

draw

最终效果

动手试试:拖拽写字即可

相关文章:

平滑曲线

实现蜡笔荧光笔效果

实现笔锋效果

画笔性能优化

清除canvas画布内容--点擦除+线擦除

最新文章

  1. 利用免费的Spire.XLS控件制作Excel报表
  2. 互联网+下PDA移动智能手持POS超市收银开单软件
  3. Qt MVC(模型-视图-代理)
  4. C#先执行一段sql等后台操作后再提示是否后续操作confrim
  5. eclipse中编译时enum出现cannot be resolved to a type错误
  6. [转]EntityFramework走马观花之CRUD(下)
  7. Spring配置文件解析--bean属性
  8. Android(java)学习笔记97:Scanner类使用
  9. Oracle 的 VKTM 进程 - virtual keeper of time
  10. Mac OS X 在捕捉AppLAN通信包
  11. Go从入门到精通(一)go语言初始
  12. C#编写影院售票系统(A project with a higher amount of gold )
  13. poj 2503 查字典
  14. ●BZOJ 3143 [Hnoi2013]游走
  15. 试试使用 eolinker 扫描 GitLab 代码注释自动生成 API 文档?
  16. Socket编程 (一)
  17. centos下安卓构建打包太慢
  18. mongodb 按配置文件mongodb.conf启动
  19. 查询Oracle性能差的、正在执行或执行过的SQL语句
  20. cf428c 模拟题

热门文章

  1. Packing data with Python
  2. unbuntu下清理磁盘空间
  3. wrk 及扩展支持 tcp 字节流协议压测
  4. Mybatis-Plus的应用场景及注入SQL原理分析
  5. .NET平台系列10 .NET统一平台愿景
  6. prometheus node-exporter增加新的自定义监控项
  7. make: g77: Command not found 修改Makefile.in中的编译文件中的g77为gfortran
  8. CGI开发-(转自 jemofh159)
  9. Linux Socket编程-(转自吴秦(Tyler))
  10. RAM与FLASH