tft每日頭條

 > 圖文

 > canvas的坐标怎麼算

canvas的坐标怎麼算

圖文 更新时间:2025-04-02 16:35:16

本文所有知識點均來源于《圖形渲染實戰 2D架構設計與實現》這本書,作者關于向量數學知識點的講解是我目前看到的最全面、最容易理解的。這裡隻是抛磚引玉,感興趣請閱讀書籍

1. 概念

向量的定義:向量是具有方向(Direction)和大小(Length / Magnitude)的空間變量。

向量的大小:已知一個向量a = [ x , y ],那麼該向量的大小可以使用|| a ||來表示,其值為一個标量(Scalar),可以由Math.sqrt(x^2 y^2)這個公式計算得出。

2.向量加減法2.1加法

canvas的坐标怎麼算(Canvas之向量數學)1

在圖6.2中,向量a(實線且大小為200)與向量b(實線且大小為282.84)相加的幾何解釋就是:平移向量,使向量b的尾部(向量b的圓點部分)連接向量a的頭部(向量a箭頭部分),接着從向量a的尾部向向量b的頭部畫一個向量(大小為446.21),該向量就是向量a b形成的新的向量,這就是向量加法的三角形法則。而在圖6.2所示的向量加法幾何特性圖示中,使用的是向量加法的平行四邊形法則繪制的。

向量加法很有用。舉個例子,在力學中,如果有兩個力(向量)同時施加在某個物體上,那麼該物體受到的合力就是這兩個力(向量)相加。

2.2減法

canvas的坐标怎麼算(Canvas之向量數學)2

向量減法的幾何含義,如圖6.3所示,可以固定任意一個向量,平移另外一個向量,讓兩個向量的尾部重合,此時如果是:會發現,向量的減法是具有方向性的,并不滿足交換律。

  • 向量a(實線且大小為200)減去向量b(實線且大小為282.84),則從向量b(實線且大小為282.84)的頭部向着向量a(實線且大小為200)的頭部畫一個新的向量(粗線且大小為200),如圖6.3左圖所示。[插圖]圖6.3 向量減法
  • 向量b(實線且大小為282.84)減去向量a(實線且大小為200),則從向量a(實線且大小為200)的頭部向着向量b(實線且大小為282.84)的頭部畫一個新的向量(粗線且大小為200),如圖6.3右圖所示。
2.3向量與标量乘法

向量與标量不能相加,但是它們能相乘。當一個标量和一個向量相乘時,将得到一個新的向量,該向量與原向量平行,但長度不同或方向相反。如圖6.5所示,畫布中心左側的向量的方向都是相同的(平行),向量的大小則以每20個單位遞增。而畫布中心右側的向量,方向相反(仍舊平行),其大小也是以每20個單位遞增。

canvas的坐标怎麼算(Canvas之向量數學)3

向量與标量相乘的本質是縮放向量,因此實現的靜态方法名為scale。現在大家應該知道标量的英文為什麼叫Scalar了,因為标量(Scalar)用來縮放(Scale)向量(Vector)

2.4向量的點乘

向量能與标量相乘,向量也能和向量相乘。兩個向量相乘被稱為點乘(也常稱為點積或内積)

canvas的坐标怎麼算(Canvas之向量數學)4

兩個向量a和b,夾角為θ,根據餘弦定律:

|| a - b ||²= || a ||² || b ||²-2 || a || || b || cosθ。

将上述表達式的左側|| a - b ||²展開,寫成|| a ||² || b ||²-2 ( a·b ),則可以得到:

|| a ||² || b ||²-2 ( a·b ) = || a ||2 || b ||2-2 || a || || b || cosθ。

從而就可以導出如下公式:

a·b = || a || || b || cosθ。

根據上面的公式,可以寫成如下形式:

cosθ = a·b / ( || a || || b || ) 其中 ( || a || || b || ) 的值總是正數。

由此得到如下重要的信息:

  • 當a·b > 0時,向量a和b的夾角θ為銳角(因為0 <= θ < Math . PI / 2的餘弦值大于0),可以認為兩個向量方向基本相同,如圖6.6左側圖示。
  • 當a·b = 0時,向量a和b的夾角θ為直角(因為Math . PI / 2的餘弦值等于0),可以認為兩個向量相互垂直,如圖6.6中間所示的圖示。
  • 當a·b < 0時,向量a和b的夾角θ為鈍角(因為Math . PI / 2 < θ <= Math .PI ]的餘弦值小于0),可以認為兩個向量方向基本相反,如圖6.6右側圖示。
  • 如果向量a、b中任意一個向量為零向量,則a·b的結果也為0,這意味着零向量和任意其他向量都是相互垂直的。
2.5向量夾角

