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).