7
Torus Knot Lattice (3D)
drag to orbit
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.