看完这篇,你也能够实现一个360度全景插件

导读

本文从绘图基础开始讲起,详细介绍了如何使用Three.js开发一个功能齐全的全景插件。html

咱们先来看一下插件的效果:前端

若是你对Three.js已经很熟悉了,或者你想跳过基础理论,那么你能够直接从全景预览开始看起。node

本项目的github地址:github.com/ConardLi/tp…webpack

1、理清关系

1.1 OpenGL

OpenGL是用于渲染2D、3D量图形的跨语言、跨平台的应用程序编程接口(API)git

这个接口由近350个不一样的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。github

OpenGL ESOpenGL三维图形API的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。web

基于OpenGL,通常使用CCpp开发,对前端开发者来讲不是很友好。npm

1.2 WebGL

WebGLJavaScriptOpenGL ES 2.0结合在一块儿,从而为前端开发者提供了使用JavaScript编写3D效果的能力。编程

WebGLHTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就能够借助系统显卡来在浏览器里更流畅地展现3D场景和模型了,还能建立复杂的导航和数据视觉化。json

1.3 Canvas

Canvas是一个能够自由制定大小的矩形区域,能够经过JavaScript能够对矩形区域进行操做,能够自由的绘制图形,文字等。

通常使用Canvas都是使用它的2dcontext功能,进行2d绘图,这是其自己的能力。

和这个相对的,WebGL是三维,能够描画3D图形,WebGL,想要在浏览器上进行呈现,它必须须要一个载体,这个载体就是Canvas,区别于以前的2dcontext,还能够从Canvas中获取webglcontext

1.4 Three.js

咱们先来从字面意思理解下:Three表明3Djs表明JavaScript,即便用JavaScript来开发3D效果。

Three.js是使用JavaScriptWebGL接口进行封装与简化而造成的一个易用的3D库。

直接使用WebGL进行开发对于开发者来讲成本相对来讲是比较高的,它须要你掌握较多的计算机图形学知识。

Three.js在必定程度上简化了一些规范和难以理解的概念,对不少API进行了简化,这大大下降了学习和开发三维效果成本。

下面咱们来具体看一下使用Three.js必需要知道的知识。

2、Three.js基础知识

使用Three.js绘制一个三维效果,至少须要如下几个步骤:

  • 建立一个容纳三维空间的场景 — Sence

  • 将须要绘制的元素加入到场景中,对元素的形状、材料、阴影等进行设置

  • 给定一个观察场景的位置,以及观察角度,咱们用相机对象(Camera)来控制

  • 将绘制好的元素使用渲染器(Renderer)进行渲染,最终呈如今浏览器上

拿电影来类比的话,场景对应于整个布景空间,相机是拍摄镜头,渲染器用来把拍摄好的场景转换成胶卷。

2.1 场景

场景容许你设置哪些对象被three.js渲染以及渲染在哪里。

咱们在场景中放置对象、灯光和相机。

很简单,直接建立一个Scene的实例便可。

_scene = new Scene();
复制代码

2.2 元素

有了场景,咱们接下来就须要场景里应该展现哪些东西。

一个复杂的三维场景每每就是由很是多的元素搭建起来的,这些元素多是一些自定义的几何体(Geometry),或者外部导入的复杂模型。

Three.js 为咱们提供了很是多的Geometry,例如SphereGeometry(球体)、 TetrahedronGeometry(四面体)、TorusGeometry(圆环体)等等。

Three.js中,材质(Material)决定了几何图形具体是以什么形式展示的。它包括了一个几何体如何形状之外的其余属性,例如色彩、纹理、透明度等等,MaterialGeometry是相辅相成的,必须结合使用。