根據這個公式:cosθ = a·b / ( || a || || b || ),那麼能夠很容易計算出向量a與向量b之間的夾角,僞代碼如下:

// 根據公式:cosθ = a·b / ( || a || || b || ),計算向量a和向量b的夾角 // 根據isRadian參數的取值來判斷是返回弧度還是返回角度 public static getAngle(a: Vec2, b: Vec2, isRadian: boolean = false): number { let dot: number = Vec2.dotProduct(a, b); let radian: number = Math.acos(dot / (a.length * b.length)); if (isRadian === false) { radian = Math2D.toDegree(radian); } return radian; }

2.6向量朝向

為了與夾角區分,使用朝向來表示物體的方向。具體代碼如下:

// 計算兩個向量的連起來與x軸的夾角 public static getOrientation( from: Vec2, to: Vec2, isRadian: boolean = false ): number { let diff: Vec2 = Vec2.difference(to, from); let radian = Math.atan2(diff.y, diff.x); if (isRadian === false) { radian = Math2D.toDegree(radian); } return radian; }

3.向量投影Demo

canvas的坐标怎麼算(Canvas之向量數學)5

背景描述:當鼠标指針的位置在向量的區域範圍之外(如圖6.7所示),則正常顯示該向量。但當鼠标指針的位置移動到向量區域範圍内,則會加粗顯示該向量,并且會:

(1)标記出鼠标指針位置(圓圈表示)、坐标信息,以及線段起點到鼠标指針處的向量。

(2)标記出鼠标指針位置在向量上的投影點(圓圈),坐标信息,以及從投影點到鼠标指針位置之間的向量。

(3)标記出鼠标指針位置與線段起點之間以角度表示的夾角。

(4)所有的坐标信息是相對全局坐标系原點(左上角)的偏移表示。

canvas的坐标怎麼算(Canvas之向量數學)6

canvas的坐标怎麼算(Canvas之向量數學)7

3.1向量投影算法

圖6.7和圖6.8所示的效果是經典的向量投影算法。簡單地說,就是将一個點(鼠标位置表示)投影到由起點和終點所形成的向量上。該算法比較常用,僞代碼如下:

