1// ===== Hero Canvas Animation ===== 2(function () { 3 const canvas = document.getElementById('heroCanvas'); 4 if (!canvas) return; 5 const ctx = canvas.getContext('2d'); 6 let w, h, particles, mouse; 7 8 const COLORS = [ 9 'rgba(124, 58, 237, ', // purple 10 'rgba(236, 72, 153, ', // pink 11 'rgba(14, 165, 233, ', // blue 12 'rgba(167, 139, 250, ', // light purple 13 'rgba(244, 114, 182, ', // light pink 14 ]; 15 16 const SHAPES = ['circle', 'diamond', 'triangle', 'square']; 17 18 mouse = { x: -1000, y: -1000 }; 19 20 function resize() { 21 const hero = canvas.parentElement; 22 w = canvas.width = hero.offsetWidth; 23 h = canvas.height = hero.offsetHeight; 24 } 25 26 function createParticle() { 27 const color = COLORS[Math.floor(Math.random() * COLORS.length)]; 28 return { 29 x: Math.random() * w, 30 y: Math.random() * h, 31 vx: (Math.random() - 0.5) * 0.4, 32 vy: (Math.random() - 0.5) * 0.4, 33 size: Math.random() * 4 + 2, 34 color: color, 35 opacity: Math.random() * 0.5 + 0.1, 36 shape: SHAPES[Math.floor(Math.random() * SHAPES.length)], 37 pulse: Math.random() * Math.PI * 2, 38 pulseSpeed: Math.random() * 0.02 + 0.005, 39 }; 40 } 41 42 function init() { 43 resize(); 44 const count = Math.min(Math.floor((w * h) / 12000), 80); 45 particles = []; 46 for (let i = 0; i < count; i++) { 47 particles.push(createParticle()); 48 } 49 } 50 51 function drawShape(p, size) { 52 ctx.beginPath(); 53 switch (p.shape) { 54 case 'circle': 55 ctx.arc(p.x, p.y, size, 0, Math.PI * 2); 56 break; 57 case 'diamond': 58 ctx.moveTo(p.x, p.y - size); 59 ctx.lineTo(p.x + size, p.y); 60 ctx.lineTo(p.x, p.y + size); 61 ctx.lineTo(p.x - size, p.y); 62 ctx.closePath(); 63 break; 64 case 'triangle': 65 ctx.moveTo(p.x, p.y - size); 66 ctx.lineTo(p.x + size * 0.866, p.y + size * 0.5); 67 ctx.lineTo(p.x - size * 0.866, p.y + size * 0.5); 68 ctx.closePath(); 69 break; 70 case 'square': 71 const half = size * 0.7; 72 ctx.rect(p.x - half, p.y - half, half * 2, half * 2); 73 break; 74 } 75 } 76 77 function animate() { 78 ctx.clearRect(0, 0, w, h); 79 80 // Draw connections 81 for (let i = 0; i < particles.length; i++) { 82 for (let j = i + 1; j < particles.length; j++) { 83 const dx = particles[i].x - particles[j].x; 84 const dy = particles[i].y - particles[j].y; 85 const dist = Math.sqrt(dx * dx + dy * dy); 86 if (dist < 150) { 87 const alpha = (1 - dist / 150) * 0.08; 88 ctx.strokeStyle = `rgba(167, 139, 250, ${alpha})`; 89 ctx.lineWidth = 0.5; 90 ctx.beginPath();
91 ctx.moveTo(particles[i].x, particles[i].y); 92 ctx.lineTo(particles[j].x, particles[j].y); 93 ctx.stroke(); 94 } 95 } 96 } 97 98 // Update and draw particles 99 particles.forEach(p => { 100 // Mouse interaction â gentle push away 101 const mdx = p.x - mouse.x; 102 const mdy = p.y - mouse.y; 103 const mDist = Math.sqrt(mdx * mdx + mdy * mdy); 104 if (mDist < 120) { 105 const force = (120 - mDist) / 120 * 0.3; 106 p.vx += (mdx / mDist) * force; 107 p.vy += (mdy / mDist) * force; 108 } 109 110 // Damping 111 p.vx *= 0.99; 112 p.vy *= 0.99; 113 114 p.x += p.vx; 115 p.y += p.vy; 116 117 // Wrap around edges 118 if (p.x < -20) p.x = w + 20; 119 if (p.x > w + 20) p.x = -20; 120 if (p.y < -20) p.y = h + 20; 121 if (p.y > h + 20) p.y = -20; 122 123 // Pulse 124 p.pulse += p.pulseSpeed; 125 const pulseAlpha = p.opacity + Math.sin(p.pulse) * 0.15; 126 const pulseSize = p.size + Math.sin(p.pulse) * 0.8; 127 128 // Draw glow 129 const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, pulseSize * 4); 130 gradient.addColorStop(0, p.color + (pulseAlpha * 0.3) + ')'); 131 gradient.addColorStop(1, p.color + '0)'); 132 ctx.fillStyle = gradient; 133 ctx.beginPath(); 134 ctx.arc(p.x, p.y, pulseSize * 4, 0, Math.PI * 2); 135 ctx.fill(); 136 137 // Draw shape 138 ctx.fillStyle = p.color + pulseAlpha + ')'; 139 drawShape(p, pulseSize); 140 ctx.fill(); 141 }); 142 143 requestAnimationFrame(animate); 144 } 145 146 // Mouse tracking on hero 147 canvas.parentElement.addEventListener('mousemove', (e) => { 148 const rect = canvas.parentElement.getBoundingClientRect(); 149 mouse.x = e.clientX - rect.left; 150 mouse.y = e.clientY - rect.top; 151 }); 152 153 canvas.parentElement.addEventListener('mouseleave', () => { 154 mouse.x = -1000; 155 mouse.y = -1000; 156 }); 157 158 window.addEventListener('resize', init); 159 init(); 160 animate(); 161})(); 162 163// Navbar scroll effect 164const navbar = document.getElementById('navbar'); 165window.addEventListener('scroll', () => { 166 navbar.classList.toggle('scrolled', window.scrollY > 50); 167}); 168 169// About section app icons (random 4 + randomized sizes) 170(function () { 171 const aboutCardStack = document.getElementById('aboutCardStack'); 172 if (!aboutCardStack) return; 173 174 const aboutImagePool = [ 175 { 176 src: 'https://play-lh.googleusercontent.com/Cps3lut7md1MgGTMT7kXO05JaskmCebmZJmLaMswdqODf-4zTU20Va2k2d1rnUA72KTn', 177 alt: 'Ludo Buzz', 178 }, 179 { 180 src: 'https://play-lh.googleusercontent.com/dRdLdkFaRiowGgt7Nap2v4cIU03M4dfVuZpPoWxHNBf3zFkC_MrN65RFOq8pdTXEojC3cFuNLQ0V9WVz5N7k5Q', 181 alt: 'Ludo Gold', 182 }, 183 { 184 src: 'https://play-lh.googleusercontent.com/FPl6546xDukrkhU7VPL4-41wT1wmrTHc6HW9JCCRX01pLWSQ4YfBoO2cBuLzkmxJLN0v2EtWjKdWeih53LYHhg', 185 alt: 'Parchisi Buzz', 186 }, 187 { 188 src: 'https://play-lh.googleusercontent.com/3uQTn1-bZx9mFyrcgXotK349r_J6snaPYqexJekl-y6lBGlzIiUuJh9TAcMnCFi4Ke1f', 189 alt: 'DOP: Draw One Part', 190 }, 191 { 192 src: 'https://play-lh.googleusercontent.com/lr5ED7GdGAx9OIGBjrbqR0OJBNd1-EhWrRaGwgHtOCi9-fHh4vUCDvD4lje26r_U-1c', 193 alt: 'Parking Jam', 194 }, 195 { 196 src: 'https://play-lh.googleusercontent.com/27N9UZRWKC_Ef0e_Z_fRqJW4C9_7Tkjy3OdKe6UlBCuVrih5N5WYXE03tmN4lt4e-jQ', 197 alt: 'Color Block Blast', 198 }, 199 { 200 src: 'https://play-lh.googleusercontent.com/f2uLkJDS8Cbk2b6z0gvsSppOWzO9FpxMF92eqAkY61qyEoO6MJRjcd86mBC53oHN7u7S', 201 alt: '3 Tile Match', 202 }, 203 { 204 src: 'https://play-lh.googleusercontent.com/N5CVW2ARYQOe085RpWu2ZWQQYrTOV3mYxlETYq-vDq7eeZnM1plxtCmgkRpf64WjQw', 205 alt: '2 Pics 1 Word', 206 }, 207 { 208 src: 'https://play-lh.googleusercontent.com/aOxzhfrBomuIzT4eKS07VQI_mS368I2TmA0ydwfxcg6zLV3pl1UHRRt-vo5QM-L6Muc', 209 alt: 'Logo Puzzle', 210 }, 211 { 212 src: 'https://play-lh.googleusercontent.com/ttG6kY-qY4M4EreqUXC9WOxm7xQennftJYBpdQy2US_mLpNcvrV-caUOnfKO5nQl09oE', 213 alt: '4 Pics 1 Word', 214 }, 215 ]; 216 217 const cardSlots = ['card-1', 'card-2', 'card-3', 'card-4']; 218 const swapDelayMinMs = 2600; 219 const swapDelayMaxMs = 8200; 220 221 function shuffle(items) { 222 const arr = [...items]; 223 for (let i = arr.length - 1; i > 0; i -= 1) { 224 const j = Math.floor(Math.random() * (i + 1)); 225 [arr[i], arr[j]] = [arr[j], arr[i]]; 226 } 227 return arr; 228 } 229 230 function getRandomInt(min, max) { 231 return Math.floor(Math.random() * (max - min + 1)) + min; 232 } 233 234 function getRandomDelay() { 235 return getRandomInt(swapDelayMinMs, swapDelayMaxMs); 236 } 237 238 function randomizeCardStyle(card, img) { 239 const cardSize = getRandomInt(104, 136); 240 const iconSize = Math.round(cardSize * 0.68); 241 const cardTilt = (Math.random() * 12 - 6).toFixed(1); // -6deg to +6deg 242 243 card.style.setProperty('--card-size', `${cardSize}px`); 244 card.style.setProperty('--image-size', `${iconSize}px`); 245 card.style.setProperty('--card-tilt', `${cardTilt}deg`); 246 img.width = iconSize; 247 img.height = iconSize; 248 } 249 250 function pickNextImage(excludedAlts, currentAlt) {
251 let candidates = aboutImagePool.filter((item) => !excludedAlts.has(item.alt) && item.alt !== currentAlt); 252 if (candidates.length === 0) { 253 candidates = aboutImagePool.filter((item) => item.alt !== currentAlt); 254 } 255 if (candidates.length === 0) return null; 256 return candidates[getRandomInt(0, candidates.length - 1)]; 257 } 258 259 function preloadImage(src, onDone) { 260 const preloader = new Image(); 261 preloader.decoding = 'async'; 262 preloader.onload = onDone; 263 preloader.onerror = onDone; 264 preloader.src = src; 265 } 266 267 const selectedImages = shuffle(aboutImagePool).slice(0, cardSlots.length); 268 const cardStates = []; 269 aboutCardStack.innerHTML = ''; 270 271 selectedImages.forEach((image, index) => { 272 const card = document.createElement('div'); 273 card.className = `about-floating-card ${cardSlots[index]}`; 274 275 const img = document.createElement('img'); 276 img.src = image.src; 277 img.alt = image.alt; 278 img.loading = 'lazy'; 279 img.decoding = 'async'; 280 img.className = 'about-card-img'; 281 282 randomizeCardStyle(card, img); 283 284 card.appendChild(img); 285 aboutCardStack.appendChild(card); 286 cardStates.push({ card, img, image }); 287 }); 288 289 function scheduleSwap(index) { 290 window.setTimeout(() => { 291 if (document.hidden) { 292 scheduleSwap(index); 293 return; 294 } 295 296 const state = cardStates[index]; 297 if (!state) return; 298 299 const otherAlts = new Set( 300 cardStates 301 .filter((_, i) => i !== index) 302 .map((item) => item.image.alt) 303 ); 304 const nextImage = pickNextImage(otherAlts, state.image.alt); 305 if (!nextImage) { 306 scheduleSwap(index); 307 return; 308 } 309 310 preloadImage(nextImage.src, () => { 311 state.img.classList.add('is-hidden'); 312 window.setTimeout(() => { 313 state.image = nextImage; 314 state.img.src = nextImage.src; 315 state.img.alt = nextImage.alt; 316 randomizeCardStyle(state.card, state.img); 317 window.requestAnimationFrame(() => { 318 state.img.classList.remove('is-hidden'); 319 }); 320 scheduleSwap(index); 321 }, 220); 322 }); 323 }, getRandomDelay()); 324 } 325 326 cardStates.forEach((_, index) => { 327 scheduleSwap(index); 328 }); 329})(); 330 331// Mobile menu toggle 332const menuToggle = document.getElementById('menuToggle'); 333const navLinks = document.getElementById('navLinks'); 334const navOverlay = document.getElementById('navOverlay'); 335 336function closeMenu() { 337 navLinks.classList.remove('active'); 338 menuToggle.classList.remove('active'); 339 navOverlay.classList.remove('active'); 340} 341 342menuToggle.addEventListener('click', () => { 343 const isOpen = navLinks.classList.toggle('active'); 344 menuToggle.classList.toggle('active'); 345 navOverlay.classList.toggle('active', isOpen); 346}); 347 348navOverlay.addEventListener('click', closeMenu); 349 350// Close mobile menu on link click 351navLinks.querySelectorAll('a').forEach(link => { 352 link.addEventListener('click', closeMenu); 353}); 354 355// Scroll animations 356const observerOptions = { 357 threshold: 0.1, 358 rootMargin: '0px 0px -50px 0px' 359}; 360 361const observer = new IntersectionObserver((entries) => { 362 entries.forEach(entry => { 363 if (entry.isIntersecting) { 364 entry.target.classList.add('visible'); 365 } 366 }); 367}, observerOptions); 368 369// Add fade-in class to elements 370document.querySelectorAll('.game-card, .feature-card, .about-content, .about-visual, .contact-form, .contact-info, .section-header, .category-title').forEach(el => { 371 el.classList.add('fade-in'); 372 observer.observe(el); 373}); 374 375// Contact form handler 376const contactForm = document.getElementById('contactForm'); 377if (contactForm) { 378 contactForm.addEventListener('submit', (e) => { 379 e.preventDefault(); 380 const btn = contactForm.querySelector('button[type="submit"]'); 381 if (!btn) return; 382 const originalText = btn.textContent; 383 btn.textContent = 'Message Sent!'; 384 btn.style.background = 'linear-gradient(135deg, #10b981, #059669)'; 385 setTimeout(() => { 386 btn.textContent = originalText; 387 btn.style.background = ''; 388 contactForm.reset(); 389 }, 3000); 390 }); 391} 392 393// Active nav link on scroll 394const sections = document.querySelectorAll('section[id]'); 395window.addEventListener('scroll', () => { 396 const scrollY = window.scrollY + 100; 397 sections.forEach(section => { 398 const sectionTop = section.offsetTop; 399 const sectionHeight = section.offsetHeight; 400 const sectionId = section.getAttribute('id'); 401 const navLink = document.querySelector(`.nav-links a[href="#${sectionId}"]`); 402 if (navLink) { 403 if (scrollY >= sectionTop && scrollY < sectionTop + sectionHeight) { 404 navLink.style.color = 'var(--text)'; 405 } else { 406 navLink.style.color = ''; 407 } 408 } 409 }); 410});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.