下面的代码咱们建立了一个长方体体,赋予它基础网孔材料(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还提供了其余几种光源,它们适用于不一样状况下对不一样材质的渲染,能够根据实际状况选择。

2.3 坐标系

在说相机以前,咱们仍是先来了解一下坐标系的概念:

在三维世界中,坐标定义了一个元素所处于三维空间的位置,坐标系的原点即坐标的基准点。

最经常使用的,咱们使用距离原点的三个长度(距离x轴、距离y轴、距离z轴)来定义一个位置,这就是直角坐标系。

在断定坐标系时,咱们一般使用大拇指、食指和中指,并互为90度。大拇指表明X轴,食指表明Y轴,中指表明Z轴。

这就产生了两种坐标系:左手坐标系和右手坐标系。

Three.js中使用的坐标系即右手坐标系。

咱们能够在咱们的场景中添加一个坐标系,这样咱们能够清楚的看到元素处于什么位置:

var axisHelper = new THREE.AxisHelper(600);
 _scene.add(axisHelper);
复制代码

其中红色表明X轴,绿色表明Y轴,蓝色表明Z轴。

2.4 相机

上面看到的几何体的效果,若是不建立一个相机(Camera),是什么也看不到的,由于默认的观察点在坐标轴原点,它处于几何体的内部。

相机(Camera)指定了咱们在什么位置观察这个三维场景,以及以什么样的角度进行观察。

2.4.1 两种相机的区别

目前Three.js提供了几种不一样的相机,最经常使用的,也是下面插件中使用的两种相机是:PerspectiveCamera(透视相机)、 OrthographicCamera(正交投影相机)。

上面的图很清楚的解释了两种相机的区别:

右侧是 OrthographicCamera(正交投影相机)他不具备透视效果,即物体的大小不受远近距离的影响,对应的是投影中的正交投影。咱们数学课本上所画的几何体大多数都采用这种投影。

左侧是PerspectiveCamera(透视相机),这符合咱们正常人的视野,近大远小,对应的是投影中的透视投影。

若是你想让场景看起来更真实,更具备立体感,那么采用透视相机最合适,若是场景中有一些元素你不想让他随着远近放大缩小,那么采用正交投影相机最合适。

2.4.2 构造参数

咱们再分别来看看两个建立两个相机须要什么参数:

_camera = new OrthographicCamera(left, right, top, bottom, near, far);
复制代码

OrthographicCamera接收六个参数,left, right, top, bottom分别对应上、下、左、右、远、近的一个距离,超过这些距离的元素将不会出如今视野范围内,也不会被浏览器绘制。实际上,这六个距离就构成了一个立方体,因此OrthographicCamera的可视范围永远在这个立方体内。

_camera = new PerspectiveCamera(fov, aspect, near, far);
复制代码

PerspectiveCamera接收四个参数,nearfar和上面的相同,分别对应相机可观测的最远和最近距离;fov表明水平范围可观测的角度,fov越大,水平范围能观测到的范围越广;aspect表明水平方向和竖直方向可观测距离的比值,因此fovaspect就能够肯定垂直范围内能观测到的范围。

2.4.3 position、lookAt

关于相机还有两个必需要知道的点,一个是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轴
复制代码

可见:咱们视野的出发点是相同的,可是视野看向的方向发生了改变。

2.4.4 两种相机对比

好,有了上面的基础,咱们再来写两个例子看一看两个相机的视角对比,为了方便观看,咱们建立两个位置不一样的几何体:

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))
复制代码

可见,这印证了咱们上面关于两种相机的理论

2.5 渲染器

上面咱们建立了场景、元素和相机,下面咱们要告诉浏览器将这些东西渲染到浏览器上。

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);
复制代码

实际上,你若是依次执行上面的代码,可能屏幕上仍是黑漆漆的一片,并无任何元素渲染出来。

这是由于上面你要渲染的元素可能并未被加载完,你就执行了渲染,而且只执行了一次,这时咱们须要一种方法,让场景和相机进行实时渲染,咱们须要用到下面的方法:

2.6 requestAnimationFrame

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);
            }
        }
复制代码

来看一下执行效果:

咱们使用requestAnimationFrameThree.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' }
        }
复制代码

2.7 总结

上面的知识是Three.js中最基础的知识,也是最重要的和最主干的。

这些知识可以让你在看到一个复杂的三维效果时有必定的思路,固然,要实现还须要很是多的细节。这些细节你能够去官方文档中查阅。

下面的章节即告诉你如何使用Three.js进行实战 — 实现一个360度全景插件。

这个插件包括两部分,第一部分是对全景图进行预览。

第二部分是对全景图的标记进行配置,并关联预览的坐标。

咱们首先来看看全景预览部分:

3、全景预览

