像梦一样奔驰|Vue + Canvas 实现流畅的钢笔涂鸦效果( 二 )


绘制基础方法
像梦一样奔驰|Vue + Canvas 实现流畅的钢笔涂鸦效果绘制曲线片段(圆)
绘制首个坐标圆点
像梦一样奔驰|Vue + Canvas 实现流畅的钢笔涂鸦效果绘制首个坐标圆点
绘制曲线根据贝塞尔曲线公式计算坐标点 。
/** * 绘制曲线. * @param curve * @param ctx */protected drawCurve(curve: Bezier,ctx?: CanvasRenderingContext2D): void {if (!ctx) ctx = this.buffer.getContext();const delta = curve.endWidth - curve.startWidth,steps = Math.floor(curve.length()) * 2;ctx.beginPath();/** 根据公式循环计算曲线坐标点 */for (let i = 0; i < steps; i += 1) {const t = i / steps,tt = t * t,ttt = tt * t;const u = 1- t,uu = u * u,uuu = uu * u;let x = uuu * curve.startPoint.x as number;x += 3 * uu * t * curve.control1.x;x += 3 * u * tt * curve.control2.x;x += ttt * curve.endPoint.x;let y = uuu * curve.startPoint.y;y += 3 * uu * t * curve.control1.y;y += 3 * u * tt * curve.control2.y;y += ttt * curve.endPoint.y;const width = Math.min(curve.startWidth + ttt * delta,this.width.max);this.drawCurveSegment(ctx, x, y, width);}ctx.closePath();}
像梦一样奔驰|Vue + Canvas 实现流畅的钢笔涂鸦效果根据贝塞尔公式计算坐标点
总结至此 , 基本上实现了钢笔的功能 , 上面的几个步骤是主要的实现过程 , 最主要的就是要根据速度来计算笔刷粗细 , 而速度则可以根据两个坐标点形成之间的时间间隔来计算 , 自己定义一个基准数值来判定多少毫秒内算快 , 多长时间又算是慢的 。 接着再根据贝塞尔曲线公式将两个坐标点之间的距离 , 再细化成很多的坐标点 , 每个坐标点绘制成指定半径的圆形即可 。
虽然涂鸦的时候 , 很流畅了(毕竟是采用离屏渲染的) , 但我在尝试“拖拽”功能时 , 由于 Canvas 是不支持事件绑定的 , 拖拽过程中需要不断的重新绘制 , 涂鸦内容少的情况下 , 看不出来有什么影响 , 但如果涂鸦内容很多 , 就会导致拖拽过程中 , 出现闪烁的情况 , 在 60HZ 的显示器下 , 16ms 内根本刷不过来 , 主要问题出现在两个坐标点之间 , 根据贝塞尔曲线公式继续细化坐标点 , 计算量有点大 。 回头我再尝试优化下 , 整理后将代码托管至自己搭建的 Gogs 平台上去 , 有需要的小伙伴们可以下载下来试试 。
最最后再附上几个基础类 , 代码量有点多 , 有些是精简过的 。
贝塞尔曲线类 Bezierimport {MiPoint, Point} from '@components/canvas/Point';export default class Bezier {constructor(public startPoint: Point,public endPoint: Point,public control1: MiPoint,public control2: MiPoint,public startWidth: number,public endWidth: number ) {}/*** 根据坐标点返回贝塞尔曲线的对象.* @param points {Point}* @param width*/ public static fromPoints(points: Point[],width: {start: number;end: number;} ): Bezier {const c1 = this.calculateControlPoint(points[1], points[2], points[3]).c1,c2 = this.calculateControlPoint(points[0], points[1], points[2]).c2;return new Bezier(points[1], points[2], c1, c2, width.start, width.end); }/*** 计算三次贝塞尔曲线的控制点.* 可以将三次贝塞尔看成由2段二次贝塞尔曲线组成的来计算.* @param p1 {MiPoint}* @param p2 {MiPoint}* @param p3 {MiPoint}*/ private static calculateControlPoint(p1: MiPoint,p2: MiPoint,p3: MiPoint ): {c1: MiPoint;c2: MiPoint; } {/** 坐标点之间的距离差(p1 与 p2 / p2 与 p3) */const dx1 = p1.x - p2.x,dy1 = p1.y - p2.y,dx2 = p2.x - p3.x,dy2 = p2.y - p3.y;/** 两点之间的中点坐标 */const mp1 = {x: (p1.x + p2.x) / 2.0,y: (p1.y + p2.y) / 2.0}, mp2 = {x: (p2.x + p3.x) / 2.0,y: (p2.y + p3.y) / 2.0};/** 直线长度 */const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1),l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);/** 中间点之间的坐标距离差 */const dxm = mp1.x - mp2.x,dym = mp1.y - mp2.y;/** 长度比例(确定平移前的位置) */const r = l2 / (l1 + l2);/** 平移 */const tm = {x: mp2.x + dxm * r,y: mp2.y + dym * r};const tx = p2.x - tm.x,ty = p2.y - tm.y;return {c1: new Point(mp1.x + tx, mp1.y + ty),c2: new Point(mp2.x + tx, mp2.y +ty)}; }/*** 计算贝塞尔曲线(直线)长度.* 曲线是直线的充分必要条件是所有的控制点都在曲线上.* 同样, 贝塞尔曲线是直线的充分必要条件是控制点共线.* 所以: 上一个点与下一个点坐标之间的直线距离, 不断累加即为曲线长度.* @return number* @see point*/ public length(): number {const steps = 10;let length = 0, px!: number, py!: number;for (let i = 0; i