2019年年初,登錄了自己好久沒(méi)上的微博@是秋先生啊。話(huà)說(shuō)為啥好久沒(méi)上了呢,因?yàn)槲移綍r(shí)也不咋上微博,微博賬號(hào)經(jīng)常忘密碼,于是就打開(kāi)微博客戶(hù)端的掃一掃來(lái)一波掃碼登錄,不知道大家有沒(méi)有感覺(jué)到曾經(jīng)微博客戶(hù)端的掃一掃特別難找,而且經(jīng)常換位置,每次找掃一掃的按鈕都給我找得挺煩,它就是不在我認(rèn)為它該出現(xiàn)的地方出現(xiàn)……繞遠(yuǎn)了。
我千辛萬(wàn)苦登上微博后,突然逛到了一張全景圖片,打開(kāi)之后的效果把我驚艷了,然后就開(kāi)始一波研究。
手機(jī)客戶(hù)端肯定是沒(méi)法研究的,沒(méi)有url,只能全屏看到圖片。于是我在電腦上打開(kāi)微博網(wǎng)站,找到了那篇微博。全景圖片看起來(lái)是這樣的:

我就不做動(dòng)圖了,大家知道它是可以360度旋轉(zhuǎn)的。
一個(gè)F12鍵打開(kāi)瀏覽器調(diào)試,看了一下這個(gè)頁(yè)面的源碼:

其實(shí)這個(gè)頁(yè)面就是一個(gè)全景圖的播放器。
打開(kāi)多個(gè)全景圖頁(yè)面源碼后,發(fā)現(xiàn)里面改變的只有幾個(gè)圖片的地址,而且調(diào)試臺(tái)的網(wǎng)絡(luò)流量顯示,真正起作用的圖片地址是第138行的“img_url”:

那么讓這個(gè)全景圖動(dòng)起來(lái)的就只有main.ac7a4be4.js這個(gè)文件了。
一個(gè)“邪惡”的想法在腦海中浮現(xiàn):
前端的東西,js代碼都是可以拿來(lái)用的,只要找到圖片的地址,就可以做出一模一樣的全景圖播放器。
一開(kāi)始不想把圖片保存在自己的服務(wù)器上,因?yàn)槠蜇ぐ娴陌⒗镌艵CS也就那么點(diǎn)兒容量,所以只想提取頁(yè)面中的URL,然后保存在數(shù)據(jù)庫(kù)里,再動(dòng)態(tài)生成頁(yè)面。事實(shí)證明這條辦法是行不通的,微博的圖片服務(wù)器做了跨域訪(fǎng)問(wèn)限制,我的域名https://www.ishareit.fun沒(méi)法請(qǐng)求他們的圖片。所以還是要寫(xiě)爬蟲(chóng)來(lái)爬圖片,并且在把圖片信息存在數(shù)據(jù)庫(kù)里。
思路:
- 用微博api的python SDK調(diào)用微博api;
- 利用微博的api訪(fǎng)問(wèn)全景博主的微博;
- 當(dāng)博主更新全景微博時(shí),爬取圖片保存在服務(wù)器,更新數(shù)據(jù)庫(kù);
- 用戶(hù)訪(fǎng)問(wèn)網(wǎng)站時(shí),生成頁(yè)面。
吐槽一下微博開(kāi)放平臺(tái):大概跟主流的前端界面差了5年左右吧,而且文檔也好久沒(méi)更新了,瞧瞧隔壁阿里云,文檔更新就很及時(shí)。
在這條思路上,本來(lái)是想做成微博開(kāi)放平臺(tái)小應(yīng)用的,因?yàn)樵L(fǎng)問(wèn)微博api需要用戶(hù)登錄,這樣的話(huà)可以整合到我網(wǎng)站自身的用戶(hù)管理系統(tǒng)中,把微博用戶(hù)轉(zhuǎn)換為我自己的網(wǎng)站用戶(hù)。但事實(shí)證明,這條思路沒(méi)有走下去,因?yàn)槲业哪康氖桥廊∪皥D片博主的微博,但微博api有限制,沒(méi)有博主的登錄授權(quán),api無(wú)法訪(fǎng)問(wèn)博主的微博。

