Implement Interactive Globe on Homepage

This commit is contained in:
2021-10-03 13:56:11 +02:00
parent 2634e0473a
commit f6da3c4b55
8 changed files with 355 additions and 121 deletions

View 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>

View File

@@ -24,7 +24,6 @@ const degToRad = deg => deg * Math.PI / 180
class WebglGlobe {
// Constructor
constructor (options) {
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.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.hasUpdateCameraPos = false
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
*/
this.markers = []
let markers = this.cities
let markers = this.locations
// Instance all markers
for (let i = 0; i < markers.length; i++) {
@@ -362,7 +361,7 @@ class WebglGlobe {
vec2.set(dir, x, y)
let center = vec2.create()
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.normalize(dir2d, dir2d)
vec2.scale(dir2d, dir2d, this.circleScreenSize)

View File

@@ -28,6 +28,7 @@
location {
name
slug
coordinates
country {
name
slug
@@ -58,6 +59,7 @@
continent {
name
slug
rotation
countries {
slug
}

View File

@@ -53,6 +53,8 @@
</div>
</div>
<InteractiveGlobe />
<Locations
locations={location}
/>

View File

@@ -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
*/

View 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;
}
}
}
}
}
}

View File

@@ -22,6 +22,9 @@
// Pages
@import "pages/homepage";
// Modules
@import "modules/globe";
// Atomic Design System
// Atoms
@@ -46,4 +49,4 @@
// @import "pages/page";
// Misc
// @import "animations";
@import "animations";

View File

@@ -4,3 +4,30 @@
export const lerp = (start: number, end: number, amt: number): number => {
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
}
}