本文從繪圖基礎開始講起,詳細介紹瞭如何使用Three.js
開發一個功能齊全的全景插件。html
咱們先來看一下插件的效果:前端
若是你對Three.js
已經很熟悉了,或者你想跳過基礎理論,那麼你能夠直接從全景預覽開始看起。node
本項目的github
地址:github.com/ConardLi/tp…webpack
OpenGL
是用於渲染2D、3D
量圖形的跨語言、跨平臺的應用程序編程接口(API)
。git
這個接口由近350
個不一樣的函數調用組成,用來從簡單的圖形比特繪製複雜的三維景象。github
OpenGL ES
是OpenGL
三維圖形API
的子集,針對手機、PDA
和遊戲主機等嵌入式設備而設計。web
基於OpenGL
,通常使用C
或Cpp
開發,對前端開發者來講不是很友好。npm
WebGL
把JavaScript
和OpenGL ES 2.0
結合在一塊兒,從而爲前端開發者提供了使用JavaScript
編寫3D
效果的能力。編程
WebGL
爲HTML5 Canvas
提供硬件3D
加速渲染,這樣Web
開發人員就能夠藉助系統顯卡來在瀏覽器裏更流暢地展現3D
場景和模型了,還能建立複雜的導航和數據視覺化。json
Canvas
是一個能夠自由制定大小的矩形區域,能夠經過JavaScript
能夠對矩形區域進行操做,能夠自由的繪製圖形,文字等。
通常使用Canvas
都是使用它的2d
的context
功能,進行2d
繪圖,這是其自己的能力。
和這個相對的,WebGL
是三維,能夠描畫3D
圖形,WebGL
,想要在瀏覽器上進行呈現,它必須須要一個載體,這個載體就是Canvas
,區別於以前的2dcontext
,還能夠從Canvas
中獲取webglcontext
。
咱們先來從字面意思理解下:Three
表明3D
,js
表明JavaScript
,即便用JavaScript
來開發3D
效果。
Three.js
是使用JavaScript
對 WebGL
接口進行封裝與簡化而造成的一個易用的3D
庫。
直接使用WebGL
進行開發對於開發者來講成本相對來講是比較高的,它須要你掌握較多的計算機圖形學知識。
Three.js
在必定程度上簡化了一些規範和難以理解的概念,對不少API
進行了簡化,這大大下降了學習和開發三維效果成本。
下面咱們來具體看一下使用Three.js
必需要知道的知識。
使用Three.js
繪製一個三維效果,至少須要如下幾個步驟:
建立一個容納三維空間的場景 — Sence
將須要繪製的元素加入到場景中,對元素的形狀、材料、陰影等進行設置
給定一個觀察場景的位置,以及觀察角度,咱們用相機對象(Camera
)來控制
將繪製好的元素使用渲染器(Renderer
)進行渲染,最終呈如今瀏覽器上
拿電影來類比的話,場景對應於整個佈景空間,相機是拍攝鏡頭,渲染器用來把拍攝好的場景轉換成膠捲。
場景容許你設置哪些對象被three.js
渲染以及渲染在哪裏。
咱們在場景中放置對象、燈光和相機。
很簡單,直接建立一個Scene
的實例便可。
_scene = new Scene();
複製代碼
有了場景,咱們接下來就須要場景裏應該展現哪些東西。
一個複雜的三維場景每每就是由很是多的元素搭建起來的,這些元素多是一些自定義的幾何體(Geometry
),或者外部導入的複雜模型。
Three.js
爲咱們提供了很是多的Geometry
,例如SphereGeometry
(球體)、 TetrahedronGeometry
(四面體)、TorusGeometry
(圓環體)等等。
在Three.js
中,材質(Material
)決定了幾何圖形具體是以什麼形式展示的。它包括了一個幾何體如何形狀之外的其餘屬性,例如色彩、紋理、透明度等等,Material
和Geometry
是相輔相成的,必須結合使用。
下面的代碼咱們建立了一個長方體體,賦予它基礎網孔材料(MeshBasicMaterial
)
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshBasicMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
複製代碼
能以這個角度看到幾何體其實是相機的功勞,這個咱們下面的章節再介紹,這讓咱們看到一個幾何體的輪廓,可是感受怪怪的,這並不像一個幾何體,實際上咱們還須要爲它添加光照和陰影,這會讓幾何體看起來更真實。
基礎網孔材料(MeshBasicMaterial
)不受光照影響的,它不會產生陰影,下面咱們爲幾何體換一種受光照影響的材料:網格標準材質(Standard Material
),併爲它添加一些光照:
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshStandardMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
// 建立平行光-照亮幾何體
var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-4, 8, 12);
_scene.add(directionalLight);
// 建立環境光
var ambientLight = new THREE.AmbientLight(0xffffff);
_scene.add(ambientLight);
複製代碼
有了光線的渲染,讓幾何體看起來更具備3D
效果,Three.js
中光源有不少種,咱們上面使用了環境光(AmbientLight
)和平行光(DirectionalLight
)。
環境光會對場景中的全部物品進行顏色渲染。
平行光你能夠認爲像太陽光同樣,從極遠處射向場景中的光。它具備方向性,也能夠啓動物體對光的反射效果。
除了這兩種光,Three.js
還提供了其餘幾種光源,它們適用於不一樣狀況下對不一樣材質的渲染,能夠根據實際狀況選擇。
在說相機以前,咱們仍是先來了解一下座標系的概念:
在三維世界中,座標定義了一個元素所處於三維空間的位置,座標系的原點即座標的基準點。
最經常使用的,咱們使用距離原點的三個長度(距離x
軸、距離y
軸、距離z
軸)來定義一個位置,這就是直角座標系。
在斷定座標系時,咱們一般使用大拇指、食指和中指,並互爲90
度。大拇指表明X
軸,食指表明Y
軸,中指表明Z
軸。
這就產生了兩種座標系:左手座標系和右手座標系。
Three.js
中使用的座標系即右手座標系。
咱們能夠在咱們的場景中添加一個座標系,這樣咱們能夠清楚的看到元素處於什麼位置:
var axisHelper = new THREE.AxisHelper(600);
_scene.add(axisHelper);
複製代碼
其中紅色表明X
軸,綠色表明Y
軸,藍色表明Z
軸。
上面看到的幾何體的效果,若是不建立一個相機(Camera
),是什麼也看不到的,由於默認的觀察點在座標軸原點,它處於幾何體的內部。
相機(Camera
)指定了咱們在什麼位置觀察這個三維場景,以及以什麼樣的角度進行觀察。
目前Three.js
提供了幾種不一樣的相機,最經常使用的,也是下面插件中使用的兩種相機是:PerspectiveCamera
(透視相機)、 OrthographicCamera
(正交投影相機)。
上面的圖很清楚的解釋了兩種相機的區別:
右側是 OrthographicCamera
(正交投影相機)他不具備透視效果,即物體的大小不受遠近距離的影響,對應的是投影中的正交投影。咱們數學課本上所畫的幾何體大多數都採用這種投影。
左側是PerspectiveCamera
(透視相機),這符合咱們正常人的視野,近大遠小,對應的是投影中的透視投影。
若是你想讓場景看起來更真實,更具備立體感,那麼採用透視相機最合適,若是場景中有一些元素你不想讓他隨着遠近放大縮小,那麼採用正交投影相機最合適。
咱們再分別來看看兩個建立兩個相機須要什麼參數:
_camera = new OrthographicCamera(left, right, top, bottom, near, far);
複製代碼
OrthographicCamera
接收六個參數,left, right, top, bottom
分別對應上、下、左、右、遠、近的一個距離,超過這些距離的元素將不會出如今視野範圍內,也不會被瀏覽器繪製。實際上,這六個距離就構成了一個立方體,因此OrthographicCamera
的可視範圍永遠在這個立方體內。
_camera = new PerspectiveCamera(fov, aspect, near, far);
複製代碼
PerspectiveCamera
接收四個參數,near
、far
和上面的相同,分別對應相機可觀測的最遠和最近距離;fov
表明水平範圍可觀測的角度,fov
越大,水平範圍能觀測到的範圍越廣;aspect
表明水平方向和豎直方向可觀測距離的比值,因此fov
和aspect
就能夠肯定垂直範圍內能觀測到的範圍。
關於相機還有兩個必需要知道的點,一個是position
屬性,一個是lookAt
函數:
position
屬性指定了相機所處的位置。
lookAt
函數指定相機觀察的方向。
實際上position
的值和lookAt
接收的參數都是一個類型爲Vector3
的對象,這個對象用來表示三維空間中的座標,它有三個屬性:x、y、z
分別表明距離x
軸、距離y
軸、距離z
軸的距離。
下面,咱們讓相機觀察的方向指向原點,另外分別讓x、y、z
爲0,另外兩個參數不爲0,看一下視野會發生什麼變化:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
_camera.position.set(0, 300, 600); // 1 - x爲0
_camera.position.set(500, 0, 600); // 2 - y爲0
_camera.position.set(500, 300, 0); // 3 - z爲0
複製代碼
很清楚的看到position
決定了咱們視野的出發點,可是鏡頭指向的方向是不變的。
下面咱們將position
固定,改變相機觀察的方向:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.position.set(500, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0)) // 1 - 視野指向原點
_camera.lookAt(new THREE.Vector3(200, 0, 0)) // 2 - 視野偏向x軸
複製代碼
可見:咱們視野的出發點是相同的,可是視野看向的方向發生了改變。
好,有了上面的基礎,咱們再來寫兩個例子看一看兩個相機的視角對比,爲了方便觀看,咱們建立兩個位置不一樣的幾何體:
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshStandardMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
var geometry = new THREE.SphereGeometry(50, 100, 100);
var ball = new THREE.Mesh(geometry, material);
ball.position.set(200, 0, -200);
_scene.add(ball);
複製代碼
正交投影相機視野:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.position.set(0, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
複製代碼
透視相機視野:
_camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1100);
_camera.position.set(0, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
複製代碼
可見,這印證了咱們上面關於兩種相機的理論
上面咱們建立了場景、元素和相機,下面咱們要告訴瀏覽器將這些東西渲染到瀏覽器上。
Three.js
也爲咱們提供了幾種不一樣的渲染器,這裏咱們主要看WebGL
渲染器(WebGLRenderer
)。顧名思義:WebGL
渲染器使用WebGL
來繪製場景,其夠利用GPU
硬件加速從而提升渲染性能。
_renderer = new THREE.WebGLRenderer();
複製代碼
你須要將你使用Three.js
繪製的元素添加到瀏覽器上,這個過程須要一個載體,上面咱們介紹,這個載體就是Canvas
,你能夠經過_renderer.domElement
獲取到這個Canvas
,並將它給定到真實DOM
中。
_container = document.getElementById('conianer');
_container.appendChild(_renderer.domElement);
複製代碼
使用setSize
函數設定你要渲染的範圍,實際上它改變的就是上面Canvas
的範圍:
_renderer.setSize(window.innerWidth, window.innerHeight);
複製代碼
如今,你已經指定了一個渲染的載體和載體的範圍,你能夠經過render
函數渲染上面指定的場景和相機:
_renderer.render(_scene, _camera);
複製代碼
實際上,你若是依次執行上面的代碼,可能屏幕上仍是黑漆漆的一片,並無任何元素渲染出來。
這是由於上面你要渲染的元素可能並未被加載完,你就執行了渲染,而且只執行了一次,這時咱們須要一種方法,讓場景和相機進行實時渲染,咱們須要用到下面的方法:
window.requestAnimationFrame()
告訴瀏覽器——你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫。
該方法須要傳入一個回調函數做爲參數,該回調函數會在瀏覽器下一次重繪以前執行。
window.requestAnimationFrame(callback);
複製代碼
若你想在瀏覽器下次重繪以前繼續更新下一幀動畫,那麼回調函數自身必須再次調用window.requestAnimationFrame()
。
使用者韓函數就意味着,你能夠在requestAnimationFrame
不停的執行繪製操做,瀏覽器就實時的知道它須要渲染的內容。
固然,某些時候你已經不須要實時繪製了,你也可使用cancelAnimationFrame
當即中止這個繪製:
window.cancelAnimationFrame(myReq);
複製代碼
來看一個簡單的例子:
var i = 0;
var animateName;
animate();
function animate() {
animateName = requestAnimationFrame(animate);
console.log(i++);
if (i > 100) {
cancelAnimationFrame(animateName);
}
}
複製代碼
來看一下執行效果:
咱們使用requestAnimationFrame
和Three.js
的渲染器結合使用,這樣就能實時繪製三維動畫了:
function animate() {
requestAnimationFrame(animate);
_renderer.render(_scene, _camera);
}
複製代碼
藉助上面的代碼,咱們能夠簡單實現一些動畫效果:
var y = 100;
var option = 'down';
function animateIn() {
animateName = requestAnimationFrame(animateIn);
mesh.rotateX(Math.PI / 40);
if (option == 'up') {
ball.position.set(200, y += 8, 0);
} else {
ball.position.set(200, y -= 8, 0);
}
if (y < 1) { option = 'up'; }
if (y > 100) { option = 'down' }
}
複製代碼
上面的知識是Three.js
中最基礎的知識,也是最重要的和最主幹的。
這些知識可以讓你在看到一個複雜的三維效果時有必定的思路,固然,要實現還須要很是多的細節。這些細節你能夠去官方文檔中查閱。
下面的章節即告訴你如何使用Three.js
進行實戰 — 實現一個360度全景插件。
這個插件包括兩部分,第一部分是對全景圖進行預覽。
第二部分是對全景圖的標記進行配置,並關聯預覽的座標。
咱們首先來看看全景預覽部分:
將一張全景圖包裹在球體的內壁
設定一個觀察點,在球的圓心
使用鼠標能夠拖動球體,從而改變咱們看到全景的視野
鼠標滾輪能夠縮放,和放大,改變觀察全景的遠近
根據座標在全景圖上掛載一些標記,如文字、圖標等,而且能夠增長事件,如點擊事件
咱們先把必要的基礎設施搭建起來:
場景、相機(選擇遠景相機,這樣可讓全景看起來更真實)、渲染器:
_scene = new THREE.Scene();
initCamera();
initRenderer();
animate();
// 初始化相機
function initCamera() {
_camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1100);
_camera.position.set(0, 0, 2000);
_camera.lookAt(new THREE.Vector3(0, 0, 0));
}
// 初始化渲染器
function initRenderer() {
_renderer = new THREE.WebGLRenderer();
_renderer.setSize(window.innerWidth, window.innerHeight);
_container = document.getElementById('panoramaConianer');
_container.appendChild(_renderer.domElement);
}
// 實時渲染
function animate() {
requestAnimationFrame(animate);
_renderer.render(_scene, _camera);
}
複製代碼
下面咱們在場景內添加一個球體,並把全景圖做爲材料包裹在球體上面:
var mesh = new THREE.Mesh(new THREE.SphereGeometry(1000, 100, 100),
new THREE.MeshBasicMaterial(
{ map: ImageUtils.loadTexture('img/p3.png') }
));
_scene.add(mesh);
複製代碼
而後咱們看到的場景應該是這樣的:
這不是咱們想要的效果,咱們想要的是從球的內部觀察全景,而且全景圖是附着外球的內壁的,而不是鋪在外面:
咱們只要需將Material
的scale
的一個屬性設置爲負值,材料便可附着在幾何體的內部:
mesh.scale.x = -1;
複製代碼
而後咱們將相機的中心點移動到球的中心:
_camera.position.set(0, 0, 0);
複製代碼
如今咱們已經在全景球的內部啦:
全景圖已經能夠瀏覽了,可是你只能看到你眼前的這一塊,並不能拖動它看到其餘部分,爲了精確的控制拖動的速度和縮放、放大等場景,咱們手動爲它增長一些事件:
監聽鼠標的mousedown
事件,在此時將開始拖動標記_isUserInteracting
設置爲true
,而且記錄起始的屏幕座標,以及起始的相機lookAt
的座標。
_container.addEventListener('mousedown', (event)=>{
event.preventDefault();
_isUserInteracting = true;
_onPointerDownPointerX = event.clientX;
_onPointerDownPointerY = event.clientY;
_onPointerDownLon = _lon;
_onPointerDownLat = _lat;
});
複製代碼
監聽鼠標的mousemove
事件,當_isUserInteracting
爲true
時,實時計算當前相機lookAt
的真實座標。
_container.addEventListener('mousemove', (event)=>{
if (_isUserInteracting) {
_lon = (_onPointerDownPointerX - event.clientX) * 0.1 + _onPointerDownLon;
_lat = (event.clientY - _onPointerDownPointerY) * 0.1 + _onPointerDownLat;
}
});
複製代碼
監聽鼠標的mouseup
事件,將_isUserInteracting
設置爲false
。
_container.addEventListener('mouseup', (event)=>{
_isUserInteracting = false;
});
複製代碼
固然,上面咱們只是改變了座標,並無告訴相機它改變了,咱們在animate
函數中來作這件事:
function animate() {
requestAnimationFrame(animate);
calPosition();
_renderer.render(_scene, _camera);
_renderer.render(_sceneOrtho, _cameraOrtho);
}
function calPosition() {
_lat = Math.max(-85, Math.min(85, _lat));
var phi = tMath.degToRad(90 - _lat);
var theta = tMath.degToRad(_lon);
_camera.target.x = _pRadius * Math.sin(phi) * Math.cos(theta);
_camera.target.y = _pRadius * Math.cos(phi);
_camera.target.z = _pRadius * Math.sin(phi) * Math.sin(theta);
_camera.lookAt(_camera.target);
}
複製代碼
監聽mousewheel
事件,對全景圖進行放大和縮小,注意這裏指定了最大縮放範圍maxFocalLength
和最小縮放範圍minFocalLength
。
_container.addEventListener('mousewheel', (event)=>{
var ev = ev || window.event;
var down = true;
var m = _camera.getFocalLength();
down = ev.wheelDelta ? ev.wheelDelta < 0 : ev.detail > 0;
if (down) {
if (m > minFocalLength) {
m -= m * 0.05
_camera.setFocalLength(m);
}
} else {
if (m < maxFocalLength) {
m += m * 0.05
_camera.setFocalLength(m);
}
}
});
複製代碼
來看一下效果吧:
在瀏覽全景圖的時候,咱們每每須要對某些特殊的位置進行一些標記,而且這些標記可能附帶一些事件,好比你須要點擊一個標記才能到達下一張全景圖。
下面咱們來看看如何在全景中增長標記,以及如何爲這些標記添加事件。
咱們可能不須要讓這些標記隨着視野的變化而放大和縮小,基於此,咱們使用正交投影相機來展示標記,只需給它一個固定的觀察高度:
_cameraOrtho = new THREE.OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 1, 10);
_cameraOrtho.position.z = 10;
_sceneOrtho = new Scene();
複製代碼
利用精靈材料(SpriteMaterial
)來實現文字標記,或者圖片標記:
// 建立文字標記
function createLableSprite(name) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const metrics = context.measureText(name);
const width = metrics.width * 1.5;
context.font = "10px 宋體";
context.fillStyle = "rgba(0,0,0,0.95)";
context.fillRect(2, 2, width + 4, 20 + 4);
context.fillText(name, 4, 20);
const texture = new Texture(canvas);
const spriteMaterial = new SpriteMaterial({ map: texture });
const sprite = new Sprite(spriteMaterial);
sprite.name = name;
const lable = {
name: name,
canvas: canvas,
context: context,
texture: texture,
sprite: sprite
};
_sceneOrtho.add(lable.sprite);
return lable;
}
// 建立圖片標記
function createSprite(position, url, name) {
const textureLoader = new TextureLoader();
const ballMaterial = new SpriteMaterial({
map: textureLoader.load(url)
});
const sp = {
pos: position,
name: name,
sprite: new Sprite(ballMaterial)
};
sp.sprite.scale.set(32, 32, 1.0);
sp.sprite.name = name;
_sceneOrtho.add(sp.sprite);
return sp;
}
複製代碼
建立好這些標記,咱們把它渲染到場景中。
咱們必須告訴場景這些標記的位置,爲了直觀的理解,咱們須要給這些標記賦予一種座標,這種座標很相似於經緯度,咱們叫它lon
和lat
,具體是如何給定的咱們在下面的章節:全景標記中會詳細介紹。
在這個過程當中,一共經歷了兩次座標轉換:
第一次轉換:將「經緯度」轉換爲三維空間座標,即咱們上面講的那種x、y、z
形式的座標。
使用geoPosition2World
函數進行轉換,獲得一個Vector3
對象,咱們能夠將當前相機_camera
做爲參數傳入這個對象的project
方法,這會獲得一個標準化後的座標,基於這個座標能夠幫咱們判斷標記是否在視野範圍內,以下面的代碼,若標準化座標在-1
和1
的範圍內,則它會出如今咱們的視野中,咱們將它進行準確渲染。
第二次轉換:將三維空間座標轉換爲屏幕座標。
若是咱們直接講上面的三維空間座標座標應用到標記中,咱們會發現不管視野如何移動,標記的位置是不會有任何變化的,由於這樣算出來的座標永遠是一個常量。
因此咱們須要藉助上面的標準化座標,將標記的三維空間座標轉換爲真實的屏幕座標,這個過程是worldPostion2Screen
函數來實現的。
關於geoPosition2World
和worldPostion2Screen
兩個函數的實現,你們有興趣能夠去個人github
源碼中查看,這裏就很少作解釋了,由於這又要牽扯到一大堆專業知識啦。😅
var wp = geoPosition2World(_sprites.lon, _sprites.lat);
var sp = worldPostion2Screen(wp, _camera);
var test = wp.clone();
test.project(_camera);
if (test.x > -1 && test.x < 1 && test.y > -1 && test.y < 1 && test.z > -1 && test.z < 1) {
_sprites[i].sprite.scale.set(32, 32, 32);
_sprites[i].sprite.position.set(sp.x, sp.y, 1);
}else {
_sprites[i].sprite.scale.set(1.0, 1.0, 1.0);
_sprites[i].sprite.position.set(0, 0, 0);
}
複製代碼
如今,標記已經添加到全景上面了,咱們來爲它添加一個點擊事件:
Three.js
並無單獨提供爲Sprite
添加事件的方法,咱們能夠藉助光線投射器(Raycaster
)來實現。
Raycaster
提供了鼠標拾取的能力:
經過setFromCamera
函數來創建當前點擊的座標(通過歸一化處理)和相機的綁定關係。
經過intersectObjects
來斷定一組對象中有哪些被命中(點擊),獲得被命中的對象數組。
這樣,咱們就能夠獲取到點擊的對象,並基於它作一些處理:
_container.addEventListener('click', (event)=>{
_mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
_mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
_raycaster.setFromCamera(_mouse, _cameraOrtho);
var intersects = _raycaster.intersectObjects(_clickableObjects);
intersects.forEach(function (element) {
alert("點擊到了: " + element.object.name);
});
});
複製代碼
點擊到一個標記,進入到下一張全景圖:
爲了讓全景圖知道,我要把標記標註在什麼地方,我須要一個工具來把原圖和全景圖上的位置關聯起來:
Three.js
關係不大,這裏我只說一下基本的實現邏輯,有興趣能夠去個人
github
倉庫查看。
創建座標和全景的映射關係,爲全景賦予一套虛擬座標
在一張平鋪的全景圖上,能夠在任意位置增長標記,並獲取標記的座標
使用座標在預覽全景增長標記,看到的標記位置和平鋪全景中的位置相同
在2D
平面上,咱們能監聽屏幕的鼠標事件,咱們能夠獲取的也只是當前的鼠標座標,咱們要作的是將鼠標座標轉換成三維空間座標。
看起來好像是不可能的,二維座標怎麼能轉換成三維座標呢?
可是,咱們能夠藉助一種中間座標來轉換,能夠把它稱之爲「經緯度」。
在這以前,咱們先來看看咱們常說的經緯度究竟是什麼。
使用經緯度,能夠精確的定位到地球上任意一個點,它的計算規則是這樣的:
一般把鏈接南極到北極的線叫作子午線也叫經線,其所對應的面叫作子午面,規定英國倫敦格林尼治天文臺原址的那條經線稱爲0°經線,也叫本初子午線其對應的面即本初子午面。
經度:球面上某店對應的子午面與本初子午面間的夾角。東正西負。
緯度 :球面上某點的法線(以該店做爲切點與球面相切的面的法線)與赤道平面的夾角。北正南負。
由此,地球上每個點都能被對應到一個經度和緯度,想對應的,也能對應到某條經線和緯線上。
這樣,即便把球面展開稱平面,咱們仍然能用經緯度表示某店點的位置:
基於上面的分析,咱們徹底能夠給平面的全景圖賦予一個虛擬的「經緯度」。咱們使用Canvas
爲它繪製一張"經緯網":
將鼠標座標轉換爲"經緯度":
function calLonLat(e) {
var h = _setContainer.style.height.split("px")[0];
var w = _setContainer.style.width.split("px")[0];
var ix = _setContainer.offsetLeft;
var iy = _setContainer.offsetTop;
iy = iy + h;
var x = e.clientX;
var y = e.clientY;
var lonS = (x - ix) / w;
var lon = 0;
if (lonS > 0.5) {
lon = -(1 - lonS) * 360;
} else {
lon = 1 * 360 * lonS;
}
var latS = (iy - y) / h;
var lat = 0;
if (latS > 0.5) {
lat = (latS - 0.5) * 180;
} else {
lat = (0.5 - latS) * 180 * -1
}
lon = lon.toFixed(2);
lat = lat.toFixed(2);
return { lon: lon, lat: lat };
}
複製代碼
這樣平面地圖上的某點就能夠和三維座標關聯起來了,固然,這還須要必定的轉換,有興趣能夠去源碼研究下geoPosition2World
和worldPostion2Screen
兩個函數。
上面的代碼中,咱們實現了全景預覽和全景標記的功能,下面,咱們要把這些功能封裝成插件。
所謂插件,便可以直接引用你寫的代碼,並添加少許的配置就能夠實現想要的功能。
咱們來看看,究竟哪些配置是能夠抽取出來的:
var options = {
container: 'panoramaConianer',
url: 'resources/img/panorama/pano-7.jpg',
lables: [],
widthSegments: 60,
heightSegments: 40,
pRadius: 1000,
minFocalLength: 1,
maxFocalLength: 100,
sprite: 'label',
onClick: () => { }
}
複製代碼
container
:dom
容器的id
url
:圖片路徑lables
:全景中的標記數組,格式爲{position:{lon:114,lat:38},logoUrl:'lableLogo.png',text:'name'}
widthSegments
:水平切段數heightSegments
:垂直切段數(值小粗糙速度快,值大精細速度慢)pRadius
:全景球的半徑,推薦使用默認值minFocalLength
:鏡頭最小拉近距離maxFocalLength
:鏡頭最大拉近距離sprite
:展現的標記類型label,icon
onClick
:標記的點擊事件上面的配置是能夠用戶配置的,那麼用戶該如何傳入插件呢?
咱們能夠在插件中聲明一些默認配置options
,用戶使用構造函數傳入參數,而後使用Object.assign
將傳入配置覆蓋到默認配置。
接下來,你就可使用this.def
來訪問這些變量了,而後只須要把寫死的代碼改爲這些配置便可。
options = {
// 默認配置...
}
function tpanorama(opt) {
this.render(opt);
}
tpanorama.prototype = {
constructor: this,
def: {},
render: function (opt) {
this.def = Object.assign(options, opt);
// 初始化操做...
}
}
複製代碼
基本邏輯和上面的相似,下面是提取出來的一些參數。
var setOpt = {
container: 'myDiv',//setting容器
imgUrl: 'resources/img/panorama/3.jpg',
width: '',//指定寬度,高度自適應
showGrid: true,//是否顯示格網
showPosition: true,//是否顯示經緯度提示
lableColor: '#9400D3',//標記顏色
gridColor: '#48D1CC',//格網顏色
lables: [],//標記 {lon:114,lat:38,text:'標記一'}
addLable: true,//開啓後雙擊添加標記 (必須開啓經緯度提示)
getLable: true,//開啓後右鍵查詢標記 (必須開啓經緯度提示)
deleteLbale: true,//開啓默認中鍵刪除 (必須開啓經緯度提示)
}
複製代碼
接下來,咱們就好考慮如何將寫好的插件讓用戶使用了。
咱們主要考慮兩種場景,直接引用和npm install
JS
爲了避免污染全局變量,咱們使用一個自執行函數(function(){}())
將代碼包起來,而後將咱們寫好的插件暴露給全局變量window
。
我把它放在originSrc
目錄下。
(function (global, undefined) {
function tpanorama(opt) {
// ...
}
tpanorama.prototype = {
// ...
}
function tpanoramaSetting(opt) {
// ...
}
tpanoramaSetting.prototype = {
// ...
}
global.tpanorama = tpanorama;
global.tpanoramaSetting = panoramaSetting;
}(window))
複製代碼
npm install
直接將寫好的插件導出:
module.exports = tpanorama;
module.exports = panoramaSetting;
複製代碼
我把它放在src
目錄下。
同時,咱們要把package.json
中的main
屬性指向咱們要導出的文件:"main": "lib/index.js"
,而後將name
、description
、version
等信息補充完整。
下面,咱們就能夠開始發佈了,首先你要有一個npm
帳號,而且登錄,若是你沒有帳號,使用下面的命令建立一個帳號。
npm adduser --registry http://registry.npmjs.org
複製代碼
若是你已經有帳號了,那麼能夠直接使用下面的命令進行登錄。
npm login --registry http://registry.npmjs.org
複製代碼
登錄成功以後,就能夠發佈了:
npm publish --registry http://registry.npmjs.org
複製代碼
注意,上面每一個命令我都手動指定了registry
,這是由於當前你使用的npm
源可能已經被更換了,可能使用的是淘寶源或者公司源,這時不手動指定會致使發佈失敗。
發佈成功後直接在npm官網
上看到你的包了。
而後,你能夠直接使用npm install tpanorama
進行安裝,而後進行使用:
var { tpanorama,tpanoramaSetting } = require('tpanorama');
複製代碼
最後不要忘了,不管使用以上哪一種方式,咱們都要使用babel
編譯後才能暴露給用戶。
在scripts
中建立一個build
命令,將源文件進行編譯,最終暴露給用戶使用的將是lib
和origin
。
"build": "babel src --out-dir lib && babel originSrc --out-dir origin",
複製代碼
你還能夠指定一些其餘的命令來供用戶測試,如我將寫好的例子所有放在examples
中,而後在scripts
定義了expamle
命令:
"example": "npm run webpack && node ./server/www"
複製代碼
這樣,用戶將代碼克隆後直接在本地運行npm run example
就能夠進行調試了。
本項目的github
地址:github.com/ConardLi/tp…
文中若有錯誤,歡迎在評論區指正,若是這篇文章幫助到了你,歡迎點贊和關注。
想閱讀更多優質文章、可關注個人github博客,你的star✨、點贊和關注是我持續創做的動力!
關注公衆號後回覆【加羣】拉你進入優質前端交流羣。