也就是說(shuō),我開(kāi)發(fā)的小應(yīng)用,只能訪(fǎng)問(wèn)我自己的微博,想獲取別人的微博,別人必須給這個(gè)小應(yīng)用授權(quán)。這個(gè)機(jī)制真是讓我無(wú)話(huà)可說(shuō),那我要你api干嘛。
這條路走不通,就暫時(shí)放棄了用微博api爬取的思路,但這時(shí)已經(jīng)寫(xiě)好了一個(gè)python CGI的發(fā)布后臺(tái),去掉爬蟲(chóng)的代碼,我把它改為了手動(dòng)發(fā)布,地址在這里。比較方便的是手機(jī)全景微博的發(fā)布,只需要三步:
- 打開(kāi)一個(gè)帶有全景圖片的微博。
- 復(fù)制微博鏈接。
- 將復(fù)制的鏈接粘貼在發(fā)布頁(yè)->手機(jī)微博的標(biāo)簽頁(yè)中,點(diǎn)擊發(fā)布。
不過(guò)還是要感謝廖雪峰大佬提供微博api的Python版SDK,學(xué)到了不少新知識(shí),后期準(zhǔn)備利用這個(gè)SDK開(kāi)發(fā)微信登錄網(wǎng)站的功能。
接下來(lái),我主要寫(xiě)了三個(gè)php文件,一個(gè)主文件show.php負(fù)責(zé)調(diào)用數(shù)據(jù)庫(kù),處理參數(shù),根據(jù)參數(shù)確定顯示所有的全景圖->交給showthem.php,還是進(jìn)入瀏覽單個(gè)全景圖交給 ->showthat.php,網(wǎng)站的第一個(gè)版本是這樣的:

點(diǎn)擊其中的一個(gè)圖片會(huì)跳轉(zhuǎn)到show.php?id=md5的頁(yè)面,show.php也就是通過(guò)判斷這個(gè)id參數(shù)是否合法來(lái)確定是否顯示某個(gè)具體全景圖。
看起來(lái)很眼熟。扒了必應(yīng)壁紙的前端頁(yè)面。說(shuō)實(shí)話(huà),必應(yīng)壁紙做得真心不錯(cuò),雖然云淡風(fēng)輕大佬寫(xiě)了三重防止查看前端代碼的代碼,類(lèi)似這樣,但我還是很不好意思地看到了前端頁(yè)面的源碼,就拿來(lái)直接用了。
說(shuō)了這么久,好像跟PhotoSphereViewer+DeviceOrientationControl.js還沒(méi)關(guān)系。的確,第一版出來(lái)之后,感覺(jué)做得差不多了,好像就可以告一段落了。但是,我自己在玩的時(shí)候發(fā)現(xiàn)用微博的js加載出來(lái)的圖片清晰度很差,好像是做了什么節(jié)省流量的措施,加載出來(lái)的圖片并不是下載下來(lái)的4096x2048高清原圖,這讓手機(jī)端的瀏覽體驗(yàn)很不友好。
我跑去問(wèn)度娘,有沒(méi)有開(kāi)源的全景圖顯示庫(kù),度娘告訴我說(shuō)有一個(gè)叫PhotoSphereViewer的貨貌似可以,這時(shí),我才正式入坑。

