何一個畫板軟件都有繪制直線的功能,如果想要基于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的中點來進行判別。
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() 填充圖形
繪制直線
*請認真填寫需求信息,我們會在24小時內與您取得聯系。