/** * 判斷點是否在線上 * @param pt * @param start * @param end * @param closePoint * @returns */ public static projectPointOnLineSegment( pt: vec2, start: vec2, end: vec2, closePoint: vec2 ): boolean { let v0: vec2 = vec2.create(); let v1: vec2 = vec2.create(); let d: number = 0; // 向量起點到鼠标位置的方向向量 vec2.difference(pt, start, v0); // 向量起點到向量終點的方向向量 vec2.difference(end, start, v1); // 原向量變成單位向量,并返回原向量的長度 d = v1.normalize(); // 将v0投影到v1上,獲取投影長度 // v0*v1 = ||v0|| * ||v1|| * cosθ // v1是單位向量,所有 ||v1|| = 1 // 于是 v0*v1 = ||v0|| * cosθ,也就是v0在v1上的投影長度 let t: number = vec2.dotProduct(v0, v1); // 如果t < 0,說明鼠标在起始點之外,返回起始點 if (t < 0) { closePoint.x = start.x; closePoint.y = start.y; return false; } else if (t > d) { // 投影長度 > 線段長度,說明鼠标位置超過線段終點範圍 closePoint.x = end.x; closePoint.y = end.y; return false; } else { // 鼠标點位于線段中間 // 使用scaleAdd計算出相對于全局坐标的坐标偏遠信息 // start 起點向量 // v1 * t 投影向量 // start v1(單位向量)*t(标量) 相對于全局坐标的向量 vec2.scaleAdd(start, v1, t, closePoint); return true; } }

3.2向量的兩種夾角

為了更好地了解getOrientation(向量朝向)和getAngle(向量夾角)方法之間的區别,參考如圖6.10所示的效果,可以知道:

canvas的坐标怎麼算(Canvas之向量數學)8

  • getOrientation方法内部使用的是Math類的atan2方法,atan2方法返回的總是與x軸正方向向量之間的夾角,其取值範圍為[ - Math . PI , Math . PI ]之間。
  • getAngle方法内部使用Math類的acos方法,acos方法返回的是兩個向量之間的夾角,其返回值的取值範圍為[ 0 , Math . PI ]。
  • 可以将getOrientation看作絕對方向的表示,能唯一地确定物體的方向。而getAngle返回的是相對方向,由于其取值範圍,無法确定角度的旋轉方向(是逆時針旋轉還是順時針旋轉)。
3.3碰撞檢測算法3.3.1點與圓的碰撞檢測

如果一個點在圓的半徑範圍之内,則說明發生了碰撞

/** * 判斷坐标是否在圓内部 * @param pt 鼠标坐标 * @param center 圓中心坐标 * @param radius 半徑 * @returns */ public static isPointInCircle( pt: vec2, center: vec2, radius: number ): boolean { let diff: vec2 = vec2.difference(pt, center); let len2: number = diff.squaredLength; // 避免使用Math.sqrt方法 if (len2 <= radius * radius) { return true; } return false; }

3.3.2點與線段的碰撞檢測

點與線段的碰撞檢測是一個比較基礎和重要的算法,該算法可以由兩部分組成:

  • 用projectPointOnLineSegment方法确定一個點是否在一條線段的表示區間,并且返該點的投影點,前面實現了該方法。
  • 再調用isPointInCircle方法,該方法判斷一個點是否和一個圓發生碰撞。

/** * 判斷坐标是否在線附近 * @param pt 鼠标位置 * @param start 線段起始點 * @param end 線段終止點 * @param radius 碰撞半徑 * @returns */ public static isPointOnLineSegment( pt: vec2, start: vec2, end: vec2, radius: number = 2 ): boolean { let closePt: vec2 = vec2.create(); if (Math2D.projectPointOnLineSegment(pt, start, end, closePt) === false) { return false; } return Math2D.isPointInCircle(pt, closePt, radius); }

3.3.3點與矩形的碰撞檢測

關于點與矩形的碰撞檢測算法非常簡單

/** * 判斷坐标是否在矩形内部 * @param ptX 鼠标位置 * @param ptY 鼠标位置 * @param x 矩形左上角 * @param y 矩形左上角 * @param w 矩形寬度 * @param h 矩形高度 * @returns */ public static isPointInRect( ptX: number, ptY: number, x: number, y: number, w: number, h: number ): boolean { if (ptX >= x && ptX <= x w && ptY >= y && ptY <= y h) { return true; } return false; }

3.3.4點與橢圓的碰撞檢測

假設橢圓的中心點定義的坐标值為[ centerX,centerY ],并且半徑分别為[ radiusX , radiusY ],在這種情況下,一個點P ( pX ,pY )如果在橢圓的内部,那麼要滿足如下公式:

canvas的坐标怎麼算(Canvas之向量數學)9

有了上述公式,就可以實現isPointInEllipse方法。

/** * 判斷坐标是否在橢圓内部 * @param ptX * @param ptY * @param centerX * @param centerY * @param radiusX * @param radiusY * @returns */ public static isPointInEllipse( ptX: number, ptY: number, centerX: number, centerY: number, radiusX: number, radiusY: number ): boolean { let diffX = ptX - centerX; let diffY = ptY - centerY; let n: number = (diffX * diffX) / (radiusX * radiusX) (diffY * diffY) / (radiusY * radiusY); return n <= 1.0; }

3.3.5點與三角形的碰撞檢測

如圖6.11所示為點與三角形的關系,從圖中可以知道,點P與[ v0 , v1 , v2 ]形成的三角形之間的關系有兩種,要麼點P在三角形内部,要麼點P在三角形的外部。

canvas的坐标怎麼算(Canvas之向量數學)10

來觀察一下它們之間的區别,你會發現:

  • 如果點P在[ v0 , v1, v2 ]形成的三角形内部,那麼三角形的三個頂點和P形成的三個子三角形[ v0 , v1 , p ]、[ v1 , v2 , p ]和[ v2 , v0 , p]的頂點順序都是按照順時針排列的。
  • 如果點P在[ v0 , v1, v2 ]形成的三角形外部,那麼三角形的三個頂點和P形成的三個子三角形[ v0 , v1 , p ]、[ v1 , v2 , p ]和[ v2 , v0 , p]的頂點順序總有一個不是按照順時針順序排列的,例如圖6.11右側圖像中,[ v2 , v0 , p ]形成的子三角形是非順時針(逆時針)排列的。
  • 更加通用的算法描述是,如果點P與[ v0 , v1 , v2 ]形成的三個子三角形的頂點排列順序一緻,那麼該點P肯定在[ v0 , v1 , v2 ]形成的三角形的内部,否則該點P就在三角形的外部。
3.3.5.1向量叉乘

向量叉乘比較特别,隻能使用3D向量,現在假設有兩個3D向量 a=[x0,y0, z0]和b=[x1, y1, z1],那麼

canvas的坐标怎麼算(Canvas之向量數學)11

為了将2D向量vec2以3D向量的形式來表示,可以将3D向量的z分量設置為0,例如a=[x0, y0,0], b=[x1, y1,0],套用上述叉積公式,會得到:

canvas的坐标怎麼算(Canvas之向量數學)12

可以看到,對于2D向量的叉積來說,其x和y分量總是為0,但是對于點與三角形碰撞檢測算法來說,叉積後的z分量x0y1-y0x1才是最關鍵的,先把z分量的計算作為vec2的一個靜态方法。

/** * 計算三角形兩條邊向量的叉乘 * @param v0 * @param v1 * @param v2 * @returns */ public static sign(v0: vec2, v1: vec2, v2: vec2): number { // v2->v0邊向量 let e1: vec2 = vec2.difference(v0, v2); // v2->v1邊向量 let e2: vec2 = vec2.difference(v1, v2); return vec2.crossProduct(e1, e2); } /** * 判斷鼠标是否在三角形内部 * @param pt * @param v0 * @param v1 * @param v2 * @returns */ public static isPointInTriangle(pt: vec2, v0: vec2, v1: vec2, v2: vec2) { // 計算三角形的三個定點與鼠标形成的三個子三角形的邊向量的叉乘 let b1: boolean = Math2D.sign(v0, v1, pt) < 0.0; let b2: boolean = Math2D.sign(v1, v2, pt) < 0.0; let b3: boolean = Math2D.sign(v2, v0, pt) < 0.0; // 三個三角形的方向一緻,說明點在三角形内部 // 否則點在三角形外部 return b1 === b2 && b2 === b3; }

通過上面的代碼,結合上圖6.11會發現,實際上并不關心子三角形兩條邊向量叉積的數值大小,更關心的是叉積的正負性,隻要三個子三角形的兩條邊向量的叉積的正負性都一緻的話,表示三個子三角形的頂點排列順序一緻,那麼該點P肯定在[v0 , v1 , v2 ]形成的三角形的内部,否則肯定在外部。

3.3.5點與任意凸多邊形的碰撞檢測

如圖6.12所示,可以将任意的凸多邊形很方便地分解成三角形,然後依次調用上一節實現的點與三角形碰撞檢測算法,這樣就能獲得點與任意凸多邊形碰撞檢測的算法。

canvas的坐标怎麼算(Canvas之向量數學)13

/** * 判斷坐标是否在凸多邊形内部 * @param pt * @param points * @returns */ public static isPointInPolygon(pt: vec2, points: vec2[]): boolean { if (points.length < 3) { return false; } // 以point[0]為共享點,遍曆多邊形點集,構成三角形,判斷點是否在三角形内部, // 一旦點與某個三角形發生碰撞,就返回true for (let i: number = 2; i < points.length; i ) { if (Math2D.isPointInTriangle(pt, points[0], points[i - 1], points[i])) { return true; } } return false; }

3.3.5.1凸多邊形判斷

canvas的坐标怎麼算(Canvas之向量數學)14

參考圖6.13會發現,左側的凸多邊形(六邊形)頂點形成的6個子三角形分别是[ v0, v1 , v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v5 ]、[ v4 , v5 , v0 ]和[v5 , v0 , v1 ],這6個子三角形頂點的順序都是順時針排列的。

而觀察上圖右側的凹多邊形,由5個頂點組成,形成的5個子三角形分别是[ v0 , v1, v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v0 ]和[ v4 , v0 , v1 ],會發現最後一個三角形[ v4 , v0 , v1]的頂點順序是逆時針排列,而其他的三角形都是順時針排列。

如何判斷一個三角形的頂點順序,已經在上一節中了解過了,那麼直接來看一下判斷凸多邊形的算法。

/** * 判斷是否凸多邊形 * @param points * @returns */ public static isConvex(points: vec2[]): boolean { // 算出第一個三角形的定點順序 let sign: boolean = Math2D.sign(points[0], points[1], points[2]) < 0; let j: number, k: number; for (let i: number = 1; i < points.length; i ) { j = (i 1) % points.length; k = (i 2) % points.length; // 如果當前多邊形的頂點順序和第一個多邊形的頂點順序不一緻,則說明是凹邊形 if (sign !== Math2D.sign(points[i], points[j], points[k]) < 0) { return false; } } // 凸多邊形 return true; }

4.總結

向量是具有大小和方向的空間變量。而以前常用的數值都是标量,其僅有大小,而沒有方向。在向量一節中,知道了如何計算向量的大小、向量的方向、向量的加減法、負向量、向量的縮放,以及向量的點乘。同時更加詳細地解釋了向量的上述這些操作相對應的幾何含義,這是本文的關鍵點。隻有深刻地理解向量的幾何含義,才能靈活地應用向量來解決問題。

核心關注點與基本幾何形體之間的碰撞檢測算法,涉及點與線段、點與圓、點與矩形、點與橢圓、點與三角形,以及點與凸多邊形之間的碰撞檢測,并且講解了多邊形的三角形化,以及如何判斷凸多邊形的算法。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2025 - www.tftnews.com All Rights Reserved