PhotoSphereViewer+DeviceOrientationControl.js开发全景图交互网站

2019年年初,登录了自己好久没上的微博@是秋先生啊。话说为啥好久没上了呢,因为我平时也不咋上微博,微博账号经常忘密码,于是就打开微博客户端的扫一扫来一波扫码登录,不知道大家有没有感觉到曾经微博客户端的扫一扫特别难找,而且经常换位置,每次找扫一扫的按钮都给我找得挺烦,它就是不在我认为它该出现的地方出现……绕远了。

我千辛万苦登上微博后,突然逛到了一张全景图片,打开之后的效果把我惊艳了,然后就开始一波研究。

手机客户端肯定是没法研究的,没有url,只能全屏看到图片。于是我在电脑上打开微博网站,找到了那篇微博。全景图片看起来是这样的:


微博全景截图

我就不做动图了,大家知道它是可以360度旋转的。

一个F12键打开浏览器调试,看了一下这个页面的源码:

微博全景源代码

其实这个页面就是一个全景图的播放器。

打开多个全景图页面源码后,发现里面改变的只有几个图片的地址,而且调试台的网络流量显示,真正起作用的图片地址是第138行的“img_url”:


微博全景网络控制台

那么让这个全景图动起来的就只有main.ac7a4be4.js这个文件了。

一个“邪恶”的想法在脑海中浮现:

前端的东西,js代码都是可以拿来用的,只要找到图片的地址,就可以做出一模一样的全景图播放器。

一开始不想把图片保存在自己的服务器上,因为乞丐版的阿里云ECS也就那么点儿容量,所以只想提取页面中的URL,然后保存在数据库里,再动态生成页面。事实证明这条办法是行不通的,微博的图片服务器做了跨域访问限制,我的域名https://www.ishareit.fun没法请求他们的图片。所以还是要写爬虫来爬图片,并且在把图片信息存在数据库里。

思路:

  1. 用微博api的python SDK调用微博api;
  2. 利用微博的api访问全景博主的微博;
  3. 当博主更新全景微博时,爬取图片保存在服务器,更新数据库;
  4. 用户访问网站时,生成页面。

吐槽一下微博开放平台:大概跟主流的前端界面差了5年左右吧,而且文档也好久没更新了,瞧瞧隔壁阿里云,文档更新就很及时。

在这条思路上,本来是想做成微博开放平台小应用的,因为访问微博api需要用户登录,这样的话可以整合到我网站自身的用户管理系统中,把微博用户转换为我自己的网站用户。但事实证明,这条思路没有走下去,因为我的目的是爬取全景图片博主的微博,但微博api有限制,没有博主的登录授权,api无法访问博主的微博。


微博API注意事项

也就是说,我开发的小应用,只能访问我自己的微博,想获取别人的微博,别人必须给这个小应用授权。这个机制真是让我无话可说,那我要你api干嘛。

这条路走不通,就暂时放弃了用微博api爬取的思路,但这时已经写好了一个python CGI的发布后台,去掉爬虫的代码,我把它改为了手动发布,地址在这里。比较方便的是手机全景微博的发布,只需要三步:

  1. 打开一个带有全景图片的微博。
  2. 复制微博链接。
  3. 将复制的链接粘贴在发布页->手机微博的标签页中,点击发布。

不过还是要感谢廖雪峰大佬提供微博api的Python版SDK,学到了不少新知识,后期准备利用这个SDK开发微信登录网站的功能。

接下来,我主要写了三个php文件,一个主文件show.php负责调用数据库,处理参数,根据参数确定显示所有的全景图->交给showthem.php,还是进入浏览单个全景图交给 ->showthat.php,网站的第一个版本是这样的:


爱享全景首页

点击其中的一个图片会跳转到show.php?id=md5的页面,show.php也就是通过判断这个id参数是否合法来确定是否显示某个具体全景图。

看起来很眼熟。扒了必应壁纸的前端页面。说实话,必应壁纸做得真心不错,虽然云淡风轻大佬写了三重防止查看前端代码的代码,类似这样,但我还是很不好意思地看到了前端页面的源码,就拿来直接用了。

说了这么久,好像跟PhotoSphereViewer+DeviceOrientationControl.js还没关系。的确,第一版出来之后,感觉做得差不多了,好像就可以告一段落了。但是,我自己在玩的时候发现用微博的js加载出来的图片清晰度很差,好像是做了什么节省流量的措施,加载出来的图片并不是下载下来的4096x2048高清原图,这让手机端的浏览体验很不友好。

我跑去问度娘,有没有开源的全景图显示库,度娘告诉我说有一个叫PhotoSphereViewer的货貌似可以,这时,我才正式入坑。