這款插件用起來(lái)是很方便的,核心代碼不多,想把一張全景平面圖加載出來(lái)只需要事先引入依賴(lài)的文件,再寫(xiě)幾行代碼:
<script src="https://cdn.jsdelivr.net/npm/three@0.99.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8.1.0/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dot@1.1.2/doT.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uevent@1.0.0/uevent.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photo-sphere-viewer@3.5.1/dist/photo-sphere-viewer.min.js"></script>-->
<link rel="stylesheet" >
<div id="viewer"></div>
<style>
#viewer {
width: 100vw;
height: 50vh;
}
</style>
<script>
var viewer = new PhotoSphereViewer({
container: 'viewer',
panorama: 'path/to/panorama.jpg'
});
</script>
實(shí)際部署中用到的比示例要復(fù)雜,但常見(jiàn)的參數(shù)用法,官網(wǎng)文檔上寫(xiě)的都很清楚。
最開(kāi)始,我的參數(shù)是這樣的:
let mobileViewer = PhotoSphereViewer({
container: "panorama", //對(duì)應(yīng)html中的元素ID
panorama: "https://cdn.ishareit.fun/panorama/images/image.ipg", //全景圖路徑
caption: 'The name of a beautiful scene', //可選的關(guān)于圖片的描述,或者說(shuō)標(biāo)題
navbar: [
'autorotate', //自動(dòng)旋轉(zhuǎn)的按鈕
'zoom', //放大縮小的按鈕
'download', //下載圖片的按鈕
'caption', //顯示上面caption參數(shù)的按鈕
'fullscreen' //全屏按鈕,只有在支持全屏的瀏覽器中生效
]
anim_speed:"2dps", //自動(dòng)旋轉(zhuǎn)時(shí)的速度,dps為度每秒,一共360度
min_fov:30, //最小的視角范圍,視角范圍越小說(shuō)明放大倍數(shù)越大
max_fov:90, //最大的視角范圍,視角范圍越大說(shuō)明放大倍數(shù)越小
default_fov: 90, //默認(rèn)視角范圍
default_lat: -0.3, //默認(rèn)緯度,控制視角的俯仰,負(fù)數(shù)為俯
mousewheel: true, //是否允許鼠標(biāo)滾輪控制縮放
touchmove_two_fingers: false, //是否必須兩個(gè)手指的滑動(dòng)才觸發(fā)視角的移動(dòng)
time_anim:0, //在自動(dòng)旋轉(zhuǎn)之前停滯的秒數(shù)
});
插件的運(yùn)行效果還不錯(cuò),能夠自適應(yīng)桌面顯示器還有手機(jī)屏幕。但是這樣的配置顯然還沒(méi)有達(dá)到預(yù)期的效果:
- 在手機(jī)上可以像微博客戶(hù)端那樣全屏看全景。
- 啟用手機(jī)陀螺儀,轉(zhuǎn)動(dòng)手機(jī)可以控制視角。
先考慮全屏的問(wèn)題,新版的瀏覽器都對(duì)全屏事件的調(diào)用做了限制:必須由人來(lái)觸發(fā)。
也就是說(shuō),想在頁(yè)面運(yùn)行時(shí)直接全屏是無(wú)法實(shí)現(xiàn)的,只能是在當(dāng)前頁(yè)面觸發(fā)一個(gè)類(lèi)似click的事件,由這個(gè)事件觸發(fā)全屏,那么所有的查看動(dòng)作都集中在一個(gè)頁(yè)面完成。說(shuō)改也可以,但是在這期間我還專(zhuān)門(mén)寫(xiě)了一個(gè)sitemap.php用來(lái)告訴搜索引擎所有的單個(gè)全景頁(yè)面,想借此提高一下索引量,界面集中后這些頁(yè)面都沒(méi)有意義了??紤]到桌面瀏覽時(shí)全屏的作用不大,而且可以很方便地手動(dòng)全屏,這時(shí)就有了用MobileDetect庫(kù)把桌面瀏覽器和手機(jī)瀏覽器區(qū)分開(kāi)的方案。
使用MobileDetect庫(kù)判斷瀏覽器類(lèi)型。
桌面端瀏覽器跳轉(zhuǎn)到單個(gè)全景圖頁(yè)面的url再展示,而手機(jī)端的瀏覽器則在show.php頁(yè)面中直接生成。
一些手機(jī)的瀏覽器可能默認(rèn)未開(kāi)啟全屏,比如iPhone中的Safari瀏覽器,需要在高級(jí)設(shè)置中才能把全屏請(qǐng)求的api打開(kāi)。
全屏的問(wèn)題不在此展開(kāi)討論,詳情請(qǐng)見(jiàn)->關(guān)于手機(jī)瀏覽器js調(diào)用全屏的api。
說(shuō)了那么多,現(xiàn)在終于到了最核心的部分,即用DeviceOrientationControl.js配合PhotoSphereViewer開(kāi)啟手機(jī)陀螺儀瀏覽全景圖片。
按照官網(wǎng)的配置說(shuō)法,首先要引入DeviceOrientationControl.js,然后在按鈕的配置部分添加上gyroscope即可:
DeviceOrientationControl.js需要在PhotoSphereViewer插件之前,three之后引入。它的作用實(shí)際上是給three.js里面的THREE添加一個(gè)DeviceOrientationControl類(lèi)(暫且這么解釋),之后才可以在PhotoSphereViewer中使用。
navbar: [
'autorotate', //自動(dòng)旋轉(zhuǎn)的按鈕
'zoom', //放大縮小的按鈕
'download', //下載圖片的按鈕
'caption', //顯示上面caption參數(shù)的按鈕
'fullscreen', //全屏按鈕,只有在支持全屏的瀏覽器中生效
'gyroscope' //開(kāi)啟設(shè)備陀螺儀的按鈕,只有在支持陀螺儀控制的設(shè)備中生效
]
按照官網(wǎng)的配置修改代碼后,手機(jī)端的顯示界面出現(xiàn)了一個(gè)類(lèi)似指南針的圖標(biāo),這個(gè)就是陀螺儀的按鈕。點(diǎn)擊這個(gè)按鈕就進(jìn)入了用手機(jī)陀螺儀控制視角的模式。
整體效果還算不錯(cuò),但是,沒(méi)有像微博里查看全景圖那樣,進(jìn)入之后直接就是陀螺儀控制模式。
再去翻文檔,發(fā)現(xiàn)在method中有一個(gè)startGyroscopeControl()的函數(shù),這個(gè)實(shí)際上就是點(diǎn)擊按鈕后觸發(fā)的一個(gè)動(dòng)作,于是我在配置完P(guān)hotoSphereControl后立即調(diào)用了這個(gè)函數(shù)。本以為大功告成,而手機(jī)并沒(méi)有按照想象的情況去工作。
let mobileViewer = PhotoSphereViewer({
container: "panorama", //對(duì)應(yīng)html中的元素ID
panorama: "https://cdn.ishareit.fun/panorama/images/image.ipg", //全景圖路徑
caption: 'The name of a beautiful scene', //可選的關(guān)于圖片的描述,或者說(shuō)標(biāo)題
navbar: [
'autorotate', //自動(dòng)旋轉(zhuǎn)的按鈕
'zoom', //放大縮小的按鈕
'download', //下載圖片的按鈕
'caption', //顯示上面caption參數(shù)的按鈕
'fullscreen' //全屏按鈕,只有在支持全屏的瀏覽器中生效
]
anim_speed:"2dps", //自動(dòng)旋轉(zhuǎn)時(shí)的速度,dps為度每秒,一共360度
min_fov:30, //最小的視角范圍,視角范圍越小說(shuō)明放大倍數(shù)越大
max_fov:90, //最大的視角范圍,視角范圍越大說(shuō)明放大倍數(shù)越小
default_fov: 90, //默認(rèn)視角范圍
default_lat: -0.3, //默認(rèn)緯度,控制視角的俯仰,負(fù)數(shù)為俯
mousewheel: true, //是否允許鼠標(biāo)滾輪控制縮放
touchmove_two_fingers: false, //是否必須兩個(gè)手指的滑動(dòng)才觸發(fā)視角的移動(dòng)
time_anim:0, //在自動(dòng)旋轉(zhuǎn)之前停滯的秒數(shù)
});
mobileViewer.startGyroscopeControl();
打開(kāi)手機(jī)chrome的調(diào)試模式,發(fā)現(xiàn)了一例報(bào)錯(cuò):
Uncaught (in promise) TypeError: Cannot read property 'rotation' of null
進(jìn)一步了解發(fā)現(xiàn),DeviceOrientationControl.js中:
THREE.DeviceOrientationControls = function ( object ) {
var scope = this;
this.object = object;
this.object.rotation.reorder( 'YXZ' );
……
這里的object是個(gè)空,也就是給THREE.DeviceOrientationControls類(lèi)傳入的初始化參數(shù)object是個(gè)空。誰(shuí)實(shí)例化了它并給他傳入了參數(shù)呢?控制臺(tái)的報(bào)錯(cuò)顯示的很清楚:在photo-sphere-viewer.js(v3.5.1)的2508行:
this.doControls = new THREE.DeviceOrientationControls(this.camera);
這正是在startGyroscopeControl()函數(shù)內(nèi)執(zhí)行的語(yǔ)句,this.camera就是傳給THREE.DeviceOrientationControls類(lèi)的那個(gè)參數(shù)。
在photo-sphere-viewer.js中查找"this.camera",找到了18例,其中17例都是在使用或修改this.camera,只有在1107行初始化了this.camera:
this.camera = new THREE.PerspectiveCamera(this.config.default_fov, this.prop.size.width / this.prop.size.height, 1, cameraDistance);
而這句話(huà)是在PhotoSphereViewer.prototype._createScene()函數(shù)中調(diào)用的,因此使用this.camera前,必須執(zhí)行_createScene()。
抱著試一試的心態(tài),我在startGyroscopeControl()前調(diào)用_createScene():
mobileViewer._createScene();
mobileViewer.startGyroscopeControl();
打開(kāi)手機(jī)運(yùn)行,控制臺(tái)沒(méi)有報(bào)錯(cuò),視角隨著手機(jī)的晃動(dòng)而移動(dòng),我成功了。
之后我又對(duì)按鈕的布局和圖標(biāo)做了一些修改:
手機(jī)端的瀏覽效果大概是這樣的:

按鈕挪到了便于單手操作的位置,整體瀏覽效果還是不錯(cuò)的。
然而這時(shí),我又發(fā)現(xiàn)了一個(gè)問(wèn)題:手指在屏幕滑動(dòng)時(shí),只有左右方向可以控制視角的轉(zhuǎn)動(dòng),上下方向并沒(méi)有反應(yīng)。這跟微博的全景圖查看器體驗(yàn)不一致,我還是不甘心,于是繼續(xù)折騰。
手指在屏幕上的滑動(dòng)事件是由_move()函數(shù)處理的:
PhotoSphereViewer.prototype._move = function(evt, log) {
if (this.prop.moving) {
var x = parseInt(evt.clientX);
var y = parseInt(evt.clientY);
var rotation = {
longitude: (x - this.prop.mouse_x) / this.prop.size.width * this.prop.move_speed * this.prop.hFov * PhotoSphereViewer.SYSTEM.pixelRatio,
latitude: (y - this.prop.mouse_y) / this.prop.size.height * this.prop.move_speed * this.prop.vFov * PhotoSphereViewer.SYSTEM.pixelRatio
};
if (this.isGyroscopeEnabled()) {
this.prop.gyro_alpha_offset += rotation.longitude;
}
else {
this.rotate({
longitude: this.prop.position.longitude - rotation.longitude,
latitude: this.prop.position.latitude + rotation.latitude
});
}
this.prop.mouse_x = x;
this.prop.mouse_y = y;
if (log !== false) {
this._logMouseMove(evt);
}
}
};
發(fā)現(xiàn)在第2045行:
this.prop.gyro_alpha_offset += rotation.longitude;
當(dāng)開(kāi)啟手機(jī)陀螺儀控制的情況下,手指滑動(dòng)只處理了alpha軸(經(jīng)度)上產(chǎn)生的增量:



要想在手指上下滑動(dòng)時(shí)產(chǎn)生視角變化,還須處理gamma軸(緯度)上的增量,直接在2045行后面插入:
this.prop.gyro_beta_offset += rotation.latitude;
顯然是不夠的,因?yàn)閠his.prop中根本就沒(méi)有g(shù)yro_beta_offset。
沒(méi)有,那就加上:
在432行后面插入:
gyro_beta_offset:0,
別忘了我們是在開(kāi)啟陀螺儀控制的模式下處理手指滑動(dòng)的事件,陀螺儀控制的那段代碼肯定也需要修改:
2517行后面插入:
this.prop.gyro_beta_offset = sphericalCoords.latitude;
2548行后面插入:
this.doControls.betaOffset = this.prop.gyro_beta_offset;
this.doControls是什么,是一個(gè)THREE.DeviceOrientationControls對(duì)象,而它一開(kāi)始是沒(méi)有betaOffset這個(gè)屬性的,我們給它加上:
DeviceOrientationControl.js第15行后面插入:
this.betaOffset = 0;
第51行改為:
var beta = device.beta ? THREE.Math.degToRad( device.beta ) + scope.betaOffset : 0;
改到這里,沒(méi)有的屬性,添加上了,需要處理的增量也處理了。提交代碼,不出所料,頁(yè)面完美運(yùn)行,手指在屏幕進(jìn)行二維滑動(dòng)可以控制視角的變化。
查看原文->PhotoSphereViewer+DeviceOrientationControl.js開(kāi)發(fā)全景圖交互網(wǎng)站