[WIP] ✨ Started new Globe from scratch using OGL
This commit is contained in:
@@ -20,7 +20,9 @@
|
|||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"embla-carousel": "^6.2.0",
|
"embla-carousel": "^6.2.0",
|
||||||
"focus-visible": "^5.2.0",
|
"focus-visible": "^5.2.0",
|
||||||
"sanitize.css": "^13.0.0"
|
"ogl": "^0.0.97",
|
||||||
|
"sanitize.css": "^13.0.0",
|
||||||
|
"tweakpane": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^1.0.0-next.61",
|
"@sveltejs/adapter-auto": "^1.0.0-next.61",
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -1,9 +1,9 @@
|
|||||||
lockfileVersion: 5.4
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@sveltejs/adapter-auto': ^1.0.0-next.60
|
'@sveltejs/adapter-auto': ^1.0.0-next.61
|
||||||
'@sveltejs/adapter-node': ^1.0.0-next.81
|
'@sveltejs/adapter-node': ^1.0.0-next.81
|
||||||
'@sveltejs/adapter-vercel': ^1.0.0-next.62
|
'@sveltejs/adapter-vercel': ^1.0.0-next.63
|
||||||
'@sveltejs/kit': ^1.0.0-next.377
|
'@sveltejs/kit': ^1.0.0-next.377
|
||||||
'@types/animejs': ^3.1.5
|
'@types/animejs': ^3.1.5
|
||||||
'@typescript-eslint/eslint-plugin': ^5.30.6
|
'@typescript-eslint/eslint-plugin': ^5.30.6
|
||||||
@@ -16,6 +16,7 @@ specifiers:
|
|||||||
eslint: ^8.19.0
|
eslint: ^8.19.0
|
||||||
eslint-plugin-svelte3: ^4.0.0
|
eslint-plugin-svelte3: ^4.0.0
|
||||||
focus-visible: ^5.2.0
|
focus-visible: ^5.2.0
|
||||||
|
ogl: ^0.0.97
|
||||||
postcss: ^8.4.14
|
postcss: ^8.4.14
|
||||||
postcss-focus-visible: ^7.0.0
|
postcss-focus-visible: ^7.0.0
|
||||||
postcss-normalize: ^10.0.1
|
postcss-normalize: ^10.0.1
|
||||||
@@ -28,6 +29,7 @@ specifiers:
|
|||||||
svelte-preprocess: ^4.10.7
|
svelte-preprocess: ^4.10.7
|
||||||
swell-node: ^4.0.10
|
swell-node: ^4.0.10
|
||||||
tslib: ^2.4.0
|
tslib: ^2.4.0
|
||||||
|
tweakpane: ^3.1.0
|
||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
vite: ^3.0.0
|
vite: ^3.0.0
|
||||||
|
|
||||||
@@ -36,7 +38,9 @@ dependencies:
|
|||||||
dayjs: 1.11.3
|
dayjs: 1.11.3
|
||||||
embla-carousel: 6.2.0
|
embla-carousel: 6.2.0
|
||||||
focus-visible: 5.2.0
|
focus-visible: 5.2.0
|
||||||
|
ogl: 0.0.97
|
||||||
sanitize.css: 13.0.0
|
sanitize.css: 13.0.0
|
||||||
|
tweakpane: 3.1.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sveltejs/adapter-auto': 1.0.0-next.61
|
'@sveltejs/adapter-auto': 1.0.0-next.61
|
||||||
@@ -1875,6 +1879,10 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ogl/0.0.97:
|
||||||
|
resolution: {integrity: sha512-8VGNwb+BnVgg80uF2MDJGX+rLja8DPvmSsW1a3KCZO4pQF8sszRCgQVQmUA2EnoIYXtMUEztChkB0fuoFcWLxw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/once/1.4.0:
|
/once/1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3029,6 +3037,10 @@ packages:
|
|||||||
typescript: 4.7.4
|
typescript: 4.7.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tweakpane/3.1.0:
|
||||||
|
resolution: {integrity: sha512-PGAp/LPQdHwzL7/iAW4lV1p9iPQTti7YMjMWO48CoYjvZRS59RmgQnhEGzKzqST1JnmOYmQUjTe8bdhlZRJs5A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/type-check/0.4.0:
|
/type-check/0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|||||||
173
src/components/organisms/InteractiveGlobe2.svelte
Normal file
173
src/components/organisms/InteractiveGlobe2.svelte
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
@import "../../style/modules/globe2";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte'
|
||||||
|
import { fade, fly } from 'svelte/transition'
|
||||||
|
import { quartOut } from 'svelte/easing'
|
||||||
|
import { Globe, type Marker } from '$modules/globe2'
|
||||||
|
import { getRandomItem } from '$utils/functions'
|
||||||
|
// Components
|
||||||
|
import Image from '$components/atoms/Image.svelte'
|
||||||
|
|
||||||
|
let innerWidth: number
|
||||||
|
let offsetWidth: number, offsetHeight: number
|
||||||
|
let globeParentEl: HTMLElement, globeEl: HTMLElement
|
||||||
|
let globe: any
|
||||||
|
let observer: IntersectionObserver
|
||||||
|
let animation: number
|
||||||
|
let popinOpen: boolean = false
|
||||||
|
let clusterLocations: Marker[] = []
|
||||||
|
|
||||||
|
$: globeResolution = innerWidth > 1440 && window.devicePixelRatio > 1 ? '4k' : '2k'
|
||||||
|
|
||||||
|
const { continents, locations } = getContext('global')
|
||||||
|
const randomContinent: any = getRandomItem(continents.filter((cont: any) => cont.countries))
|
||||||
|
const markers = locations.map(({ name, slug, country, coordinates: { coordinates }}): Marker => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
country: {
|
||||||
|
name: country.name,
|
||||||
|
slug: country.slug,
|
||||||
|
flag: country.flag,
|
||||||
|
},
|
||||||
|
lat: coordinates[1],
|
||||||
|
lng: coordinates[0],
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
globe = new Globe({
|
||||||
|
el: globeEl,
|
||||||
|
parent: globeParentEl,
|
||||||
|
width: offsetWidth,
|
||||||
|
height: offsetHeight,
|
||||||
|
mapFile: `/images/globe-map-${globeResolution}.png`,
|
||||||
|
dpr: Math.min(Math.round(window.devicePixelRatio), 2),
|
||||||
|
autoRotate: true,
|
||||||
|
speed: 0.0035,
|
||||||
|
rotationStart: randomContinent.rotation,
|
||||||
|
markers,
|
||||||
|
pane: import.meta.env.DEV,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define cluster locations
|
||||||
|
clusterLocations = locations.filter((loc: any) => loc.country.slug === 'france')
|
||||||
|
|
||||||
|
// console.log(globe)
|
||||||
|
|
||||||
|
resize()
|
||||||
|
|
||||||
|
// Render only if in viewport
|
||||||
|
observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(({ isIntersecting }: IntersectionObserverEntry) => {
|
||||||
|
if (isIntersecting) {
|
||||||
|
update()
|
||||||
|
console.log('render globe2')
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
console.log('stop globe2')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: '0px 0px 0px'
|
||||||
|
})
|
||||||
|
observer.observe(globeEl)
|
||||||
|
|
||||||
|
|
||||||
|
// Destroy
|
||||||
|
return () => {
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods
|
||||||
|
*/
|
||||||
|
// Update
|
||||||
|
const update = () => {
|
||||||
|
animation = requestAnimationFrame(update)
|
||||||
|
globe.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
const stop = () => {
|
||||||
|
cancelAnimationFrame(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
const resize = () => {
|
||||||
|
globe.resize({
|
||||||
|
width: offsetWidth,
|
||||||
|
height: offsetHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy
|
||||||
|
const destroy = () => {
|
||||||
|
stop()
|
||||||
|
globe.destroy()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
on:resize={resize}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="globe" bind:this={globeParentEl}>
|
||||||
|
<div class="globe__inner">
|
||||||
|
<div class="globe__canvas"
|
||||||
|
bind:this={globeEl}
|
||||||
|
bind:offsetWidth bind:offsetHeight
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="globe__markers">
|
||||||
|
{#each markers as { name, slug, country, lat, lng }}
|
||||||
|
<li class="globe__marker" data-location={slug} data-lat={lat} data-lng={lng}>
|
||||||
|
<a href="/{country.slug}/{slug}" sveltekit:noscroll>
|
||||||
|
<dl>
|
||||||
|
<dt class="title-small">{name}</dt>
|
||||||
|
<dd class="text-label text-label--small">{country.name}</dd>
|
||||||
|
</dl>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<li class="globe__cluster">
|
||||||
|
<button on:click={() => popinOpen = !popinOpen} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if popinOpen}
|
||||||
|
<div class="globe__popin" transition:fly={{ y: 16, duration: 500, easing: quartOut }}>
|
||||||
|
<ul>
|
||||||
|
{#each clusterLocations as { name, slug, country }, index (slug)}
|
||||||
|
<li in:fade={{ duration: 400, delay: 200 + (50 * index) }}>
|
||||||
|
<a href="/{country.slug}/{slug}" sveltekit:noscroll tabindex="0">
|
||||||
|
<Image
|
||||||
|
class="flag"
|
||||||
|
id={country.flag.id}
|
||||||
|
sizeKey="square-small"
|
||||||
|
width={32} height={32}
|
||||||
|
alt="Flag of {country.name}"
|
||||||
|
/>
|
||||||
|
<dl>
|
||||||
|
<dt class="title-small">{name}</dt>
|
||||||
|
<dd class="text-label text-label--small">{country.name}</dd>
|
||||||
|
</dl>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<button class="close" aria-label="Close" on:click={() => popinOpen = false}>
|
||||||
|
<svg width="9" height="9">
|
||||||
|
<use xlink:href="#cross" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
37
src/modules/globe2/frag.glsl
Normal file
37
src/modules/globe2/frag.glsl
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
precision highp float;
|
||||||
|
varying vec3 v_normal;
|
||||||
|
varying vec3 v_surfaceToLight;
|
||||||
|
varying vec3 v_surfaceToView;
|
||||||
|
varying vec2 v_uv;
|
||||||
|
uniform float u_dt;
|
||||||
|
uniform float u_shininess;
|
||||||
|
uniform sampler2D map;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Re-normalize interpolated varyings
|
||||||
|
vec3 normal = normalize(v_normal);
|
||||||
|
vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
|
||||||
|
vec3 surfaceToViewDirection = normalize(v_surfaceToView);
|
||||||
|
// Calculate Half-Vector, Vector that bisects the angle of reflection.
|
||||||
|
// This vector indecates the "brightest point" A "refrence vector" if you will.
|
||||||
|
vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
|
||||||
|
// Then we can get the brightness at any point by seeing "how similar" the surface normal is to the refrence vector.
|
||||||
|
float light = dot(normal, surfaceToLightDirection);
|
||||||
|
|
||||||
|
// By raising the specular vector to a power we can control the intensity of the light
|
||||||
|
float specular = 0.0;
|
||||||
|
|
||||||
|
if (light > 0.0) {
|
||||||
|
specular = pow(dot(normal, halfVector), u_shininess * 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping textures
|
||||||
|
vec4 map = texture2D(map, v_uv).rgba;
|
||||||
|
// vec3 spec = texture2D(specMap, v_uv).rgb;
|
||||||
|
|
||||||
|
gl_FragColor.rgba = map;
|
||||||
|
// Add Point Lighting
|
||||||
|
gl_FragColor.rgba *= light;
|
||||||
|
// Add Specular Highlights
|
||||||
|
// gl_FragColor.rgb += specular * spec;
|
||||||
|
}
|
||||||
324
src/modules/globe2/index.ts
Normal file
324
src/modules/globe2/index.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl'
|
||||||
|
// Shaders
|
||||||
|
import VERTEX_SHADER from '../../modules/globe2/vertex.glsl?raw'
|
||||||
|
import FRAGMENT_SHADER from '../../modules/globe2/frag.glsl?raw'
|
||||||
|
|
||||||
|
|
||||||
|
export class Globe {
|
||||||
|
constructor (options: Options) {
|
||||||
|
// Options
|
||||||
|
this.options = options
|
||||||
|
this.el = options.el
|
||||||
|
this.parent = options.parent
|
||||||
|
this.markers = options.markers || []
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
this.params = {
|
||||||
|
autoRotate: options.autoRotate,
|
||||||
|
speed: options.speed,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
this.hoveringMarker = false
|
||||||
|
this.dragging = false
|
||||||
|
this.webgl = WebGLSupport() !== null
|
||||||
|
this.pane = undefined
|
||||||
|
|
||||||
|
// Run globe after check for WebGL support
|
||||||
|
if (this.webgl) {
|
||||||
|
this.build()
|
||||||
|
this.resize({
|
||||||
|
width: this.options.width,
|
||||||
|
height: this.options.height,
|
||||||
|
})
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add GUI panel if activated
|
||||||
|
if (this.options.pane) {
|
||||||
|
import('./pane').then(({ createPane }) => {
|
||||||
|
createPane(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build scene
|
||||||
|
*/
|
||||||
|
build () {
|
||||||
|
// Create renderer
|
||||||
|
this.renderer = new Renderer({
|
||||||
|
dpr: this.options.dpr || 1,
|
||||||
|
alpha: true,
|
||||||
|
premultiplyAlpha: false,
|
||||||
|
antialias: this.options.antialias || true,
|
||||||
|
})
|
||||||
|
this.gl = this.renderer.gl
|
||||||
|
|
||||||
|
// Create camera
|
||||||
|
this.camera = new Camera(this.gl)
|
||||||
|
this.camera.position.set(0, 0, 1.315)
|
||||||
|
|
||||||
|
// Create controls
|
||||||
|
this.controls = new Orbit(this.camera, {
|
||||||
|
element: this.el,
|
||||||
|
target: new Vec3(0,0,0),
|
||||||
|
enableZoom: false,
|
||||||
|
enablePan: false,
|
||||||
|
autoRotate: false,
|
||||||
|
ease: 0.2,
|
||||||
|
minPolarAngle: Math.PI / 4,
|
||||||
|
maxPolarAngle: Math.PI / 1.5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Append canvas to scene
|
||||||
|
this.el.appendChild(this.gl.canvas)
|
||||||
|
|
||||||
|
// Create scene and geometry
|
||||||
|
this.scene = new Transform()
|
||||||
|
this.geometry = new Sphere(this.gl, {
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 64,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create light
|
||||||
|
// this.light = new Vec3(0, 50, 150)
|
||||||
|
this.light = new Vec3(0, 0, 1000)
|
||||||
|
|
||||||
|
// Add map texture
|
||||||
|
const map = new Texture(this.gl)
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => (map.image = img)
|
||||||
|
img.src = this.options.mapFile
|
||||||
|
|
||||||
|
// Create program
|
||||||
|
this.program = new Program(this.gl, {
|
||||||
|
vertex: VERTEX_SHADER,
|
||||||
|
fragment: FRAGMENT_SHADER,
|
||||||
|
uniforms: {
|
||||||
|
u_dt: { value: 0 },
|
||||||
|
u_lightWorldPosition: { value: this.light }, // Position of the Light
|
||||||
|
u_shininess: { value: 1.0 },
|
||||||
|
map: { value: map }, // Color Map
|
||||||
|
},
|
||||||
|
transparent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create mesh
|
||||||
|
this.mesh = new Mesh(this.gl, {
|
||||||
|
geometry: this.geometry,
|
||||||
|
program: this.program,
|
||||||
|
})
|
||||||
|
this.mesh.setParent(this.scene)
|
||||||
|
|
||||||
|
// Define random continent position
|
||||||
|
if (this.options.rotationStart) {
|
||||||
|
this.mesh.rotation.y = degToRad(this.options.rotationStart * -1) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add events
|
||||||
|
this.addEvents()
|
||||||
|
|
||||||
|
// Setup markers
|
||||||
|
if (this.markers) {
|
||||||
|
this.setupMarkers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add events
|
||||||
|
*/
|
||||||
|
addEvents () {
|
||||||
|
// When clicking on globe
|
||||||
|
this.gl.canvas.addEventListener('mousedown', () => {
|
||||||
|
this.dragging = true
|
||||||
|
this.gl.canvas.classList.add('is-grabbing')
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// When releasing globe click
|
||||||
|
this.gl.canvas.addEventListener('mouseup', () => {
|
||||||
|
this.dragging = false
|
||||||
|
this.gl.canvas.classList.remove('is-grabbing')
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markers
|
||||||
|
*/
|
||||||
|
// Get marker from DOM element
|
||||||
|
getMarker (id: string) {
|
||||||
|
const marker = this.parent.querySelector(`[data-location="${id}"]`)
|
||||||
|
if (marker) {
|
||||||
|
return marker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup markers
|
||||||
|
setupMarkers () {
|
||||||
|
this.markers.forEach((marker: Marker) => {
|
||||||
|
const markerEl = this.getMarker(marker.slug)
|
||||||
|
|
||||||
|
// Entering marker
|
||||||
|
markerEl.addEventListener('mouseenter', () => {
|
||||||
|
this.hoveringMarker = true
|
||||||
|
}, false)
|
||||||
|
// Leaving marker
|
||||||
|
markerEl.addEventListener('mouseleave', () => {
|
||||||
|
this.hoveringMarker = false
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// Define position
|
||||||
|
const position = lonlatVec3(marker.lng, marker.lat)
|
||||||
|
|
||||||
|
// Scale marker position to fit globe size
|
||||||
|
marker.position = [position[0] *= 0.5, position[1] *= 0.5, position[2] *= 0.5]
|
||||||
|
|
||||||
|
console.log(marker)
|
||||||
|
return marker
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update markers
|
||||||
|
updateMarkers () {
|
||||||
|
this.markers.forEach((marker: Marker) => {
|
||||||
|
const markerEl = this.getMarker(marker.slug)
|
||||||
|
const screenVector = new Vec3(0,0,0)
|
||||||
|
screenVector.copy(marker.position)
|
||||||
|
this.camera.project(screenVector)
|
||||||
|
|
||||||
|
// let posX = (screenVector.x + 1) * (this.options.width / 1.315)
|
||||||
|
// // posX /= this.mesh.rotation.y
|
||||||
|
// let posY = (1 - screenVector.y) * (this.options.height / 1.315)
|
||||||
|
// markerEl.style.transform = `translate3d(${posX}px, ${posY}px, 0)`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize method
|
||||||
|
*/
|
||||||
|
resize (options: any) {
|
||||||
|
// this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
this.renderer.setSize(options.width || this.options.width, options.height || this.options.height)
|
||||||
|
this.camera.perspective({
|
||||||
|
aspect: this.gl.canvas.width / this.gl.canvas.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update method
|
||||||
|
*/
|
||||||
|
render () {
|
||||||
|
// Stop render if not dragging but hovering marker
|
||||||
|
if (!this.dragging && this.hoveringMarker) return
|
||||||
|
|
||||||
|
// Update globe rotation
|
||||||
|
if (this.params.autoRotate) {
|
||||||
|
this.mesh.rotation.y += this.params.speed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update controls and renderer
|
||||||
|
this.controls.update(this.params)
|
||||||
|
this.renderer.render({
|
||||||
|
scene: this.scene,
|
||||||
|
camera: this.camera,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update light
|
||||||
|
// this.light.set(this.camera.position)
|
||||||
|
// this.program.uniforms.u_lightWorldPosition.value = [this.mesh.rotation.y * 1, 50, 150]
|
||||||
|
|
||||||
|
// Update markers
|
||||||
|
this.updateMarkers()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy
|
||||||
|
*/
|
||||||
|
destroy () {
|
||||||
|
console.log('destroy globe2')
|
||||||
|
|
||||||
|
this.gl = null
|
||||||
|
this.scene = null
|
||||||
|
this.camera = null
|
||||||
|
this.mesh = null
|
||||||
|
this.renderer = null
|
||||||
|
this.controls.remove()
|
||||||
|
|
||||||
|
if (this.pane) {
|
||||||
|
this.pane.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
|
type Options = {
|
||||||
|
el: HTMLElement
|
||||||
|
parent: HTMLElement
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
mapFile: string
|
||||||
|
dpr: number
|
||||||
|
autoRotate: boolean
|
||||||
|
speed: number
|
||||||
|
rotationStart?: number
|
||||||
|
markers?: any[]
|
||||||
|
pane?: boolean
|
||||||
|
}
|
||||||
|
export type Marker = {
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
country: {
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
flag: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lat: number
|
||||||
|
lng: number
|
||||||
|
position?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
HELPERS
|
||||||
|
========================================================================== */
|
||||||
|
/**
|
||||||
|
* Detect WebGL support
|
||||||
|
*/
|
||||||
|
function WebGLSupport () {
|
||||||
|
try {
|
||||||
|
var canvas = document.createElement('canvas')
|
||||||
|
return !!window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
|
||||||
|
} catch(e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert lat/lng to Vec3
|
||||||
|
*/
|
||||||
|
function lonlatVec3 (longitude: number, latitude: number) {
|
||||||
|
const lat = latitude * Math.PI / 180
|
||||||
|
const lng = -longitude * Math.PI / 180
|
||||||
|
return new Vec3(
|
||||||
|
Math.cos(lat) * Math.cos(lng),
|
||||||
|
Math.sin(lat),
|
||||||
|
Math.cos(lat) * Math.sin(lng)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Degrees to Radians
|
||||||
|
*/
|
||||||
|
const degToRad = (deg: number) => deg * Math.PI / 180
|
||||||
18
src/modules/globe2/pane.ts
Normal file
18
src/modules/globe2/pane.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Pane } from 'tweakpane'
|
||||||
|
|
||||||
|
export const createPane = (ctx: any) => {
|
||||||
|
ctx.pane = new Pane({
|
||||||
|
container: ctx.parent,
|
||||||
|
title: 'Settings',
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.pane.addInput(ctx.params, 'autoRotate', {
|
||||||
|
label: 'Auto-rotate',
|
||||||
|
})
|
||||||
|
ctx.pane.addInput(ctx.params, 'speed', {
|
||||||
|
label: 'Rotation speed',
|
||||||
|
min: 0.0005,
|
||||||
|
max: 0.025,
|
||||||
|
step: 0.00025,
|
||||||
|
})
|
||||||
|
}
|
||||||
32
src/modules/globe2/vertex.glsl
Normal file
32
src/modules/globe2/vertex.glsl
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
attribute vec2 uv;
|
||||||
|
attribute vec3 position;
|
||||||
|
attribute vec3 normal;
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 modelViewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
uniform mat3 normalMatrix;
|
||||||
|
uniform vec3 u_lightWorldPosition;
|
||||||
|
uniform vec3 cameraPosition;
|
||||||
|
|
||||||
|
varying vec3 v_normal;
|
||||||
|
varying vec3 v_surfaceToLight;
|
||||||
|
varying vec3 v_surfaceToView;
|
||||||
|
varying vec2 v_uv;
|
||||||
|
|
||||||
|
void main () {
|
||||||
|
// Pass UV information to Fragment Shader
|
||||||
|
v_uv = uv;
|
||||||
|
|
||||||
|
// Calculate World Space Normal
|
||||||
|
v_normal = normalMatrix * normal;
|
||||||
|
|
||||||
|
// Compute the world position of the surface
|
||||||
|
vec3 surfaceWorldPosition = mat3(modelMatrix) * position;
|
||||||
|
|
||||||
|
// Vector from the surface, to the light
|
||||||
|
v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
|
||||||
|
|
||||||
|
// Vector from the surface, to the camera
|
||||||
|
v_surfaceToView = cameraPosition - surfaceWorldPosition;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import PageTransition from '$components/PageTransition.svelte'
|
import PageTransition from '$components/PageTransition.svelte'
|
||||||
import BoxCTA from '$components/atoms/BoxCTA.svelte'
|
import BoxCTA from '$components/atoms/BoxCTA.svelte'
|
||||||
import Heading from '$components/molecules/Heading.svelte'
|
import Heading from '$components/molecules/Heading.svelte'
|
||||||
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
|
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||||||
import ListCTAs from '$components/organisms/ListCTAs.svelte'
|
import ListCTAs from '$components/organisms/ListCTAs.svelte'
|
||||||
import Locations from '$components/organisms/Locations.svelte'
|
import Locations from '$components/organisms/Locations.svelte'
|
||||||
import ShopModule from '$components/organisms/ShopModule.svelte'
|
import ShopModule from '$components/organisms/ShopModule.svelte'
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</ListCTAs>
|
</ListCTAs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InteractiveGlobe />
|
<InteractiveGlobe2 />
|
||||||
<Locations {locations} />
|
<Locations {locations} />
|
||||||
|
|
||||||
<div class="grid-modules">
|
<div class="grid-modules">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Metas from '$components/Metas.svelte'
|
import Metas from '$components/Metas.svelte'
|
||||||
import PageTransition from '$components/PageTransition.svelte'
|
import PageTransition from '$components/PageTransition.svelte'
|
||||||
import SiteTitle from '$components/atoms/SiteTitle.svelte'
|
import SiteTitle from '$components/atoms/SiteTitle.svelte'
|
||||||
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
|
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||||||
import Image from '$components/atoms/Image.svelte'
|
import Image from '$components/atoms/Image.svelte'
|
||||||
|
|
||||||
export let data: any
|
export let data: any
|
||||||
@@ -139,5 +139,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<InteractiveGlobe type="cropped" />
|
<InteractiveGlobe2 type="cropped" />
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte'
|
import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte'
|
||||||
import BoxCTA from '$components/atoms/BoxCTA.svelte'
|
import BoxCTA from '$components/atoms/BoxCTA.svelte'
|
||||||
import DiscoverText from '$components/atoms/DiscoverText.svelte'
|
import DiscoverText from '$components/atoms/DiscoverText.svelte'
|
||||||
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
|
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||||||
import Collage from '$components/organisms/Collage.svelte'
|
import Collage from '$components/organisms/Collage.svelte'
|
||||||
import Locations from '$components/organisms/Locations.svelte'
|
import Locations from '$components/organisms/Locations.svelte'
|
||||||
import ListCTAs from '$components/organisms/ListCTAs.svelte'
|
import ListCTAs from '$components/organisms/ListCTAs.svelte'
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="homepage__locations">
|
<section class="homepage__locations">
|
||||||
<InteractiveGlobe />
|
<InteractiveGlobe2 />
|
||||||
|
|
||||||
<ScrollingTitle tag="p" class="title-world mask">
|
<ScrollingTitle tag="p" class="title-world mask">
|
||||||
<SplitText text="World" mode="chars" />
|
<SplitText text="World" mode="chars" />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Components
|
// Components
|
||||||
import Metas from '$components/Metas.svelte'
|
import Metas from '$components/Metas.svelte'
|
||||||
import PageTransition from '$components/PageTransition.svelte'
|
import PageTransition from '$components/PageTransition.svelte'
|
||||||
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
|
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||||||
import Locations from '$components/organisms/Locations.svelte'
|
import Locations from '$components/organisms/Locations.svelte'
|
||||||
import ShopModule from '$components/organisms/ShopModule.svelte'
|
import ShopModule from '$components/organisms/ShopModule.svelte'
|
||||||
import NewsletterModule from '$components/organisms/NewsletterModule.svelte'
|
import NewsletterModule from '$components/organisms/NewsletterModule.svelte'
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section class="explore__locations">
|
<section class="explore__locations">
|
||||||
<InteractiveGlobe />
|
<InteractiveGlobe2 />
|
||||||
<Locations {locations} />
|
<Locations {locations} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import Image from '$components/atoms/Image.svelte'
|
import Image from '$components/atoms/Image.svelte'
|
||||||
import Heading from '$components/molecules/Heading.svelte'
|
import Heading from '$components/molecules/Heading.svelte'
|
||||||
import EmailForm from '$components/molecules/EmailForm.svelte'
|
import EmailForm from '$components/molecules/EmailForm.svelte'
|
||||||
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
|
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||||||
|
|
||||||
export let data: any
|
export let data: any
|
||||||
export let issues: any[]
|
export let issues: any[]
|
||||||
@@ -96,5 +96,5 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<InteractiveGlobe type="cropped" />
|
<InteractiveGlobe2 type="cropped" />
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
@include bp (sm) {
|
@include bp (sm) {
|
||||||
font-size: clamp(#{rem(40px)}, 7vw, #{rem(88px)});
|
font-size: clamp(#{rem(40px)}, 7vw, #{rem(88px)});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// House Number
|
// House Number
|
||||||
@@ -157,4 +156,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
font-size: rem(10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
257
src/style/modules/_globe2.scss
Normal file
257
src/style/modules/_globe2.scss
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
// Globe
|
||||||
|
.globe {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
|
||||||
|
// Inner
|
||||||
|
&__inner {
|
||||||
|
position: relative;
|
||||||
|
width: clamp(700px, 100vw, 1315px);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Canvas
|
||||||
|
&__canvas {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate3d(-50%, 0, 0);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:global(canvas) {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markers
|
||||||
|
&__markers {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker
|
||||||
|
&__marker {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
user-select: none;
|
||||||
|
transform: translate3d(var(--x), var(--y), 0);
|
||||||
|
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
color: $color-secondary;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
dl > * {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
color: $color-gray;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: -16px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: $color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** States
|
||||||
|
*/
|
||||||
|
// Has name
|
||||||
|
&.is-dot-only {
|
||||||
|
dt, dd {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has country
|
||||||
|
&.has-country {
|
||||||
|
dd {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster
|
||||||
|
&__cluster {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 300px;
|
||||||
|
left: 300px;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: rgba($color-secondary, 0.2);
|
||||||
|
transition: box-shadow 0.5s var(--ease-quart), background 0.5s var(--ease-quart);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
button {
|
||||||
|
background: rgba($color-secondary, 0.3);
|
||||||
|
box-shadow: 0 0 0 8px rgba($color-secondary, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popin
|
||||||
|
&__popin {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 12vw;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate3d(-50%, 0, 0);
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 546px;
|
||||||
|
padding: 24px 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
--shadow-color: #{rgba(45, 4, 88, 0.05)};
|
||||||
|
box-shadow:
|
||||||
|
0 6px 6px var(--shadow-color),
|
||||||
|
0 12px 12px var(--shadow-color),
|
||||||
|
0 24px 24px var(--shadow-color),
|
||||||
|
0 40px 40px var(--shadow-color);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px 16px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.3s var(--ease-quart);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($color-secondary, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag
|
||||||
|
:global(.flag) {
|
||||||
|
display: block;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 100%;
|
||||||
|
transform: translateZ(0);
|
||||||
|
|
||||||
|
:global(img) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
dl {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
color: $color-gray;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close buttom
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $color-primary-darker;
|
||||||
|
background: rgba($color-secondary, 0.15);
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: background 0.3s var(--ease-quart);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($color-secondary, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
** States and Variants
|
||||||
|
*/
|
||||||
|
// When dragging
|
||||||
|
:global(.is-grabbing) {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tweakpane
|
||||||
|
:global(.tp-rotv) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user