Custom Code Experiment •
ThreeJS: Torus Knot 3
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ThreeJS: Torus Knot Remix • Cyberpunk Duo (No Stars)</title>
<style>
body { margin: 0; overflow: hidden; background-color: #0a0a14; }
.info {
position: fixed;
bottom: 20px;
right: 20px;
color: #aaccff;
font-family: 'Segoe UI', system-ui, monospace;
font-size: 15px;
letter-spacing: 2px;
pointer-events: none;
z-index: 20;
backdrop-filter: blur(8px);
padding: 8px 20px;
border-radius: 40px;
background: rgba(10, 20, 30, 0.5);
border: 1px solid rgba(100, 180, 255, 0.3);
box-shadow: 0 0 20px rgba(0, 160, 255, 0.2);
text-shadow: 0 0 8px rgba(0,200,255,0.5);
}
.info span {
color: #ff66cc;
font-weight: bold;
margin: 0 4px;
}
</style>
<!-- Import map using esm.sh (same as original) -->
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.128.0",
"three/addons/": "https://esm.sh/three@0.128.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
// --- Setup core: scene, camera, renderer (with subtle tone mapping) ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a14); // deep cosmic
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 2, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);
// --- Environment map (Swedish Royal Castle) provides all reflections, no lights needed ---
const loader = new THREE.CubeTextureLoader();
const envTexture = loader.load([
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/px.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nx.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/py.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/ny.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/pz.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nz.jpg'
]);
scene.environment = envTexture; // automatically used by all physical materials
// --- UTILITY: create a knot group (mesh, wireframe, points) with custom style ---
function createKnotGroup(geometryParams, style) {
const {
p = 2, q = 3,
radius = 1.2, tube = 0.35,
tubularSegments = 256, radialSegments = 24,
materialColor, materialOpacity = 0.9, metalness = 0.8, roughness = 0.15, envIntensity = 2.5,
wireframeColor,
pointsColor, pointsSize = 0.09, pointsAdditive = true
} = style;
// Base geometry
const geo = new THREE.TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q);
// Main reflective shape (physical material, takes env map from scene)
const mat = new THREE.MeshPhysicalMaterial({
color: materialColor,
metalness: metalness,
roughness: roughness,
clearcoat: 1.0,
clearcoatRoughness: 0.1,
emissive: 0x000000,
envMapIntensity: envIntensity,
transparent: materialOpacity < 1.0,
opacity: materialOpacity,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geo, mat);
// Wireframe edges
const edges = new THREE.EdgesGeometry(geo);
const lineMat = new THREE.LineBasicMaterial({
color: wireframeColor,
depthTest: false, // always on top
linewidth: 1 // (not supported everywhere, but adds intent)
});
const wireframe = new THREE.LineSegments(edges, lineMat);
// Vertex nodes (points) with optional additive blending
const pointsGeo = new THREE.BufferGeometry();
pointsGeo.setAttribute('position', geo.attributes.position.clone()); // use same positions
const pointsMat = new THREE.PointsMaterial({
color: pointsColor,
size: pointsSize,
sizeAttenuation: true,
depthTest: true,
blending: pointsAdditive ? THREE.AdditiveBlending : THREE.NormalBlending
});
const points = new THREE.Points(pointsGeo, pointsMat);
const group = new THREE.Group();
group.add(mesh);
group.add(wireframe);
group.add(points);
return group;
}
// --- STYLE 1: Primary knot (cyan/blue tones) ---
const style1 = {
p: 2, q: 3,
radius: 1.2,
tube: 0.35,
tubularSegments: 280,
radialSegments: 32,
materialColor: 0x33aaff,
materialOpacity: 0.7,
metalness: 0.9,
roughness: 0.1,
envIntensity: 3.0,
wireframeColor: 0x66ffff, // cyan
pointsColor: 0x88ddff,
pointsSize: 0.1,
pointsAdditive: true
};
const knot1 = createKnotGroup({}, style1);
knot1.scale.set(1.2, 1.2, 1.2); // make it slightly larger
// --- STYLE 2: Secondary knot (magenta/pink tones) with different knot parameters ---
const style2 = {
p: 3, q: 7, // different knot structure
radius: 1.0,
tube: 0.3,
tubularSegments: 240,
radialSegments: 24,
materialColor: 0xff44aa,
materialOpacity: 0.6,
metalness: 0.7,
roughness: 0.2,
envIntensity: 2.2,
wireframeColor: 0xff88ff, // magenta
pointsColor: 0xffaa66,
pointsSize: 0.12,
pointsAdditive: true
};
const knot2 = createKnotGroup({}, style2);
knot2.scale.set(0.9, 0.9, 0.9); // a bit smaller
// Add both knots to scene
scene.add(knot1);
scene.add(knot2);
// --- small extra: floating dust particles (tiny, subtle) near the center ---
const dustGeo = new THREE.BufferGeometry();
const dustCount = 400;
const dustPos = new Float32Array(dustCount * 3);
for (let i = 0; i < dustCount * 3; i += 3) {
// random within a sphere radius 3
const r = Math.random() * 3.5;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
dustPos[i] = Math.sin(phi) * Math.cos(theta) * r;
dustPos[i+1] = Math.sin(phi) * Math.sin(theta) * r;
dustPos[i+2] = Math.cos(phi) * r;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dustMat = new THREE.PointsMaterial({
color: 0x99aaff,
size: 0.04,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
transparent: true,
opacity: 0.4
});
const dustParticles = new THREE.Points(dustGeo, dustMat);
scene.add(dustParticles);
// --- Animation variables for color cycling (subtle) ---
let clock = new THREE.Clock();
// --- Animation loop ---
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsedTime = performance.now() / 1000; // seconds
// Rotate knots independently to create interweaving effect
knot1.rotation.x = elapsedTime * 0.25;
knot1.rotation.y = elapsedTime * 0.5;
knot1.rotation.z = elapsedTime * 0.1;
knot2.rotation.x = elapsedTime * 0.4 + 1.2;
knot2.rotation.y = elapsedTime * 0.3 + 0.5;
knot2.rotation.z = elapsedTime * 0.2;
// Subtle color shifts on wireframes and points (optional, just for life)
// Access first child material (wireframe) and second points material (index 1 and 2)
// knot1 children: [0]=mesh, [1]=wireframe, [2]=points
if (knot1.children[1] && knot1.children[1].material) {
// slight hue shift over minutes, barely noticeable but adds organic feel
const hue = (elapsedTime * 0.02) % 1.0;
knot1.children[1].material.color.setHSL(hue, 0.9, 0.7); // wireframe
}
if (knot1.children[2] && knot1.children[2].material) {
const hue = (elapsedTime * 0.01 + 0.5) % 1.0;
knot1.children[2].material.color.setHSL(hue, 1.0, 0.7); // points
}
if (knot2.children[1] && knot2.children[1].material) {
const hue = (elapsedTime * 0.03 + 0.8) % 1.0;
knot2.children[1].material.color.setHSL(hue, 1.0, 0.6);
}
if (knot2.children[2] && knot2.children[2].material) {
const hue = (elapsedTime * 0.015 + 0.2) % 1.0;
knot2.children[2].material.color.setHSL(hue, 0.9, 0.7);
}
// Dust particles slowly follow
dustParticles.rotation.y += 0.0005;
dustParticles.rotation.x += 0.0003;
// Dynamic camera orbit (more playful than original)
const radius = 8.0;
const speed = 0.2;
const angle = elapsedTime * speed;
const height = Math.sin(elapsedTime * 0.4) * 1.8;
camera.position.x = radius * Math.sin(angle);
camera.position.z = radius * Math.cos(angle);
camera.position.y = height + 1.0; // offset
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
}
animate();
// --- Handle window resize ---
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Small console reminder that env map is active
console.log('✨ Remix: Dual knots with cyberpunk glow & subtle dust. No lights, all env map. (Stars removed)');
</script>
</body>
</html>