function isMobileDevice() { if (navigator.userAgentData && navigator.userAgentData.mobile) { return true; } const regex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i; if (regex.test(navigator.userAgent)) { return true; } if (window.matchMedia("(max-width: 768px)").matches) { return true; } return false; } // Import the necessary components from the Three.js library import * as THREE from 'three'; // Import the OrbitControls for mouse interaction import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // --- Global State Variables --- let isThreeJsInitialized = false; let threeJsApp = null; // Will hold our animation state and objects // --- Three.js Initialization and Control --- function initializeThreeJs() { if (isMobileDevice()) { console.log("Mobile device detected, skipping Three.js initialization."); document.body.classList.add('is-mobile'); const container = document.querySelector('#home-content #scene-container'); if (container) { container.innerHTML = `
The interactive globe is best viewed on a desktop. We're working on a mobile-friendly experience!
`; } return; } if (isThreeJsInitialized) return; const container = document.querySelector('#home-content #scene-container'); if (!container) return; isThreeJsInitialized = true; // Clear any previous canvas while (container.firstChild) { container.removeChild(container.firstChild); } const loadingOverlay = document.getElementById('loading-overlay'); const sloganElement = document.getElementById('main-slogan'); const finaleLogoContainer = document.getElementById('finale-logo-container'); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 5; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.sortObjects = false; container.appendChild(renderer.domElement); const loadingManager = new THREE.LoadingManager(() => { loadingOverlay.style.opacity = '0'; setTimeout(() => { loadingOverlay.style.display = 'none'; }, 500); }); scene.add(new THREE.AmbientLight(0xffffff, 0.3)); const pointLight = new THREE.PointLight(0xffffff, 1.2); pointLight.position.set(5, 3, 5); scene.add(pointLight); const textureLoader = new THREE.TextureLoader(loadingManager); const starfieldTexture = textureLoader.load('assets/starfield.jpg'); starfieldTexture.wrapS = starfieldTexture.wrapT = THREE.RepeatWrapping; starfieldTexture.repeat.set(4, 4); const starfield = new THREE.Mesh(new THREE.SphereGeometry(90, 64, 64), new THREE.MeshBasicMaterial({ map: starfieldTexture, side: THREE.BackSide })); scene.add(starfield); const earth = new THREE.Mesh(new THREE.SphereGeometry(2, 64, 64), new THREE.MeshPhongMaterial({ map: textureLoader.load('assets/earth_daymap.jpg'), bumpMap: textureLoader.load('assets/earth_bumpmap.jpg'), bumpScale: 0.05, specularMap: textureLoader.load('assets/earth_watermap.png'), specular: new THREE.Color('grey') })); scene.add(earth); const initialCities = [ { name: 'Tokyo', lat: 35.68, lon: 139.69 }, { name: 'London', lat: 51.50, lon: -0.12 }, { name: 'Paris', lat: 48.85, lon: 2.35 }, { name: 'Sydney', lat: -33.86, lon: 151.20 }, { name: 'São Paulo', lat: -23.55, lon: -46.63 }, { name: 'Cairo', lat: 30.04, lon: 31.23 }, { name: 'Beijing', lat: 39.90, lon: 116.40 }, { name: 'Singapore', lat: 1.35, lon: 103.81 }, { name: 'New York', lat: 40.71, lon: -74.00 }, { name: 'Los Angeles', lat: 34.05, lon: -118.24 }, { name: 'Chicago', lat: 41.87, lon: -87.62 }, { name: 'Houston', lat: 29.76, lon: -95.36 }, { name: 'Phoenix', lat: 33.44, lon: -112.07 }, { name: 'San Francisco', lat: 37.77, lon: -122.41 }, { name: 'Dallas', lat: 32.77, lon: -96.79 }, { name: 'Seattle', lat: 47.60, lon: -122.33 }, { name: 'Miami', lat: 25.76, lon: -80.19 }, { name: 'Atlanta', lat: 33.74, lon: -84.38 }, { name: 'Boston', lat: 42.36, lon: -71.05 }, { name: 'Denver', lat: 39.73, lon: -104.99 }, ]; const growthCities = [ { name: 'Portland', lat: 45.51, lon: -122.67 }, { name: 'Charlotte', lat: 35.22, lon: -80.84 }, { name: 'Mexico City', lat: 19.43, lon: -99.13 }, { name: 'Berlin', lat: 52.52, lon: 13.40 }, { name: 'Mumbai', lat: 19.07, lon: 72.87 }, { name: 'Vancouver', lat: 49.28, lon: -123.12 }, ]; const nodesGroup = new THREE.Group(); const linesGroup = new THREE.Group(); const pulsesGroup = new THREE.Group(); const cityMeshes = []; linesGroup.renderOrder = 1; pulsesGroup.renderOrder = 2; nodesGroup.renderOrder = 3; const logoTexture = textureLoader.load('assets/logo.png'); const idleLineColor = new THREE.Color(0x00aaff); const glowLineColor = new THREE.Color(0x0077ff); const greenPulseColor = new THREE.Color(0x00ffaa); function createCityNode(city) { const material = new THREE.SpriteMaterial({ map: logoTexture, color: 0x00aaff, transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false }); const sprite = new THREE.Sprite(material); const phi = (90 - city.lat) * (Math.PI / 180); const theta = (city.lon + 180) * (Math.PI / 180); const radius = 2.05; sprite.position.set(-(radius * Math.sin(phi) * Math.cos(theta)), (radius * Math.cos(phi)), (radius * Math.sin(phi) * Math.sin(theta))); sprite.userData = { city: city.name, lastPulseTime: Math.random() * 5.0, pulseInterval: 5.0 + Math.random() * 5.0, animState: 'initial', animProgress: 0 }; return sprite; } initialCities.forEach(city => { const node = createCityNode(city); node.scale.set(0.1, 0.1, 1); node.userData.animState = 'stable'; nodesGroup.add(node); cityMeshes.push(node); }); scene.add(nodesGroup, linesGroup, pulsesGroup); const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const context = canvas.getContext('2d'); const gradient = context.createRadialGradient(32, 32, 0, 32, 32, 32); gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); context.fillStyle = gradient; context.fillRect(0, 0, 64, 64); const pulseTexture = new THREE.CanvasTexture(canvas); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; const raycaster = new THREE.Raycaster(); const mousePosition = new THREE.Vector2(); threeJsApp = { appState: 'GROWTH', isGlobeFocused: true, timeSinceLastSpawn: 0, spawnInterval: 3.0, nextCityIndex: 0, activePulses: [], lineMeshes: [], cityMeshes, earth, sloganElement, finaleLogoContainer, renderer, scene, camera, nodesGroup, linesGroup, pulsesGroup, clock: new THREE.Clock(), controls, raycaster, mousePosition, createNetworkLine, createCityNode, triggerPulse, idleLineColor, glowLineColor, greenPulseColor, pulseTexture, growthCities }; resetAnimation(); animate(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // Only add mousemove listener for custom cursor if not a mobile device if (!isMobileDevice()) { window.addEventListener('mousemove', (event) => { mousePosition.x = (event.clientX / window.innerWidth) * 2 - 1; mousePosition.y = -(event.clientY / window.innerHeight) * 2 + 1; document.getElementById('custom-cursor').style.left = event.clientX + 'px'; document.getElementById('custom-cursor').style.top = event.clientY + 'px'; }); } } function resetAnimation() { if (!threeJsApp) return; threeJsApp.appState = 'GROWTH'; threeJsApp.isGlobeFocused = true; threeJsApp.timeSinceLastSpawn = 0; threeJsApp.nextCityIndex = 0; threeJsApp.spawnInterval = 3.0; threeJsApp.activePulses.forEach(p => threeJsApp.pulsesGroup.remove(p)); threeJsApp.activePulses = []; threeJsApp.lineMeshes.forEach(l => threeJsApp.linesGroup.remove(l)); threeJsApp.lineMeshes = []; for (let i = 0; i < threeJsApp.cityMeshes.length; i++) { for (let j = i + 1; j < threeJsApp.cityMeshes.length; j++) { const line = createNetworkLine(threeJsApp.cityMeshes[i], threeJsApp.cityMeshes[j]); threeJsApp.linesGroup.add(line); threeJsApp.lineMeshes.push(line); } } threeJsApp.sloganElement.classList.remove('visible'); threeJsApp.finaleLogoContainer.classList.remove('visible'); const focusRotationY = 0 * (Math.PI / 180); const focusRotationX = 30 * (Math.PI / 180); threeJsApp.earth.rotation.set(focusRotationX, focusRotationY, 0); threeJsApp.nodesGroup.rotation.set(focusRotationX, focusRotationY, 0); threeJsApp.linesGroup.rotation.set(focusRotationX, focusRotationY, 0); threeJsApp.pulsesGroup.rotation.set(focusRotationX, focusRotationY, 0); animate(); } function triggerPulse(line, originNode, color = threeJsApp.glowLineColor) { if (!threeJsApp) return; const pulseMaterial = new THREE.SpriteMaterial({ map: threeJsApp.pulseTexture, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false }); pulseMaterial.color.copy(color); const pulse = new THREE.Sprite(pulseMaterial); pulse.userData = { curve: line.userData.curve, progress: 0, isForward: line.userData.curve.v0.equals(originNode.position), isBlue: color.equals(threeJsApp.glowLineColor) }; threeJsApp.pulsesGroup.add(pulse); threeJsApp.activePulses.push(pulse); } function createNetworkLine(startNode, endNode) { if (!threeJsApp) return; const startPos = startNode.position; const endPos = endNode.position; const midPos = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5).normalize().multiplyScalar(2.2); const curve = new THREE.QuadraticBezierCurve3(startPos, midPos, endPos); const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(curve.getPoints(50)), new THREE.LineBasicMaterial({ color: threeJsApp.idleLineColor, transparent: true, opacity: 0.15, blending: THREE.AdditiveBlending })); line.userData.curve = curve; line.userData.glowIntensity = 0; return line; } function animate() { if (!threeJsApp || !document.getElementById('home-content').classList.contains('active')) { return; } requestAnimationFrame(animate); const delta = threeJsApp.clock.getDelta(); if (!threeJsApp.isGlobeFocused) { const rotationSpeed = 0.05 * delta; threeJsApp.earth.rotation.y += rotationSpeed; threeJsApp.nodesGroup.rotation.y += rotationSpeed; threeJsApp.linesGroup.rotation.y += rotationSpeed; threeJsApp.pulsesGroup.rotation.y += rotationSpeed; } if (threeJsApp.appState === 'GROWTH') { threeJsApp.timeSinceLastSpawn += delta; if (threeJsApp.timeSinceLastSpawn > threeJsApp.spawnInterval && threeJsApp.nextCityIndex < threeJsApp.growthCities.length) { const newCityData = threeJsApp.growthCities[threeJsApp.nextCityIndex]; const newNode = threeJsApp.createCityNode(newCityData); newNode.scale.set(0, 0, 0); threeJsApp.cityMeshes.forEach(existingNode => { const newLine = createNetworkLine(newNode, existingNode); threeJsApp.linesGroup.add(newLine); threeJsApp.lineMeshes.push(newLine); }); threeJsApp.nodesGroup.add(newNode); threeJsApp.cityMeshes.push(newNode); threeJsApp.nextCityIndex++; threeJsApp.timeSinceLastSpawn = 0; threeJsApp.spawnInterval = Math.max(0.5, threeJsApp.spawnInterval * 0.9); } else if (threeJsApp.nextCityIndex >= threeJsApp.growthCities.length) { threeJsApp.appState = 'FINALE'; } } else if (threeJsApp.appState === 'FINALE') { threeJsApp.appState = 'FINALE_SEQUENCE'; threeJsApp.sloganElement.textContent = "Thinking BEYOND AI"; threeJsApp.sloganElement.classList.add('visible'); setTimeout(() => { if (threeJsApp.appState !== 'FINALE_SEQUENCE') return; threeJsApp.finaleLogoContainer.classList.add('visible'); setTimeout(() => { if (threeJsApp.appState !== 'FINALE_SEQUENCE') return; threeJsApp.isGlobeFocused = false; threeJsApp.appState = 'IDLE'; }, 2000); }, 3000); } threeJsApp.raycaster.setFromCamera(threeJsApp.mousePosition, threeJsApp.camera); const sourceNode = threeJsApp.raycaster.intersectObjects(threeJsApp.nodesGroup.children)[0]?.object || null; threeJsApp.cityMeshes.forEach(node => { if (node.userData.animState !== 'stable') { node.userData.animProgress += delta * 2; let scale = 0; if (node.userData.animState === 'initial') { scale = THREE.MathUtils.lerp(0, 0.12, node.userData.animProgress); if (node.userData.animProgress >= 1) { node.userData.animState = 'shrink'; node.userData.animProgress = 0; }} else if (node.userData.animState === 'shrink') { scale = THREE.MathUtils.lerp(0.12, 0.09, node.userData.animProgress); if (node.userData.animProgress >= 1) { node.userData.animState = 'growFinal'; node.userData.animProgress = 0; }} else if (node.userData.animState === 'growFinal') { scale = THREE.MathUtils.lerp(0.09, 0.1, node.userData.animProgress); if (node.userData.animProgress >= 1) { node.userData.animState = 'stable'; }} node.scale.set(scale, scale, 1); } const isSource = (node === sourceNode); if(node.userData.animState === 'stable') { node.scale.x = node.scale.y = THREE.MathUtils.lerp(node.scale.x, isSource ? 0.2 : 0.1, delta * 5); } node.userData.lastPulseTime += delta; if (isSource) { if (node.userData.lastPulseTime > 0.5) { node.userData.lastPulseTime = 0; threeJsApp.lineMeshes.forEach(line => { if (line.userData.curve.v0.equals(node.position) || line.userData.curve.v2.equals(node.position)) { triggerPulse(line, node); } }); } } else { if (node.userData.lastPulseTime > node.userData.pulseInterval) { node.userData.lastPulseTime = 0; threeJsApp.lineMeshes.forEach(line => { if (line.userData.curve.v0.equals(node.position) || line.userData.curve.v2.equals(node.position)) { triggerPulse(line, node, threeJsApp.greenPulseColor); } }); } } }); threeJsApp.activePulses.forEach((pulse, index) => { pulse.userData.progress += delta * 0.4; if (pulse.userData.progress >= 1) { threeJsApp.pulsesGroup.remove(pulse); threeJsApp.activePulses.splice(index, 1); } else { const t = pulse.userData.isForward ? pulse.userData.progress : 1 - pulse.userData.progress; pulse.userData.curve.getPoint(t, pulse.position); const baseScale = pulse.userData.isBlue ? 0.03 + Math.random() * 0.04 : 0.01 + Math.random() * 0.02; const scale = baseScale * Math.sin(Math.PI * pulse.userData.progress); pulse.scale.set(scale, scale, 1); pulse.material.opacity = Math.sin(Math.PI * pulse.userData.progress); } }); threeJsApp.controls.update(); threeJsApp.renderer.render(threeJsApp.scene, threeJsApp.camera); } // --- UI Logic --- function setupNavigation() { const navLinks = document.querySelectorAll('.top-nav a[data-page]'); navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetPageId = link.getAttribute('data-page'); const targetPage = document.getElementById(targetPageId); document.querySelector('.content-section.active')?.classList.remove('active'); if (targetPage) { targetPage.classList.add('active'); if(targetPageId === 'home-content') { if (!isThreeJsInitialized) { initializeThreeJs(); } else { resetAnimation(); } } } }); }); } function setupPortfolioCards() { const cards = document.querySelectorAll('.portfolio-card'); cards.forEach(card => { card.addEventListener('click', function() { // Toggle the active class on the clicked card this.classList.toggle('active'); }); }); } function setupContactForm() { const contactForm = document.querySelector('.contact-form'); if (!contactForm) return; contactForm.addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('name').value; const contactInfo = document.getElementById('contact-info').value; const message = document.getElementById('message').value; const submitButton = contactForm.querySelector('.submit-button'); const subject = `Project Inquiry from ${name}`; const body = `Name: ${name}\nContact Info: ${contactInfo}\n\nMessage:\n${message}`; const mailtoLink = `mailto:contact@knightTechStudios.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; const originalButtonText = submitButton.textContent; submitButton.textContent = 'Opening Email Client...'; window.location.href = mailtoLink; setTimeout(() => { submitButton.textContent = originalButtonText; }, 3000); }); } // --- SCRIPT INITIALIZATION --- try { initializeThreeJs(); setupNavigation(); setupPortfolioCards(); setupContactForm(); } catch (error) { console.error("A critical error occurred in the main script:", error); }