⚠️ The interactive globe has arrived (WIP but pretty good)

- Control the width/height of the globe via CSS
This commit is contained in:
2020-04-09 20:22:10 +02:00
parent 08c541c37a
commit 7a44d5b0ed
11 changed files with 208 additions and 121 deletions

View File

@@ -247,6 +247,8 @@ class Camera extends Object3d {
this._isPointerDown = true; this._isPointerDown = true;
this._pointerParent.classList.add('is-grabbing')
this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event; this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event;
this.touchEventPageX = this.touchEvent.pageX; this.touchEventPageX = this.touchEvent.pageX;
@@ -273,7 +275,7 @@ class Camera extends Object3d {
return; return;
} }
event.preventDefault(); // event.preventDefault();
this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event; this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event;
this.touchEventPageX = this.touchEvent.pageX; this.touchEventPageX = this.touchEvent.pageX;
@@ -281,6 +283,7 @@ class Camera extends Object3d {
this.touchEventPageX -= window.pageXOffset || document.documentElement.scrollLeft; this.touchEventPageX -= window.pageXOffset || document.documentElement.scrollLeft;
this.touchEventPageY -= window.pageYOffset || document.documentElement.scrollTop; this.touchEventPageY -= window.pageYOffset || document.documentElement.scrollTop;
if (this.isRightClick) { if (this.isRightClick) {
this.pointerXMove = this.startPointerX + (this.touchEventPageX - this.pointerXDown); this.pointerXMove = this.startPointerX + (this.touchEventPageX - this.pointerXDown);
this.pointerYMove = this.startPointerY + (this.touchEventPageY - this.pointerYDown); this.pointerYMove = this.startPointerY + (this.touchEventPageY - this.pointerYDown);
@@ -291,6 +294,11 @@ class Camera extends Object3d {
this.theta = this.thetaDown + ( this.pointerXOrbiter / this.winWidth * 2 * Math.PI); this.theta = this.thetaDown + ( this.pointerXOrbiter / this.winWidth * 2 * Math.PI);
this.phi = this.phiDown + ( this.pointerYOrbiter / this.winHeight * 2 * Math.PI * -1); this.phi = this.phiDown + ( this.pointerYOrbiter / this.winHeight * 2 * Math.PI * -1);
this.phi = Math.max( this._minPolarAngle, Math.min( this._maxPolarAngle, this.phi ) ); this.phi = Math.max( this._minPolarAngle, Math.min( this._maxPolarAngle, this.phi ) );
if( TOUCH ) {
this.phi = 0;
}
} }
@@ -299,6 +307,7 @@ class Camera extends Object3d {
_onPointerUp() { _onPointerUp() {
this._isPointerDown = false; this._isPointerDown = false;
this.isRightClick = false; this.isRightClick = false;
this._pointerParent.classList.remove('is-grabbing')
} }
update(force) { update(force) {

View File

@@ -218,6 +218,7 @@ Texture.fromUrl = function(gl, url, options) {
img.onload = null; img.onload = null;
img.onerror = null; img.onerror = null;
TEXTURE_CACHE[url] = img; TEXTURE_CACHE[url] = img;
options && options.loaded && options.loaded()
texture.bindImage(img); texture.bindImage(img);
}; };

View File

@@ -11,11 +11,11 @@ import GlobeFS from './globe-fs'
function hexToRgb(hex) { function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [ return result ? [
parseInt(result[1], 16) / 255, parseInt(result[1], 16) / 255,
parseInt(result[2], 16) / 255, parseInt(result[2], 16) / 255,
parseInt(result[3], 16) / 255 parseInt(result[3], 16) / 255
] : [0,0,0]; ] : [0,0,0];
} }
function lonLatToVector3(lng, lat) { function lonLatToVector3(lng, lat) {
var phi = (90-lat)*(Math.PI/180), var phi = (90-lat)*(Math.PI/180),
@@ -29,6 +29,12 @@ function lonLatToVector3(lng, lat) {
class WebglGlobe { class WebglGlobe {
// Constructor // Constructor
constructor (options) { constructor (options) {
//camera position cache
this.cameraX = 0;
this.cameraY = 0;
this.cameraZ = 0;
this.options = options; this.options = options;
this.$el = options.el; this.$el = options.el;
this.cities = options.markers this.cities = options.markers
@@ -50,29 +56,38 @@ class WebglGlobe {
// Build // Build
buildWebglScene () { buildWebglScene () {
this.renderer = new Renderer({ this.renderer = new Renderer({
alpha: this.alpha, alpha: this.alpha,
antialias: this.antialias, antialias: window.innerWidth < 768 ? true : false,// this.antialias,
}); });
this.$el.appendChild(this.renderer.canvas); this.$el.appendChild(this.renderer.canvas);
this.texture = Texture.fromUrl(this.renderer.gl, this.options.texture) this.texture = Texture.fromUrl(this.renderer.gl, this.options.texture, {
loaded: ()=>{
requestAnimationFrame(()=>{
requestAnimationFrame(()=>{
this.imageLoaded = true;
})
})
}
})
this.scene = new Container() this.scene = new Container()
this.camera = new Camera({ this.camera = new Camera({
fov: settings.fov, fov: settings.fov,
near: 1, near: 1,
far: settings.cameraFar, far: settings.cameraFar,
type: 'perspective', type: 'perspective',
orbitControl: true, orbitControl: true,
firstPerson: false, firstPerson: false,
lookAt: [0,0,0], lookAt: [0,0,0],
position: [0,0, (3) / 2 / Math.tan(Math.PI * settings.fov / 360)], position: [0,0, (this.options.cameraDistance) / 2 / Math.tan(Math.PI * settings.fov / 360)],
pointerParent: this.$el
}); });
this.camera.lookAt = vec3.create() this.camera.lookAt = vec3.create()
this.inverseViewProjectionMatrix = mat4.create() this.inverseViewProjectionMatrix = mat4.create()
this.viewProjectionMatrix = mat4.create() this.viewProjectionMatrix = mat4.create()
this.cameraDirection = vec3.create(); this.cameraDirection = vec3.create();
this.cameraPosition = vec3.create(); this.cameraPosition = vec3.create();
this.globeMesh = new Mesh(); this.globeMesh = new Mesh();
@@ -90,7 +105,6 @@ class WebglGlobe {
}) })
this.scene.add( this.globeMesh ) this.scene.add( this.globeMesh )
this.markers = [] this.markers = []
let markers = this.cities let markers = this.cities
@@ -108,18 +122,32 @@ class WebglGlobe {
widthSegments: 10, heightSegments: 10 widthSegments: 10, heightSegments: 10
}) })
let p = lonLatToVector3( markers[i].lng, markers[i].lat ) let p = lonLatToVector3( markers[i].lng, markers[i].lat )
markerMesh.position[0] = p[0] * 1.
markerMesh.position[1] = p[1] * 1. markerMesh.position[0] = p[0]
markerMesh.position[2] = p[2] * 1. markerMesh.position[1] = p[1]
markerMesh.position[2] = p[2]
this.scene.add( markerMesh ) this.scene.add( markerMesh )
let el = document.createElement('div') let el = document.createElement('a')
el.setAttribute('href', '/location/' + markers[i].countrySlug + '/' + markers[i].slug )
el.setAttribute('sapper-noscroll','')
let span = document.createElement('span') let span = document.createElement('span')
span.innerHTML = markers[i].name span.classList.add('marker__label')
el.appendChild(span) el.appendChild(span)
el.addEventListener('click', ()=>{ el.addEventListener('click', ()=>{
alert('click on ' + markers[i].name) this.options.onLinkClicked && this.options.onLinkClicked()
}) })
let spanCity = document.createElement('span')
spanCity.classList.add('marker__city')
spanCity.innerHTML = markers[i].name;
span.appendChild(spanCity)
let spanCountry = document.createElement('span')
spanCountry.classList.add('marker__country')
spanCountry.innerHTML = markers[i].countryName;
span.appendChild(spanCountry)
el.classList.add('marker') el.classList.add('marker')
this.$el.appendChild( el ) this.$el.appendChild( el )
this.markers.push({ this.markers.push({
@@ -135,14 +163,17 @@ class WebglGlobe {
resize () { resize () {
this.width = this.$el.clientWidth; this.width = this.$el.clientWidth;
this.height = this.$el.clientHeight; this.height = this.$el.clientHeight;
// console.log('GLOBE RESIZE', this.width, this.height)
settings.resolution[0] = this.width //* settings.devicePixelRatio settings.resolution[0] = this.width //* settings.devicePixelRatio
settings.resolution[1] = this.height //* settings.devicePixelRatio settings.resolution[1] = this.height //* settings.devicePixelRatio
if (!this.supportWebgl) { if (!this.supportWebgl) {
return return
} }
this.renderer.setPixelRatio(settings.devicePixelRatio); this.renderer.setPixelRatio(window.innerWidth < 768 ? 1 : settings.devicePixelRatio);
this.renderer.resize(settings.resolution[0], settings.resolution[1]) this.renderer.resize(settings.resolution[0], settings.resolution[1])
this.camera.aspect = settings.resolution[0] / settings.resolution[1] this.camera.aspect = settings.resolution[0] / settings.resolution[1]
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
@@ -168,13 +199,13 @@ class WebglGlobe {
let refPos = vec3.create() let refPos = vec3.create()
vec3.set(refPos, 0, 1, 0 ) vec3.set(refPos, 0, 1, 0 )
vec3.transformMat4(refPos, refPos, this.viewProjectionMatrix); vec3.transformMat4(refPos, refPos, this.viewProjectionMatrix);
let refx = ( (refPos[0] + 1) / 2) * window.innerWidth let refx = ( (refPos[0] + 1) / 2) * this.width
let refy = (1. - (refPos[1] + 1) / 2) * window.innerHeight let refy = (1. - (refPos[1] + 1) / 2) * this.height
let dir2 = vec2.create() let dir2 = vec2.create()
vec2.set( dir2, refx, refy ) vec2.set( dir2, refx, refy )
let center2 = vec2.create() let center2 = vec2.create()
vec2.set( center2, window.innerWidth/2, window.innerHeight/2 ) vec2.set( center2, this.width/2, this.height/2 )
let dir2d2 = vec2.clone(dir2, dir2) let dir2d2 = vec2.clone(dir2, dir2)
vec2.subtract( dir2d2, dir2d2, center2 ) vec2.subtract( dir2d2, dir2d2, center2 )
this.circleScreenSize = vec2.length( dir2d2 ) * 1.04 this.circleScreenSize = vec2.length( dir2d2 ) * 1.04
@@ -187,11 +218,6 @@ class WebglGlobe {
return; return;
} }
// this.currPointer[0] += ( this.pointerRatio[0] - this.currPointer[0]) * 0.05
// this.currPointer[1] += ( this.pointerRatio[1] - this.currPointer[1]) * 0.05
// this.globeMesh.rotation[1] = this.currPointer[0]
// this.globeMesh.rotation[2] = this.currPointer[1]
//manually call this as we prevent ithe camera from update between passes //manually call this as we prevent ithe camera from update between passes
this.camera.update(); this.camera.update();
vec3.set(this.cameraPosition, this.camera.worldMatrix[12], this.camera.worldMatrix[13], this.camera.worldMatrix[14]); vec3.set(this.cameraPosition, this.camera.worldMatrix[12], this.camera.worldMatrix[13], this.camera.worldMatrix[14]);
@@ -202,15 +228,29 @@ class WebglGlobe {
mat4.multiply(this.viewProjectionMatrix, this.viewProjectionMatrix, this.camera.inverseWorldMatrix); mat4.multiply(this.viewProjectionMatrix, this.viewProjectionMatrix, this.camera.inverseWorldMatrix);
mat4.invert(this.inverseViewProjectionMatrix, this.viewProjectionMatrix); mat4.invert(this.inverseViewProjectionMatrix, this.viewProjectionMatrix);
let needsUpdate = false;
if (this.cameraX != this.camera.worldMatrix[12] ||
this.cameraY != this.camera.worldMatrix[13] ||
this.cameraZ != this.camera.worldMatrix[14]) {
this.cameraX = this.camera.worldMatrix[12];
this.cameraY = this.camera.worldMatrix[13];
this.cameraZ = this.camera.worldMatrix[14];
needsUpdate = true;
}
if (!needsUpdate && this.imageLoaded) {
return
}
// console.log('RENDER WEBGL')
let screenPos = vec3.create() let screenPos = vec3.create()
this.markers.forEach((marker, i)=>{ this.markers.forEach((marker, i)=>{
vec3.set(screenPos, marker.position[0], marker.position[1], marker.position[2] ) vec3.set(screenPos, marker.position[0], marker.position[1], marker.position[2] )
vec3.transformMat4(screenPos, screenPos, this.viewProjectionMatrix); vec3.transformMat4(screenPos, screenPos, this.viewProjectionMatrix);
let x = ( (screenPos[0] + 1) / 2) * window.innerWidth let x = ( (screenPos[0] + 1) / 2) * this.width
let y = (1. - (screenPos[1] + 1) / 2) * window.innerHeight let y = (1. - (screenPos[1] + 1) / 2) * this.height
let N = vec3.create() let N = vec3.create()
vec3.set( N, marker.position[0], marker.position[1], marker.position[2] ) vec3.set( N, marker.position[0], marker.position[1], marker.position[2] )
@@ -222,11 +262,11 @@ class WebglGlobe {
vec3.normalize( V, V ); vec3.normalize( V, V );
//behind //behind
if ( vec3.dot( V, N ) * -1 < 0) { if ( vec3.dot( V, N ) * -1 < 0 ) {
let dir = vec2.create() let dir = vec2.create()
vec2.set( dir, x, y ) vec2.set( dir, x, y )
let center = vec2.create() let center = vec2.create()
vec2.set( center, window.innerWidth/2, window.innerHeight/2 ) vec2.set( center, this.width/2, this.height/2 )
let dir2d = vec2.clone(dir, dir) let dir2d = vec2.clone(dir, dir)
vec2.subtract( dir2d, dir2d, center ) vec2.subtract( dir2d, dir2d, center )
vec2.normalize( dir2d, dir2d ); vec2.normalize( dir2d, dir2d );
@@ -247,39 +287,6 @@ class WebglGlobe {
this.renderer.clear() this.renderer.clear()
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
} }
// initPointer() {
// this.currPointer = [0,0]
// this.pointer = [0,0]
// this.pointerRatio = [0,0]
// this._onPointerDown = this._onPointerDown.bind(this)
// this._onPointerMove = this._onPointerMove.bind(this)
// this._onPointerUp = this._onPointerUp.bind(this)
// this._handleOrientation = this._handleOrientation.bind(this)
// document.addEventListener(support.pointerdown, this._onPointerDown, false);
// document.addEventListener(support.pointermove, this._onPointerMove, false);
// document.addEventListener(support.pointerup, this._onPointerUp, false);
// window.addEventListener('deviceorientation', this._handleOrientation);
// }
// _handleOrientation(event) {
// this.pointerRatio[0] = (event.gamma) / 25
// this.pointerRatio[1] = (event.beta-45) / 25
// }
// _onPointerDown() {
// this._isPointerDown = true;
// }
// _onPointerUp() {
// this._isPointerDown = false
// }
// _onPointerMove(event) {
// let pe = support.touch && event.type != 'mousemove' ? (event.touches[0] || event.changedTouches[0]) : event;
// this.pointer[0] = pe.pageX;
// this.pointer[1] = pe.pageY;
// this.pointer[1] -= window.pageYOffset || document.documentElement.scrollTop
// this.pointerRatio[0] = (this.pointer[0] / window.innerWidth - .5) * 2
// this.pointerRatio[1] = (this.pointer[1] / window.innerHeight - .5) * 2
// }
} }
// window.WebglGlobe = WebglGlobe // window.WebglGlobe = WebglGlobe

View File

@@ -8,7 +8,10 @@
let globe let globe
// Functions // Functions
const resize = () => globe.resize() const resize = () => {
globe.resize()
globe.update()
}
const update = () => { const update = () => {
requestAnimationFrame(update) requestAnimationFrame(update)
globe.update() globe.update()
@@ -20,37 +23,36 @@
*/ */
onMount(async () => { onMount(async () => {
// For browser only // For browser only
// if (process.browser) { if (process.browser) {
// // Import libraries and code // Import libraries and code
// let WebglGlobe let WebglGlobe
// await import('globe').then(module => WebglGlobe = module.default) await import('globe').then(module => WebglGlobe = module.default)
// // Init the globe from library // Init the globe from library
// globe = new WebglGlobe({ globe = new WebglGlobe({
// el: scope, el: scope,
// texture: '/img/globe/map-4k.png', texture: '/img/globe/map-2k.png',
// markers: [...$locations.map(location => { markers: [...$locations.map(location => {
// return { return {
// name: location.name, name: location.name,
// slug: location.slug, slug: location.slug,
// countrySlug: location.country.slug, countryName: location.country.name,
// lat: location.coordinates.lat, countrySlug: location.country.slug,
// lng: location.coordinates.lng lat: location.coordinates.lat,
// } lng: location.coordinates.lng
// })] }
// }) })],
onLinkClicked: () => {},
cameraDistance: 3
})
// // Run the globe // Run the globe
// resize() resize()
// update() update()
// } }
}) })
</script> </script>
<!-- <svelte:window on:resize={resize} /> --> <svelte:window on:resize={resize} />
<div class="globe" class:globe--part={type === 'part'} bind:this={scope}> <div class="globe" bind:this={scope} />
<div class="wrap">
</div>
</div>

View File

@@ -11,7 +11,7 @@
// Components // Components
import IconArrow from 'atoms/IconArrow' import IconArrow from 'atoms/IconArrow'
import TitleSite from 'atoms/TitleSite' import TitleSite from 'atoms/TitleSite'
import Globe from 'molecules/InteractiveGlobe' import InteractiveGlobe from 'molecules/InteractiveGlobe'
import Locations from 'organisms/Locations' import Locations from 'organisms/Locations'
import Footer from 'organisms/Footer' import Footer from 'organisms/Footer'
import Transition from 'utils/Transition' import Transition from 'utils/Transition'
@@ -65,7 +65,7 @@
</div> </div>
</div> </div>
<Globe /> <InteractiveGlobe />
<Locations /> <Locations />
</section> </section>

View File

@@ -78,7 +78,9 @@
{/if} {/if}
</div> </div>
<InteractiveGlobe type="part" /> <div class="globe__cut">
<InteractiveGlobe type="part" />
</div>
<Footer /> <Footer />
</section> </section>

View File

@@ -11,8 +11,21 @@
} }
} }
// Globe
.globe {
margin-top: -25vw;
margin-bottom: -18vw;
@include breakpoint (xl) {
margin-top: -320px;
margin-bottom: -320px;
}
}
// Browse // Browse
.browse { .browse {
position: relative;
z-index: 3;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
} }

View File

@@ -3,17 +3,31 @@
position: relative; position: relative;
z-index: 2; z-index: 2;
width: 100vw; width: 100vw;
height: 100vh; height: 120vw;
overflow: hidden;
cursor: grab;
user-select: none;
.wrap { // Cut
@include breakpoint (xs) { &__cut {
padding: 0; opacity: 0.5;
overflow: hidden;
width: 100vw;
height: 35vw;
min-height: 400px;
padding: 0;
// Partial globe
.globe {
margin-top: -18vw;
} }
} }
// Marker
.marker { .marker {
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
display: block;
top: 0; top: 0;
left: 0; left: 0;
width: 8px; width: 8px;
@@ -22,26 +36,53 @@
opacity: 1; opacity: 1;
background: #ff6c89; background: #ff6c89;
// Location name
span { span {
transition: color 0.4s $ease-quart;
}
&__label {
position: absolute; position: absolute;
bottom: 100%; bottom: -230%;
left: 100%; left: 230%;
color: transparent;
}
// Location city
&__city {
font-family: $font-serif; font-family: $font-serif;
font-size: rem(24px); font-size: rem(24px);
color: transparent; line-height: 1;
transition: color 0.4s $ease-quart; }
// Location country
&__country {
display: block;
opacity: 0.8;
font-family: $font-sans;
font-size: rem(10px);
line-height: 1;
text-transform: uppercase;
} }
// Active // Active
&.is-active { &.is-active {
opacity: 1; &, span {
opacity: 1;
}
span { .marker {
color: #FF6C89; &__city {
color: #FF6C89;
}
&__country {
color: $color-text;
}
} }
} }
} }
// Grabbing
&.is-grabbing {
cursor: grabbing;
}
} }

View File

@@ -66,10 +66,22 @@
margin-top: 248px; margin-top: 248px;
} }
// Description
.style-description { .style-description {
position: relative;
z-index: 3;
color: $color-tertiary; color: $color-tertiary;
} }
// Globe
.globe {
margin-bottom: -52vh;
@include breakpoint (xl) {
margin-bottom: -550px;
}
}
// Browse // Browse
.browse { .browse {
margin-top: 0; margin-top: 0;

View File

@@ -88,7 +88,7 @@
} }
// Globe // Globe
.globe--part { .globe__cut {
margin-top: 8vw; margin-top: 8vw;
} }
} }

BIN
static/img/globe/map-2k.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB