画线准备

准备一个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画布内容--点擦除+线擦除

canvas小画板——(3)笔锋效果的更多相关文章

  1. canvas小画板——(2)荧光笔效果

    我们在上一篇文章中讲了如何绘制平滑曲线 canvas小画板——(1)平滑曲线. 透明度实现荧光笔 现在我们需要加另外一种画笔效果,带透明度的荧光笔.那可能会觉得绘制画笔的时候加上透明度就可以了.我们来 ...

  2. canvas小画板--(1)平滑曲线

    功能需求 项目需求:需要实现一个可以自由书写的小画板 简单实现 对于熟悉canvas的同学来说,这个需求很简单,短短几十行代码就能实现: <!doctype html> <html& ...

  3. 如何开发一个简单的HTML5 Canvas 小游戏

    原文:How to make a simple HTML5 Canvas game 想要快速上手HTML5 Canvas小游戏开发?下面通过一个例子来进行手把手教学.(如果你怀疑我的资历, A Wiz ...

  4. 两个Canvas小游戏

    或许连小游戏都算不上,可以叫做mini游戏. 没有任何框架或者稍微有点深度的东西,所以有js基础的或者要追求炫酷效果的可以直接ctrl+w了. 先贴出两个游戏的试玩地址: 是男人就走30步 是男人就忍 ...

  5. 用HTML5 Canvas 做擦除及扩散效果

    2013年的时候曾经使用canvas实现了一个擦除效果的需求,即模拟用户在模糊的玻璃上擦除水雾看到清晰景色的交互效果.好在2012年的时候学习HTML5的时候研究过canvas了,所以在比较短的时间内 ...

  6. canvas实现画板

    canvas实现画板 主要用到知识点: 鼠标事件onmousedown() onmousemove() onmouseup() onmouseleave() 事件委托 canvas的一些方法 如:绘制 ...

  7. 浅谈canvas中的拖尾效果

    引言 很早就想了解以下 canvas 中的拖尾效果(如彗星,烟花等效果)是怎么实现的,但是一直没有深入了解,正巧在 codepen 上看到一个 demo,代码简单,效果炫酷,故有此文. 什么黑科技 在 ...

  8. 基于canvas的原生JS时钟效果

    概述 运用html5新增画布canvas技术,绘制时钟效果,无需引用任何插件,纯js. 详细 代码下载:http://www.demodashi.com/demo/11935.html 给大家介绍一个 ...

  9. 使用JavaScript和Canvas打造真实的雨滴效果

    使用JavaScript和Canvas打造真实的雨滴效果 寸志 · 1 年前 我最近搞了一个有趣的项目——rainyday.js .我认为这个项目并不怎么样,而且,事实上这是我第一次尝试接触一些比弹窗 ...

随机推荐

  1. 推荐一些学习MySQL的资源

    前言: 在日常工作与学习中,无论是开发.运维.还是测试,对于数据库的学习是不可避免的,同时也是日常工作的必备技术之一.在互联网公司,开源数据库用得比较多的当属MySQL了,相信各位小伙伴关注我的原因也 ...

  2. Spring 中使用了那些设计模式呢?

    一. Spring 中常见的设计模式 工厂模式 : BeanFactory 装饰器模式: BeanWrapper 代理模式: AopProxy 单例模式: ApplicationContext 委派模 ...

  3. Davinci 可视化系统部署安装及简单使用

    Davinci 是一个目前比较热门的国内开源BI系统,功能比较完善,各种可视化效果也挺不错.主要获取数据的方式是通过编写SQL 创建数据视图来展示各种图表的. Davinci面向业务人员/数据工程师/ ...

  4. Spring与Springboot

    1.Spring能做什么 1.1.Spring的能力 1.2.Spring的生态 https://spring.io/projects/spring-boot 覆盖了: web开发 数据访问 安全控制 ...

  5. VS·卸载进程卡死"正在配置您的系统,这可能需要一些时间"

    阅文时长 | 0.34分钟 字数统计 | 596.8字符 主要内容 | 1.引言&背景 2.声明与参考资料 『VS·卸载进程卡死"正在配置您的系统,这可能需要一些时间"』 ...

  6. 正则表达式、编辑器(vi、sed、awk)

    1. vi 2. 正则表达式 3. sed 1)打印命令:p 2)删除命令:d 3)替换命令:s 4. awk 1)awk 基本用途 2)匹配打印 3)判断打印 4)数组 1. vi vi 是 Lin ...

  7. lua编译为二进制方式

    当不想使用户看到lua源码,文本文件可以通过luac,把lua文本文件"编译"成二进制的文件. lc@lc-virtual-machine:~/lua$ luac -o redis ...

  8. linux查看文件的编码格式的方法 set fileencoding PYTHON

    linux查看文件的编码格式的方法 set fileencoding   乱码原因:因为你的文件声明为utf-8,并且也应该是用utf-8的编码保存的源文件.但是windows的本地默认编码是cp93 ...

  9. Microk8s 安装helm3

    Microk8s 安装helm3 原本使用官方的命令时这样的.但是使用的是官方的源,网络极不稳定.就出现了下面的状况.解决方法是更改为国内的源. microk8s enable helm3 curl: ...

  10. Java 单例模式:懒加载(延迟加载)和即时加载

    引言 在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制(或懒加载.延时加载),也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用.这 ...