HTMLify
script.js
Views: 18 | Author: karbonsites
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 | document.addEventListener('DOMContentLoaded', () => { // ===================================================================== // 1. CORE UTILITIES (GSAP, Locomotive Scroll, SplitText Setup) // ===================================================================== // Initialize Locomotive Scroll const scroll = new Locomotive.Scroll({ el: document.querySelector('[data-scroll-container]'), smooth: true, lerp: 0.1, getDirection: true, smartphone: { smooth: true, lerp: 0.1 }, tablet: { smooth: true, lerp: 0.1 } }); // Update Locomotive Scroll on window resize window.addEventListener('resize', () => { scroll.update(); }); // Initialize GSAP ScrollTrigger gsap.registerPlugin(ScrollTrigger, SplitText); // Text Reveal Animation (Applies to elements with data-split-text) document.querySelectorAll('[data-split-text]').forEach(el => { const text = new SplitText(el, { type: "words, chars" }); gsap.from(text.chars, { opacity: 0, y: 50, rotationX: 90, stagger: 0.03, duration: 0.8, ease: "power3.out", scrollTrigger: { trigger: el, start: "top 80%", toggleActions: "play none none reverse" } }); // Apply simple hover effect for title-main using the ::before content if (el.classList.contains('title-main')) { const dataText = el.getAttribute('data-text'); const beforeEl = el.querySelector('::before') || el.parentElement.querySelector('.title-main::before'); // Since we can't directly manipulate ::before content via JS easily on existing elements, // we apply the hover effect directly to the main element for the title glow/pulse effect. gsap.to(el, { color: 'white', duration: 0.5, paused: true, onUpdate: () => { // Simple text color shift on scroll/hover is enough for this structure } }); } }); // Apply is-reveal class for general fade-ins (Locomotive Scroll integration) gsap.utils.toArray("[data-scroll]").forEach(section => { gsap.from(section, { opacity: 0, y: 50, duration: 1, ease: "power3.out", scrollTrigger: { trigger: section, start: "top 90%", toggleActions: "play none none reverse", // We don't need to set end or markers here as Locomotive handles the main scroll physics } }); }); // ===================================================================== // 2. HERO SECTION LOGIC (Three.js Abstract Geometry) // ===================================================================== const heroContainer = document.getElementById('hero-3d-container'); let scene, camera, renderer, geometry, material, instancedMesh, mouse; const NUM_NODES = 150; function initHero3D() { const width = heroContainer.clientWidth; const height = heroContainer.clientHeight; // Scene, Camera, Renderer scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.setPixelRatio(window.devicePixelRatio); heroContainer.appendChild(renderer.domElement); // Lighting (Subtle) const ambientLight = new THREE.AmbientLight(0x404040, 5); scene.add(ambientLight); // Geometry (Low-Poly Sphere/Icosahedron for nodes) const sphereGeo = new THREE.IcosahedronGeometry(0.5, 1); material = new THREE.MeshStandardMaterial({ color: 0x00FFB3, emissive: 0x00FFB3, emissiveIntensity: 0.5, roughness: 0.5, metalness: 0.8, }); // Instanced Mesh for performance instancedMesh = new THREE.InstancedMesh(sphereGeo, material, NUM_NODES); scene.add(instancedMesh); // Position and Scale Storage const basePositions = []; const baseScales = []; const dummy = new THREE.Object3D(); for (let i = 0; i < NUM_NODES; i++) { // Initial random positions spread out in a cube const x = (Math.random() - 0.5) * 40; const y = (Math.random() - 0.5) * 40; const z = (Math.random() - 0.5) * 40; basePositions.push(new THREE.Vector3(x, y, z)); baseScales.push(Math.random() * 0.5 + 0.1); // Initial matrix setup dummy.position.set(x, y, z); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } // Mouse interaction setup mouse = new THREE.Vector2(); const raycaster = new THREE.Raycaster(); const center = new THREE.Vector3(0, 0, 0); // Camera positioning camera.position.z = 25; // GSAP ScrollTrigger for Entrance and Scroll Deconstruction gsap.set(instancedMesh.rotation, { x: 0, y: 0, z: 0 }); // Entrance Animation (Explode from center) gsap.from(instancedMesh.rotation, { x: Math.PI * 2, y: Math.PI * 2, duration: 2, ease: "power3.out" }); // Scroll Trigger: Deconstruction gsap.to(instancedMesh.rotation, { x: Math.PI * 4, y: Math.PI * 4, ease: "none", scrollTrigger: { trigger: "#hero", start: "top top", end: "bottom top", scrub: 1, onUpdate: (self) => { // Apply subtle scale reduction based on scroll progress const scaleFactor = 1 - self.progress * 0.5; instancedMesh.scale.set(scaleFactor, scaleFactor, scaleFactor); } } }); // Animation Loop function animate() { requestAnimationFrame(animate); // 1. Mouse Proximity Effect (Pulse) raycaster.setFromCamera(mouse, camera); const matrixWorld = instancedMesh.matrixWorld.clone(); const intersectionPoint = new THREE.Vector3(); let hasHit = false; for (let i = 0; i < NUM_NODES; i++) { dummy.applyMatrix4(instancedMesh.getMatrixAt(i, dummy.matrix)); intersectionPoint.copy(dummy.position); dummy.updateMatrixWorld(); const distance = intersectionPoint.distanceTo(center); let scale = baseScales[i]; // Check distance to mouse ray (simplified check for performance) const screenX = mouse.x * 2 - 1; const screenY = -(mouse.y * 2 - 1); // We check if the mouse is generally near the center, and if so, pulse everything slightly. const mouseProximity = 1 - Math.min(1, Math.max(0, (Math.abs(mouse.x) + Math.abs(mouse.y)) * 1.5)); scale += mouseProximity * 0.8; // Apply scale update dummy.scale.set(scale, scale, scale); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } instancedMesh.instanceMatrix.needsUpdate = true; // 2. Constant Rotation instancedMesh.rotation.y += 0.0005; instancedMesh.rotation.x += 0.0002; renderer.render(scene, camera); } // Handle mouse movement for proximity effect function onMouseMove(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; } window.addEventListener('mousemove', onMouseMove); window.addEventListener('resize', onResizeHero); function onResizeHero() { const w = heroContainer.clientWidth; const h = heroContainer.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); } animate(); } // Only initialize 3D if we are on the index page if (heroContainer) { initHero3D(); } // ===================================================================== // 3. EXPERIENCE SECTION LOGIC (Vertical SVG Path Tracing) // ===================================================================== const timelinePath = document.getElementById('timeline-path'); if (timelinePath) { // Calculate total length of the path const length = timelinePath.getTotalLength(); timelinePath.style.strokeDashoffset = length; gsap.to(timelinePath, { strokeDashoffset: 0, duration: 3, // Initial draw duration ease: "power2.out", scrollTrigger: { trigger: "#experience", start: "top 50%", end: "bottom 20%", scrub: 1, onEnter: () => { // Ensure it starts drawing when entering view timelinePath.style.strokeDashoffset = length; } } }); } // ===================================================================== // 4. PROJECTS SECTION LOGIC (Horizontal Scroll & Raycasting) // ===================================================================== const projectScrollWrapper = document.querySelector('.horizontal-scroller'); if (projectScrollWrapper) { // Configure horizontal scrolling via Locomotive Scroll properties projectScrollWrapper.setAttribute('data-scroll-direction', 'horizontal'); projectScrollWrapper.setAttribute('data-scroll-speed', '1'); // Raycasting / 3D Lift Effect on Project Cards const projectCards = document.querySelectorAll('.project-card'); let raycasterScene, raycasterCamera, raycasterRenderer, raycasterMesh, raycasterMouse; function initRaycasting() { const container = document.querySelector('.project-container'); const raycastTargets = document.querySelectorAll('[data-raycast-target]'); // Set up Three.js scene for raycasting effects on cards raycasterScene = new THREE.Scene(); raycasterCamera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 1000); raycasterCamera.position.z = 5; // Use a simple plane geometry to represent the image placeholder const planeGeo = new THREE.PlaneGeometry(3.5, 2.5, 1, 1); const planeMat = new THREE.MeshBasicMaterial({ color: 0x222222, wireframe: true, transparent: true, opacity: 0.5 }); raycasterMesh = new THREE.InstancedMesh(planeGeo, planeMat, raycastTargets.length); raycasterScene.add(raycasterMesh); raycasterMouse = new THREE.Vector2(); const dummy = new THREE.Object3D(); raycastTargets.forEach((target, index) => { // Initial matrix setup (offsetting them horizontally) dummy.position.x = (index - (raycastTargets.length - 1) / 2) * 4; dummy.updateMatrix(); raycasterMesh.setMatrixAt(index, dummy.matrix); }); raycasterMesh.instanceMatrix.needsUpdate = true; function animateRaycaster() { requestAnimationFrame(animateRaycaster); // Convert normalized device coordinates (mouse) to raycaster coordinates raycasterMouse.x = (event.clientX / window.innerWidth) * 2 - 1; raycasterMouse.y = - (event.clientY / window.innerHeight) * 2 + 1; // Simple proximity scaling based on mouse position relative to screen center const scaleFactor = 1 + (Math.abs(raycasterMouse.x) + Math.abs(raycasterMouse.y)) * 0.1; for (let i = 0; i < raycastTargets.length; i++) { dummy.applyMatrix4(raycasterMesh.getMatrixAt(i, dummy.matrix)); // Apply subtle rotation/lift based on global mouse proximity const targetCard = projectCards[i]; const rect = targetCard.getBoundingClientRect(); // Calculate normalized mouse position relative to the card const mouseXRel = (event.clientX - (rect.left + rect.width / 2)) / (rect.width / 2); const mouseYRel = (event.clientY - (rect.top + rect.height / 2)) / (rect.height / 2); // Tilt effect proportional to distance from card center const tiltX = mouseYRel * 0.3; const tiltY = mouseXRel * -0.3; dummy.rotation.set(tiltX, tiltY, 0); // Overall scale boost on hover (Requires checking if mouse is over card, which is complex here, so we simplify to global motion) dummy.scale.set(scaleFactor, scaleFactor, scaleFactor); dummy.updateMatrix(); raycasterMesh.setMatrixAt(i, dummy.matrix); } raycasterMesh.instanceMatrix.needsUpdate = true; // renderer.render(raycasterScene, raycasterCamera); // Commented out as we rely on CSS transforms for the final look } // Since we are using Locomotive Scroll for *horizontal* scroll, we map the horizontal scroll position // directly to the rotation of the entire horizontal container, simulating the 3D lift/depth. gsap.to(container, { rotationY: (i, target) => { // Map the scroll progress (which is horizontal) to rotation const progress = ScrollTrigger.isInViewport(target, {partial: true}) ? (ScrollTrigger.maxScroll(window) / (ScrollTrigger.maxScroll(window) + 1)) : 0; return progress * -20; // Max tilt of -20 degrees }, transformOrigin: "center center", ease: "none", scrollTrigger: { trigger: '#projects', start: "top bottom", end: "bottom top", scrub: 1, } }); // Add simple hover scale effect on cards projectCards.forEach(card => { card.addEventListener('mouseenter', () => { gsap.to(card, { scale: 1.03, duration: 0.4, ease: "power1.out" }); }); card.addEventListener('mouseleave', () => { gsap.to(card, { scale: 1, duration: 0.4, ease: "power1.out" }); }); }); // animateRaycaster(); // Disabled raycaster renderer for simplicity in this single-page structure, focusing on CSS/GSAP transforms. } if (document.body.contains(projectScrollWrapper)) { initRaycasting(); } } // ===================================================================== // 5. CONTACT SECTION LOGIC (3D Terminal Interface) // ===================================================================== const terminalHost = document.getElementById('terminal-3d-host'); if (terminalHost) { let termScene, termCamera, termRenderer, termCube, termDummy; function initTerminal3D() { const width = terminalHost.clientWidth; const height = terminalHost.clientHeight; termScene = new THREE.Scene(); termCamera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100); termRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); termRenderer.setSize(width, height); termRenderer.setPixelRatio(window.devicePixelRatio); terminalHost.appendChild(termRenderer.domElement); // Terminal Cube Geometry (Represents the floating console) const cubeGeo = new THREE.BoxGeometry(3, 2.5, 0.5); const cubeMat = new THREE.MeshStandardMaterial({ color: 0x00FFB3, emissive: 0x00FFB3, emissiveIntensity: 0.1, wireframe: true, transparent: true, opacity: 0.8 }); termCube = new THREE.Mesh(cubeGeo, cubeMat); termScene.add(termCube); termCamera.position.z = 5; termDummy = new THREE.Object3D(); // GSAP ScrollTrigger: Tilting based on mouse movement (simulated tilt based on scroll position) gsap.to(termCube.rotation, { x: 0.5, // Slight tilt forward y: 0.5, // Slight rotation right z: 0.2, // Slight roll ease: "power2.out", scrollTrigger: { trigger: "#contact", start: "top 50%", end: "bottom 50%", scrub: 1.5, onUpdate: (self) => { // Tilt based on mouse position relative to viewport center const mouseX = (window.event ? window.event.clientX : window.innerWidth / 2) / window.innerWidth - 0.5; const mouseY = (window.event ? window.event.clientY : window.innerHeight / 2) / window.innerHeight - 0.5; termCube.rotation.x = 0.5 + mouseY * 1.5; // Tilt up/down based on vertical mouse termCube.rotation.y = 0.5 + mouseX * -1.5; // Tilt left/right based on horizontal mouse } } }); function animateTerminal() { requestAnimationFrame(animateTerminal); termCube.rotation.y += 0.001; termRenderer.render(termScene, termCamera); } animateTerminal(); } // Add listener for mouse move to update terminal tilt (only when on contact section) let currentSection = 'hero'; scroll.on('scroll', (instance) => { const offset = instance.scroll.y; const contactTop = document.getElementById('contact').offsetTop - 100; const heroBottom = document.getElementById('hero').offsetHeight; if (offset > contactTop) { currentSection = 'contact'; } else if (offset < heroBottom) { currentSection = 'hero'; } else { currentSection = 'other'; } }); window.addEventListener('mousemove', (event) => { if(currentSection === 'contact' && termCube) { // Re-trigger GSAP update to capture mouse input for tilt if section is active // Note: GSAP ScrollTrigger handles the main scroll-based rotation, this just handles the immediate mouse tilt const mouseX = event.clientX / window.innerWidth - 0.5; const mouseY = event.clientY / window.innerHeight - 0.5; termCube.rotation.x = 0.5 + mouseY * 1.5; termCube.rotation.y = 0.5 + mouseX * -1.5; } }); window.addEventListener('resize', () => { if(termRenderer) { const w = terminalHost.clientWidth; const h = terminalHost.clientHeight; termCamera.aspect = w / h; termCamera.updateProjectionMatrix(); termRenderer.setSize(w, h); } }); initTerminal3D(); } // Final update to ensure layout is correct after all elements load scroll.update(); }); |