Implement Interactive Globe on Homepage
This commit is contained in:
130
src/components/organisms/InteractiveGlobe.svelte
Normal file
130
src/components/organisms/InteractiveGlobe.svelte
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, getContext } from 'svelte'
|
||||||
|
import { getPosition, getRandomElement } from '$utils/functions'
|
||||||
|
|
||||||
|
export let type: string = undefined
|
||||||
|
export let autoRotate: boolean = true
|
||||||
|
export let scrollSmooth: number = 0.5
|
||||||
|
export let opacity: number = 1
|
||||||
|
|
||||||
|
const { continent, location } = getContext('global')
|
||||||
|
|
||||||
|
let globe: any
|
||||||
|
let Globe: any
|
||||||
|
let globeEl: HTMLElement
|
||||||
|
let windowHeight: number, windowWidth: number
|
||||||
|
let containerTop: number = 0, containerHeight: number = 0
|
||||||
|
let observer: IntersectionObserver
|
||||||
|
|
||||||
|
const randomContinent = getRandomElement(continent.filter(cont => cont.countries))
|
||||||
|
const globeResolution = windowWidth > 1440 && window.devicePixelRatio > 1 ? '4k' : '2k'
|
||||||
|
const locations = location.map(({ name, slug, country, coordinates: { coordinates }}: any) => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
countryName: country.name,
|
||||||
|
countrySlug: country.slug,
|
||||||
|
lat: coordinates[1],
|
||||||
|
lng: coordinates[0],
|
||||||
|
// className: location.close ? 'is-close' : '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globe update
|
||||||
|
*/
|
||||||
|
const update = () => {
|
||||||
|
requestAnimationFrame(update)
|
||||||
|
globe.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When scrolling
|
||||||
|
*/
|
||||||
|
const handleScroll = () => {
|
||||||
|
let scrollDiff = (containerTop + windowHeight + (containerHeight - windowHeight) / 2) - document.documentElement.scrollTop
|
||||||
|
let scrollRatio = (1 - (scrollDiff / windowHeight)) * 2
|
||||||
|
if (globe) {
|
||||||
|
globe.updateCameraPos(scrollRatio, scrollDiff - windowHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When resizing
|
||||||
|
*/
|
||||||
|
const handleResize = () => {
|
||||||
|
if (globeEl) {
|
||||||
|
containerTop = getPosition(globeEl).top
|
||||||
|
containerHeight = globeEl.clientHeight
|
||||||
|
}
|
||||||
|
if (globe) {
|
||||||
|
globe.resize()
|
||||||
|
globe.update()
|
||||||
|
}
|
||||||
|
handleScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// Load globe library
|
||||||
|
Globe = await import('$modules/globe')
|
||||||
|
|
||||||
|
// Instantiate globe
|
||||||
|
globe = new Globe.default({
|
||||||
|
el: globeEl,
|
||||||
|
//cameraDistance: size, // Smaller number == larger globe
|
||||||
|
autoRotationSpeed: autoRotate ? -0.0025 : 0,
|
||||||
|
rotationStart: randomContinent.rotation, // In degrees
|
||||||
|
scrollSmoothing: scrollSmooth,
|
||||||
|
opacity,
|
||||||
|
texture: `/images/globe-map-${globeResolution}.png`,
|
||||||
|
markers: locations,
|
||||||
|
onLinkClicked: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe the globe
|
||||||
|
observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
globe.enable()
|
||||||
|
} else {
|
||||||
|
globe.disable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: '0px 0px 0px'
|
||||||
|
})
|
||||||
|
observer.observe(globeEl)
|
||||||
|
|
||||||
|
// Run the globe
|
||||||
|
update()
|
||||||
|
handleResize()
|
||||||
|
|
||||||
|
|
||||||
|
// Destroy
|
||||||
|
return () => {
|
||||||
|
if (globe) {
|
||||||
|
globe.destroy()
|
||||||
|
observer.unobserve(globeEl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
bind:innerHeight={windowHeight}
|
||||||
|
bind:innerWidth={windowWidth}
|
||||||
|
on:scroll={handleScroll}
|
||||||
|
on:resize={handleResize}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="globe">
|
||||||
|
{#if type === 'cropped'}
|
||||||
|
<div class="globe--cropped">
|
||||||
|
<div class="globe" bind:this={globeEl} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="globe" bind:this={globeEl} />
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -24,7 +24,6 @@ const degToRad = deg => deg * Math.PI / 180
|
|||||||
|
|
||||||
|
|
||||||
class WebglGlobe {
|
class WebglGlobe {
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor (options) {
|
constructor (options) {
|
||||||
this.$el = options.el // The DOM reference node
|
this.$el = options.el // The DOM reference node
|
||||||
@@ -34,7 +33,7 @@ class WebglGlobe {
|
|||||||
this.options.cameraDistance = 1 // this.options.cameraDistance || 1 // A multiplier to move camera backward or forward
|
this.options.cameraDistance = 1 // this.options.cameraDistance || 1 // A multiplier to move camera backward or forward
|
||||||
this.options.opacity = this.options.opacity || 1
|
this.options.opacity = this.options.opacity || 1
|
||||||
|
|
||||||
this.cities = options.markers // List of cities with their options
|
this.locations = options.markers // List of locations 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
|
||||||
@@ -157,7 +156,7 @@ class WebglGlobe {
|
|||||||
* Create DOM nodes for markers and 3D positions
|
* Create DOM nodes for markers and 3D positions
|
||||||
*/
|
*/
|
||||||
this.markers = []
|
this.markers = []
|
||||||
let markers = this.cities
|
let markers = this.locations
|
||||||
|
|
||||||
// Instance all markers
|
// Instance all markers
|
||||||
for (let i = 0; i < markers.length; i++) {
|
for (let i = 0; i < markers.length; i++) {
|
||||||
@@ -362,7 +361,7 @@ class WebglGlobe {
|
|||||||
vec2.set(dir, x, y)
|
vec2.set(dir, x, y)
|
||||||
let center = vec2.create()
|
let center = vec2.create()
|
||||||
vec2.set(center, this.width/2, this.height/2)
|
vec2.set(center, this.width/2, this.height/2)
|
||||||
let dir2d = vec2.clone(dir, dir)
|
let dir2d = vec2.clone(dir) // vec2.clone(dir, dir)
|
||||||
vec2.subtract(dir2d, dir2d, center)
|
vec2.subtract(dir2d, dir2d, center)
|
||||||
vec2.normalize(dir2d, dir2d)
|
vec2.normalize(dir2d, dir2d)
|
||||||
vec2.scale(dir2d, dir2d, this.circleScreenSize)
|
vec2.scale(dir2d, dir2d, this.circleScreenSize)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
location {
|
location {
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
|
coordinates
|
||||||
country {
|
country {
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
continent {
|
continent {
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
|
rotation
|
||||||
countries {
|
countries {
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<InteractiveGlobe />
|
||||||
|
|
||||||
<Locations
|
<Locations
|
||||||
locations={location}
|
locations={location}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,119 +1,3 @@
|
|||||||
/* ==========================================================================
|
|
||||||
PAGE TRANSITION
|
|
||||||
========================================================================== */
|
|
||||||
.transition {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 400;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: wait;
|
|
||||||
|
|
||||||
&, * {
|
|
||||||
will-change: transform, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content
|
|
||||||
&__loader {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background
|
|
||||||
&__background {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $color-primary;
|
|
||||||
transform-origin: 50% 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hidden
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
REVEAL ANIMATIONS
|
|
||||||
========================================================================== */
|
|
||||||
.anim-mask {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
KEYFRAMES ANIMATIONS
|
|
||||||
========================================================================== */
|
|
||||||
// Rotate button dashes
|
|
||||||
@keyframes rotateDashes {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Spinning globe
|
|
||||||
*/
|
|
||||||
@keyframes moveContinents {
|
|
||||||
0% { transform: translate(0,0); }
|
|
||||||
100% { transform: translate(-80.26px, 28.2px); }
|
|
||||||
}
|
|
||||||
.anim-spinGlobe {
|
|
||||||
animation: moveContinents 1.7s linear infinite;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
// Small
|
|
||||||
@keyframes moveContinentsSmall {
|
|
||||||
0% { transform: translate(0,0); }
|
|
||||||
100% { transform: translate(-96.95px, 0); }
|
|
||||||
}
|
|
||||||
.anim-spinGlobeSmall {
|
|
||||||
animation: moveContinentsSmall 1.5s linear infinite;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Layout
|
|
||||||
*/
|
|
||||||
// List
|
|
||||||
@keyframes layoutListOdd {
|
|
||||||
0% { transform: translateX(0); }
|
|
||||||
50% { transform: translateX(2px); }
|
|
||||||
}
|
|
||||||
@keyframes layoutListEven {
|
|
||||||
0% { transform: translateX(0); }
|
|
||||||
50% { transform: translateX(-4px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grid
|
|
||||||
@keyframes layoutGridOdd {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(2px); }
|
|
||||||
}
|
|
||||||
@keyframes layoutGridEven {
|
|
||||||
0% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-3px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Globe
|
** Globe
|
||||||
*/
|
*/
|
||||||
|
|||||||
187
src/style/modules/_globe.scss
Normal file
187
src/style/modules/_globe.scss
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// Globe
|
||||||
|
.globe {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 1315px;
|
||||||
|
height: clamp(700px, 100vw, 1315px);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@include bp (sm) {
|
||||||
|
// height: 130vw;
|
||||||
|
}
|
||||||
|
@include bp (md) {
|
||||||
|
// height: 112vw;
|
||||||
|
}
|
||||||
|
@include bp (xl) {
|
||||||
|
// height: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEBUG //
|
||||||
|
// background: rgba(red, 0.2);
|
||||||
|
// &:after {
|
||||||
|
// content: "";
|
||||||
|
// display: block;
|
||||||
|
// position: absolute;
|
||||||
|
// top: 50%;
|
||||||
|
// left: 0;
|
||||||
|
// background: blue;
|
||||||
|
// width: 100%;
|
||||||
|
// height: 2px;
|
||||||
|
// margin-top: -1px;
|
||||||
|
// }
|
||||||
|
// END DEBUG //
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Cropped globe
|
||||||
|
*/
|
||||||
|
&--cropped {
|
||||||
|
overflow: hidden;
|
||||||
|
height: clamp(300px, 30vw, 500px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Markers
|
||||||
|
*/
|
||||||
|
&__markers {
|
||||||
|
z-index: 210;
|
||||||
|
|
||||||
|
// When dragging
|
||||||
|
&.is-grabbing {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker
|
||||||
|
.marker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
padding: 4px;
|
||||||
|
opacity: 1;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
// Dot
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: $color-secondary;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
transition: color 0.4s $ease-quart, opacity 0.3s $ease-inout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover glow effect
|
||||||
|
&.hover {
|
||||||
|
&:before {
|
||||||
|
animation: globeMarkerPulse 1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
&__label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -16px;
|
||||||
|
left: 16px;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
// Location city
|
||||||
|
&__city {
|
||||||
|
font-family: $font-serif;
|
||||||
|
font-size: rem(18px);
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@include bp (sm) {
|
||||||
|
font-size: rem(24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Location country
|
||||||
|
&__country {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: rem(8px);
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
@include bp (sm) {
|
||||||
|
font-size: rem(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active
|
||||||
|
&.is-active {
|
||||||
|
&, span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.marker {
|
||||||
|
&__city {
|
||||||
|
color: $color-secondary;
|
||||||
|
}
|
||||||
|
&__country {
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is light
|
||||||
|
&.is-light {
|
||||||
|
&.is-active {
|
||||||
|
.marker {
|
||||||
|
&__city {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&__country {
|
||||||
|
color: #d2b7e4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Dot
|
||||||
|
&:before {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
}
|
||||||
|
// Label
|
||||||
|
.marker__label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show labels on hover
|
||||||
|
&:hover {
|
||||||
|
.marker__label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
// Pages
|
// Pages
|
||||||
@import "pages/homepage";
|
@import "pages/homepage";
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
@import "modules/globe";
|
||||||
|
|
||||||
|
|
||||||
// Atomic Design System
|
// Atomic Design System
|
||||||
// Atoms
|
// Atoms
|
||||||
@@ -46,4 +49,4 @@
|
|||||||
// @import "pages/page";
|
// @import "pages/page";
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
// @import "animations";
|
@import "animations";
|
||||||
@@ -4,3 +4,30 @@
|
|||||||
export const lerp = (start: number, end: number, amt: number): number => {
|
export const lerp = (start: number, end: number, amt: number): number => {
|
||||||
return (1 - amt) * start + amt * end
|
return (1 - amt) * start + amt * end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a random element from an array
|
||||||
|
*/
|
||||||
|
export const getRandomElement = (array: any[]): any => {
|
||||||
|
return ~~(array.length * Math.random())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a DOM element's position
|
||||||
|
*/
|
||||||
|
export const getPosition = (node, scope?: HTMLElement) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user