本文首發於公衆號:符合預期的CoyPan
最近作了一個移動端活動頁的需求,大概就是diy一個頁面。用戶能夠對物料進行拖動、縮放、旋轉,來達到diy的目的。用DOM來實現是不現實的,我採用了canvas來實現和用戶的交互。開發過程當中,涉及到了canvas中對物料元素的拖動、縮放、旋轉等。本文將詳細介紹在不使用任何第三方庫的狀況下,如何實現這些功能。最終的效果demo,能夠參考上面的gif圖。demo體驗地址在這裏:請用手機或瀏覽器模擬手機訪問javascript
本文先介紹整個需求中須要注意的數學知識。html
具體的代碼實現,已經更新了: canvas中的拖拽、縮放、旋轉 (下) —— 代碼實現java
整個需求的大體流程是:canvas
在canvas中實現拖動、縮放、旋轉等交互,最核心的兩個點就是:segmentfault
咱們知道,canvas中最基礎的是座標系統。本次的需求中,兩個最關鍵的點是:數組
首先提供一種簡單的方法,canvas中有一個isPointInPath方法能夠判斷一個落點是否在某個路徑中。不過若是canvas中的元素是圖片,那麼咱們必須在畫每一張圖片時,爲其加上一個路徑包裹起來。這是一種解決落點問題的方案。這裏不作深刻介紹了,我採用的是下面的方案。瀏覽器
爲了使問題簡單化,能夠大膽假設:函數
canvas中的全部元素(圖片、各類圖形等)都是長方形的。這已經能覆蓋大部分狀況了。長方形 四個頂點的座標就能肯定一個元素的位置了。
而當用戶觸摸canvas時,經過觸摸事件,能夠拿到用戶觸摸的座標。若是觸摸座標在長方形頂點座標"內部",則表示觸摸到了元素。因而,咱們的問題能夠抽象爲:spa
已知長方形四個頂點座標,某點的座標,如何判斷這個點是否在這個長方形內部。3d
若是長方形沒有產生旋轉,那麼問題很簡單,只用判斷點的橫縱座標均在長方形的橫縱座標範圍內便可。若是長方形產生了旋轉,這種方法就沒用了。要解決這個問題,先複習一下高中數學 。
咱們能夠將二維平面上的座標都轉化爲向量來計算,能夠將問題簡化不少。
兩個向量的叉積運算結果是一個向量而不是一個標量。叉積的方向與這兩個向量所在的平面垂直。
對於平面中的兩個向量,第三維方向上的值都爲0,其叉積的值爲:
換句話說,咱們能夠很方便地判斷:
一個平面中,在旋轉角不超過180度的狀況下,從一個向量到另一個向量,是順時針轉動仍是逆時針轉動。
直接以canvas的二維座標系統爲例:
能夠獲得這樣的結論:
在canvas二維平面中,設向量A與向量B的叉積對應的二項式的值爲m。若是m>0,則向量A順時針轉動一個角度(小於180度),就可以到達向量B的方向。若是m<0,則須要逆時針轉動。
落點在長方形內的情景以下:
因而,判斷【是否觸摸到了canvas中某元素】的函數就有了:
/** * 判斷落點是否在長方形內 * * @param {Array} point 落點座標。 數組:[x, y] * @param {Array} rect 長方形座標, 按順序分別是:左上、右上、左下、右下。 * 數組:[[x1, y1], [x2, y2], [x3, y3], [x4, y4]] * * @return {boolean} */ function isPointInRect(point, rect) { const [touchX, touchY] = point; // 長方形四個點的座標 const [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] = rect; // 四個向量 const v1 = [x1 - touchX, y1 - touchY]; const v2 = [x2 - touchX, y2 - touchY]; const v3 = [x3 - touchX, y3 - touchY]; const v4 = [x4 - touchX, y4 - touchY]; if( (v1[0] * v2[1] - v2[0] * v1[1]) > 0 && (v2[0] * v4[1] - v4[0] * v2[1]) > 0 && (v4[0] * v3[1] - v3[0] * v4[1]) > 0 && (v3[0] * v1[1] - v1[0] * v3[1]) > 0 ){ return true; } return false; }
用戶能夠對canvas中的元素進行旋轉,那麼如何經過用戶先後兩次的觸摸落點座標求旋轉角度呢?
從上面的等式能夠看出,點積和叉積都能求夾角。選用哪個呢?
在旋轉的場景中,旋轉的方向(逆時針or順時針)是很重要的,而點積最終獲得的只是一個標量,是沒有方向。叉積是一個向量,是有方向的。咱們選擇叉積來計算旋轉角度。
一、通常以canvas中的元素的中心爲旋轉原點,用戶在canvas中觸摸移動時,經過事件監聽函數獲得的先後兩次觸摸點的位移是很小的,與旋轉中心造成的向量夾角必然是小於90度的。
二、向量的叉積正負值能夠肯定旋轉方向。
三、反正弦函數是在負90度到90度之間單調遞增的。
經過以上三點,能夠獲得:
因而,在canvas中,能夠用如下函數來計算連續兩次觸摸落點與旋轉中心造成的旋轉角度:
/** * 計算旋轉角度 * * @param {Array} centerPoint 旋轉中心座標 * @param {Array} startPoint 旋轉起點 * @param {Array} endPoint 旋轉終點 * * @return {number} 旋轉角度 */ function getRotateAngle(centerPoint, startPoint, endPoint) { const [centerX, centerY] = centerPoint; const [rotateStartX, rotateStartY] = startPoint; const [touchX, touchY] = endPoint; // 兩個向量 const v1 = [rotateStartX - centerX, rotateStartY - centerY]; const v2 = [touchX - centerX, touchY - centerY]; // 公式的分子 const numerator = v1[0] * v2[1] - v1[1] * v2[0]; // 公式的分母 const denominator = Math.sqrt(Math.pow(v1[0], 2) + Math.pow(v1[1], 2)) * Math.sqrt(Math.pow(v2[0], 2) + Math.pow(v2[1], 2)); const sin = numerator / denominator; return Math.asin(sin); }
已知旋轉起點、旋轉中心以及旋轉角度,求旋轉終點座標的函數以下:
/** * * 根據旋轉起點、旋轉中心和旋轉角度計算旋轉終點的座標 * * @param {Array} startPoint 起點座標 * @param {Array} centerPoint 旋轉點座標 * @param {number} angle 旋轉角度 * * @return {Array} 旋轉終點的座標 */ function getEndPointByRotate(startPoint, centerPoint, angle) { const [centerX, centerY] = centerPoint; const [x1, y1] = [startPoint[0] - centerX, startPoint[1] - centerY]; const x2 = x1 * Math.cos(angle) - y1 * Math.sin(angle); const y2 = x1 * Math.sin(angle) + y1 * Math.cos(angle); return [x2 + centerX, y2 + centerY]; }
拖拽和縮放在本次需求中,對於數學上的要求並不高。
拖拽須要計算好觸摸點橫縱座標的差值,加到canvas中的元素上便可。
本文主要介紹了在canvas中實現拖拽、縮放、旋轉等交互時,所須要的一些數學知識。若有不對的地方,歡迎指正。同時,若是有其餘解決需求的思路,歡迎交流。
下一篇文章將介紹【使用本文介紹的數學知識,來實現文章開頭的demo】的過程。
符合預期。