整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何实现使用鼠标绘制直线?

何一个画板软件都有绘制直线的功能,如果想要基于web实现鼠标画直线该怎么做哪?本文基于html canvas介绍一种实现鼠标绘制直线的方法,最终效果如下。

画一条直线

首先,我们来尝试先做到画单条直线,html canvas提供了画线的方法,这里稍作封装,draw_line方法使用canvas上下文提供的方法绘制一条直线,start_vector、end_vector使用向量来表示起点和终点。

// 构造向量[x,y]
function make_vect(x, y) {
    return [x ,y];
}
// 获取向量x坐标
function xcor_vect(vector) {
    return vector[0];
}
// 获取向量y坐标
function ycor_vect(vector) {
    return vector[1];
}

function draw_line(start_vector, end_vector) {
    ctx.beginPath();
    //移动到起点
    ctx.moveTo(xcor_vect(start_vector), ycor_vect(start_vector));
    //绘制起点到终点的直线段
    ctx.lineTo(xcor_vect(end_vector), ycor_vect(end_vector));
    ctx.stroke();
}

有了上面的draw_line方法,只要通过监听鼠标按下事件获取鼠标按下时的坐标点作为起点,再获取鼠标释放时的坐标点作为终点,然后调用draw_line方法就可以做到画一条直线了,示例代码如下。

...以上代码省略
// container为html中canvas元素id
var canvas = document.getElementById("container");
var ctx = canvas.getContext("2d");

// 监听鼠标按下事件
canvas.onmousedown = function(event) {
    //按下鼠标获取起点
    let start_point = make_vect(event.clientX, event.clientY);

    // 监听鼠标松开事件
    canvas.onmouseup = function(event) {
        // 释放鼠标获取终点
        let end_point = make_vect(event.clientX, event.clientY);
        draw_line(start_point, end_point);
    }
}

开启web服务后访问页面,选择canvas合适位置点击鼠标并拖动可以绘制一条直线,但是只有鼠标释放时直线才会绘制出来,这样的体验显然是不好的,我们希望直线能够一直跟踪鼠标的轨迹。

跟踪鼠标轨迹

要跟踪鼠标轨迹,还需要监听鼠标移动事件,在鼠标拖动的每个时刻都将鼠标位置作为终点进行画线,代码如下:

// container为html中canvas元素id
var canvas = document.getElementById("container");
var ctx = canvas.getContext("2d");
var pressed = false;

// 监听鼠标按下事件
canvas.onmousedown = function(event) {
    //按下鼠标获取起点
    let start_point = make_vect(event.clientX, event.clientY);
    pressed = true;

    // 监听鼠标移动事件
    canvas.onmousemove = function(event) {
        //只有鼠标按下时拖动才需要画线
        if (pressed) {
            let end_point = make_vect(event.clientX, event.clientY);
            draw_line(start_point, end_point);
        }    
    }

    // 监听鼠标松开事件
    canvas.onmouseup = function(event) {
        let end_point = make_vect(event.clientX, event.clientY);
        draw_line(start_point, end_point);        
        pressed = false;
    }
}

再次测试发现结果很离谱,竟然画出了很多条线,但值得肯定的是这样做到了跟踪鼠标的拖动轨迹,之所以会出现很多条线,是因为每个鼠标拖动位置的直线都已经画在画布上了,只要每次鼠标移动事件处理前把之前已画的线擦除掉就可以了。

// container为html中canvas元素id
var canvas = document.getElementById("container");
var ctx = canvas.getContext("2d");
var pressed = false;

// 监听鼠标按下事件
canvas.onmousedown = function(event) {
    //按下鼠标获取起点
    let start_point = make_vect(event.clientX, event.clientY);
    pressed = true;

    // 监听鼠标移动事件
    canvas.onmousemove = function(event) {
        //只有鼠标按下时推动才需要画线
        if (pressed) {
	        // 先擦除画布
            ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
            let end_point = make_vect(event.clientX, event.clientY);
            draw_line(start_point, end_point);
        }    
    }

    // 监听鼠标松开事件
    canvas.onmouseup = function(event) {
        let end_point = make_vect(event.clientX, event.clientY);
        draw_line(start_point, end_point);        
        pressed = false;
    }
}

再次测试发现可以很方便的使用鼠标画一条直线了,而且可以实时跟踪鼠标的位置来调整直线直到鼠标释放才会确定绘制哪条直线。

保留痕迹

还有一个问题需要解决,现在画完一条直线后,继续画下一条直线就会把之前的清掉,画布上始终只有一条直线,这样的画板是没什么价值的,我们需要画布既要能够实时跟踪当前绘制直线的样子,还要能够保留以前绘制好的直线,就像本文开头展示的效果一样。显然,可以通过定义一个数组来存放历史直线的坐标数据,每次清空画布后再把它们绘制出来就可以轻松做到想要的功能了。完整代码如下:

据直线的一般表示形式y=mx+b我们可以很容易得出经过两点p0(x0,y0)、p1(x1,y1)的直接方程为

稍作变换表示成函数的形式如下

显示屏幕是由如下图所示一个个微小的像素组成的,屏幕坐标是离散的,绘制一条线段实际上就是设置一系列像素的颜色来近似模拟一条直线,Bresenham方式是一种采用中点算法来实现直线绘制的方法。

