在Bresenham画线算法及实践一文中介绍了如何绘制直线段,在诸如图表等可视化场景中,往往需要使用各类不同的线型,如虚线、点划线等,本文介绍一种实现绘制这类线型的方法,并使用TypeScript语言在HTML canvas画布中进行绘制。
这里借鉴OpenGL中的定义的线型函数glLineStripple(repeatFactor, pattern),其中pattern使用16bit位来定义线型属性,其中1表示对象像素打开,0表示像素关闭。参见下图,如pattern设置为0x1C47,bit为1则设置线的颜色值,为0则设置为背景色,重复这个模式就形成了点划线;同样地,0x00FF就表示了虚线、0xFFFF表示实线。另外一个参数repeatFactor为整数参数,表示每一个bit重复多少次才轮到下一个bit,这个参数可以调节线型疏密程度。
在原有的Bresenham画线算法的基础上,只需要在绘制每一个像素之前,先根据线型函数配置的参数确定该像素是否是打开状态,如打开则绘制像素为线的颜色值,关闭就绘制像素为背景颜色即可。判断像素是否打开,可以通过pattern循环与0x0001~0x8000进行逻辑与操作,结果不为0则表示像素是打开状态,以0x1C47为例,逻辑与操作的结果参见下图。
在下面的程序中,通过绘制点线、虚线、点划线、实线来展示了线属性函数的使用,生成的数据图见下图。
var canvas = <HTMLCanvasElement>document.getElementById("canvas001");
var webgl = new WebGL(canvas);
webgl.glClearColor(1, 1, 1, 1);
webgl.glBgColor(1, 1, 1);
webgl.glColor(0, 0, 0);
function linePlot(dataPts: Array<any>) {
for (let k = 0; k < dataPts.length; k++) {
webgl.glVertex2(dataPts[k][0], dataPts[k][1]);
}
webgl.drawArrays(GL_LINES_STRIP);
}
// 点划线
let data_points = [[0, 50], [230, 120], [440, 320], [660, 260], [900, 50]];
webgl.glLineStipple(3, 0x1C47);
linePlot(data_points);
// 实线
data_points = [[0, 10], [230, 80], [440, 280], [660, 220], [900, 10]];
webgl.glLineStipple(1, 0xFFFF);
linePlot(data_points);
// 虚线
data_points = [[0, 100], [220, 290], [430, 280], [710, 500], [900, 420]];
webgl.glLineStipple(1, 0x00FF);
linePlot(data_points);
// 点线
data_points = [[0, 200], [250, 240], [440, 390], [700, 300], [900, 520]];
webgl.glLineStipple(2, 0x0101);
linePlot(data_points);
完整参考代码如下:
app.ts
// 常量
const GL_POINTS = 0;
const GL_LINES = 1;
const GL_LINES_STRIP = 2;
const GL_LINES_LOOP = 3;
const GL_LINE_STIPPLE = 4;
class Vector2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class Color {
r: number;
g: number;
b: number;
a: number;
constructor(r: number, g: number, b:number, a: number = 1) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
class WebGL {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
vertices: Array<Vector2>;
attr_color: Color;
attr_bg_color: Color;
attr_repeatFactor: number;
attr_pattern: number;
attr_repeatFactor_index: number;
attr_pattern_mask: number;
// 构造函数
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.vertices = [];
this.attr_bg_color = new Color(1, 1, 1);
this.attr_color = new Color(0, 0, 0);
this.attr_repeatFactor = 1;
this.attr_pattern = 0xFFFF;
this.attr_pattern_mask = 0x0001;
this.attr_repeatFactor_index = 0;
}
// 将canvas坐标系转换为常用的y轴向上的坐标系
transToScreenXY(x: number, y: number) {
return [x, this.canvas.height - y];
}
setFillStyle(r: number, g: number, b: number, a: number = 1) {
this.ctx.fillStyle = "rgba(" + r*255 + "," + g*255 + "," + b*255 + "," + a + ")";
}
glEnd() {
this.vertices = [];
this.attr_pattern_mask = 0x0001;
this.attr_repeatFactor_index = 0;
}
// 设置颜色值
glColor(r: number, g: number, b: number, a: number = 1) {
this.setFillStyle(r, g, b, a);
this.attr_color = new Color(r, g, b, a);
}
// 设置背景色
glBgColor(r: number, g: number, b: number, a: number = 1) {
this.setFillStyle(r, g, b, a);
this.attr_bg_color = new Color(r, g, b, a);
}
// 填充1x1像素的矩形(即一个像素大小)来实现单个像素颜色设置
setPixel(x: number, y: number) {
let [sx, sy] = this.transToScreenXY(x, y);
this.ctx.fillRect(sx, sy, 1, 1);
}
// 填充矩形区域
fillRect(x0: number, y0: number, x1: number, y1: number) {
let [min_x, max_x] = [Math.min(x0, x1), Math.max(x0, x1)];
let [min_y, max_y] = [Math.min(y0, y1), Math.max(y0, y1)];
for (let i = min_x; i < max_x; i++) {
for (let j = min_y; j < max_y; j++) {
this.setPixel(i, j);
}
}
}
// rgba指定的颜色清空整个canvas
glClearColor(r: number, g: number, b: number, a:number) {
this.setFillStyle(r, g, b, a);
this.ctx.fillRect(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
}
// 添加顶点坐标
glVertex2(x: number, y: number) {
this.vertices.push(new Vector2(x, y));
}
/* 线型设置
repeatFactor: 每一位重复多少次再继续下一位
pattern: 默认值为0xFFFF每一位均为1,表示绘制的是实现,为0的位绘制背景色
*/
glLineStipple(repeatFactor: number, pattern: number = 0xFFFF) {
this.attr_repeatFactor = repeatFactor;
this.attr_pattern = pattern;
}
// 获取像素的开关状态, true-表示像素打开、false-表示像素关闭
glPixelSwitchStatus() {
// 计算pattern与运算
let logic_and = this.attr_pattern & this.attr_pattern_mask;
// 每bit重复次数repeatFactor处理
this.attr_repeatFactor_index = (this.attr_repeatFactor_index + 1) % this.attr_repeatFactor;
if (this.attr_repeatFactor_index == 0) {
// 如已完成重复bit次数,则参与与运算掩码左移一位
this.attr_pattern_mask *= 2;
}
// 掩码到达最大值后重新开始
if (this.attr_pattern_mask > 0x8000) {
this.attr_pattern_mask = 0x0001;
}
if (logic_and) {
return true;
} else {
return false;
}
}
setPoint(x: number, y: number) {
let r, g, b;
if (this.glPixelSwitchStatus()) {
[r, g, b] = [this.attr_color.r, this.attr_color.g, this.attr_color.b];
} else {
[r, g, b] = [this.attr_bg_color.r, this.attr_bg_color.g, this.attr_bg_color.b];
}
this.setFillStyle(r, g, b);
this.setPixel(x, y);
}
// Bresenham画线算法
lineBres(p0_x: number, p0_y: number, p1_x: number, p1_y: number) {
let [int_p0_x, int_p0_y] = [Math.round(p0_x), Math.round(p0_y)];
let [int_p1_x, int_p1_y] = [Math.round(p1_x), Math.round(p1_y)];
let x, y;
// 确定x0 < xEnd
if (int_p0_x > int_p1_x) {
x = int_p1_x;
y = int_p1_y;
int_p1_x = int_p0_x;
int_p1_y = int_p0_y;
} else {
x = int_p0_x;
y = int_p0_y;
}
let dx = Math.abs(int_p1_x - x);
let dy = Math.abs(int_p1_y - y);
let p = 2 * dy - dx;
let twoDy = 2 * dy;
let twoDyminusDx = 2 * (dy - dx);
let twoDx = 2 * dx;
// 计算斜率
let m = dy / dx;
if (int_p1_y < y) {
m = -m;
}
let real_line_size;
// 斜率(0,1]
if (m > 0 && m <= 1) {
while (x < int_p1_x) {
this.setPoint(x, y);
x += 1;
if (p < 0) {
p += twoDy;
} else {
y += 1;
p += twoDyminusDx;
}
}
}
// 斜率(1,+无穷)
if (m > 1) {
while (y < int_p1_y) {
this.setPoint(x, y);
y += 1;
if (p < 0) {
p += twoDx;
} else {
x += 1;
p -= twoDyminusDx;
}
}
}
// 斜率(-1,0]
if (m > -1 && m <= 0) {
while (x < int_p1_x) {
this.setPoint(x, y);
x += 1;
if (p < 0) {
p += twoDy;
} else {
y -= 1;
p += twoDyminusDx;
}
}
}
// 斜率(-无穷,-1]
if (m <= -1) {
while (y > int_p1_y) {
this.setPoint(x, y);
y -= 1;
if (p < 0) {
p += twoDx;
} else {
x += 1;
p -= twoDyminusDx;
}
}
}
}
drawArrays(mode: number = GL_POINTS) {
let r, g, b;
if (mode == GL_LINES_STRIP) {
for (let i = 0; i < this.vertices.length - 1; i++) {
let p0 = this.vertices[i];
let p1 = this.vertices[i+1];
this.lineBres(p0.x, p0.y, p1.x, p1.y);
}
}
this.glEnd();
}
}
var canvas = <HTMLCanvasElement>document.getElementById("canvas001");
var webgl = new WebGL(canvas);
webgl.glClearColor(1, 1, 1, 1);
webgl.glBgColor(1, 1, 1);
webgl.glColor(0, 0, 0);
function linePlot(dataPts: Array<any>) {
for (let k = 0; k < dataPts.length; k++) {
webgl.glVertex2(dataPts[k][0], dataPts[k][1]);
}
webgl.drawArrays(GL_LINES_STRIP);
}
// 点划线
let data_points = [[0, 50], [230, 120], [440, 320], [660, 260], [900, 50]];
webgl.glLineStipple(3, 0x1C47);
linePlot(data_points);
// 实线
data_points = [[0, 10], [230, 80], [440, 280], [660, 220], [900, 10]];
webgl.glLineStipple(1, 0xFFFF);
linePlot(data_points);
// 虚线
data_points = [[0, 100], [220, 290], [430, 280], [710, 500], [900, 420]];
webgl.glLineStipple(1, 0x00FF);
linePlot(data_points);
// 点线
data_points = [[0, 200], [250, 240], [440, 390], [700, 300], [900, 520]];
webgl.glLineStipple(2, 0x0101);
linePlot(data_points);
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>线的属性</title>
</head>
<body>
<canvas id="canvas001" width="900" height="600"></canvas>
<script src="./dist/app.js"></script>
</body>
</html>
参考文献
[1]. 《计算机图形学》(第三版)Donald Hearn、M.PaulineBaker著,4.8.2 线型函数,P154页;
[2]. Bresenham画线算法及实践