3.1 基本逻辑

  • 将一张全景图包裹在球体的内壁

  • 设定一个观察点,在球的圆心

  • 使用鼠标能够拖动球体,从而改变咱们看到全景的视野

  • 鼠标滚轮能够缩放,和放大,改变观察全景的远近

  • 根据坐标在全景图上挂载一些标记,如文字、图标等,而且能够增长事件,如点击事件

3.2 初始化

咱们先把必要的基础设施搭建起来:

场景、相机(选择远景相机,这样可让全景看起来更真实)、渲染器:

_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);
复制代码

而后咱们看到的场景应该是这样的:

这不是咱们想要的效果,咱们想要的是从球的内部观察全景,而且全景图是附着外球的内壁的,而不是铺在外面:

咱们只要需将Materialscale的一个属性设置为负值,材料便可附着在几何体的内部:

mesh.scale.x = -1;
复制代码

而后咱们将相机的中心点移动到球的中心:

_camera.position.set(0, 0, 0);
复制代码

如今咱们已经在全景球的内部啦:

3.3 事件处理

全景图已经能够浏览了,可是你只能看到你眼前的这一块,并不能拖动它看到其余部分,为了精确的控制拖动的速度和缩放、放大等场景,咱们手动为它增长一些事件:

监听鼠标的mousedown事件,在此时将开始拖动标记_isUserInteracting设置为true,而且记录起始的屏幕坐标,以及起始的相机lookAt的坐标。

_container.addEventListener('mousedown', (event)=>{
  event.preventDefault();
  _isUserInteracting = true;
  _onPointerDownPointerX = event.clientX;
  _onPointerDownPointerY = event.clientY;
  _onPointerDownLon = _lon;
  _onPointerDownLat = _lat;
});
复制代码

监听鼠标的mousemove事件,当_isUserInteractingtrue时,实时计算当前相机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);
    }
  }
});
复制代码

来看一下效果吧:

3.4 增长标记

在浏览全景图的时候,咱们每每须要对某些特殊的位置进行一些标记,而且这些标记可能附带一些事件,好比你须要点击一个标记才能到达下一张全景图。

下面咱们来看看如何在全景中增长标记,以及如何为这些标记添加事件。

咱们可能不须要让这些标记随着视野的变化而放大和缩小,基于此,咱们使用正交投影相机来展示标记,只需给它一个固定的观察高度:

_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;
}
复制代码

建立好这些标记,咱们把它渲染到场景中。

咱们必须告诉场景这些标记的位置,为了直观的理解,咱们须要给这些标记赋予一种坐标,这种坐标很相似于经纬度,咱们叫它lonlat,具体是如何给定的咱们在下面的章节:全景标记中会详细介绍。

在这个过程当中,一共经历了两次坐标转换:

第一次转换:将“经纬度”转换为三维空间坐标,即咱们上面讲的那种x、y、z形式的坐标。

使用geoPosition2World函数进行转换,获得一个Vector3对象,咱们能够将当前相机_camera做为参数传入这个对象的project方法,这会获得一个标准化后的坐标,基于这个坐标能够帮咱们判断标记是否在视野范围内,以下面的代码,若标准化坐标在-11的范围内,则它会出如今咱们的视野中,咱们将它进行准确渲染。

第二次转换:将三维空间坐标转换为屏幕坐标。

若是咱们直接讲上面的三维空间坐标坐标应用到标记中,咱们会发现不管视野如何移动,标记的位置是不会有任何变化的,由于这样算出来的坐标永远是一个常量。

因此咱们须要借助上面的标准化坐标,将标记的三维空间坐标转换为真实的屏幕坐标,这个过程是worldPostion2Screen函数来实现的。

关于geoPosition2WorldworldPostion2Screen两个函数的实现,你们有兴趣能够去个人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);
  });
});
复制代码

点击到一个标记,进入到下一张全景图:

4、全景标记

为了让全景图知道,我要把标记标注在什么地方,我须要一个工具来把原图和全景图上的位置关联起来:

因为这部分代码和 Three.js关系不大,这里我只说一下基本的实现逻辑,有兴趣能够去个人 github仓库查看。

4.1 要求

  • 创建坐标和全景的映射关系,为全景赋予一套虚拟坐标

  • 在一张平铺的全景图上,能够在任意位置增长标记,并获取标记的坐标

  • 使用坐标在预览全景增长标记,看到的标记位置和平铺全景中的位置相同

