// Wait for everything to load window.onload = function() { // Make sure THREE is available if (typeof THREE === 'undefined') { console.error('THREE.js is not loaded from CDN'); // Provide visual feedfront in the container const container = document.getElementById('icosahedron-container'); container.innerHTML = '
THREE.js not loaded
'; return; } // Initialize the scene const container = document.getElementById('icosahedron-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // Setup renderer const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(containerWidth, containerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.setClearColor(0x000000, 0); // Transparent frontground container.appendChild(renderer.domElement); // Setup scene const scene = new THREE.Scene(); // Setup camera const camera = new THREE.PerspectiveCamera(26, containerWidth / containerHeight, 0.1, 100); camera.position.z = 4; // Create icosahedron with manually defined vertices function createIcosahedron(radius) { // Golden ratio for icosahedron vertices const t = (1 + Math.sqrt(5)) / 2; // Normalize radius const normRadius = radius / Math.sqrt(1 + t * t); // Create vertices const vertices = [ [-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1] ].map(v => new THREE.Vector3(v[0] * normRadius, v[1] * normRadius, v[2] * normRadius)); // Define edges (pairs of vertex indices) const edges = [ [0, 11], [0, 5], [0, 1], [0, 7], [0, 10], [1, 5], [1, 7], [1, 8], [1, 9], [2, 3], [2, 4], [2, 6], [2, 10], [2, 11], [3, 4], [3, 6], [3, 8], [3, 9], [4, 5], [4, 9], [4, 11], [5, 9], [5, 11], [6, 7], [6, 8], [6, 10], [7, 8], [7, 10], [8, 9], /*[8, 10],*/ /*[9, 11],*/ [10, 11] ]; return { vertices: vertices, edges: edges }; } // Convert vertex indices to actual vertices and create a line function createLine(icosa, edgeIndex, material) { const startIndex = icosa.edges[edgeIndex][0]; const endIndex = icosa.edges[edgeIndex][1]; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array([ icosa.vertices[startIndex].x, icosa.vertices[startIndex].y, icosa.vertices[startIndex].z, icosa.vertices[endIndex].x, icosa.vertices[endIndex].y, icosa.vertices[endIndex].z ]); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); return new THREE.Line(geometry, material); } // Function to create the dual line rendering (thin back lines, thick front lines) function createDualLineRendering(radius) { const group = new THREE.Group(); const icosa = createIcosahedron(radius); // Create materials const backMaterial = new THREE.MeshBasicMaterial({ color: 0x63a8b8, // green for back lines depthTest: false, // Don't test depth for back lines transparent: true, opacity: 0.4, side: THREE.DoubleSide }); const frontMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, // white for front lines transparent: true, depthTest: true, // Use depth test for front lines side: THREE.DoubleSide }); // For each edge, create: // 1. A thin cylindrical tube with back material that ignores depth // 2. A cylindrical tube with front material that uses depth testing const backRadius = 0.03; // thin for back lines const frontRadius = 0.035; // thick for front lines icosa.edges.forEach((edge, index) => { const start = icosa.vertices[edge[0]]; const end = icosa.vertices[edge[1]]; // Create back line (thick) const backLine = createCylinderBetweenPoints(start, end, backRadius, 1, backMaterial); backLine.renderOrder = 1; // Render after front lines group.add(backLine); // Create front line (thin) const frontLine = createCylinderBetweenPoints(start, end, frontRadius, 0.95, frontMaterial); frontLine.renderOrder = 3; // Render before back lines group.add(frontLine); }); // Create faces for depth testing (invisible) const faceMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0, depthWrite: true, side: THREE.DoubleSide }); // Create faces for internal icosahedron const internalMaterial = new THREE.MeshBasicMaterial({ color: 0x63a8b8, depthTest: false, transparent: true, opacity: 0.2, side: THREE.DoubleSide }); // Define faces of icosahedron (each is a triangle of vertex indices) const faces = [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1] ]; // Create invisible faces for depth testing faces.forEach(face => { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array([ icosa.vertices[face[0]].x, icosa.vertices[face[0]].y, icosa.vertices[face[0]].z, icosa.vertices[face[1]].x, icosa.vertices[face[1]].y, icosa.vertices[face[1]].z, icosa.vertices[face[2]].x, icosa.vertices[face[2]].y, icosa.vertices[face[2]].z ]); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const faceMesh = new THREE.Mesh(geometry, faceMaterial); faceMesh.scale.set(0.95,0.95,0.95); faceMesh.renderOrder = 0; // Render first for depth buffer group.add(faceMesh); const internalMesh1 = new THREE.Mesh(geometry, internalMaterial); const internalMesh2 = new THREE.Mesh(geometry, internalMaterial); const internalMesh3 = new THREE.Mesh(geometry, internalMaterial); const internalMesh4 = new THREE.Mesh(geometry, internalMaterial); const internalMesh5 = new THREE.Mesh(geometry, internalMaterial); internalMesh1.scale.set(0.85,0.85,0.85); internalMesh1.renderOrder = 1; // Render between front and back group.add(internalMesh1); internalMesh2.scale.set(0.65,0.65,0.65); internalMesh2.renderOrder = 1; // Render between front and back group.add(internalMesh2); internalMesh3.scale.set(0.5,0.5,0.5); internalMesh3.renderOrder = 1; // Render between front and back group.add(internalMesh3); internalMesh4.scale.set(0.4,0.4,0.4); internalMesh4.renderOrder = 1; // Render between front and back group.add(internalMesh4); internalMesh5.scale.set(0.3,0.3,0.3); internalMesh5.renderOrder = 1; // Render between front and back group.add(internalMesh5); }); return group; } // Function to create a cylinder between two points function createCylinderBetweenPoints(pointX, pointY, radius, lengthmultiplier, material) { // Direction from pointX to pointY const direction = new THREE.Vector3().subVectors(pointY, pointX); const length = direction.length(); // Create cylinder const geometry = new THREE.CylinderGeometry(radius, radius, length*lengthmultiplier, 3, 1); // By default, cylinder is along Y-axis, so rotate it geometry.rotateX(Math.PI / 2); // Create mesh const cylinder = new THREE.Mesh(geometry, material); // Position and orient cylinder const midpoint = new THREE.Vector3().addVectors(pointX, pointY).multiplyScalar(0.5); cylinder.position.copy(midpoint); cylinder.lookAt(pointY); return cylinder; } // Create our icosahedron const icosahedronGroup = createDualLineRendering(0.85); scene.add(icosahedronGroup); // Animation state let animating = true; let lastFrameTime = 0; const targetFPS = 20; const animMultiplier = 30/targetFPS; const frameDuration = 1000 / targetFPS; // Animation function function animate(now) { requestAnimationFrame(animate); const delta = now - lastFrameTime; if (delta < frameDuration) return; lastFrameTime = now; // Rotate if animation is enabled if (animating) { icosahedronGroup.rotation.x -= 0.002 * animMultiplier; icosahedronGroup.rotation.y += 0.004 * animMultiplier; icosahedronGroup.rotation.z -= 0.001 * animMultiplier; } renderer.render(scene, camera); } /* // Set up controls const toggleAnimationBtn = document.getElementById('toggle-animation'); const resetRotationBtn = document.getElementById('reset-rotation'); toggleAnimationBtn.addEventListener('click', function() { animating = !animating; this.textContent = animating ? 'Pause' : 'Resume'; }); resetRotationBtn.addEventListener('click', function() { icosahedronGroup.rotation.set(0, 0, 0); }); */ // Start animation animate(); // Handle window resize window.addEventListener('resize', function() { // Only update if container dimensions change const newWidth = container.clientWidth; const newHeight = container.clientHeight; if (newWidth !== containerWidth || newHeight !== containerHeight) { camera.aspect = newWidth / newHeight; camera.updateProjectionMatrix(); renderer.setSize(newWidth, newHeight); } }); console.log('Icosahedron wireframe created successfully'); };