7

Torus Knot Lattice (3D)

drag to orbit

A 3D lattice of twenty torus knots, each with its own parameters and its own axis of rotation, drifting against a sparse starfield. A ** torus knot** is the curve that wraps times around the central axis of a torus and times around its tube; when the result is a single closed strand that can't be untangled into a circle without cutting it. Vary and you get visually distinct knots — is the classic trefoil, is its mirror, looks like a tightly wound braid. Every cell here uses a different coprime pair, shaded with `MeshNormalMaterial` so surface orientation maps directly to RGB. Drag to orbit the camera; let go and the whole field auto-rotates.

idle
129 lines · three
view source
// Three.js — a 3D lattice of torus knots with MeshNormalMaterial rainbow shading.
//
// Contract: main-thread iframe, window.THREE available. init() builds the
// scene; tick() spins everything. The runtime auto-renders after tick, but
// we render explicitly anyway to be safe.

const GRID_X = 5;
const GRID_Y = 4;
const GRID_Z = 1; // 5×4×1 = 20 knots — sweet spot for visual density
const SPACING = 7;
const KNOT_RADIUS = 1.3;
const TUBE_RADIUS = 0.42;

let knots = [];          // { mesh, spin: THREE.Vector3 }
let latticeGroup;        // parent group for the whole field
let starsPoints;
let userYaw = 0.6, userPitch = 0.25;
let isDragging = false;
let lastMouseX = 0, lastMouseY = 0;
let idleTime = 0;        // seconds since user last dragged

function init({ scene, camera, renderer, width, height }) {
  renderer.setClearColor(0x05081a, 1); // very dark navy
  camera.position.set(0, 0, 38);
  camera.lookAt(0, 0, 0);

  latticeGroup = new THREE.Group();
  scene.add(latticeGroup);

  // Sweep (p, q) across the grid so every cell is a visually distinct knot.
  // Skip (p,q) where gcd != 1 (those degenerate to torus links, less interesting)
  // by picking from a curated palette.
  const PALETTE = [
    [2, 3],  [3, 2],  [2, 5],  [5, 2],  [3, 4],
    [4, 3],  [3, 5],  [5, 3],  [2, 7],  [7, 2],
    [3, 7],  [7, 3],  [4, 5],  [5, 4],  [4, 7],
    [7, 4],  [5, 7],  [7, 5],  [5, 6],  [6, 5],
    [3, 8],  [8, 3],  [2, 9],  [9, 2],
  ];

  const halfX = (GRID_X - 1) / 2;
  const halfY = (GRID_Y - 1) / 2;
  const halfZ = (GRID_Z - 1) / 2;

  let i = 0;
  for (let gx = 0; gx < GRID_X; gx++) {
    for (let gy = 0; gy < GRID_Y; gy++) {
      for (let gz = 0; gz < GRID_Z; gz++) {
        const [p, q] = PALETTE[i % PALETTE.length];
        i++;
        const geo = new THREE.TorusKnotGeometry(
          KNOT_RADIUS,
          TUBE_RADIUS,
          96,   // tubularSegments — smooth enough at this scale
          12,   // radialSegments
          p,
          q,
        );
        const mat = new THREE.MeshNormalMaterial({ flatShading: false });
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.set(
          (gx - halfX) * SPACING,
          (gy - halfY) * SPACING,
          (gz - halfZ) * SPACING,
        );
        latticeGroup.add(mesh);
        // Each knot spins on its own axis at its own rate.
        const spin = new THREE.Vector3(
          (Math.random() - 0.5) * 1.2,
          (Math.random() - 0.5) * 1.2,
          (Math.random() - 0.5) * 1.2,
        );
        // Normalize-ish but keep magnitude variation so speeds differ.
        const speedScale = 0.4 + Math.random() * 0.9;
        spin.normalize().multiplyScalar(speedScale);
        knots.push({ mesh, spin });
      }
    }
  }

  // Cheap starfield: 600 points uniformly inside a big sphere shell.
  const STAR_COUNT = 600;
  const starPositions = new Float32Array(STAR_COUNT * 3);
  for (let s = 0; s < STAR_COUNT; s++) {
    // sample on a sphere shell, then jitter the radius
    const u = Math.random() * 2 - 1;
    const theta = Math.random() * Math.PI * 2;
    const r = 80 + Math.random() * 40;
    const sq = Math.sqrt(1 - u * u);
    starPositions[s * 3]     = r * sq * Math.cos(theta);
    starPositions[s * 3 + 1] = r * sq * Math.sin(theta);
    starPositions[s * 3 + 2] = r * u;
  }
  const starGeo = new THREE.BufferGeometry();
  starGeo.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
  const starMat = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 0.35,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0.85,
  });
  starsPoints = new THREE.Points(starGeo, starMat);
  scene.add(starsPoints);

  return { scene, camera };
}

function tick({ dt, scene, camera, renderer, width, height, input }) {
  // Drain click buffer so it doesn't grow unbounded; we don't use clicks here.
  input.consumeClicks();

  // Mouse-drag camera orbit (and auto-rotate when idle for >0.4s).
  if (input.mouseDown) {
    idleTime = 0;
    if (!isDragging) {
      isDragging = true;
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    } else {
      const dx = input.mouseX - lastMouseX;
      const dy = input.mouseY - lastMouseY;
      userYaw   -= dx * 0.006;
      userPitch -= dy * 0.006;
      userPitch = Math.max(-1.3, Math.min(1.3, userPitch));
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    }
  } else {
    isDragging = false;
    idleTime += dt;
    if (idleTime > 0.4) {
      userYaw += dt * 0.12;
    }
  }

  // Orbital camera.
  const r = 38;
  const cy = Math.cos(userYaw), sy = Math.sin(userYaw);
  const cp = Math.cos(userPitch), sp = Math.sin(userPitch);
  camera.position.set(r * cp * sy, r * sp, r * cp * cy);
  camera.lookAt(0, 0, 0);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height, false);

  // Each knot spins on its own axis.
  for (let i = 0; i < knots.length; i++) {
    const k = knots[i];
    k.mesh.rotation.x += k.spin.x * dt;
    k.mesh.rotation.y += k.spin.y * dt;
    k.mesh.rotation.z += k.spin.z * dt;
  }

  // The whole lattice slowly tumbles — slow enough to feel meditative.
  latticeGroup.rotation.y += dt * 0.08;
  latticeGroup.rotation.x += dt * 0.03;

  // Slow star drift opposite the lattice spin for parallax-y vibes.
  starsPoints.rotation.y -= dt * 0.01;

  renderer.render(scene, camera);
}

Comments (0)

Log in to comment.