2021-09-12

自製繪製圖形工具

最近使用 Raspberry Pi Pico 控制 ST7789 TTF LCD 熒幕繪製一些簡單的圖案
但 MicroPython 及 CircuitPython 都沒有原生的圖像工具函式庫
因此在下嘗試試自己編寫一些自己需要使用的功能
雖然沒有原生函式庫,但其實有很多第三方函式庫
不過在下還是希望自己懂得及編寫自己的函式庫
可以在使用其他工具中時,即使沒有第三方函式庫都能夠自行編寫

座標幾何

以 SVG 為例,繪製直線最基本只需要 line 再設定 (x1,y1)(x2,y2) 的 2組座標,例如:

<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 100 100" version="1.1">
	<rect width="100" height="100" fill="#FFFF00"/>
	<line x1="10" y1="15" x2="70" y2="95" stroke="#000000"/>
</svg>
雖然 SVG 可以很簡單定義直線,但仍然需要渲染引擎將圖形渲染到熒幕上,才能讓使用者看到實際的圖案及效果
而且熒幕是以 像素(Pixel) 為單位
亦即是2組座標中間的線段,因此仍然需要像 點陣圖 般逐點繪製,才能呈現在熒幕上
因此若沒有渲染引擎,便需要計算中間的線段每個像素的座標

直線工具

在 座標幾何 中,可以使用
y=ax+b

來表達任何直線線段

當中的 a 為 斜率(Slope) ,可以使用

a=\frac{y_{2}-y_{1}}{x_{2}-x_{1}}
方程式來計算

而 b 則需要將 x 及 y 代入到方程式中計算
(已知 x1, y1, x2, y2 數值, x 及 y 為 其中1組座標即可)

y_{1}=\frac{y_{2}-y_{1}}{x_{2}-x_{1}}x_{1}+b
用先前的 SVG 線段例子,將所以 x 及 y 代入


15=\frac{95-15}{70-10}10+b
計算出 a 為 4/3 及 b 為 5/3

即是 SVG 線段例子的方程式為
y=\frac{4}{3}x+\frac{5}{3}

便可以代入 x 或 y 計算座標

代入 x 計算 y 的效果


代入 y 計算 x 的效果

會發現 代入 x 計算 y 及 代入 y 計算 x 顯示效果為有些微分別
由於線段的座標必定為 非負整數 ,當計算的結果出現小數,不論使用
  • 向下取整 (Floor / Round Down)
  • 向上取整 (Ceil / Round Up)
  • 四捨五入 (Round)
運算後必定會有數值誤差

另外,由於例子線段的 闊度 (Width) 少於 高度 (Height) ,代入 x 計算 y 會缺漏部分 y 資料,令線段不連貫
因此要讓線段連貫,必須讓長邊的變數作為代入值

在下編寫了一個簡單的偽代碼
function drawLine(x1, y1, x2, y2) {
	if (x1 == x2) {
		for (y = y1 to y2) {
			drawPixel(x1, y)
		}
	} else if (y1 == y2) {
		for (x = x1 to x2) {
			drawPixel(x, y1)
		}
	} else {
		h = y2 - y1
		w = x2 - x1
		a = h / w
		b = y1 - a * x1
		if (abs(h) < abs(w)) {
			if (x1 > x2) {
				swap(x1, x2)
			}
			for (x = x1 to x2) {
				y = a * x + b
				drawPixel(round(x), round(y))
			}
		} else {
			if (y1 > y2) {
				swap(y1, y2)
			}
			for (y = y1 to y2) {
				x = (y - b) / a
				drawPixel(round(x), round(y))
			}
		}
	}
}

圓形及橢圓形工具

SVG 圓形例子

<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 100 100" version="1.1">
	<rect width="100" height="100" fill="#FFFF00"/>
	<circle cx="40" cy="30" r="20" fill="none" stroke="#000000"/>
</svg>
SVG 橢圓形例子

<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 100 100" version="1.1">
	<rect width="100" height="100" fill="#FFFF00"/>
	<ellipse cx="40" cy="30" rx="20" ry="10" fill="none" stroke="#000000"/>
</svg>
圖形及橢圓形 在 SVG 同樣也很簡單,設定圓形中心的座標(cx,cy),再設定圓形的半徑(r) 便可以定義圓形
但座標幾何的方式,是透過渲染引擎協助下繪製出線條,並非以像素形式畫出
因此需要計算每個像素的座標,才能製作圓形的點陣圖

P_{x}=C_{x}+r_{x}\sin{\theta}
P_{y}=C_{y}+r_{y}\cos{\theta}

當橢圓形的 rx 與 ry 相同,就是圓形

偽代碼程式
function drawEllipse(cx, cy, rx, ry) {
	for (d = 0 to 359) {
		x = cx + rx * sin(d)
        y = cy + ry * cos(d)
        drawPixel(round(x), round(y))
	}
}


點陣圖方式繪製圓形


點陣圖方式繪製橢圓形

曲線工具

如果要繪製曲線,在 SVG 都會使用 path 配 d 屬性來繪製曲,亦即是 貝茲曲線 (Beizer Curve)

<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 100 100" version="1.1">
	<rect width="100" height="100" fill="#FFFF00"/>
	<path d="M 20,30 Q 60,50 20,70" fill="none" stroke="#000000"/>
</svg>

從資料顯示,貝茲曲線的線段座標可以通過

P(t)=\sum_{i=0}^{n}P_{i}\binom{n}{i}t^{i}(1-t)^{n-i}

這方程式列出線段上的座標,但留意 t 的範圍為 0 至 1

偽代碼程式
function factorial(n) {
	product = 1
	for (i = n to 1 step -1) {
		product *= i
	}
	return product
}

function nPr(n, r) {
	return round(factorial(n) / factorial(n - r))
}

function nCr(n, r) {
	return round(nPr(n, r) / factorial(r))
}

function drawBezierCurve(ps) {
	n = ps.length
	for (t = 0 to 1 step 0.01) {
		x = 0
		y = 0
		for (i in ps.indices) {
			m = nCr(n - 1, i) * ((1 - t) ^ (n - 1 - i)) * (t ^ i)
			x += ps[i].x * m
			y += ps[i].y * m
		}
		drawPixel(round(x), round(y))
	}
}


點陣圖方式繪製曲線


製作多段曲線

總結

由於這個函式實際是一些數學運算,除了貝茲曲線,其他繪製方法都是回憶一些中小學的數學知識而編寫
將舊知識溫顧知新可以鞏固技術,沒有工具便自行製作工具,可以避免受別人的工具而限制自己

參考資料

沒有留言 :

張貼留言