摘要:兩條平行的直線在無窮遠的地方看起來會匯集到一起,而匯集的點,在透視里稱作消失點。小孔成像三維空間的火焰,透過小孔,在二維成像屏上顯示了二維的畫面。
前言
不好意思,標題其實是開了個玩笑。大家都知道,Canvas 獲取繪畫上下文的 api 是 getContext("2d")。我第一次看到這個 api 定義的時候,就很自然的認為,既然有 2d 那一定是有 3d 的咯? 但是我接著我看到了 api 介紹的這句話
提示:在未來,如果 canvas 標簽擴展到支持 3D 繪圖,getContext() 方法可能允許傳遞一個 "3d" 字符串參數(shù)。
what? 我有一句媽賣批不知當講不當講... 從接觸 canvas 之后我就一直等這個未來,等到后來我學(xué)習(xí) three.js... 再等到現(xiàn)在,這個 getContext("3d") 還是沒有出來??赡苁且驗樵絹碓蕉酁g覽器都已經(jīng)支持 webGL 的原因把,這個 getContext("3d") 有可能再也不會來了。
webGL 就是瀏覽器端的 3D 繪圖標準,它直接借助系統(tǒng)顯卡來渲染 3D 場景,它能制作的 3D 應(yīng)用,是普通 canvas 無法相比的。所以,你有復(fù)雜的 3D 前端項目,且不考慮 IE 的兼容性的話。不用說,直接使用 webGL 吧。
不使用 webGL 制作簡單的 3D 效果然而,有的時候我們只需要實現(xiàn)簡單的 3D 效果。在沒有學(xué)習(xí) webGL 或這方面的框架的情況下,我們其實也可以在普通的 canvas api 基礎(chǔ)上制作出來。而且,我們可以兼容 IE 9。先來看看,我們都能做些什么效果。
https://www.meizu.com/products/pro6/summary.html
https://www.meizu.com/products/pro6/performance.html
這的兩個效果都是工作時簡單的 3D 效果需求,沒有必要使用 webGL。然而當時我并沒有使用今天介紹的辦法,因為沒有擴展到 3D 坐標去實現(xiàn)所以只能很繁瑣的轉(zhuǎn)換成 2D 平面圖形分析出來。
如果當時能使用今天介紹方法,將可以很簡單、在很短時間就能實現(xiàn)。
素描知識的啟發(fā)因為平時以前在學(xué)校的時候?qū)W習(xí)過素描,現(xiàn)在平常也會簡單畫一點,所以對素描知識我有一點點了解。畫畫描繪真實世界的三維場景,需要用到透視。這里我當然不介紹太多,簡單來說就是我們理解的近大遠小,可以用簡單的線條連接表示出來。兩條平行的直線在無窮遠的地方看起來會匯集到一起,而匯集的點,在透視里稱作消失點。通過找到這個消失點,還有平行線,就可以畫出簡單的立體感覺的圖像。
觀察上面這幅圖,在這里所畫的三維空間,所有的直線都是垂直與畫面的,也就是所,如果用坐標描述每條直線上的任一點 v(x,y,z) 他們的 x,y 都是相等的。在畫面上,離我們眼睛觀察點越遠的點,就越趨向與眼睛觀察點的 x,y 。 那三維空間的坐標 v(x,y,z),對應(yīng)到平面的坐標 p(x",y") 其中這個 x,y 會隨著 z 的變化,是不是會呈現(xiàn)一定的規(guī)律對應(yīng)到 x", y" 呢?
回憶中學(xué)物理課我想起了中學(xué)學(xué)習(xí)過的一節(jié)物理課。小孔成像
三維空間的火焰,透過小孔,在二維成像屏上顯示了二維的畫面。那時候老師教我們,這其實最簡單的照相機,和我們眼睛一樣,光透過瞳孔,最終到達視網(wǎng)膜,在轉(zhuǎn)換成我們看到的影像。照相機模擬我們的眼睛,所以拍出來的照片和我們眼睛看到的感覺是一樣的。
我們試著把剛才的實驗轉(zhuǎn)換到簡單的幾何坐標中看。
觀察 yz (x=0) 截面,假設(shè)小孔為坐標原點 (0,0,0) 成像屏到小孔的距離為 d,圖中火焰上的一個點 a(0,y,z) 投射到成像屏對應(yīng)點 a2,可以求的 a2 在成像屏中的平面坐標:x2 = 0, y2 = y * (d/z)。我天,這么簡單就找到了這個對應(yīng)關(guān)系? 先別急,為了方便開發(fā),我們還需要做一點小轉(zhuǎn)換。
像 CSS 3D 一樣表示坐標在 CSS 3D transform 中,我們需要定義 perspective 屬性,用來說明觀察點到屏幕的距離。如果一個點的 z 軸是 0, 那這個點是處于二維點一樣的位置。z 軸越?。ㄟh離屏幕),對應(yīng)到屏幕上的顯示的點 xy 就越趨向于定義 perspective 屬性容器的中心,也就是觀察點、眼睛對應(yīng)到屏幕的 xy。我們的目標就是用這種 CSS 3D 的方式表示三維的坐標(z = 0 的時候三維坐標的 xy 是和屏幕坐標的 xy 一樣的),然后再套用我們找到的公式,計算出對應(yīng)到屏幕中的二維坐標是多少,然后我們就可以用三維坐標描述點的位置,真正在 canvas 繪畫的時候呢,通過簡單的轉(zhuǎn)換,用計算出來的二維坐標繪畫。
上一步求的 a2 對應(yīng)的平面坐標是倒立的(成像屏的火焰也是倒過來的),我們可以想想在小孔與成像屏前方等距的位置放置顯示屏,我們像 CSS 3D 一樣,讓坐標系原點就是顯示屏的中點。而小孔,就成了我們的觀察點,既眼睛所在的位置,眼睛離顯示屏的距離就是 p(perspective)。由全等三角形的知識可以知道,上圖中 a2" 剛好是 a2 正過來的坐標。咦,看來屏幕坐標完全可以簡化三維坐標點和眼睛的連線與屏幕的交點。這樣,一個三維空間的點坐標對應(yīng)到屏幕坐標的關(guān)系就找出來了。
將這個關(guān)系用一個縮放值表示既然已經(jīng)描述出來這個關(guān)系了,我們再用把它表示成簡單的公式。以便直接在代碼中完成三維坐標到平面坐標的轉(zhuǎn)換。
已知觀察者到屏幕的距離 p (perspective), 三維空間一個點的坐標 a(x,y,z),求這個點在屏幕上的坐標。 圖中,三維坐標 a 在坐標 xy 平面上的向量長度 d 和該點對應(yīng)到屏幕上的點 a2" 在 xy 平面上的向量長度 d",根據(jù)相似三角形,有這樣的關(guān)系:
d"/d = p/(p+z)
x 和 y 的值同理:
x"/x = p/(p+z) y"/y = p/(p+z)
原來,三維空間的點坐標的 x 和 y 對應(yīng)到屏幕平面上是關(guān)于 z 和 p 成比例變化的這個比例值就是
scale = p/(p+z)
這個 scale 隨著物體到屏幕的距離的值的變大而變小。這也很好地解釋了為什么我們看東西會近大遠小的原因:
縮放值的使用實例假設(shè)我們的眼睛看的就是屏幕中央,我們現(xiàn)在在 y = cvs.height + 5 的 xz 平面上一個正方形區(qū)域畫一系列的變長為 5 的矩形點。如果不做處理,那么可以想到我們直接使用些點的 x, y 坐標畫的點,肯定在畫布上是看不到的,因為范圍超出了畫布。而真實的世界里,我們是可以看到遠處的點的,遠處的點是趨向與屏幕中央的。
代碼 1:let cvs = document.querySelector("canvas"); let ctx = cvs.getContext("2d"); class Point { constructor(x, y, z) { this.x = x; this.y = y; this.z = z; } } // 根據(jù) perspective 和 z 獲取三維坐標對應(yīng)二維坐標的xy縮放值 function getScaleByZ(z, p=600) { let scale; if (z > p) { scale = Infinity; } else { scale = p / (-z + p); } return scale; } function draw() { ctx.clearRect(0,0,cvs.width,cvs.height); let rectWidth = 5; points.forEach((point)=>{ let scale = getScaleByZ(point.z); let drawX = center.x + (point.x - center.x) * scale; let drawY = center.y + (point.y - center.y) * scale; let drawWidth = rectWidth * scale ctx.fillStyle = "#abcdef"; ctx.fillRect(drawX, drawY, drawWidth, drawWidth); }); } let center = new Point(cvs.width/2, cvs.height/2, 0); let points = []; let xCount = 20; // x 方向的點數(shù) let zCount = 20; // z 方向的點數(shù) let step = cvs.width / xCount; // x 方向點之間的間隔 for (let i = -(xCount - 1) / 2; i <= (xCount - 1) / 2; i++) { for (let j = -(zCount - 1) / 2; j <= (zCount - 1) / 2; j++) { let x = i; let z = j; let y = 0; console.log(x,y,z); points.push( new Point((x + xCount/2) * step, cvs.height + 1, z * step) ); } } draw();效果 1:
在 draw 方法里,我把三維的坐標轉(zhuǎn)換成了屏幕坐標。并且,邊長也根據(jù)縮放值重新計算了,遠處的點,邊長越小。代碼最終運行的結(jié)果是我們可以看到遠處的點,還是有 3D 的感覺的,不過不是很明顯。我們改變生成點的邏輯,這一次,我們生成一個球面上的點。
代碼 2:let center = new Point(cvs.width/2, cvs.height/2, 0); let points = []; let circlePointCount = 30; let angelStep = Math.PI * 2 / circlePointCount; let radius = 10; let step = 40; for (let i = -radius; i <= radius; i++) { let y = i; for (let j = 0; j < circlePointCount; j++) { let xzRadius = Math.sqrt(radius * radius - y * y); let xzAngel = j * angelStep; let x = xzRadius * Math.cos(xzAngel); let z = xzRadius * Math.sin(xzAngel); // console.log(x,y,z); points.push( new Point( x * step + cvs.width/2, y * step + cvs.height/2, z * step - cvs.width/2 ) ); } } draw();效果 2
或者,再直接讓它旋轉(zhuǎn)起來。
代碼 3function update(angelOffset) { points = []; for (let i = -radius; i <= radius; i++) { let y = i; for (let j = 0; j < circlePointCount; j++) { let xzRadius = Math.sqrt(radius * radius - y * y); let xzAngel = j * angelStep + angelOffset; let x = xzRadius * Math.cos(xzAngel); let z = xzRadius * Math.sin(xzAngel); // console.log(x,y,z); points.push( new Point( x * step + cvs.width/2, y * step + cvs.height/2, z * step - cvs.width/2 ) ); } } } (function() { let angelOffset = 0; function tick() { update(angelOffset += 0.006); draw(); window.requestAnimationFrame(tick); } tick(); })();效果 3 F3.js
因為學(xué)過 three.js,three.js 有豐富的三維向量計算 api。我從源碼里提取了這些計算向量的 api 再結(jié)合這篇文章里總結(jié)的轉(zhuǎn)換方法計算二維的坐標寫了一個專門在 canvas(2d) 上繪制三維場景的組件,因為是并非真的是調(diào)用3D api,所以我取名字叫 F3.js (fake3D)
https://github.com/gnauhca/f3.js
使用 F3.js 制作的簡單的 Demo:文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/82266.html
在vue項目中canvas實現(xiàn)截圖功能是常用的,下面是具體代碼: 實現(xiàn)效果: 在vue項目中做的一個截圖功能(只能夠截取圖片),只用鼠標就可以在畫面中進行框選截取?! 崿F(xiàn):做一個彈窗,打開彈窗的時候傳入要截的圖,接下來在這個窗口里面,點擊截圖按鈕,開始截圖;點擊取消按鈕,取消截圖。 窗口里面的html主要是三個部分,一個是可截圖區(qū)域,一個是截取圖片的回顯,一個是操作按鈕(截圖按鈕和取消...
上傳視頻要提供視頻封面(視頻封面必填),這是在開發(fā)中實際問題。封面可以用戶自己制作并上傳,但這樣脫離網(wǎng)站,體驗不好,常見的處理方案就是用戶未選擇或上傳封面時,自動截取視頻第一幀作為封面,但這樣并不友好。因此考慮視頻上傳后,在播放中由人員自行截取畫面作為視頻封面?! 『唵涡Ч鐖D: 前端代碼如下: <template> <div> <videosrc=&...
背景:在開發(fā)移動端內(nèi)部應(yīng)用的時候,涉及安全問題,我們經(jīng)常在企業(yè)微信或者圖片上看到水印,防止信息被泄露,針對這次開發(fā)做個復(fù)盤,記錄下。效果圖如下: 一、實現(xiàn)原理1、首先用canvas繪制水印2、創(chuàng)建蒙層div,可以覆蓋在頁面上,并設(shè)置pointer-events:none屬性3、將canvas繪制的水印作為背景圖重復(fù)渲染在第二步創(chuàng)建的div上4、將第三步水印div插入容器中二、組件封裝1、新建移動端...
背景:在開發(fā)移動端內(nèi)部應(yīng)用的時候,涉及安全問題,我們經(jīng)常在企業(yè)微信或者圖片上看到水印,防止信息被泄露,針對這次開發(fā)做個復(fù)盤,記錄下。效果圖如下: 一、實現(xiàn)原理1、首先用canvas繪制水印2、創(chuàng)建蒙層div,可以覆蓋在頁面上,并設(shè)置pointer-events:none屬性3、將canvas繪制的水印作為背景圖重復(fù)渲染在第二步創(chuàng)建的div上4、將第三步水印div插入容器中二、組件封裝1、新建移動端...
閱讀 1850·2023-04-25 14:49
閱讀 3133·2021-09-30 09:47
閱讀 3125·2021-09-06 15:00
閱讀 2237·2019-08-30 13:16
閱讀 1452·2019-08-30 10:48
閱讀 2683·2019-08-29 15:11
閱讀 1300·2019-08-26 14:06
閱讀 1680·2019-08-26 13:30