modelsdev 0.11.4

A fast TUI and CLI for browsing AI models, benchmarks, and coding agents
---
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;
    /*top: -75%;*/
    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);
  }

  // Activation
  document.addEventListener("globe:activate", () => {
    initGlobe();
  });

  // Hover — smoothly speed up / slow down rotation
  document.addEventListener("globe:hover-start", () => {
    animatable.speed(0.003);
  });
  document.addEventListener("globe:hover-end", () => {
    animatable.speed(0.0015);
  });

  // Click — outage flash (all markers red, then recover to green)
  document.addEventListener("globe:click", () => {
    if (globeSwapping) return;
    globeSwapping = true;
    const canvas = document.querySelector<HTMLCanvasElement>(".globe-graphic");
    if (!canvas) {
      globeSwapping = false;
      return;
    }

    // Phase 1: all markers flash red
    createGlobeInstance(canvas, allLocations, [0.95, 0.27, 0.27]);

    // Phase 2: recover to green after 1.5s
    setTimeout(() => {
      createGlobeInstance(canvas, allLocations, [0.29, 0.87, 0.5]);
      globeSwapping = false;
    }, 1500);
  });
</script>