// col-3d.js — Shared Procedural Three.js Backgrounds for Class Of Learners // Desktop only. No external models. Each page gets a unique scene. ;(function () { 'use strict' // Skip on mobile/touch devices for performance, and respect reduced-motion preference if (window.matchMedia('(pointer: coarse)').matches || window.innerWidth < 960) return if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return // Wait for Three.js to load function waitForThree(cb) { if (typeof THREE !== 'undefined') { cb() return } var checks = 0 var interval = setInterval(function () { checks++ if (typeof THREE !== 'undefined') { clearInterval(interval) cb() } if (checks > 50) clearInterval(interval) // give up after 5s }, 100) } waitForThree(function () { // Find the canvas var canvas = document.getElementById('orrery') || document.getElementById('constellation') || document.getElementById('cosmos') || document.getElementById('bgCanvas') || document.getElementById('schoolBg') || document.getElementById('webgl-canvas') if (!canvas) return // Determine which page we're on var path = window.location.pathname.split('/').pop() || 'home' path = path.replace('.html', '').toLowerCase() // Map index to home if (path === '' || path === 'index' || path === 'home') path = 'home' // Setup renderer var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true, powerPreference: 'high-performance' }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setClearColor(0x000000, 0) var scene = new THREE.Scene() // Fog gives distant particles/shapes a sense of depth instead of all rendering at full brightness scene.fog = new THREE.FogExp2(0x070a14, 0.0055) var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000) camera.position.z = 50 // Pause rendering while the tab isn't visible to save battery/GPU var isVisible = true document.addEventListener('visibilitychange', function () { isVisible = !document.hidden }) // Mouse tracking for parallax var mouseX = 0, mouseY = 0 document.addEventListener('mousemove', function (e) { mouseX = (e.clientX / window.innerWidth) * 2 - 1 mouseY = -(e.clientY / window.innerHeight) * 2 + 1 }) // Resize handler window.addEventListener('resize', function () { if (window.innerWidth < 960) return camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) }) // Color palette from CoL design system var COL = { signal: 0xf2b84b, ion: 0x5ed4f5, teal: 0x00f0cc, plasma: 0xb89bff, em: 0x34d399, dim: 0x8891aa, void2: 0x0c1224 } // ==================== SCENE BUILDERS ==================== var scenes = { // HOME — Orbital Orrery (rings of particles orbiting a glowing center) home: function () { camera.position.set(0, 5, 60) var group = new THREE.Group() // Central glow var centerGeo = new THREE.SphereGeometry(1.2, 32, 32) var centerMat = new THREE.MeshBasicMaterial({ color: COL.signal, transparent: true, opacity: 0.9 }) var center = new THREE.Mesh(centerGeo, centerMat) group.add(center) // Glow sprite var glowCanvas = document.createElement('canvas') glowCanvas.width = 128 glowCanvas.height = 128 var gCtx = glowCanvas.getContext('2d') var grad = gCtx.createRadialGradient(64, 64, 0, 64, 64, 64) grad.addColorStop(0, 'rgba(242, 184, 75, 0.6)') grad.addColorStop(0.4, 'rgba(242, 184, 75, 0.15)') grad.addColorStop(1, 'rgba(242, 184, 75, 0)') gCtx.fillStyle = grad gCtx.fillRect(0, 0, 128, 128) var glowSprite = new THREE.Sprite( new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(glowCanvas), transparent: true, blending: THREE.AdditiveBlending }) ) glowSprite.scale.set(12, 12, 1) group.add(glowSprite) // Orbital rings of particles var rings = [ { radius: 12, count: 60, color: COL.ion, size: 0.3, speed: 0.008 }, { radius: 20, count: 80, color: COL.teal, size: 0.25, speed: -0.005 }, { radius: 30, count: 100, color: COL.plasma, size: 0.2, speed: 0.003 }, { radius: 42, count: 120, color: COL.dim, size: 0.15, speed: -0.002 } ] var ringGroups = [] rings.forEach(function (r) { var positions = new Float32Array(r.count * 3) for (var i = 0; i < r.count; i++) { var angle = (i / r.count) * Math.PI * 2 var jitter = (Math.random() - 0.5) * 2 positions[i * 3] = Math.cos(angle) * (r.radius + jitter) positions[i * 3 + 1] = (Math.random() - 0.5) * 3 positions[i * 3 + 2] = Math.sin(angle) * (r.radius + jitter) } var geo = new THREE.BufferGeometry() geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)) var mat = new THREE.PointsMaterial({ color: r.color, size: r.size, transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending }) var points = new THREE.Points(geo, mat) group.add(points) ringGroups.push({ points: points, speed: r.speed }) }) // Thin orbit lines rings.forEach(function (r) { var linePoints = [] for (var i = 0; i <= 128; i++) { var angle = (i / 128) * Math.PI * 2 linePoints.push(new THREE.Vector3(Math.cos(angle) * r.radius, 0, Math.sin(angle) * r.radius)) } var lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints) var lineMat = new THREE.LineBasicMaterial({ color: r.color, transparent: true, opacity: 0.08 }) group.add(new THREE.Line(lineGeo, lineMat)) }) group.rotation.x = 0.5 scene.add(group) return function (time) { ringGroups.forEach(function (rg) { rg.points.rotation.y += rg.speed }) center.material.opacity = 0.7 + Math.sin(time * 2) * 0.2 group.rotation.y += (mouseX * 0.3 - group.rotation.y) * 0.02 group.rotation.x = 0.5 + (mouseY * 0.15 - (group.rotation.x - 0.5)) * 0.02 } }, // ABOUT — Constellation Network (nodes connected by lines) about: function () { camera.position.set(0, 0, 80) var group = new THREE.Group() var nodeCount = 60 var nodes = [] for (var i = 0; i < nodeCount; i++) { var geo = new THREE.SphereGeometry(0.3 + Math.random() * 0.4, 8, 8) var colors = [COL.signal, COL.ion, COL.teal, COL.plasma, COL.em] var color = colors[Math.floor(Math.random() * colors.length)] var mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.7 }) var mesh = new THREE.Mesh(geo, mat) mesh.position.set((Math.random() - 0.5) * 100, (Math.random() - 0.5) * 60, (Math.random() - 0.5) * 40) mesh.userData = { vel: new THREE.Vector3((Math.random() - 0.5) * 0.02, (Math.random() - 0.5) * 0.02, (Math.random() - 0.5) * 0.01), baseColor: color } group.add(mesh) nodes.push(mesh) } // Connection lines var lineMat = new THREE.LineBasicMaterial({ color: COL.ion, transparent: true, opacity: 0.06 }) var lineGroup = new THREE.Group() group.add(lineGroup) scene.add(group) return function (time) { // Move nodes nodes.forEach(function (n) { n.position.add(n.userData.vel) if (Math.abs(n.position.x) > 55) n.userData.vel.x *= -1 if (Math.abs(n.position.y) > 35) n.userData.vel.y *= -1 if (Math.abs(n.position.z) > 25) n.userData.vel.z *= -1 }) // Rebuild connections every 30 frames if (Math.floor(time * 60) % 30 === 0) { while (lineGroup.children.length) lineGroup.remove(lineGroup.children[0]) for (var i = 0; i < nodes.length; i++) { for (var j = i + 1; j < nodes.length; j++) { var dist = nodes[i].position.distanceTo(nodes[j].position) if (dist < 20) { var pts = [nodes[i].position.clone(), nodes[j].position.clone()] var g = new THREE.BufferGeometry().setFromPoints(pts) lineGroup.add(new THREE.Line(g, lineMat)) } } } } group.rotation.y += (mouseX * 0.15 - group.rotation.y) * 0.01 group.rotation.x += (mouseY * 0.08 - group.rotation.x) * 0.01 } }, // SCHOOL — Floating Geometric Knowledge school: function () { camera.position.set(0, 0, 60) var group = new THREE.Group() var shapes = [] var geometries = [ new THREE.IcosahedronGeometry(2, 0), new THREE.OctahedronGeometry(2, 0), new THREE.TetrahedronGeometry(2, 0), new THREE.DodecahedronGeometry(1.5, 0), new THREE.TorusGeometry(1.5, 0.4, 8, 12) ] var colors = [COL.ion, COL.signal, COL.teal, COL.plasma, COL.em] for (var i = 0; i < 25; i++) { var geoIdx = i % geometries.length var wireMat = new THREE.MeshBasicMaterial({ color: colors[geoIdx], wireframe: true, transparent: true, opacity: 0.35 }) var mesh = new THREE.Mesh(geometries[geoIdx].clone(), wireMat) mesh.position.set((Math.random() - 0.5) * 80, (Math.random() - 0.5) * 50, (Math.random() - 0.5) * 30 - 10) mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0) var s = 0.5 + Math.random() * 1.5 mesh.scale.set(s, s, s) mesh.userData = { rotSpeed: new THREE.Vector3((Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.005), floatSpeed: 0.3 + Math.random() * 0.5, floatOffset: Math.random() * Math.PI * 2 } group.add(mesh) shapes.push(mesh) } scene.add(group) return function (time) { shapes.forEach(function (s) { s.rotation.x += s.userData.rotSpeed.x s.rotation.y += s.userData.rotSpeed.y s.position.y += Math.sin(time * s.userData.floatSpeed + s.userData.floatOffset) * 0.02 }) group.rotation.y += (mouseX * 0.2 - group.rotation.y) * 0.015 group.rotation.x += (mouseY * 0.1 - group.rotation.x) * 0.015 } }, // CAREER — Deep Space / Cosmos career: function () { camera.position.set(0, 0, 50) var group = new THREE.Group() // Star field var starCount = 2000 var starPos = new Float32Array(starCount * 3) var starColors = new Float32Array(starCount * 3) for (var i = 0; i < starCount; i++) { starPos[i * 3] = (Math.random() - 0.5) * 200 starPos[i * 3 + 1] = (Math.random() - 0.5) * 200 starPos[i * 3 + 2] = (Math.random() - 0.5) * 200 var c = new THREE.Color([COL.ion, COL.signal, COL.plasma, 0xffffff][Math.floor(Math.random() * 4)]) starColors[i * 3] = c.r starColors[i * 3 + 1] = c.g starColors[i * 3 + 2] = c.b } var starGeo = new THREE.BufferGeometry() starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)) starGeo.setAttribute('color', new THREE.BufferAttribute(starColors, 3)) group.add( new THREE.Points( starGeo, new THREE.PointsMaterial({ size: 0.3, vertexColors: true, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending }) ) ) // Nebula sprites for (var j = 0; j < 5; j++) { var nc = document.createElement('canvas') nc.width = 128 nc.height = 128 var nctx = nc.getContext('2d') var nebulaColors = ['rgba(184,155,255,', 'rgba(94,212,245,', 'rgba(242,184,75,', 'rgba(0,240,204,', 'rgba(52,211,153,'] var ng = nctx.createRadialGradient(64, 64, 0, 64, 64, 64) ng.addColorStop(0, nebulaColors[j] + '0.15)') ng.addColorStop(0.5, nebulaColors[j] + '0.05)') ng.addColorStop(1, nebulaColors[j] + '0)') nctx.fillStyle = ng nctx.fillRect(0, 0, 128, 128) var sprite = new THREE.Sprite( new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(nc), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }) ) sprite.position.set((Math.random() - 0.5) * 80, (Math.random() - 0.5) * 60, -30 - Math.random() * 30) sprite.scale.set(40 + Math.random() * 30, 40 + Math.random() * 30, 1) group.add(sprite) } scene.add(group) return function (time) { group.rotation.y += 0.0003 group.rotation.x += (mouseY * 0.08 - group.rotation.x) * 0.01 group.rotation.y += mouseX * 0.08 * 0.005 } }, // DATABASE_LOGIC — Data Grid / Network Topology database_logic: function () { camera.position.set(0, 15, 45) camera.lookAt(0, 0, 0) var group = new THREE.Group() // Grid var gridHelper = new THREE.GridHelper(80, 40, COL.teal, 0x0a1420) gridHelper.material.transparent = true gridHelper.material.opacity = 0.15 group.add(gridHelper) // Data nodes (cylinders) var dataNodes = [] for (var i = 0; i < 30; i++) { var h = 1 + Math.random() * 6 var geo = new THREE.CylinderGeometry(0.4, 0.4, h, 8) var mat = new THREE.MeshBasicMaterial({ color: [COL.teal, COL.ion, COL.em][Math.floor(Math.random() * 3)], transparent: true, opacity: 0.5, wireframe: Math.random() > 0.5 }) var cyl = new THREE.Mesh(geo, mat) cyl.position.set((Math.random() - 0.5) * 60, h / 2, (Math.random() - 0.5) * 60) cyl.userData = { targetH: h, baseY: 0, pulseSpeed: 0.5 + Math.random() * 2, pulseOffset: Math.random() * Math.PI * 2 } group.add(cyl) dataNodes.push(cyl) } // Connection lines between nearby nodes var connMat = new THREE.LineBasicMaterial({ color: COL.ion, transparent: true, opacity: 0.08 }) for (var a = 0; a < dataNodes.length; a++) { for (var b = a + 1; b < dataNodes.length; b++) { if (dataNodes[a].position.distanceTo(dataNodes[b].position) < 18) { var pts = [dataNodes[a].position.clone(), dataNodes[b].position.clone()] pts[0].y = 0 pts[1].y = 0 group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), connMat)) } } } scene.add(group) return function (time) { dataNodes.forEach(function (n) { var pulse = Math.sin(time * n.userData.pulseSpeed + n.userData.pulseOffset) var newH = n.userData.targetH * (0.5 + pulse * 0.5) n.scale.y = Math.max(0.1, newH / n.userData.targetH) n.material.opacity = 0.3 + pulse * 0.3 }) group.rotation.y += (mouseX * 0.2 - group.rotation.y) * 0.01 } }, // PRIVACY — Shield Hex Grid privacy: function () { camera.position.set(0, 0, 50) var group = new THREE.Group() // Hexagonal grid var hexSize = 3 var cols = 12, rows = 10 var hexes = [] for (var row = 0; row < rows; row++) { for (var col = 0; col < cols; col++) { var x = col * hexSize * 1.75 - (cols * hexSize * 1.75) / 2 var y = row * hexSize * 1.55 - (rows * hexSize * 1.55) / 2 if (row % 2) x += hexSize * 0.875 var shape = new THREE.Shape() for (var k = 0; k < 6; k++) { var angle = (Math.PI / 3) * k - Math.PI / 6 var hx = Math.cos(angle) * hexSize * 0.85 var hy = Math.sin(angle) * hexSize * 0.85 if (k === 0) shape.moveTo(hx, hy) else shape.lineTo(hx, hy) } shape.closePath() var geo = new THREE.ShapeGeometry(shape) var mat = new THREE.MeshBasicMaterial({ color: COL.teal, transparent: true, opacity: 0.03 + Math.random() * 0.04, side: THREE.DoubleSide }) var hex = new THREE.Mesh(geo, mat) hex.position.set(x, y, 0) hex.userData = { pulseDelay: Math.random() * Math.PI * 2, baseOpacity: mat.opacity } group.add(hex) hexes.push(hex) // Edge lines var edgeGeo = new THREE.EdgesGeometry(geo) var edgeMat = new THREE.LineBasicMaterial({ color: COL.teal, transparent: true, opacity: 0.1 }) var edges = new THREE.LineSegments(edgeGeo, edgeMat) edges.position.copy(hex.position) group.add(edges) } } // Shield icon (simple triangle outline) var shieldPts = [ new THREE.Vector3(0, 8, 0.1), new THREE.Vector3(-6, 3, 0.1), new THREE.Vector3(-5, -4, 0.1), new THREE.Vector3(0, -7, 0.1), new THREE.Vector3(5, -4, 0.1), new THREE.Vector3(6, 3, 0.1), new THREE.Vector3(0, 8, 0.1) ] var shieldGeo = new THREE.BufferGeometry().setFromPoints(shieldPts) var shieldLine = new THREE.Line(shieldGeo, new THREE.LineBasicMaterial({ color: COL.em, transparent: true, opacity: 0.3 })) group.add(shieldLine) scene.add(group) return function (time) { hexes.forEach(function (h) { var pulse = Math.sin(time * 0.8 + h.userData.pulseDelay) h.material.opacity = h.userData.baseOpacity + pulse * 0.03 }) shieldLine.material.opacity = 0.2 + Math.sin(time * 1.5) * 0.15 group.rotation.y += (mouseX * 0.08 - group.rotation.y) * 0.01 group.rotation.x += (mouseY * 0.04 - group.rotation.x) * 0.01 } }, // TERMS — Matrix / Code Rain terms: function () { camera.position.set(0, 0, 50) var group = new THREE.Group() // Falling particle columns var columnCount = 25 var particlesPerCol = 20 var columns = [] for (var c = 0; c < columnCount; c++) { var colX = (c - columnCount / 2) * 4 var positions = new Float32Array(particlesPerCol * 3) var opacities = new Float32Array(particlesPerCol) for (var p = 0; p < particlesPerCol; p++) { positions[p * 3] = colX + (Math.random() - 0.5) * 0.5 positions[p * 3 + 1] = (Math.random() - 0.5) * 80 positions[p * 3 + 2] = (Math.random() - 0.5) * 20 opacities[p] = Math.random() } var geo = new THREE.BufferGeometry() geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)) var mat = new THREE.PointsMaterial({ color: [COL.em, COL.teal, COL.ion][c % 3], size: 0.4, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending }) var points = new THREE.Points(geo, mat) points.userData = { speed: 0.1 + Math.random() * 0.15 } group.add(points) columns.push(points) } scene.add(group) return function (time) { columns.forEach(function (col) { var pos = col.geometry.attributes.position.array for (var i = 0; i < pos.length; i += 3) { pos[i + 1] -= col.userData.speed if (pos[i + 1] < -40) pos[i + 1] = 40 } col.geometry.attributes.position.needsUpdate = true }) group.rotation.y += (mouseX * 0.05 - group.rotation.y) * 0.01 } }, // FEEDBACK — Signal Waves / Communication Ripples feedback: function () { camera.position.set(0, 0, 60) var group = new THREE.Group() // Concentric rings var ringCount = 8 var rings = [] for (var i = 0; i < ringCount; i++) { var radius = 5 + i * 5 var ringGeo = new THREE.RingGeometry(radius - 0.15, radius + 0.15, 64) var ringMat = new THREE.MeshBasicMaterial({ color: [COL.signal, COL.ion][i % 2], transparent: true, opacity: 0.12 - i * 0.01, side: THREE.DoubleSide }) var ring = new THREE.Mesh(ringGeo, ringMat) ring.userData = { baseRadius: radius, phase: i * 0.5 } group.add(ring) rings.push(ring) } // Floating signal dots var dotCount = 80 var dotPositions = new Float32Array(dotCount * 3) for (var d = 0; d < dotCount; d++) { var angle = Math.random() * Math.PI * 2 var dist = 5 + Math.random() * 40 dotPositions[d * 3] = Math.cos(angle) * dist dotPositions[d * 3 + 1] = Math.sin(angle) * dist dotPositions[d * 3 + 2] = (Math.random() - 0.5) * 10 } var dotGeo = new THREE.BufferGeometry() dotGeo.setAttribute('position', new THREE.BufferAttribute(dotPositions, 3)) group.add( new THREE.Points( dotGeo, new THREE.PointsMaterial({ color: COL.signal, size: 0.4, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending }) ) ) scene.add(group) return function (time) { rings.forEach(function (r) { var pulse = Math.sin(time * 1.2 + r.userData.phase) var scale = 1 + pulse * 0.08 r.scale.set(scale, scale, 1) r.material.opacity = (0.12 - rings.indexOf(r) * 0.01) * (0.6 + pulse * 0.4) }) group.rotation.z += 0.001 group.rotation.y += (mouseX * 0.1 - group.rotation.y) * 0.01 group.rotation.x += (mouseY * 0.05 - group.rotation.x) * 0.01 } }, // SNEH-ASHA — skip (already has its own procedural 3D) 'sneh-asha': null, sneh_asha: null } // Find the right scene var sceneBuilder = scenes[path] || scenes[path.replace('-', '_')] || scenes[path.replace('_', '-')] // For Database_Logic.html (path becomes "database_logic") if (!sceneBuilder && path.indexOf('database') >= 0) sceneBuilder = scenes.database_logic // For Career.html if (!sceneBuilder && path.indexOf('career') >= 0) sceneBuilder = scenes.career if (!sceneBuilder) { // Fallback: generic starfield for any unknown page sceneBuilder = scenes.career // reuse cosmos } if (sceneBuilder === null) return // explicitly null = page handles its own 3D var updateFn = sceneBuilder() // Fade in canvas var hasFadedIn = false canvas.style.opacity = '0' canvas.style.transition = 'opacity 1.5s ease' setTimeout(function () { hasFadedIn = true syncThemeOpacity() }, 100) if (canvas.classList) canvas.classList.add('v') // These scenes are tuned for the dark theme's neon palette — fade them down in light mode // instead of letting them render at full brightness against a light background. function syncThemeOpacity() { if (!hasFadedIn) return var isLight = document.body.classList.contains('lm') canvas.style.opacity = isLight ? '0.12' : '1' } new MutationObserver(syncThemeOpacity).observe(document.body, { attributes: true, attributeFilter: ['class'] }) // Animation loop var clock = new THREE.Clock() function animate() { requestAnimationFrame(animate) if (!isVisible) return // skip rendering while tab is hidden to save battery/GPU var elapsed = clock.getElapsedTime() if (updateFn) updateFn(elapsed) renderer.render(scene, camera) } animate() }) })()