注:本文是对原创的改进,感谢原创暮志未晚Webgl的分享。
之前在网上,断断续续筛选了好多Canvas曲线绘制算法。绝大多数,都是在讲解贝塞尔曲线算法。核心是,通过“控制点”来控制曲线的曲率。但是,如何计算点与点之间的曲率?却是没有一篇文章有说明。好在皇天不负有心人,找到了墓志未晚的一篇文章,以下是原文链接:canvas 将折线转换成曲线_暮志未晚Webgl的博客-CSDN博客
但是,原文使用方法,也有些繁琐,于是对其加以封装,遂有了本篇文章。本文主要采用ES6面向对象的Class语法,对原创进行封装,更加通俗易懂。
主要改进内容如下:
1、封装了二维向量Vector2D;
2、封装了获取曲线控制点方法GetCurveControlPoints;
3、封装了曲线绘制方法DrawCurves;
一、依赖的Point坐标点对象
/**
* Point:坐标对象。
* Author:睿凝奇兵
* Date:2020-07-20
* Version:1.0.7.3
*/
export default class Point {
public X: number = 0;
public Y: number = 0;
/**
* 构造
*/
constructor();
/**
* 构造
* @param piX X 轴坐标
* @param piY Y轴坐标
*/
constructor(piX: number, piY: number);
/**
* 构造
* @param piX X 轴坐标
* @param piY Y轴坐标
*/
constructor(piX?: number, piY?: number) {
this.X = piX === undefined ? 0 : piX;
this.Y = piY === undefined ? 0 : piY;
}
/**
* 获取当前坐标的副本。
* @returns 返回坐标。
*/
Clone() {
let loPoint = new Point(this.X, this.Y);
return loPoint;
}
/**
* 获取当前坐标是否为空。
*/
get IsEmpty() {
let llEmpty = false;
if (
(this.X === 0 || this.X === undefined || this.X === null) &&
(this.Y === 0 || this.Y === undefined || this.Y === null)
) {
llEmpty = true;
}
return llEmpty;
}
/**
* 置空坐标。
*/
Empty() {
this.X = 0;
this.Y = 0;
}
/**
* 比较两个坐标是否一致。
* @param poBounds 待比较的坐标
* @returns 返回比较结果。
*/
Equals(poPosition: Point) {
return this.X === poPosition.X && this.Y === poPosition.Y;
}
/**
* 对坐标做偏移处理。
* @param piX X 轴偏移量
* @param piY Y 轴偏移量
*/
Offset(piX: number, piY: number) {
this.X += piX;
this.Y += piY;
}
/**
* 更新坐标。
* @param piX X 轴坐标
* @param piY Y 轴坐标
*/
Update(piX: number, piY: number) {
this.X = piX;
this.Y = piY;
}
/**
* 获取一个新的空坐标对象。
* @returns 返回坐标
*/
static Empty() {
return new Point(0, 0);
}
}
二、依赖的Pen画笔对象:
/**
* Pen:画笔对象。
* Author:睿凝奇兵
* Date:2020-07-20
* Version:1.0.7.3
*/
export default class Pen {
public Color: string = 'Black';
public DashPattern: number[] = [];
public EndCap: boolean = false;
public Width: number = 1;
constructor(poColor: any);
constructor(poColor: any, piWidth: number);
constructor(poColor: any, piWidth?: number) {
this.Color = 'Black';
if (poColor) {
this.Color = poColor;
}
if (piWidth) {
this.Width = piWidth;
}
}
}
三、Vector2D二维向量封装
import Point from './Point';
/**
* Vector2D:二维向量
* Author:睿凝奇兵
* Date:2020-07-20
* Version:1.0.7.3
*/
export default class Vector2D {
public X: number = 0;
public Y: number = 0;
/**
* 构造
* @param piX X轴坐标
* @param piY Y轴坐标
*/
constructor(piX: number, piY: number) {
this.X = piX;
this.Y = piY;
}
/**
* 获取向量长度
*/
get Length() {
return Math.sqrt(this.X * this.X + this.Y * this.Y);
}
/**
* 获取单位向量
*/
get Normalize() {
var inv = 1 / this.Length;
return new Vector2D(this.X * inv, this.Y * inv);
}
/**
* 向量叠加
* @param poVector 待叠加的向量
* @returns 返回新的向量
*/
Add(poVector: Point) {
return new Vector2D(this.X + poVector.X, this.Y + poVector.Y);
}
/**
* 向量翻倍
* @param piMultiple 倍数
* @returns
*/
Multiply(piMultiple: number) {
return new Vector2D(this.X * piMultiple, this.Y * piMultiple);
}
/**
* 内积
* @param poVector 目标向量
* @returns 返回内积
*/
Dot(poVector: Vector2D) {
return this.X * poVector.X + this.Y * poVector.Y;
}
/**
* 求两个向量的夹角
* @param poVector 目标向量
* @returns 返回夹角
*/
Angle(poVector: Vector2D) {
return (Math.acos(this.Dot(poVector) / (this.Length * poVector.Length)) * 180) / Math.PI;
}
}
四、画布封装(仅公开部分代码)
import Point from './Point';
import Pen from './Pen';
import Vector2D from './Vector2D';
/**
* ICanvasComponent:Canvas 组件接口。
*/
export interface ICanvasComponent {
FontFamily: string;
FontSize: number;
Graphics: EniacGraphics;
UpdateComponentSize(): void;
UpdateComponent(plRelayout: boolean): void;
}
/**
* EniacGraphics:画布对象。
* Author:睿凝奇兵
* Date:2020-07-20
* Version:1.0.7.3
*/
export default class EniacGraphics {
private static _SnapshotCanvas: any = null;
private _Canvas: any;
public FontFamily: string | undefined = '微软雅黑';
public FontSize: number | undefined = 15;
/**
* 构造
*/
constructor() {
this._Canvas = null;
}
/**
* 获取Canvas对象。
*/
get Canvas() {
return this._Canvas;
}
/**
* 获取画布上下文。
* @returns 返回上下文。
*/
GetContext() {
let loContext = this.Canvas.getContext('2d');
loContext.textBaseline = 'middle';
loContext.textAlign = 'center';
loContext.font = `${this.FontSize}px ${this.FontFamily}`;
return loContext;
}
/**
* 获取曲线上的控制点集。
* @param poPoints 数据点集
* @returns 返回控制点集
*/
GetCurveControlPoints(poPoints: Point[]) {
let liMultiple = 0.3;
let loList = [];
for (let liIndex = 0; liIndex < poPoints.length - 2; liIndex++) {
let a = poPoints[liIndex];
let b = poPoints[liIndex + 1];
let c = poPoints[liIndex + 2];
let v1 = new Vector2D(a.X - b.X, a.Y - b.Y);
let v2 = new Vector2D(c.X - b.X, c.Y - b.Y);
let v1Len = v1.Length;
let v2Len = v2.Length;
let centerV = v1.Normalize.Add(new Point(v2.Normalize.X, v2.Normalize.Y)).Normalize;
let ncp1 = new Vector2D(centerV.Y, centerV.X * -1);
let ncp2 = new Vector2D(centerV.Y * -1, centerV.X);
if (ncp1.Angle(v1) < 90) {
let p1 = ncp1.Multiply(v1Len * liMultiple).Add(b);
let p2 = ncp2.Multiply(v2Len * liMultiple).Add(b);
loList.push(p1, p2);
} else {
let p1 = ncp1.Multiply(v2Len * liMultiple).Add(b);
let p2 = ncp2.Multiply(v1Len * liMultiple).Add(b);
loList.push(p2, p1);
}
}
return loList;
}
/**
* 填充圆形。
* @param poColor 颜色
* @param poCenter 原点
* @param piRadius 半径
*/
FillCircle(poColor: any, poCenter: Point, piRadius: number) {
let loContext = this.GetContext();
loContext.beginPath();
loContext.strokeStyle = poColor;
loContext.fillStyle = poColor;
loContext.arc(poCenter.X, poCenter.Y, piRadius, 0, Math.PI * 2);
loContext.fill();
loContext.closePath();
}
/**
* 绘制曲线。
* @param poPen 画笔
* @param poPoints 坐标集
*/
DrawCurves(poPen: Pen, poPoints: Point[]) {
let loContext = this.GetContext();
loContext.beginPath();
loContext.lineWidth = poPen.Width;
loContext.strokeStyle = poPen.Color;
if (poPen.DashPattern && poPen.DashPattern.length > 0) {
loContext.setLineDash(poPen.DashPattern);
} else {
loContext.setLineDash([]);
}
let loControls = this.GetCurveControlPoints(poPoints);
loContext.beginPath();
let int = 0;
for (let i = 0; i < poPoints.length; i++) {
if (i == 0) {
loContext.moveTo(poPoints[0].X, poPoints[0].Y);
loContext.quadraticCurveTo(loControls[0].X, loControls[0].Y, poPoints[1].X, poPoints[1].Y);
int = int + 1;
} else if (i < poPoints.length - 2) {
loContext.moveTo(poPoints[i].X, poPoints[i].Y);
loContext.bezierCurveTo(
loControls[int].X,
loControls[int].Y,
loControls[int + 1].X,
loControls[int + 1].Y,
poPoints[i + 1].X,
poPoints[i + 1].Y,
);
int += 2;
} else if (i == poPoints.length - 2) {
loContext.moveTo(poPoints[poPoints.length - 2].X, poPoints[poPoints.length - 2].Y);
loContext.quadraticCurveTo(
loControls[loControls.length - 1].X,
loControls[loControls.length - 1].Y,
poPoints[poPoints.length - 1].X,
poPoints[poPoints.length - 1].Y,
);
}
}
loContext.stroke();
loContext.closePath();
for (let liIndex = 0; liIndex < poPoints.length; liIndex++) {
this.FillCircle(poPen.Color, poPoints[liIndex], 3);
}
}
}
五、效果图:
本人对GDI+、Canvas相当痴迷,并小有成就。欢迎咨询、洽谈,相互学习、互相进步。