---
interface Props {
locations: [number, number][];
}
const { locations } = Astro.props;
---
<canvas
class="globe-graphic"
width="240"
height="240"
aria-hidden="true"
data-locations={JSON.stringify(locations)}></canvas>
<style>
.globe-graphic {
position: absolute;
right: 1%;
width: clamp(80px, 35%, 120px);
aspect-ratio: 1;
transform: translateY(-80%);
pointer-events: none;
z-index: 0;
}
</style>
<script>
import createGlobe from "cobe";
import type { Globe } from "cobe";
import { createAnimatable } from "animejs";
let globeInstance: Globe | null = null;
let globePhi = 0;
const globeState = { speed: 0.0015 };
const animatable = createAnimatable(globeState, {
speed: { duration: 600, ease: "outQuad" },
});
let globeSpinning = false;
let globeSwapping = false;
let allLocations: [number, number][] = [];
let globeRafId: number | null = null;
let globeVisible = true;
function createGlobeInstance(
canvas: HTMLCanvasElement,
locations: [number, number][],
markerColor: [number, number, number],
markerSize = 0.06,
): void {
if (globeInstance) {
globeInstance.destroy();
globeInstance = null;
}
globeInstance = createGlobe(canvas, {
devicePixelRatio: 1,
width: 240,
height: 240,
phi: globePhi,
theta: 0.15,
dark: 1,
diffuse: 1.2,
mapSamples: 8000,
mapBrightness: 4,
mapBaseBrightness: 0.02,
baseColor: [0.12, 0.16, 0.26],
markerColor,
glowColor: [0.08, 0.12, 0.2],
scale: 1,
offset: [0, 0],
markers: locations.map((loc) => ({ location: loc, size: markerSize })),
});
}
function initGlobe(): void {
if (globeSpinning) return;
globeSpinning = true;
const canvas = document.querySelector<HTMLCanvasElement>(".globe-graphic");
if (!canvas) return;
allLocations = JSON.parse(canvas.dataset.locations || "[]");
createGlobeInstance(canvas, allLocations, [0.29, 0.87, 0.5]);
function spin() {
if (!globeVisible) {
globeRafId = null;
return;
}
globePhi += globeState.speed;
globeInstance?.update({ phi: globePhi });
globeRafId = requestAnimationFrame(spin);
}
spin();
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
if (!globeVisible) {
globeVisible = true;
if (globeRafId === null) {
spin();
}
}
} else {
globeVisible = false;
}
}
},
{ rootMargin: "50px", threshold: 0 },
);
observer.observe(canvas);
}
document.addEventListener("globe:activate", () => {
initGlobe();
});
document.addEventListener("globe:hover-start", () => {
animatable.speed(0.003);
});
document.addEventListener("globe:hover-end", () => {
animatable.speed(0.0015);
});
document.addEventListener("globe:click", () => {
if (globeSwapping) return;
globeSwapping = true;
const canvas = document.querySelector<HTMLCanvasElement>(".globe-graphic");
if (!canvas) {
globeSwapping = false;
return;
}
createGlobeInstance(canvas, allLocations, [0.95, 0.27, 0.27]);
setTimeout(() => {
createGlobeInstance(canvas, allLocations, [0.29, 0.87, 0.5]);
globeSwapping = false;
}, 1500);
});
</script>