PhotoSphereViewer

这款插件用起来是很方便的,核心代码不多,想把一张全景平面图加载出来只需要事先引入依赖的文件,再写几行代码:

<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" href="https://cdn.jsdelivr.net/npm/photo-sphere-viewer@3.5.0/dist/photo-sphere-viewer.min.css">

<div id="viewer"></div>
<style>
  #viewer {
    width: 100vw;
    height: 50vh;
  }
</style>
<script>
  var viewer = new PhotoSphereViewer({
    container: 'viewer',
    panorama: 'path/to/panorama.jpg'
  });
</script>

实际部署中用到的比示例要复杂,但常见的参数用法,官网文档上写的都很清楚。

最开始,我的参数是这样的:

let mobileViewer = PhotoSphereViewer({
    container: "panorama", //对应html中的元素ID
    panorama: "https://cdn.ishareit.fun/panorama/images/image.ipg", //全景图路径
    caption: 'The name of a beautiful scene', //可选的关于图片的描述,或者说标题
    navbar: [
        'autorotate', //自动旋转的按钮
        'zoom', //放大缩小的按钮
        'download', //下载图片的按钮
        'caption', //显示上面caption参数的按钮
        'fullscreen' //全屏按钮,只有在支持全屏的浏览器中生效
    ]
    anim_speed:"2dps", //自动旋转时的速度,dps为度每秒,一共360度
    min_fov:30, //最小的视角范围,视角范围越小说明放大倍数越大
    max_fov:90, //最大的视角范围,视角范围越大说明放大倍数越小
    default_fov: 90, //默认视角范围
    default_lat: -0.3, //默认纬度,控制视角的俯仰,负数为俯
    mousewheel: true, //是否允许鼠标滚轮控制缩放
    touchmove_two_fingers: false, //是否必须两个手指的滑动才触发视角的移动
    time_anim:0, //在自动旋转之前停滞的秒数
});

插件的运行效果还不错,能够自适应桌面显示器还有手机屏幕。但是这样的配置显然还没有达到预期的效果:

  1. 在手机上可以像微博客户端那样全屏看全景。
  2. 启用手机陀螺仪,转动手机可以控制视角。

先考虑全屏的问题,新版的浏览器都对全屏事件的调用做了限制:必须由人来触发。

也就是说,想在页面运行时直接全屏是无法实现的,只能是在当前页面触发一个类似click的事件,由这个事件触发全屏,那么所有的查看动作都集中在一个页面完成。说改也可以,但是在这期间我还专门写了一个sitemap.php用来告诉搜索引擎所有的单个全景页面,想借此提高一下索引量,界面集中后这些页面都没有意义了。考虑到桌面浏览时全屏的作用不大,而且可以很方便地手动全屏,这时就有了用MobileDetect库把桌面浏览器和手机浏览器区分开的方案。

使用MobileDetect库判断浏览器类型。

桌面端浏览器跳转到单个全景图页面的url再展示,而手机端的浏览器则在show.php页面中直接生成。

一些手机的浏览器可能默认未开启全屏,比如iPhone中的Safari浏览器,需要在高级设置中才能把全屏请求的api打开。

全屏的问题不在此展开讨论,详情请见->关于手机浏览器js调用全屏的api。

说了那么多,现在终于到了最核心的部分,即用DeviceOrientationControl.js配合PhotoSphereViewer开启手机陀螺仪浏览全景图片。

按照官网的配置说法,首先要引入DeviceOrientationControl.js,然后在按钮的配置部分添加上gyroscope即可:

DeviceOrientationControl.js需要在PhotoSphereViewer插件之前,three之后引入。它的作用实际上是给three.js里面的THREE添加一个DeviceOrientationControl类(暂且这么解释),之后才可以在PhotoSphereViewer中使用。

navbar: [
        'autorotate', //自动旋转的按钮
        'zoom', //放大缩小的按钮
        'download', //下载图片的按钮
        'caption', //显示上面caption参数的按钮
        'fullscreen', //全屏按钮,只有在支持全屏的浏览器中生效
        'gyroscope' //开启设备陀螺仪的按钮,只有在支持陀螺仪控制的设备中生效
    ]

按照官网的配置修改代码后,手机端的显示界面出现了一个类似指南针的图标,这个就是陀螺仪的按钮。点击这个按钮就进入了用手机陀螺仪控制视角的模式。

整体效果还算不错,但是,没有像微博里查看全景图那样,进入之后直接就是陀螺仪控制模式。

