Baktiyar Ablaikhan

Line sphere

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

A wireframe globe turns on its own; drag to spin it any way you like.

Code

"use client";

import { useEffect, useRef } from "react";
import { fitCanvas, prefersReducedMotion, Surface } from "../ui";

export default function LineSphere() {
  const canvas = useRef<HTMLCanvasElement>(null);
  const input = useRef({ drag: false, lx: 0, ly: 0, vy: 0.006, vx: 0, py: 0, pp: 0, tiltY: 0, hover: false });

  useEffect(() => {
    const cv = canvas.current;
    if (!cv) return;
    const still = prefersReducedMotion();
    let ctx: CanvasRenderingContext2D | null = null;
    let W = 0;
    let H = 0;
    let raf = 0;
    let yaw = 0.4;
    let pitch = -0.35;
    const size = () => {
      const r = cv.getBoundingClientRect();
      W = r.width;
      H = r.height;
      ctx = fitCanvas(cv, W, H);
    };
    size();
    const ro = new ResizeObserver(size);
    ro.observe(cv);

    const LAT = 13;
    const LON = 18;
    const SEG = 64;

    const loop = () => {
      const s = input.current;
      if (!s.drag) {
        s.vy += ((s.hover ? 0.002 : 0.006) - s.vy) * 0.03;
        s.vx *= 0.95;
      }
      if (s.drag) {
        yaw += s.py;
        pitch = Math.max(-1.2, Math.min(1.2, pitch + s.pp));
        s.py = 0;
        s.pp = 0;
      } else {
        yaw += s.vy;
        pitch = Math.max(-1.2, Math.min(1.2, pitch + s.vx + (s.tiltY * 0.4 - 0.35 - pitch) * 0.02));
      }
      if (ctx) {
        const R = Math.min(W, H) * 0.36;
        const cx = W / 2;
        const cy = H / 2;
        ctx.clearRect(0, 0, W, H);
        const cyw = Math.cos(yaw);
        const syw = Math.sin(yaw);
        const cp = Math.cos(pitch);
        const sp = Math.sin(pitch);
        const project = (x: number, y: number, z: number) => {
          // yaw around y, then pitch around x
          const x1 = x * cyw + z * syw;
          const z1 = -x * syw + z * cyw;
          const y2 = y * cp - z1 * sp;
          const z2 = y * sp + z1 * cp;
          return { x: cx + x1 * R, y: cy + y2 * R, z: z2 };
        };
        const stroke = (pts: { x: number; y: number; z: number }[], accent: boolean) => {
          for (let i = 1; i < pts.length; i++) {
            const a = pts[i - 1];
            const b = pts[i];
            const front = (a.z + b.z) / 2;
            ctx!.strokeStyle = accent ? `rgba(43,70,184,${front > 0 ? 0.95 : 0.18})` : `rgba(21,21,21,${front > 0 ? 0.72 : 0.12})`;
            ctx!.lineWidth = front > 0 ? 1.2 : 0.8;
            ctx!.beginPath();
            ctx!.moveTo(a.x, a.y);
            ctx!.lineTo(b.x, b.y);
            ctx!.stroke();
          }
        };
        for (let i = 1; i < LAT; i++) {
          const phi = (i / LAT) * Math.PI - Math.PI / 2;
          const pts = [];
          for (let j = 0; j <= SEG; j++) {
            const th = (j / SEG) * Math.PI * 2;
            pts.push(project(Math.cos(phi) * Math.cos(th), Math.sin(phi), Math.cos(phi) * Math.sin(th)));
          }
          stroke(pts, i === Math.floor(LAT / 2));
        }
        for (let i = 0; i < LON; i++) {
          const th = (i / LON) * Math.PI;
          const pts = [];
          for (let j = 0; j <= SEG; j++) {
            const phi = (j / SEG) * Math.PI * 2;
            pts.push(project(Math.cos(phi) * Math.cos(th), Math.sin(phi), Math.cos(phi) * Math.sin(th)));
          }
          stroke(pts, false);
        }
      }
      if (!still) raf = requestAnimationFrame(loop);
    };
    loop();
    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
    };
  }, []);

  return (
    <Surface>
      <canvas
        ref={canvas}
        className="absolute inset-0 h-full w-full cursor-grab touch-none active:cursor-grabbing"
        onPointerEnter={() => (input.current.hover = true)}
        onPointerLeave={() => Object.assign(input.current, { hover: false, drag: false, tiltY: 0 })}
        onPointerDown={(e) => {
          e.currentTarget.setPointerCapture(e.pointerId);
          Object.assign(input.current, { drag: true, lx: e.clientX, ly: e.clientY });
        }}
        onPointerMove={(e) => {
          const s = input.current;
          const r = e.currentTarget.getBoundingClientRect();
          s.tiltY = ((e.clientY - r.top) / r.height - 0.5) * -2;
          if (!s.drag) return;
          s.vy = (e.clientX - s.lx) * 0.01;
          s.vx = -(e.clientY - s.ly) * 0.01;
          s.py += s.vy;
          s.pp += s.vx;
          s.lx = e.clientX;
          s.ly = e.clientY;
        }}
        onPointerUp={() => (input.current.drag = false)}
        onPointerCancel={() => (input.current.drag = false)}
      />
    </Surface>
  );
}
Prompt

Build "Line sphere", a 3d animation. What it does: A wireframe globe turns on its own; drag to spin it any way you like. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. Technique: an HTML canvas, a requestAnimationFrame loop, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).