这里以直线斜率在(0,1]区间的情况为例来介绍Bresenham算法的核心内容,斜率在此区间内x轴的增长要快于y轴的增长,因此我们直接遍历x0~x1之间的像素(像素坐标都是整数),关键步骤就是确定x对应的y轴像素值。

假如已经绘制了像素(x,y),下一步就是确定是绘制(x+1,y+1)还是(x+1,y),Bresenham算法采用计算直线方程的函数形式的值f(x+1, y+0.5),也就是y+1和y的中点来进行判别。

  • 如果f(x+1, y+0.5)>0说明直线在y+1和y中点的下方,此时绘制(x+1,y)比较合理;
  • 如果f(x+1, y+0.5)<0说明直线在y+1和y中点的上方,此时绘制(x+1,y+1)比较合理;
  • 如果f(x+1, y+0.5)=0说明直线刚好穿过y+1和y中点,此时绘制那一个都是可以的;
    表示为伪代码的形式为:
y = y0
for x = x0 to x1 do
	draw(x,y)
	if f(x+1, y+0.5) < 0 then
		y = y + 1


其他斜率曲线的判断方法是类似的,需注意的是如果y的变化快于x的变化,需要遍历y0到y1,通过中点算法来确定x的值。另外需要特别注意的就是直接方程f(x,y)的值大于0还是小于0的判断要仔细判别,这里判断方法并不是复杂,只是比较容易混淆。

x、y象限内直线斜率可以换分为(-∞,-1]、(-1,0]、(0,1]、(1,+∞)四个区间,见下图。

最后,我们用html的canvas元素来演示Bresenham画线算法,理解了该算法的原理,代码也就顺理成章了。示例绘制了四个斜率的线段,效果如下,可以看到某些斜率下的锯齿效果还是非常明显的,后续会继续介绍如何利用抗锯齿算法来生成更平滑的直线。


注:canvas的y轴正向是向下的。
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>Bresenham画线算法示例</title>

</head>

<body>

    <canvas id="canvas" width="780" height="780"></canvas>

    <script src="./bresenham.js"></script>

</body>

</html>

bresenham.js

var canvas = document.getElementById("canvas");

var ctx = canvas.getContext("2d");

  

function setPixel(x, y, color='rgba(255, 255, 255, 1.0)') {

    ctx.fillStyle = color;

    ctx.fillRect(x, y, 1, 1);

}

  

function  drawLine(p0, p1, color='rgba(255, 255, 255, 1.0)') {

    let x0, y0, x1, y1;

    // 保持x1>=x0

    if (p0.x > p1.x) {

        [x0, y0] = [Math.round(p1.x), Math.round(p1.y)];

        [x1, y1] = [Math.round(p0.x), Math.round(p0.y)];

    } else {

        [x0, y0] = [Math.round(p0.x), Math.round(p0.y)];

        [x1, y1] = [Math.round(p1.x), Math.round(p1.y)];

    }

    let m = (y1-y0)/(x1-x0);

    // 直线方程

    let f = (x,y) => (y0-y1)*x + (x1-x0)*y + x0*y1 - x1*y0;

    // 区间(0,1]

    if (m > 0 && m <= 1) {

        let y = y0;

        for (let x = x0; x < x1; x++) {

            this.setPixel(x, y, color);

            if (f(x+1, y+0.5) < 0) {

                y += 1;

            }

        }

    }

    // 区间(1,正无穷)

    else if (m > 1) {

        let x = x0;

        for (let y = y0; y < y1; y++) {

            this.setPixel(x, y, color);

            if (f(x+0.5, y+1) > 0) {

                x += 1;

            }

        }

    }

    // 区间(-1,0]

    else if (m > -1 && m <= 0) {

        let y = y0;

        for (let x = x0; x < x1; x++) {

            this.setPixel(x, y, color);

            if (f(x+1, y-0.5) > 0) {

                y -= 1;

            }

        }

    }

    // 区间(负无穷,-1]

    else if (m <= -1) {

        let x = x0;

        for (let y = y0; y > y1; y--) {

            this.setPixel(x, y, color);

            if (f(x+0.5, y-1) < 0) {

                x += 1;

            }

        }

    }

}

// (1,+∞)

drawLine({x: 300, y: 590}, {x: 480, y: 190}, 'rgba(255, 0, 0, 1.0)');  

// (-1,0]

drawLine({x: 300, y: 400}, {x: 480, y: 380}, 'rgba(255, 255, 0, 1.0)');

// (0,1]

drawLine({x: 300, y: 380}, {x: 480, y: 400}, 'rgba(0, 0, 255, 1.0)');  

// (-∞,-1]

drawLine({x: 300, y: 190}, {x: 480, y: 590}, 'rgba(0, 255, 0, 1.0)');

参考文献

[1]. 《fundamentals of computer graphics》9.1.1 line drawing, p179.

制各种图形步骤

1 开始新路径

beginPath() 开始新路径 绘制矩形和填充矩形可省略此步骤

2 设置路径

moveTo(x,y) 移动起始点到x,y

lineTo(x,y) 绘制目前端点到x,y的直线

arc(x,y,r,startAngle,endAngle,antiClockwise) 绘制圆形或圆弧

fillRect(x,y,width,height) 绘制填满矩形

strokeRect(x,y,width,height) 绘制轮廓矩形(只有边框,不填充颜色)

3 将路径头尾相连

closePath() 关闭路径

4 将路径绘制到canvas绘图区

Stroke() 绘制边框

Fill() 填充图形



绘制直线