[WIP] Started new Globe from scratch using OGL

This commit is contained in:
2022-07-16 18:55:47 +02:00
parent 025bb522a9
commit 4f274e52ce
14 changed files with 872 additions and 14 deletions

View File

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

@@ -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'}

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

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

View 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,
})
}

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

View File

@@ -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">

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
} }

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