初學路徑繪製的時候,大部分人應該會發現一種讓人疑惑的狀況。
那就是當繪製的路徑稍微複雜一點且路徑線段產生交錯的時候,有些透過路徑線圍起來的區域,在發動ctx.fill()填充顏色之後,仍然維持未填充的狀態。
之所以產生這種狀況的原因,是因為『你的大腦』和『程式邏輯』判斷封閉區域的規則不一樣。
而這篇文章的重點就在於講解『程式邏輯』判斷一個『路徑』是否存在『封閉區域』的判斷依據。
這個『判斷依據』一共有兩種模式,一種稱為『非零纏繞(Nonzero)』,另外一種則叫做『奇偶規則(Evenodd)』。
能最簡單體現這兩種判斷邏輯差別的方式就是畫兩個五角星,然後在ctx.fill()這個方法內導入填充模式的參數(也就是"evenodd" or "nonzero")。
ctx.fill() 的參數型別相關資訊可以看這篇MDN上的介紹
<div>
<h1>evenodd</h1>
<canvas width="300" height="300" id="canvas1"></canvas>
</div>
<div>
<h1>nonzero</h1>
<canvas width="300" height="300" id="canvas2"></canvas>
</div>
function draw1(){
let cvs = document.querySelector('#canvas1');
let ctx = cvs.getContext('2d');
ctx.beginPath();
// 把筆尖的座標移動到200,200
ctx.moveTo(50,50);
ctx.lineTo(200,200);
ctx.lineTo(140,30);
ctx.lineTo(10,100);
ctx.lineTo(190,100);
ctx.lineTo(50,50);
// 設定邊框顏色
ctx.fillStyle="red";
// 賦予框線
ctx.fill('evenodd'); // 事實上fill會自帶closePath的效果
ctx.closePath(); // 也就是說這一行可以不寫也沒差
}
function draw2(){
let cvs = document.querySelector('#canvas2');
let ctx = cvs.getContext('2d');
ctx.beginPath();
// 把筆尖的座標移動到200,200
ctx.moveTo(50,50);
ctx.lineTo(200,200);
ctx.lineTo(140,30);
ctx.lineTo(10,100);
ctx.lineTo(190,100);
ctx.lineTo(50,50);
// 設定邊框顏色
ctx.fillStyle="red";
// 賦予框線
ctx.fill('nonzero');
ctx.closePath();
}
(()=>{
draw1();
draw2();
})()
codepen連結:
https://codepen.io/mizok_contest/pen/BaZmdOv
好啦, 我知道我的五角星很醜, 不要再嫌了
這邊我們可以發現,左邊evenodd規則所畫出來的圖形,中間並沒有被填滿,但是nonezero規則下的圖形卻是相反過來的狀況。
這是為什麼? 接下來我們就是要來解釋這兩種規則的差異。
非零纏繞(nonzero)、奇偶規則(evenodd) 其實是在電腦圖學一個很常見的概念(SVG也會牽涉到這兩個東西),這兩種概念是用在“當判斷一個座標是否處於一個封閉路徑內部時”,採用的兩種基準點。
上圖是同一個path , 採用不同的規則時,在被Fill之後的樣子
我們可以看到這個path是由相同的一組編號1~5的向量線所形成的一個path。
基本上這兩種模式判斷的依據都是透過向量的改變狀況,還有向量的夾角來判定。
這邊我們先來複習一下高中的數學/物理~所謂的向量,指的是一種從座標A移動到座標B的附帶方向的移動量。
而『向量的夾角』指的則是兩條向量之前夾的最小角度(意思就是說『夾角』永遠是指小於180度的那個角)。
另外,夾角的計算~必須要是讓兩組向量從同一個座標點出發才能夠判定
像下面這邊的案例是透過把A向量拉出延伸, 直到A向量與B向量自同一座標出發
我們在接下來的講解其實還會提到向量夾角的正負值,所以我們這邊也簡單的做一些說明:
向量夾角正負的判斷, 這邊就會牽涉到我們前面講到的canvas座標系問題
還記得我們前面有提到過canvas的座標系是屬於左手定則坐標系嗎?而且左手定則坐標系是『順時針方向為正』
當我們有兩條向量(A與B), 假設今天我們要讓A轉變成B, 其實可以想像有一台以A向量方向前進的車,而突然這台車受到某種外力的干涉,導致車子必須變成以B向量方向行駛:
BTW,對高中數學還有印象的人可能還會記得這個公式~
假設有一個向量圍成的三角形如下:
如果我們要求取AC向量和AB向量的夾角,則可以透過這個公式來求得
而這樣的公式因爲完全是數理邏輯,所以我們其實也可以把它改寫成程式
接下來我們看看兩種規則是怎麼透過向量夾角機制來判定封閉區域是否存在 :D
由點A向外隨便一個方向拉一條無限延伸的線(淡藍色的線),當這條線和1~5編號的向量交接時,若交接的夾角是呈逆時針,則-1,若為順時針則+1,最後的總和若不為0,代表點A在Path內部(也就是說A在一個封閉路徑內部),若為0則反之。
奇偶規則的判定比較簡單,同時他也跟向量判定沒太大關係。
由點A向外隨便一個方向拉一條無限延伸的線(淡藍色的線),當這條線和1~5編號的向量交接時,每碰到一條線就+1,
最後的總和若為奇數,代表點A在Path內部(也就是說A在一個封閉路徑內部),若為偶數則反之。
一般來說,大部分情況下evenodd的填充方式不會去涵蓋到shade region
(就是容易因為模式改變而轉變為 開放/封閉 區域 的地方)。
所以當我們想要用path去畫一個鏤空的圖形,一般會先把fillRule 改成evenodd。
但是,evenodd & 鏤空 這兩件事其實不是充要條件,而是就統計學上來講,evenodd模式容易創造出比較多的鏤空區。
根據繪製路徑的細節,nonzero模式同樣也可能創造出鏤空區。例如下面這個案例。
這個路徑是以nonzero方式填充,但卻仍然有鏤空區存在。