4.2 坐标

2D平面上,咱们能监听屏幕的鼠标事件,咱们能够获取的也只是当前的鼠标坐标,咱们要作的是将鼠标坐标转换成三维空间坐标。

看起来好像是不可能的,二维坐标怎么能转换成三维坐标呢?

可是,咱们能够借助一种中间坐标来转换,能够把它称之为“经纬度”。

在这以前,咱们先来看看咱们常说的经纬度究竟是什么。

4.3 经纬度

使用经纬度,能够精确的定位到地球上任意一个点,它的计算规则是这样的:

一般把链接南极到北极的线叫作子午线也叫经线,其所对应的面叫作子午面,规定英国伦敦格林尼治天文台原址的那条经线称为0°经线,也叫本初子午线其对应的面即本初子午面。

经度:球面上某店对应的子午面与本初子午面间的夹角。东正西负。

纬度 :球面上某点的法线(以该店做为切点与球面相切的面的法线)与赤道平面的夹角。北正南负。

由此,地球上每个点都能被对应到一个经度和纬度,想对应的,也能对应到某条经线和纬线上。

这样,即便把球面展开称平面,咱们仍然能用经纬度表示某店点的位置:

4.4 坐标转换

基于上面的分析,咱们彻底能够给平面的全景图赋予一个虚拟的“经纬度”。咱们使用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 };
}

复制代码

这样平面地图上的某点就能够和三维坐标关联起来了,固然,这还须要必定的转换,有兴趣能够去源码研究下geoPosition2WorldworldPostion2Screen两个函数。

5、插件封装

上面的代码中,咱们实现了全景预览和全景标记的功能,下面,咱们要把这些功能封装成插件。

所谓插件,便可以直接引用你写的代码,并添加少许的配置就能够实现想要的功能。

5.1 全景预览封装

咱们来看看,究竟哪些配置是能够抽取出来的:

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);
    // 初始化操做...
  }
}
复制代码

5.2 全景标记封装

基本逻辑和上面的相似,下面是提取出来的一些参数。

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,//开启默认中键删除 (必须开启经纬度提示)
}
复制代码

6、发布

接下来,咱们就好考虑如何将写好的插件让用户使用了。

咱们主要考虑两种场景,直接引用和npm install

6.1 直接引用JS

为了避免污染全局变量,咱们使用一个自执行函数(function(){}())将代码包起来,而后将咱们写好的插件暴露给全局变量window

我把它放在originSrc目录下。

(function (global, undefined) {

    function tpanorama(opt) {
        // ...
    }

    tpanorama.prototype = {
        // ...
    }

    function tpanoramaSetting(opt) {
        // ...
    }

    tpanoramaSetting.prototype = {
        // ...
    }

    global.tpanorama = tpanorama;
    global.tpanoramaSetting = panoramaSetting;
}(window))
复制代码

6.2 使用npm install

直接将写好的插件导出:

module.exports = tpanorama;
module.exports = panoramaSetting;
复制代码

我把它放在src目录下。

同时,咱们要把package.json中的main属性指向咱们要导出的文件:"main": "lib/index.js",而后将namedescriptionversion等信息补充完整。

下面,咱们就能够开始发布了,首先你要有一个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');
复制代码

6.3 babel编译

最后不要忘了,不管使用以上哪一种方式,咱们都要使用babel编译后才能暴露给用户。

scripts中建立一个build命令,将源文件进行编译,最终暴露给用户使用的将是liborigin

"build": "babel src --out-dir lib && babel originSrc --out-dir origin",
复制代码

你还能够指定一些其余的命令来供用户测试,如我将写好的例子所有放在examples中,而后在scripts定义了expamle命令:

"example": "npm run webpack && node ./server/www"
复制代码

这样,用户将代码克隆后直接在本地运行npm run example就能够进行调试了。

7、小结

本项目的github地址:github.com/ConardLi/tp…

文中若有错误,欢迎在评论区指正,若是这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可关注个人github博客,你的star✨、点赞和关注是我持续创做的动力!

关注公众号后回复【加群】拉你进入优质前端交流群。

相关文章
相关标签/搜索