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