Globe edits WIP
All checks were successful
continuous-integration/drone/push Build is passing

- Added visible continents to options
This commit is contained in:
2020-04-19 14:49:52 +02:00
parent c1bb2f31bc
commit 094614f83c
8 changed files with 238 additions and 210 deletions

View File

@@ -23,17 +23,15 @@ class WebglGlobe {
// Constructor // Constructor
constructor (options) { constructor (options) {
this.$el = options.el // The DOM reference node
this.$el = options.el//the Dom reference node
this.options = options this.options = options
this.options.autoRotationSpeed = this.options.autoRotationSpeed || 0 this.options.autoRotationSpeed = this.options.autoRotationSpeed || 0
this.options.scrollSmoothing = this.options.scrollSmoothing || 0.5 // smooth the globe position to avoid janks on scroll(lower==smoother) this.options.scrollSmoothing = this.options.scrollSmoothing || 0.5 // Smooth the globe position to avoid janks on scroll (lower == smoother)
this.options.cameraDistance = this.options.cameraDistance || 1 //a multiplier to move camera backward or forward this.options.cameraDistance = this.options.cameraDistance || 1 // A multiplier to move camera backward or forward
this.cities = options.markers//list of cities with their options this.cities = options.markers // List of cities with their options
this._canUpdate = false this._canUpdate = false
this.hasUpdateCameraPos = false this.hasUpdateCameraPos = false
this.referenceHeight = 1;//used to set camera distance from globe where referenceHeight == window height this.referenceHeight = 1 // Used to set camera distance from globe where referenceHeight == window height
this.currMarkerScrollOffset = 0 this.currMarkerScrollOffset = 0
this.markersScrollOffset = 0 this.markersScrollOffset = 0
this.globeScrollOffset = 0 this.globeScrollOffset = 0
@@ -51,14 +49,13 @@ class WebglGlobe {
this.resize() this.resize()
} }
} }
// Build // Build
buildWebglScene () { buildWebglScene () {
this.renderer = new Renderer({ this.renderer = new Renderer({
//to allow transparent background on webgl canvas // To allow transparent background on webgl canvas
alpha: true, alpha: true,
//only enable antialiasing if screen is small with no retina(for performances reasons) // Enable antialiasing only if screen is small with no retina (for performances reasons)
antialias: window.innerWidth < 768 || window.devicePixelRatio == 1 ? true : false, antialias: window.innerWidth < 768 || window.devicePixelRatio == 1 ? true : false,
}) })
@@ -75,7 +72,7 @@ class WebglGlobe {
// The markers DOM nodes wrapper // The markers DOM nodes wrapper
// this wrapper is added just next to the canvas, at the end of body tag // this wrapper is added just next to the canvas, at the end of body tag
this.$markerWrapper = document.createElement('div') this.$markerWrapper = document.createElement('div')
this.$markerWrapper.classList.add('markerWrapper') this.$markerWrapper.classList.add('globe__markers')
this.$markerWrapper.style.position = 'fixed' this.$markerWrapper.style.position = 'fixed'
this.$markerWrapper.style.top = 0 this.$markerWrapper.style.top = 0
this.$markerWrapper.style.left = 0 this.$markerWrapper.style.left = 0
@@ -111,9 +108,9 @@ class WebglGlobe {
}) })
this.camera.lookAt = vec3.create() this.camera.lookAt = vec3.create()
/** /**
* used to compute screen position of markers, * used to compute screen position of markers,
* to move the corresponding DOM nodes * to move the corresponding DOM nodes
*/ */
this.viewProjectionMatrix = mat4.create() this.viewProjectionMatrix = mat4.create()
this.cameraPosition = vec3.create() this.cameraPosition = vec3.create()
@@ -157,11 +154,11 @@ class WebglGlobe {
// Position marker // Position marker
let p = lonLatToVector3(markers[i].lng, markers[i].lat) let p = lonLatToVector3(markers[i].lng, markers[i].lat)
//scale marker position to fit globe size // Scale marker position to fit globe size
p[0] *= this.referenceHeight/2; p[0] *= this.referenceHeight/2;
p[1] *= this.referenceHeight/2; p[1] *= this.referenceHeight/2;
p[2] *= this.referenceHeight/2; p[2] *= this.referenceHeight/2;
// Wrap marker in link // Wrap marker in link
let el = document.createElement('a') let el = document.createElement('a')
el.style.pointerEvents = 'auto' el.style.pointerEvents = 'auto'
@@ -211,7 +208,6 @@ class WebglGlobe {
// Resize method // Resize method
resize () { resize () {
if (!this.supportWebgl) { if (!this.supportWebgl) {
return return
} }
@@ -219,21 +215,21 @@ class WebglGlobe {
this.width = window ? window.innerWidth : 0 this.width = window ? window.innerWidth : 0
this.height = window ? window.innerHeight : 0 this.height = window ? window.innerHeight : 0
//remove retina on small screen(aka mobile) to boost perfs // Remove retina on small screen (aka mobile) to boost perfs
this.renderer.setPixelRatio( window.innerWidth < 768 ? 1 : window.devicePixelRatio) this.renderer.setPixelRatio( window.innerWidth < 768 ? 1 : window.devicePixelRatio)
this.renderer.resize(this.width , this.height) this.renderer.resize(this.width , this.height)
//update camera aspect ratio // Update camera aspect ratio
this.camera.aspect = this.width / this.height this.camera.aspect = this.width / this.height
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
//at which distance to put the camera when rotating arounf the globe // Distance to put the camera when rotating around the globe
this.camera._cameraDistance = (this.referenceHeight * this.options.cameraDistance) / 2 / Math.tan(Math.PI * FOV / 360); this.camera._cameraDistance = (this.referenceHeight * this.options.cameraDistance) / 2 / Math.tan(Math.PI * FOV / 360);
this.camera.update(true) this.camera.update(true)
/** /**
* When markers are behind the globe, clamp their position * When markers are behind the globe, clamp their position
* to this size to make them move along the circle edge * to this size to make them move along the circle edge
*/ */
this.circleScreenSize = (this.height / 2) * (1 / this.options.cameraDistance); this.circleScreenSize = (this.height / 2) * (1 / this.options.cameraDistance);
} }
@@ -242,11 +238,11 @@ class WebglGlobe {
* As the camera rotates arount the globe, we cant simply move the Y position of the camera or the globe * As the camera rotates arount the globe, we cant simply move the Y position of the camera or the globe
* Instead, we move the globe vertex inside the vertex shadder after compute the project on screen, so we pass the 'scroll' position to a uniform * Instead, we move the globe vertex inside the vertex shadder after compute the project on screen, so we pass the 'scroll' position to a uniform
*/ */
updateCameraPos(y, scrollDiff) { updateCameraPos (y, scrollDiff) {
this.globeScrollOffset = y; this.globeScrollOffset = y;
this.markersScrollOffset = scrollDiff this.markersScrollOffset = scrollDiff
//avoid jump due to smoothing when setting it for first time as the inital values are 0 // Avoid jump due to smoothing when setting it for first time as the inital values are 0
if (!this.hasUpdateCameraPos) { if (!this.hasUpdateCameraPos) {
this.hasUpdateCameraPos = true this.hasUpdateCameraPos = true
if (this.globeMesh.material.uniforms.uCameraOffsetY) { if (this.globeMesh.material.uniforms.uCameraOffsetY) {
@@ -260,17 +256,16 @@ class WebglGlobe {
* Flag to stop rendering the webgl if the globe isnt visible * Flag to stop rendering the webgl if the globe isnt visible
* This helps saving perfs and battery * This helps saving perfs and battery
*/ */
enable() { enable () {
this._canUpdate = true this._canUpdate = true
} }
disable() { disable () {
this._canUpdate = false this._canUpdate = false
} }
// Update // Update
update () { update () {
if (!this.supportWebgl || !this._canUpdate || !this.imageLoaded || !this.hasUpdateCameraPos) { if (!this.supportWebgl || !this._canUpdate || !this.imageLoaded || !this.hasUpdateCameraPos) {
return return
} }
@@ -281,44 +276,41 @@ class WebglGlobe {
this.currMarkerScrollOffset += (this.markersScrollOffset - this.currMarkerScrollOffset) * this.options.scrollSmoothing this.currMarkerScrollOffset += (this.markersScrollOffset - this.currMarkerScrollOffset) * this.options.scrollSmoothing
//compute the camera view-projection matrix to use it on the markers // Compute the camera view-projection matrix to use it on the markers
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])
mat4.copy(this.viewProjectionMatrix, this.camera.projectionMatrix) mat4.copy(this.viewProjectionMatrix, this.camera.projectionMatrix)
mat4.multiply(this.viewProjectionMatrix, this.viewProjectionMatrix, this.camera.inverseWorldMatrix) mat4.multiply(this.viewProjectionMatrix, this.viewProjectionMatrix, this.camera.inverseWorldMatrix)
//Auto rotate the globe // Auto rotate the globe
this.globeMesh.rotation[1] += this.options.autoRotationSpeed this.globeMesh.rotation[1] += this.options.autoRotationSpeed
this.globeMesh.updateMatrix() this.globeMesh.updateMatrix()
this.globeMesh.updateWorldMatrix() this.globeMesh.updateWorldMatrix()
let screenPos = vec3.create() let screenPos = vec3.create()
this.markers.forEach((marker, i) => { this.markers.forEach((marker, i) => {
// Get marker 3D position and project it on screen to get 2D position
//get marker 3D position and project it on screen to get 2D position
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.globeMesh.worldMatrix) vec3.transformMat4(screenPos, screenPos, this.globeMesh.worldMatrix)
vec3.transformMat4(screenPos, screenPos, this.viewProjectionMatrix) vec3.transformMat4(screenPos, screenPos, this.viewProjectionMatrix)
//marker 2D screen position (starting from top left corner of screen) // Marker 2D screen position (starting from top left corner of screen)
let x = ((screenPos[0] + 1) / 2) * this.width let x = ((screenPos[0] + 1) / 2) * this.width
let y = (1. - (screenPos[1] + 1) / 2) * this.height let y = (1. - (screenPos[1] + 1) / 2) * this.height
//compute marker Normal // Compute marker Normal
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])
vec3.transformMat4(N, N, this.globeMesh.worldMatrix) vec3.transformMat4(N, N, this.globeMesh.worldMatrix)
vec3.normalize(N, N) vec3.normalize(N, N)
//compute view vector (camera direction) // Compute view vector (camera direction)
let V = vec3.create() let V = vec3.create()
vec3.set(V, marker.position[0], marker.position[1], marker.position[2]) vec3.set(V, marker.position[0], marker.position[1], marker.position[2])
vec3.subtract(V, V, this.cameraPosition) vec3.subtract(V, V, this.cameraPosition)
vec3.normalize(V, V) vec3.normalize(V, V)
// Marker is behind the globe: clamp it to the globe edge
//the marker is behind the globe: clamp it to the globe edge
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)
@@ -334,7 +326,7 @@ class WebglGlobe {
marker.el.style.transform = `translate(${dir2d[0]}px, ${dir2d[1]}px) translateZ(0)` marker.el.style.transform = `translate(${dir2d[0]}px, ${dir2d[1]}px) translateZ(0)`
marker.el.classList.remove('is-active') marker.el.classList.remove('is-active')
} }
//marker is in front of the globe; update 2D position // Marker is in front of the globe; update 2D position
else { else {
y += this.currMarkerScrollOffset y += this.currMarkerScrollOffset
marker.el.style.transform = `translate(${x}px, ${y}px) translateZ(0)` marker.el.style.transform = `translate(${x}px, ${y}px) translateZ(0)`
@@ -342,8 +334,8 @@ class WebglGlobe {
} }
}) })
//render webgl frame // Render WebGL frame
this.renderer.clearColor(0,0,0,0)//[RGBA] alpha is set to 0 to have a transparent background on the webgl this.renderer.clearColor(0,0,0,0) //[RGBA] alpha is set to 0 to have a transparent background on the webgl
this.renderer.clear() this.renderer.clear()
this.renderer.render(this.scene, this.camera) this.renderer.render(this.scene, this.camera)
} }

View File

@@ -1,106 +1,94 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { locations } from 'utils/store' import { continents, locations } from 'utils/store'
import { getPosition } from 'utils/functions'
// Dependencies
import ScrollOut from 'scroll-out' import ScrollOut from 'scroll-out'
import Lazy from 'svelte-lazy'
function getPosition (node, scope) {
var root = scope || document;
var offsetTop = node.offsetTop;
var offsetLeft = node.offsetLeft;
while (node && node.offsetParent && node.offsetParent != document && node !== root && root !== node.offsetParent ) {
offsetTop += node.offsetParent.offsetTop;
offsetLeft += node.offsetParent.offsetLeft;
node = node.offsetParent;
}
return {
top: offsetTop,
left: offsetLeft
};
}
// Props // Props
export let type = '' export let type = ''
export let size = 0.575
let scope let scope
let globe let globe
let containerTop = 0 let containerTop = 0
let containerHeight = 0 let containerHeight = 0
let winHeight = window ? window.innerHeight : 0
const resize = () => {
winHeight = window ? window.innerHeight : 0 /*
** Functions
*/
// Globe update
const update = () => {
requestAnimationFrame(update)
globe.update()
}
// On resize
const onResize = () => {
if (scope) { if (scope) {
containerTop = getPosition( scope ).top containerTop = getPosition(scope).top
containerHeight = scope.clientHeight containerHeight = scope.clientHeight
} }
globe.resize() globe.resize()
globe.update() globe.update()
} }
const update = () => { // On scroll
requestAnimationFrame(update) const onScroll = () => {
globe.update() let scrollDiff = (containerTop + window.innerHeight + (containerHeight - window.innerHeight) / 2) - document.documentElement.scrollTop
let scrollRatio = (1 - (scrollDiff / window.innerHeight)) * 2
globe && globe.updateCameraPos(scrollRatio, scrollDiff - window.innerHeight)
} }
const onScroll = (e)=> {
let scrollDiff = (containerTop + window.innerHeight + (containerHeight - window.innerHeight) /2 ) - document.documentElement.scrollTop
let scrollRatio = (1 - ( scrollDiff / window.innerHeight ) ) * 2
globe && globe.updateCameraPos( scrollRatio, scrollDiff - window.innerHeight)
}
/* /*
** Run code when mounted ** Run code when mounted
*/ */
onMount(async () => { onMount(async () => {
let InteractiveGlobe
// Import libraries and code
await import('globe/index').then(module => InteractiveGlobe = module.default)
// Init the globe from library
globe = new InteractiveGlobe({
el: scope,
cameraDistance: size, // Smaller number == larger globe
// autoRotationSpeed: -0.0025,
scrollSmoothing: 0.5,
texture: `/img/globe/map-${window.innerWidth > 1440 && window.devicePixelRatio > 1 ? '4k' : '2k'}.png`,
markers: [ ...$locations.map(location => {
return {
name: location.name,
slug: location.slug,
countryName: location.country.name,
countrySlug: location.country.slug,
lat: location.coordinates.lat,
lng: location.coordinates.lng,
className: location.close ? 'is-close' : '',
}
}) ],
centerPositions: [ ...$continents.map(continent => continent.countries) ],
onLinkClicked: () => {}
})
console.log(globe.options.centerPositions)
// Run the globe
onResize()
update()
// Enable the globe only when shown
const globeScroll = ScrollOut({ const globeScroll = ScrollOut({
once: false, once: false,
targets: scope, targets: scope,
// threshold: 1, onShown: () => {
onShown: () => {
globe.enable() globe.enable()
onScroll()
}, },
onHidden: () => { onHidden: () => globe.disable()
globe.disable()
},
}) })
let InteractiveGlobe
if (process.browser) {
// Import libraries and code
await import('globe/index').then(module => InteractiveGlobe = module.default)
// Init the globe from library
globe = new InteractiveGlobe({
el: scope,
cameraDistance: 0.5,//smaller number == larger globe
autoRotationSpeed: -0.0025,
scrollSmoothing: 0.5,
texture: `/img/globe/map-${window.innerWidth > 1440 && window.devicePixelRatio > 1 ? '4k' : '2k'}.png`,
markers: [...$locations.map(location => {
return {
name: location.name,
slug: location.slug,
countryName: location.country.name,
countrySlug: location.country.slug,
lat: location.coordinates.lat,
lng: location.coordinates.lng,
className: location.slug === 'marseille' ? 'is-left' : '',
}
})],
onLinkClicked: () => {}
})
// Run the globe
resize()
update()
}
}) })
</script> </script>
<svelte:window on:resize={resize} on:scroll={onScroll} /> <svelte:window on:resize={onResize} on:scroll={onScroll} />
{#if type === 'part'} <div class="globe" class:globe--part={type === 'part'} bind:this={scope} />
<div class="globe globe--cut" bind:this={scope} />
{:else}
<div class="globe" bind:this={scope} />
{/if}

View File

@@ -23,6 +23,7 @@
data { data {
id id
name name
coordinates
} }
} }
countries { countries {
@@ -42,6 +43,7 @@
region region
country { id } country { id }
description description
close
coordinates coordinates
illu_desktop { full_url } illu_desktop { full_url }
illu_desktop_2x { full_url } illu_desktop_2x { full_url }

View File

@@ -8,6 +8,8 @@
pageReady, pageReady,
pageAnimation pageAnimation
} from 'utils/store' } from 'utils/store'
// Dependencies
import Lazy from 'svelte-lazy'
// Components // Components
import IconArrow from 'atoms/IconArrow' import IconArrow from 'atoms/IconArrow'
import TitleSite from 'atoms/TitleSite' import TitleSite from 'atoms/TitleSite'
@@ -65,7 +67,11 @@
</div> </div>
</div> </div>
<InteractiveGlobe /> {#if process.browser}
<Lazy offset={window.innerHeight}>
<InteractiveGlobe />
</Lazy>
{/if}
<Locations /> <Locations />
</section> </section>

View File

@@ -1,7 +1,9 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { stores } from '@sapper/app' import { stores } from '@sapper/app'
import { site, pageReady } from 'utils/store' import { site, pageReady, pageAnimation } from 'utils/store'
// Dependencies
import Lazy from 'svelte-lazy'
// Components // Components
import IconArrow from 'atoms/IconArrow' import IconArrow from 'atoms/IconArrow'
import TitleSite from 'atoms/TitleSite' import TitleSite from 'atoms/TitleSite'
@@ -76,7 +78,11 @@
{/if} {/if}
</div> </div>
<InteractiveGlobe type="part" /> {#if process.browser}
<Lazy offset={window.innerHeight}>
<InteractiveGlobe type="part" />
</Lazy>
{/if}
<Footer /> <Footer />
</section> </section>

View File

@@ -19,11 +19,11 @@
// position: absolute; // position: absolute;
// left: 0; top: 50%; // left: 0; top: 50%;
// width: 100%; // width: 100%;
// height: 2px; // height: 2px;
// margin-top:-1px; // margin-top:-1px;
// } // }
////END DEBUG//// ////END DEBUG////
@include breakpoint (sm) { @include breakpoint (sm) {
height: 140vw; height: 140vw;
} }
@@ -31,102 +31,120 @@
height: 2000px; height: 2000px;
} }
// Cut
&--cut { /*
opacity: 0.5; ** Partial globe
*/
&--part {
overflow: hidden; overflow: hidden;
width: 100vw; height: 30vw;
height: 35vw; min-height: 300px;
min-height: 400px; opacity: 0.5;
padding: 0;
}
}
// Marker
.marker {
position: absolute;
z-index: 2;
cursor: pointer;
display: block;
top: 0;
left: 0;
width: 8px;
height: 8px;
border-radius: 100%;
opacity: 1;
background: #ff6c89;
will-change: transform;
span {
transition: color 0.4s $ease-quart;
} }
// Hover glow effect
&.hover {
animation: globeMarkerPulse 1s;
}
// Label
&__label {
position: absolute;
bottom: -230%;
left: 230%;
color: transparent;
}
// Location city
&__city {
font-family: $font-serif;
font-size: rem(24px);
line-height: 1;
}
// Location country
&__country {
display: block;
opacity: 0.8;
font-family: $font-sans;
font-size: rem(10px);
line-height: 1;
text-transform: uppercase;
}
// Active
&.is-active { /*
&, span { ** Markers
opacity: 1; */
&__markers {
// When dragging
&.is-grabbing {
cursor: grabbing;
} }
// Marker
.marker { .marker {
&__city { position: absolute;
color: #FF6C89; z-index: 2;
cursor: pointer;
display: block;
top: -4px;
left: -4px;
width: 8px;
height: 8px;
padding: 4px;
border-radius: 100%;
opacity: 1;
background: #ff6c89;
will-change: transform;
span {
transition: color 0.4s $ease-quart, opacity 0.4s $ease-quart;
} }
// Hover glow effect
&.hover {
animation: globeMarkerPulse 1s;
}
// Label
&__label {
position: absolute;
bottom: -16px;
left: 16px;
color: transparent;
}
// Location city
&__city {
font-family: $font-serif;
font-size: rem(24px);
line-height: 1;
}
// Location country
&__country { &__country {
color: $color-text; display: block;
opacity: 0.8;
font-family: $font-sans;
font-size: rem(10px);
line-height: 1;
text-transform: uppercase;
}
// Active
&.is-active {
&, span {
opacity: 1;
}
.marker {
&__city {
color: #FF6C89;
}
&__country {
color: $color-text;
}
}
}
// Left positioned
&.is-left {
.marker {
&__label {
left: auto;
right: 32px;
}
&__country {
text-align: right;
}
}
}
// Marker is close to another one
// Show the marker infos only on hover
&.is-close {
.marker__label {
opacity: 0;
pointer-events: none;
}
// Show labels on hover
&:hover {
.marker__label {
opacity: 1;
pointer-events: auto;
}
}
} }
} }
} }
}
.marker.is-left {
.marker__label {
left: auto;
right: 360%;
}
.marker__country {
text-align: right;
}
}
// Grabbing
.markerWrapper.is-grabbing {
cursor: grabbing;
}
// Part globe
.globe--part {
overflow: hidden;
height: 30vw;
min-height: 300px;
opacity: 0.5;
} }

View File

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

View File

@@ -44,6 +44,22 @@ export function throttle (fn, delay) {
} }
/*
** Get a DOM element's position
*/
export const getPosition = (node, scope) => {
const root = scope || document
let offsetTop = node.offsetTop
let offsetLeft = node.offsetLeft
while (node && node.offsetParent && node.offsetParent != document && node !== root && root !== node.offsetParent) {
offsetTop += node.offsetParent.offsetTop
offsetLeft += node.offsetParent.offsetLeft
node = node.offsetParent
}
return { top: offsetTop, left: offsetLeft }
}
/* /*
** Wrap string's each letters into a span ** Wrap string's each letters into a span
*/ */