Custom Code Experiment •
Ophanim
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, user-scalable=no">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#000;cursor:crosshair}
canvas{display:block}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
scene.fog = new THREE.FogExp2(0x000000, 0.003);
const camera = new THREE.PerspectiveCamera(72, innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const sun = new THREE.DirectionalLight(0xffffff, 0.9);
sun.position.set(1,2,1.5); scene.add(sun);
const backLight = new THREE.PointLight(0xffaa66, 0.7);
backLight.position.set(-30,20,-40); scene.add(backLight);
const fillLight = new THREE.PointLight(0x66aaff, 0.4);
fillLight.position.set(0,-60,0); scene.add(fillLight);
const movingLight = new THREE.PointLight(0xff8855, 0.6);
movingLight.position.set(40,30,50); scene.add(movingLight);
const pulseLight = new THREE.PointLight(0xcc44ff, 0.0);
pulseLight.position.set(0,0,0); scene.add(pulseLight);
const ATTRACT_DIST = 28, REPULSE_DIST = 14;
const ATTRACT_STR = 0.00012, REPULSE_STR = 0.018;
const DAMPING = 0.88, ANCHOR_STR = 0.012;
const netGroup = new THREE.Group();
scene.add(netGroup);
const LAYERS = 10, NODES = 15, R = 50;
const nodes = [], lineObjects = [], layersNodes = [];
for (let i = 0; i < LAYERS; i++) {
const layerArr = [];
const theta = (Math.PI*2*i)/LAYERS;
for (let j = 0; j < NODES; j++) {
const phi = (Math.PI*2*j)/NODES;
const ox = R*Math.sin(theta)*Math.cos(phi);
const oy = R*Math.sin(theta)*Math.sin(phi);
const oz = R*Math.cos(theta);
const baseRadius = 0.45 + Math.random()*1.8;
const mat = new THREE.MeshPhongMaterial({color:0xFF9C66, shininess:90, emissive:new THREE.Color(0x221100)});
const mesh = new THREE.Mesh(new THREE.SphereGeometry(baseRadius,18,12), mat);
mesh.position.set(ox,oy,oz);
netGroup.add(mesh);
if (baseRadius > 1.5) {
for (let k = 0; k < 3; k++) {
const ang = (Math.PI*2*k)/3;
const sMat = new THREE.MeshPhongMaterial({color:0xcc44ff,shininess:120,emissive:new THREE.Color(0x220033),transparent:true,opacity:0.85});
const sMesh = new THREE.Mesh(new THREE.SphereGeometry(baseRadius*0.28,8,6), sMat);
const sx = ox+Math.cos(ang)*baseRadius*2.2;
const sy = oy+Math.sin(ang)*baseRadius*2.2;
sMesh.position.set(sx,sy,oz);
netGroup.add(sMesh);
const sg = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(ox,oy,oz),new THREE.Vector3(sx,sy,oz)]);
netGroup.add(new THREE.Line(sg, new THREE.LineBasicMaterial({color:0xcc44ff,transparent:true,opacity:0.22})));
if (k>0) {
const pa = (Math.PI*2*(k-1))/3;
const px2=ox+Math.cos(pa)*baseRadius*2.2, py2=oy+Math.sin(pa)*baseRadius*2.2;
const sg2=new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(sx,sy,oz),new THREE.Vector3(px2,py2,oz)]);
netGroup.add(new THREE.Line(sg2, new THREE.LineBasicMaterial({color:0x8833ff,transparent:true,opacity:0.15})));
}
}
}
const nd = {
mesh, mat,
origin: new THREE.Vector3(ox,oy,oz),
vel: new THREE.Vector3((Math.random()-0.5)*0.1,(Math.random()-0.5)*0.1,(Math.random()-0.5)*0.1),
phase: Math.random()*Math.PI*2,
baseRadius,
dragged: false,
dragTarget: null
};
nodes.push(nd);
layerArr.push(nd);
}
layersNodes.push(layerArr);
}
for (let li = 1; li < LAYERS; li++) {
const prev = layersNodes[li-1], curr = layersNodes[li];
for (const cn of curr) {
for (const pn of prev) {
const hot = Math.random() < 0.07;
const pts = [cn.mesh.position.clone(), pn.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const mat = hot
? new THREE.LineBasicMaterial({color:0xff6633,transparent:true,opacity:0.0})
: new THREE.LineBasicMaterial({color:0xffffff,transparent:true,opacity:0.14});
const line = new THREE.Line(geo, mat);
netGroup.add(line);
lineObjects.push({line, a:cn, b:pn, hot});
}
}
}
// Elastic drag threads — visual rubber bands from dragged node to its origin
const elasticLines = [];
for (let i = 0; i < nodes.length; i++) {
const geo = new THREE.BufferGeometry().setFromPoints([nodes[i].mesh.position.clone(), nodes[i].origin.clone()]);
const mat = new THREE.LineBasicMaterial({color:0x00ffcc, transparent:true, opacity:0.0, linewidth:1});
const line = new THREE.Line(geo, mat);
netGroup.add(line);
elasticLines.push(line);
}
const starPos = new Float32Array(1800*3);
for (let i=0;i<1800;i++){
starPos[i*3]=(Math.random()-0.5)*700;
starPos[i*3+1]=(Math.random()-0.5)*500;
starPos[i*3+2]=(Math.random()-0.5)*400-100;
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position',new THREE.BufferAttribute(starPos,3));
const stars = new THREE.Points(starGeo,new THREE.PointsMaterial({color:0xaaccff,size:0.26,transparent:true,opacity:0.45}));
scene.add(stars);
const auraSphere = new THREE.Mesh(
new THREE.SphereGeometry(72,32,16),
new THREE.MeshBasicMaterial({color:0x336699,wireframe:true,transparent:true,opacity:0.055})
);
scene.add(auraSphere);
function buildMobius(majorR,minorR,twists,color,opacity){
const segs=200,tube=14,verts=[],idx=[];
for(let i=0;i<=segs;i++){
const u=(i/segs)*Math.PI*2, twist=(twists*u)/2;
for(let j=0;j<=tube;j++){
const v=(j/tube)*Math.PI*2;
const cv=Math.cos(v+twist),sv=Math.sin(v+twist),cu=Math.cos(u),su=Math.sin(u);
verts.push((majorR+minorR*cv)*cu,(majorR+minorR*cv)*su,minorR*sv);
}
}
for(let i=0;i<segs;i++) for(let j=0;j<tube;j++){
const a=i*(tube+1)+j,b=a+tube+1;
idx.push(a,b,a+1,b,b+1,a+1);
}
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.BufferAttribute(new Float32Array(verts),3));
geo.setIndex(idx); geo.computeVertexNormals();
return new THREE.Mesh(geo,new THREE.MeshPhongMaterial({
color,transparent:true,opacity,side:THREE.DoubleSide,
shininess:80,emissive:new THREE.Color(color),emissiveIntensity:0.1
}));
}
const escherGroup = new THREE.Group();
scene.add(escherGroup);
const r1 = buildMobius(80,2.0,1,0x8833ff,0.22);
escherGroup.add(r1);
const r2wrap = new THREE.Group(); r2wrap.rotation.x=Math.PI/2;
r2wrap.add(buildMobius(80,1.4,3,0xff3388,0.18)); escherGroup.add(r2wrap);
const r3wrap = new THREE.Group(); r3wrap.rotation.z=Math.PI/3;
r3wrap.add(buildMobius(80,1.0,5,0x33aaff,0.14)); escherGroup.add(r3wrap);
const innerRing = buildMobius(30,1.1,1,0xcc66ff,0.30); escherGroup.add(innerRing);
const innerInner = buildMobius(11,0.55,1,0xff99cc,0.34); escherGroup.add(innerInner);
// Mouse / drag system
const raycaster = new THREE.Raycaster();
raycaster.params.Points = {threshold:2};
const mouse = new THREE.Vector2();
let dragNode = null;
let dragPlane = new THREE.Plane();
let dragOffset = new THREE.Vector3();
let camPaused = false;
let camAngle = 0;
let camAnglePaused = 0;
const meshToNode = new Map();
for (const nd of nodes) meshToNode.set(nd.mesh, nd);
function getNDC(clientX, clientY) {
return new THREE.Vector2(
(clientX/innerWidth)*2-1,
-(clientY/innerHeight)*2+1
);
}
function pickNode(ndc) {
raycaster.setFromCamera(ndc, camera);
const meshes = nodes.map(n=>n.mesh);
const hits = raycaster.intersectObjects(meshes);
if (hits.length > 0) return meshToNode.get(hits[0].object);
return null;
}
renderer.domElement.addEventListener('mousedown', e => {
mouse.copy(getNDC(e.clientX, e.clientY));
const nd = pickNode(mouse);
if (nd) {
dragNode = nd;
nd.dragged = true;
nd.vel.set(0,0,0);
// drag plane faces camera
const camDir = camera.position.clone().normalize();
dragPlane.setFromNormalAndCoplanarPoint(camDir, nd.mesh.position);
camPaused = true;
camAnglePaused = camAngle;
document.body.style.cursor = 'grabbing';
}
});
renderer.domElement.addEventListener('mousemove', e => {
if (!dragNode) return;
mouse.copy(getNDC(e.clientX, e.clientY));
raycaster.setFromCamera(mouse, camera);
const target = new THREE.Vector3();
raycaster.ray.intersectPlane(dragPlane, target);
if (target) {
dragNode.dragTarget = target.clone();
}
});
renderer.domElement.addEventListener('mouseup', () => {
if (dragNode) {
// release: give node a flick velocity from stretch
const stretch = new THREE.Vector3().subVectors(dragNode.mesh.position, dragNode.origin);
dragNode.vel.copy(stretch.multiplyScalar(-0.08));
dragNode.dragged = false;
dragNode.dragTarget = null;
dragNode = null;
camPaused = false;
document.body.style.cursor = 'crosshair';
}
});
// Touch support
renderer.domElement.addEventListener('touchstart', e => {
const t = e.touches[0];
mouse.copy(getNDC(t.clientX, t.clientY));
const nd = pickNode(mouse);
if (nd) {
dragNode = nd;
nd.dragged = true;
nd.vel.set(0,0,0);
const camDir = camera.position.clone().normalize();
dragPlane.setFromNormalAndCoplanarPoint(camDir, nd.mesh.position);
camPaused = true;
}
}, {passive:true});
renderer.domElement.addEventListener('touchmove', e => {
if (!dragNode) return;
const t = e.touches[0];
mouse.copy(getNDC(t.clientX, t.clientY));
raycaster.setFromCamera(mouse, camera);
const target = new THREE.Vector3();
raycaster.ray.intersectPlane(dragPlane, target);
if (target) dragNode.dragTarget = target.clone();
}, {passive:true});
renderer.domElement.addEventListener('touchend', () => {
if (dragNode) {
const stretch = new THREE.Vector3().subVectors(dragNode.mesh.position, dragNode.origin);
dragNode.vel.copy(stretch.multiplyScalar(-0.08));
dragNode.dragged = false;
dragNode.dragTarget = null;
dragNode = null;
camPaused = false;
}
}, {passive:true});
const _tmp = new THREE.Vector3();
function physicsStep(t) {
for (let ai=0;ai<nodes.length;ai++) {
const A = nodes[ai];
const pos = A.mesh.position;
// Pulse
const pulse = Math.sin(t*0.0028+A.phase);
A.mesh.scale.setScalar(1.0+pulse*0.32);
const hot=(pulse+1)*0.5;
A.mat.emissive.setRGB(hot*0.4,hot*0.08,0);
if (A.dragged && A.dragTarget) {
// Elastic: smoothly pull toward drag target
const diff = new THREE.Vector3().subVectors(A.dragTarget, pos);
A.vel.add(diff.multiplyScalar(0.18));
} else {
// Normal physics
for (let bi=ai+1;bi<nodes.length;bi++) {
const B = nodes[bi];
_tmp.subVectors(B.mesh.position, pos);
const dist = _tmp.length();
if (dist < 0.001) continue;
let force = 0;
if (dist < REPULSE_DIST) force = -REPULSE_STR/(dist*dist);
else if (dist < ATTRACT_DIST) force = ATTRACT_STR*(dist-REPULSE_DIST);
if (force === 0) continue;
const nx=_tmp.x/dist*force, ny=_tmp.y/dist*force, nz=_tmp.z/dist*force;
A.vel.x+=nx; A.vel.y+=ny; A.vel.z+=nz;
B.vel.x-=nx; B.vel.y-=ny; B.vel.z-=nz;
}
A.vel.x+=(A.origin.x-pos.x)*ANCHOR_STR;
A.vel.y+=(A.origin.y-pos.y)*ANCHOR_STR;
A.vel.z+=(A.origin.z-pos.z)*ANCHOR_STR;
}
A.vel.multiplyScalar(DAMPING);
pos.add(A.vel);
}
}
function updateLines(t) {
for (const {line,a,b,hot} of lineObjects) {
const pa=line.geometry.attributes.position;
pa.setXYZ(0,a.mesh.position.x,a.mesh.position.y,a.mesh.position.z);
pa.setXYZ(1,b.mesh.position.x,b.mesh.position.y,b.mesh.position.z);
pa.needsUpdate=true;
if (hot) line.material.opacity=0.1+Math.abs(Math.sin(t*0.003+a.phase))*0.6;
}
// Elastic threads
for (let i=0;i<nodes.length;i++) {
const nd = nodes[i];
const el = elasticLines[i];
const pa = el.geometry.attributes.position;
pa.setXYZ(0,nd.mesh.position.x,nd.mesh.position.y,nd.mesh.position.z);
pa.setXYZ(1,nd.origin.x,nd.origin.y,nd.origin.z);
pa.needsUpdate=true;
// Only show elastic thread when node is displaced beyond threshold
const stretch = nd.mesh.position.distanceTo(nd.origin);
el.material.opacity = nd.dragged
? Math.min(0.9, stretch*0.06)
: Math.max(0, el.material.opacity - 0.04);
// Color shifts from cyan to hot orange as stretch increases
if (stretch > 2) {
const t2 = Math.min(1, stretch/30);
el.material.color.setRGB(1-t2*0.5, 0.4+t2*0.3, 1-t2);
}
}
}
function animate() {
requestAnimationFrame(animate);
const t = Date.now();
if (!camPaused) camAngle += 0.0021;
camera.position.set(
115*Math.sin(camAngle)*Math.cos(Math.PI/9),
115*Math.sin(Math.PI/9),
115*Math.cos(camAngle)*Math.cos(Math.PI/9)
);
camera.lookAt(0,0,0);
physicsStep(t);
updateLines(t);
escherGroup.rotation.y+=0.0017;
r1.rotation.y+=0.0008;
r2wrap.rotation.z+=0.0012;
r3wrap.rotation.x+=0.001;
innerRing.rotation.z+=0.003;
innerInner.rotation.y+=0.006;
innerInner.rotation.x+=0.004;
pulseLight.intensity=Math.max(0,Math.sin(t*0.0018)*0.8);
pulseLight.color.setHSL((t*0.00005)%1,0.8,0.5);
movingLight.position.x=48*Math.sin(t*0.0006);
movingLight.position.z=48*Math.cos(t*0.0007);
movingLight.intensity=0.5+Math.sin(t*0.002)*0.2;
stars.rotation.y+=0.00035;
auraSphere.rotation.y+=0.0005;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize',()=>{
renderer.setSize(innerWidth,innerHeight);
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
});
</script>
</body>
</html>