再去翻文档,发现在method中有一个startGyroscopeControl()的函数,这个实际上就是点击按钮后触发的一个动作,于是我在配置完PhotoSphereControl后立即调用了这个函数。本以为大功告成,而手机并没有按照想象的情况去工作。

let mobileViewer = PhotoSphereViewer({
    container: "panorama", //对应html中的元素ID
    panorama: "https://cdn.ishareit.fun/panorama/images/image.ipg", //全景图路径
    caption: 'The name of a beautiful scene', //可选的关于图片的描述,或者说标题
    navbar: [
        'autorotate', //自动旋转的按钮
        'zoom', //放大缩小的按钮
        'download', //下载图片的按钮
        'caption', //显示上面caption参数的按钮
        'fullscreen' //全屏按钮,只有在支持全屏的浏览器中生效
    ]
    anim_speed:"2dps", //自动旋转时的速度,dps为度每秒,一共360度
    min_fov:30, //最小的视角范围,视角范围越小说明放大倍数越大
    max_fov:90, //最大的视角范围,视角范围越大说明放大倍数越小
    default_fov: 90, //默认视角范围
    default_lat: -0.3, //默认纬度,控制视角的俯仰,负数为俯
    mousewheel: true, //是否允许鼠标滚轮控制缩放
    touchmove_two_fingers: false, //是否必须两个手指的滑动才触发视角的移动
    time_anim:0, //在自动旋转之前停滞的秒数
});
mobileViewer.startGyroscopeControl();

打开手机chrome的调试模式,发现了一例报错:

Uncaught (in promise) TypeError: Cannot read property 'rotation' of null

进一步了解发现,DeviceOrientationControl.js中:

THREE.DeviceOrientationControls = function ( object ) {
    var scope = this;
    this.object = object;
    this.object.rotation.reorder( 'YXZ' );
    ……

这里的object是个空,也就是给THREE.DeviceOrientationControls类传入的初始化参数object是个空。谁实例化了它并给他传入了参数呢?控制台的报错显示的很清楚:在photo-sphere-viewer.js(v3.5.1)的2508行:

this.doControls = new THREE.DeviceOrientationControls(this.camera);

这正是在startGyroscopeControl()函数内执行的语句,this.camera就是传给THREE.DeviceOrientationControls类的那个参数。

在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);

而这句话是在PhotoSphereViewer.prototype._createScene()函数中调用的,因此使用this.camera前,必须执行_createScene()。

抱着试一试的心态,我在startGyroscopeControl()前调用_createScene():

mobileViewer._createScene();
mobileViewer.startGyroscopeControl();

打开手机运行,控制台没有报错,视角随着手机的晃动而移动,我成功了。

之后我又对按钮的布局和图标做了一些修改:

手机端的浏览效果大概是这样的:


image

按钮挪到了便于单手操作的位置,整体浏览效果还是不错的。

然而这时,我又发现了一个问题:手指在屏幕滑动时,只有左右方向可以控制视角的转动,上下方向并没有反应。这跟微博的全景图查看器体验不一致,我还是不甘心,于是继续折腾。

手指在屏幕上的滑动事件是由_move()函数处理的:

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);
    }
  }
};

发现在第2045行:

this.prop.gyro_alpha_offset += rotation.longitude;

当开启手机陀螺仪控制的情况下,手指滑动只处理了alpha轴(经度)上产生的增量:


image

image

image

要想在手指上下滑动时产生视角变化,还须处理gamma轴(纬度)上的增量,直接在2045行后面插入:

this.prop.gyro_beta_offset += rotation.latitude;

显然是不够的,因为this.prop中根本就没有gyro_beta_offset。
没有,那就加上:
在432行后面插入:

gyro_beta_offset:0,

别忘了我们是在开启陀螺仪控制的模式下处理手指滑动的事件,陀螺仪控制的那段代码肯定也需要修改:
2517行后面插入:

this.prop.gyro_beta_offset = sphericalCoords.latitude;

2548行后面插入:

this.doControls.betaOffset = this.prop.gyro_beta_offset;

this.doControls是什么,是一个THREE.DeviceOrientationControls对象,而它一开始是没有betaOffset这个属性的,我们给它加上:
DeviceOrientationControl.js第15行后面插入:

this.betaOffset = 0;

第51行改为:

var beta = device.beta ? THREE.Math.degToRad( device.beta ) + scope.betaOffset : 0;

改到这里,没有的属性,添加上了,需要处理的增量也处理了。提交代码,不出所料,页面完美运行,手指在屏幕进行二维滑动可以控制视角的变化。

查看原文->PhotoSphereViewer+DeviceOrientationControl.js开发全景图交互网站

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335