本文从绘图基础开始讲起,详细介绍了如何使用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✨、点赞和关注是我持续创做的动力!
关注公众号后回复【加群】拉你进入优质前端交流群。