内容简介:实现手写有多种方式。一种比较容易做出的是对鼠标移动轨迹画点,再将两点之间以
signature_pad 一个基于Canvas的平滑手写画板工具
介绍
实现手写有多种方式。
一种比较容易做出的是对鼠标移动轨迹画点,再将两点之间以 直线
相连,最后再进行平滑处理,这种方案不需要什么算法支持,但同样,它面对一个性能和美观的抉择,打的点多,密集,性能相对较低,但更加美观,视觉上更平滑;
此处用的另一种方案,画贝塞尔曲线。
由于 canvas
没有默认的画出贝塞尔曲线方法,因此曲线是通过不断画出 一个个点
形成的,那么问题来了,这些点谁来定?
这里使用了贝塞尔曲线的一系列算法,包括 求控制点
, 求长度
, 计算当前点的大小
,最后用 canvas
画出每一个确定位置的点。
参数及配置介绍
提供的可配置参数如下
export interface IOptions {
// 点的大小(不是线条)
dotSize?: number | (() => number);
// 最粗的线条宽度
minWidth?: number;
// 最细的线条宽度
maxWidth?: number;
// 最小间隔距离(这个距离用贝塞尔曲线填充)
minDistance?: number;
// 背景色
backgroundColor?: string;
// 笔颜色
penColor?: string;
// 节流的间隔
throttle?: number;
// 当前画笔速度的计算率,默认0.7,意思就是 当前速度=当前实际速度*0.7+上一次速度*0.3
velocityFilterWeight?: number;
// 初始回调
onBegin?: (event: MouseEvent | Touch) => void;
// 结束回调
onEnd?: (event: MouseEvent | Touch) => void;
}
这里要注意的是并没有 线条粗细
这个选项,因为这里面的粗细不等线条都是通过一个个大小不同的点构造而成;
throttle
这个配置可以参考 loadsh
或者 underscore
的 _.throttle
,功能一致,就是为了提高性能。
注册事件
在 constructor
内部,除了配置传入的参数外,就是注册事件。
这里优先使用了 PointerEvent
触点事件, PointerEvent
可以说是触摸以及点击事件的统一,如果设备支持,不需要再分别为 mouse
和 touch
写两套事件了。
状态数据储存
状态开关:
-
this._mouseButtonDown当执行
move事件时,会检查此状态,只有在true的情况下才会执行。
数据储存分为2种格式:
-
pointGroup这是当前笔画的点的一个集合,内部储存了当前笔画的颜色
color和所有的点points<Array>。 -
this._data这是一个储存所有笔画的栈,格式为
[pointGroup, pointGroup, ..., pointGroup],当需要执行undo的时候,只需要删除this._data中的最后一条数据。
事件流程及方法
mouseDown
事件
当鼠标(触点)按下时,改变状态 this._mouseButtonDown = true
,调用 onBegin
回调,创建当前笔画的一个新的集合,然后对 当前点执行更新
。
mouseMove
事件
首先检查 this._mouseButtonDown
状态,对 当前点执行更新
。
mouseUp
事件
改变状态 this._mouseButtonDown = false;
,调用 onEnd
回调,对 当前点执行更新
。
可以看到,上面的每一个事件内部都调用对当前点执行更新的方法。
_strokeUpdate
——点的更新方法
private _strokeUpdate(event: MouseEvent | Touch): void {
// 获取当前触点的位置
const x = event.clientX;
const y = event.clientY;
// 创建点
const point = this._createPoint(x, y);
// 调出最后一个点集
const lastPointGroup = this._data[this._data.length - 1];
// 获取最后一个点集的点的数组
const lastPoints = lastPointGroup.points;
// 如果存在上一个点,获取上一个点
const lastPoint =
lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
// 判断上一个点到当前点是否太近(也就是小于配置的最小间隔距离)
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
// 调出点集的颜色
const color = lastPointGroup.color;
// Skip this point if it's too close to the previous one
// 存在上一个点但是太近,跳过,其余的执行
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
// 向上一次的点数组中添加当前点,并且生成一个新的贝塞尔曲线实例
// 包括4个点 (初始点,2个控制点,结束点)
// 初始宽度,最终宽度
const curve = this._addPoint(point);
// 如果不存在lastPoint,即当前点是第一个点
if (!lastPoint) {
// 画一个点
this._drawDot({ color, point });
// 如果存在lastPoint 并且能形成一个贝塞尔曲线实例(3个点以上)
} else if (curve) {
// 画出参数中curve实例中两点之间的曲线
this._drawCurve({ color, curve });
}
// 添加到当前笔画的点数组
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
});
}
}
这个方法前面就是一系列判断
- 判断是否是第一个点
- 判断是否能加入点的集合(满足点的最小间隔)
-
判断是否能画出贝塞尔曲线(满足至少3个点)
对于能画出贝塞尔曲线的点,执行算法,求出
Besier实例,包括4个点初始点,结束点,控制点1,控制点2以及当前曲线中线条的的初始宽度和结束宽度。具体如何算的,请参考源码
src/bezier.ts和 这篇文章 。
对于能画出贝塞尔曲线的,对已经求出的 Bezier
实例,执行 this._drawCurve
,否则执行 this._drawDot
this._drawDot
——画点的方法
获取配置中的 dotSize
,执行 canvas
画点。
this.__drawCurve
——画线的方法
-
求出当前
Bezier实例初始点和结束点之间的距离,这个距离不是直线距离,而是贝塞尔曲线距离。 -
对这个距离进行扩展,例如,计算得到距离为
50,那就扩展为100个点,即我需要在50这个距离内画出100个点;
这么做可以保证在正常或者稍微快速的书写中,不出现断层。
- 接着又是 算法 ,目的是求出这个距离内的每一个点的大小,这是一个变化值,是的粗细变化更加平滑。
-
最后同样是
canvas画点。
以上就是整个基本流程。
总结
阅读一遍后,这个库说白就是基础的事件操作+贝塞尔曲线算法,但是,它内部的代码格式非常清晰, 细粒度+代码复用
使得维护起来非常方便。
同时可以对贝塞尔曲线有一个更深层的了解(算法还是没法手撕囧),但起码有一个比较完整的思路;
一些可以借鉴的东西:
- canvas+贝塞尔曲线
-
节流
throttle的写法(参考源码src/throttle.ts) -
数据结构及实现
undo的方案
源码阅读专栏 对一些中小型热门项目进行源码阅读和分析,对其整体做出导图,以便快速了解内部关系及执行顺序。
当前源码(带注释),以及更多源码阅读内容: https://github.com/stonehank/sourcecode-analysis
,欢迎 fork
,求
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Google将带来什么?
杰夫·贾维斯 / 陈庆新、赵艳峰、胡延平 / 中华工商联合出版社 / 2009-8 / 39.00元
《Google将带来什么?》是一本大胆探索、至关重要的书籍,追寻当今世界最紧迫问题的答案:Google将带来什么?在兼具预言、宣言、思想探险和生存手册性质的这样一《Google将带来什么?》里,互联网监督和博客先锋杰夫·贾维斯对Google这个历史上发展速度最快的公司进行了逆向工程研究,发现了40种直截了当、清晰易懂的管理与生存原则。与此同时,他还向我们阐明了互联网一代的新世界观:尽管它具有挑战性......一起来看看 《Google将带来什么?》 这本书的介绍吧!