Day6 - 2D渲染环境基础篇 II [同场加映 - 非零缠绕与奇偶规则] - 成为Canvas Ninja ~ 理解2D渲染的精髓

路径绘制常令人感到疑惑的点 - 非零缠绕与奇偶规则

初学路径绘制的时候,大部分人应该会发现一种让人疑惑的状况。

那就是当绘制的路径稍微复杂一点且路径线段产生交错的时候,有些透过路径线围起来的区域,在发动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

非零缠绕(nonzero)

由点A向外随便一个方向拉一条无限延伸的线(淡蓝色的线),当这条线和1~5编号的向量交接时,若交接的夹角是呈逆时针,则-1,若为顺时针则+1,最後的总和若不为0,代表点A在Path内部(也就是说A在一个封闭路径内部),若为0则反之。

奇偶规则(evenodd)

奇偶规则的判定比较简单,同时他也跟向量判定没太大关系。

由点A向外随便一个方向拉一条无限延伸的线(淡蓝色的线),当这条线和1~5编号的向量交接时,每碰到一条线就+1,
最後的总和若为奇数,代表点A在Path内部(也就是说A在一个封闭路径内部),若为偶数则反之。

小结

一般来说,大部分情况下evenodd的填充方式不会去涵盖到shade region
(就是容易因为模式改变而转变为 开放/封闭 区域 的地方)。

所以当我们想要用path去画一个镂空的图形,一般会先把fillRule 改成evenodd。

但是,evenodd & 镂空 这两件事其实不是充要条件,而是就统计学上来讲,evenodd模式容易创造出比较多的镂空区。

根据绘制路径的细节,nonzero模式同样也可能创造出镂空区。例如下面这个案例。

这个路径是以nonzero方式填充,但却仍然有镂空区存在。


<<:  [Day 06] 特徵图想让人分群 ~模型们的迁移学习战~ 第一季 (迁移学习)

>>:  Day10 - LinearLayout线性布局

Kneron - 在Raspberry Pi 4(Raspbian Buster)上安装 OpenCV 参考笔记

Kneron - 在Raspberry Pi 4(Raspbian Buster)上安装 OpenC...

etcd 元件浅解

在 Kubernetes 中元件间的通讯都是藉由 API Server 通讯,而 API Serve...

[鼠年全马] W33 - Vue出一个旅馆预约平台(7)

这周继续来做 [预约页面] 回顾一下上周切的区块 [标题] (已完成) [预约功能] [房间详细] ...

[Day8] IoT Maker之Coding知识科普 - (缩排&条件逻辑判断)

1.前言 在各式各样的程序语言中,都有属於自己的语系,像是Arduino就偏向於C语言,而每种语言都...

Day 20 - WooCommerce: 定义信用卡付款闸道

永丰金流收款 API 在目前我们从文件看到的,支援信用卡付款及虚拟帐号 ATM 付款。本次铁人赛在也...