diff --git a/2048_Audio.js b/2048_Audio.js index 807377b..39ac26e 100644 --- a/2048_Audio.js +++ b/2048_Audio.js @@ -1,25 +1,43 @@ -/* ------------------------ - 2. AUDIO MANAGER - ------------------------ */ +/* ========================================== + 2048 AUDIO - SOUND MANAGEMENT SYSTEM + ========================================== + fitur: + 1. 3 Audio objects (bg, pop, merge) + 2. Volume control terpisah untuk setiap audio + 3. Icon dinamis sesuai level volume + 4. Panel volume dengan inputLock integration + ========================================== */ + +/* ========================================== + AUDIO OBJECTS + ========================================== */ const audio = { - bg: new Audio("Background_Music.mp3"), - pop: new Audio("Pop.mp3"), - merge: new Audio("Merge.mp3") + bg: new Audio("Background_Music.mp3"), // Background music + pop: new Audio("Pop.mp3"), // Sound saat tile spawn + merge: new Audio("Merge.mp3") // Sound saat tile merge }; -audio.bg.loop = true; +audio.bg.loop = true; // Background music loop terus -// Update audio volumes based on state & sliders +/* ========================================== + UPDATE VOLUME DARI STATE & SLIDERS + ========================================== */ function updateAudioVolumes() { + // Formula: volume = enabled ? (slider / 100) : 0 audio.bg.volume = soundState.bg ? (volumeState.music / 100) : 0; audio.pop.volume = soundState.pop ? (volumeState.pop / 100) : 0; audio.merge.volume = soundState.merge ? (volumeState.merge / 100) : 0; } +/* ========================================== + PLAY BACKGROUND MUSIC (dengan unlock) + ========================================== */ function tryPlayBg() { if (!soundState.bg || volumeState.music === 0) return; + // Coba play, kalau di-block browser (autoplay policy) audio.bg.play().catch(() => { + // Setup unlock: tunggu user interaction const unlock = () => { if (soundState.bg && volumeState.music > 0) audio.bg.play().catch(()=>{}); window.removeEventListener("keydown", unlock); @@ -30,19 +48,24 @@ function tryPlayBg() { }); } -/* Safe playSound with mute check */ +/* ========================================== + PLAY SOUND (dengan mute check) + ========================================== */ function playSound(soundObj) { try { + // Guard: cek mute state sebelum play if (soundObj === audio.pop && (!soundState.pop || volumeState.pop === 0)) return; if (soundObj === audio.merge && (!soundState.merge || volumeState.merge === 0)) return; if (soundObj === audio.bg && (!soundState.bg || volumeState.music === 0)) return; - soundObj.currentTime = 0; - soundObj.play().catch(() => {}); + soundObj.currentTime = 0; // Restart dari awal + soundObj.play().catch(() => {}); // Play with error handling } catch (e) {} } -// Initialize Volume Sliders +/* ========================================== + INIT VOLUME SLIDERS - Setup Event Listeners + ========================================== */ function initVolumeControl() { updateAudioVolumes(); @@ -50,28 +73,33 @@ function initVolumeControl() { const popSlider = document.getElementById('vol-pop'); const mergeSlider = document.getElementById('vol-merge'); + /* --- MUSIC SLIDER --- */ if (musicSlider) { + // Load nilai dari localStorage musicSlider.value = volumeState.music; updateSliderFill(musicSlider, volumeState.music); document.getElementById('vol-music-display').textContent = volumeState.music + '%'; + // Event listener saat slider digeser musicSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value); - volumeState.music = val; - audio.bg.volume = val / 100; // Direct update - localStorage.setItem('vol_music', val); + volumeState.music = val; // Update state + audio.bg.volume = val / 100; // Set volume langsung + localStorage.setItem('vol_music', val); // Save ke browser document.getElementById('vol-music-display').textContent = val + '%'; - updateSliderFill(e.target, val); - updateMainSoundIcon(); + updateSliderFill(e.target, val); // Update visual fill + updateMainSoundIcon(); // Update icon + // Auto play/pause background music if (val > 0 && audio.bg.paused && soundState.bg) { - tryPlayBg(); + tryPlayBg(); // Play kalau volume > 0 } else if (val === 0) { - audio.bg.pause(); + audio.bg.pause(); // Pause kalau volume = 0 (muted) } }); } + /* --- POP SLIDER (sama seperti music) --- */ if (popSlider) { popSlider.value = volumeState.pop; updateSliderFill(popSlider, volumeState.pop); @@ -88,6 +116,7 @@ function initVolumeControl() { }); } + /* --- MERGE SLIDER (sama seperti music) --- */ if (mergeSlider) { mergeSlider.value = volumeState.merge; updateSliderFill(mergeSlider, volumeState.merge); @@ -105,86 +134,110 @@ function initVolumeControl() { } updateMainSoundIcon(); - setupVolumePanelEvents(); + setupVolumePanelEvents(); // Setup panel interactions } +/* ========================================== + SETUP PANEL EVENTS - Open/Close Logic + ========================================== + Ini yang nge-link dengan inputLocked di Controls + ========================================== */ function setupVolumePanelEvents() { const btnSoundMain = document.getElementById('btn-sound-main'); const volumePanel = document.getElementById('volume-panel'); const volumeBackdrop = document.getElementById('volume-backdrop'); if (btnSoundMain && volumePanel) { + // Event: Klik tombol sound main btnSoundMain.addEventListener('click', (e) => { - e.stopPropagation(); + e.stopPropagation(); // Jangan trigger event di parent const isActive = volumePanel.classList.contains('active'); if (isActive) { + // TUTUP PANEL volumePanel.classList.remove('active'); - if (volumeBackdrop) volumeBackdrop.classList.remove('active'); - inputLocked = false; // 🔓 buka panel -> input kembali + if (volumeBackdrop) volumeBackdrop.classList.remove('active'); + inputLocked = false; // UNLOCK - game input aktif lagi } else { + // BUKA PANEL volumePanel.classList.add('active'); if (volumeBackdrop) volumeBackdrop.classList.add('active'); - inputLocked = true; // 🔒 panel sound aktif -> swipe & keyboard mati + inputLocked = true; // LOCK - blokir swipe & keyboard } }); - // Close when clicking backdrop + // Event: Klik backdrop (tutup panel) volumeBackdrop.addEventListener('click', () => { volumePanel.classList.remove('active'); volumeBackdrop.classList.remove('active'); - inputLocked = false; // 🔓 unlock - }); - + inputLocked = false; // UNLOCK + }); - // Close when clicking outside (desktop) + // Event: Klik di luar panel (desktop) document.addEventListener('click', (e) => { if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target) && (!volumeBackdrop || !volumeBackdrop.contains(e.target))) { volumePanel.classList.remove('active'); if (volumeBackdrop) volumeBackdrop.classList.remove('active'); - inputLocked = false; // 🔓 unlock input + inputLocked = false; // UNLOCK } }); + // Event: Klik di dalam panel (jangan tutup) volumePanel.addEventListener('click', (e) => { - e.stopPropagation(); + e.stopPropagation(); // Prevent close }); } } +/* ========================================== + UPDATE VISUAL FILL SLIDER + ========================================== */ function updateSliderFill(slider, value) { + // Set CSS custom property untuk animasi fill + // Dipakai di CSS: background: linear-gradient(...) slider.style.setProperty('--value', value + '%'); } +/* ========================================== + UPDATE ICON DINAMIS - Sesuai Volume Level + ========================================== */ function updateMainSoundIcon() { const btnMain = document.getElementById('btn-sound-main'); if (!btnMain) return; + // Ambil semua icon const iconFull = btnMain.querySelector('.sound-full'); const iconMedium = btnMain.querySelector('.sound-medium'); const iconLow = btnMain.querySelector('.sound-low'); const iconMuted = btnMain.querySelector('.sound-muted'); + // Hitung rata-rata volume dari 3 slider const totalVolume = volumeState.music + volumeState.pop + volumeState.merge; const avgVolume = totalVolume / 3; + // Hide semua icon dulu if (iconFull) iconFull.style.display = 'none'; if (iconMedium) iconMedium.style.display = 'none'; if (iconLow) iconLow.style.display = 'none'; if (iconMuted) iconMuted.style.display = 'none'; + // Show icon yang sesuai dengan level volume if (totalVolume === 0) { + // Semua muted if (iconMuted) iconMuted.style.display = 'block'; btnMain.classList.add('all-muted'); } else { btnMain.classList.remove('all-muted'); if (avgVolume >= 60) { + // Volume tinggi (≥60%) if (iconFull) iconFull.style.display = 'block'; } else if (avgVolume >= 30) { + // Volume sedang (30-59%) if (iconMedium) iconMedium.style.display = 'block'; } else { + // Volume rendah (1-29%) if (iconLow) iconLow.style.display = 'block'; } } diff --git a/2048_Floating_Particles.js b/2048_Floating_Particles.js index c08ce20..1a520fb 100644 --- a/2048_Floating_Particles.js +++ b/2048_Floating_Particles.js @@ -1,48 +1,75 @@ +/* ========================================== + 2048 FLOATING PARTICLES - BACKGROUND DECORATION + ========================================== + fungsi + - createParticle() - Buat partikel yang naik dari bawah + - Self-recycling system (partikel hilang → buat baru) + - Efek drift horizontal untuk gerakan natural + ========================================== */ + // Floating Particles System - Particles rising from bottom (function() { + // Ambil container untuk partikel const container = document.getElementById('floating-particles'); - if (!container) return; - + if (!container) return; // Guard: kalau container nggak ada, skip + + // Config warna partikel (5 warna neon) const particleColors = ['cyan', 'pink', 'purple', 'green', 'orange']; - const particleCount = 25; // Number of particles + const particleCount = 25; // Total partikel yang muncul + /* ========================================== + CREATE PARTICLE - Buat 1 partikel + ========================================== */ function createParticle() { + // Buat element div untuk partikel const particle = document.createElement('div'); - particle.className = `floating-particle ${particleColors[Math.floor(Math.random() * particleColors.length)]}`; - // Random horizontal position + // Pilih warna random dari array + const randomColor = particleColors[Math.floor(Math.random() * particleColors.length)]; + particle.className = `floating-particle ${randomColor}`; + + // POSISI HORIZONTAL RANDOM (0-100%) const leftPos = Math.random() * 100; particle.style.left = leftPos + '%'; - // Random drift amount (horizontal movement during float) - const drift = (Math.random() - 0.5) * 150; // -75px to +75px + // DRIFT HORIZONTAL (gerakan ke kiri/kanan saat naik) + // Range: -75px sampai +75px + const drift = (Math.random() - 0.5) * 150; particle.style.setProperty('--drift', drift + 'px'); - // Random animation duration (slower = more dramatic) - const duration = 8 + Math.random() * 10; // 8-18 seconds + // DURASI ANIMASI RANDOM (8-18 detik) + // Semakin lama = semakin smooth & dramatis + const duration = 8 + Math.random() * 10; particle.style.animationDuration = duration + 's'; - // Random delay for staggered effect + // DELAY RANDOM (0-5 detik) + // Biar nggak semua partikel muncul bareng (staggered) const delay = Math.random() * 5; particle.style.animationDelay = delay + 's'; - // Random size variation - const size = 6 + Math.random() * 8; // 6-14px + // UKURAN RANDOM (6-14px) + const size = 6 + Math.random() * 8; particle.style.width = size + 'px'; particle.style.height = size + 'px'; + // Append ke container container.appendChild(particle); - // Remove and recreate after animation completes + // ♻️ SELF-RECYCLING SYSTEM + // Setelah animasi selesai → hapus & buat baru setTimeout(() => { - particle.remove(); - createParticle(); - }, (duration + delay) * 1000); + particle.remove(); // Hapus partikel lama + createParticle(); // Buat partikel baru (infinite loop) + }, (duration + delay) * 1000); // Total waktu = duration + delay } - // Initialize particles + /* ========================================== + INITIALIZE - Buat semua partikel awal + ========================================== */ + // Loop buat 25 partikel for (let i = 0; i < particleCount; i++) { - // Stagger initial creation + // Stagger creation: jeda 200ms per partikel + // Biar nggak semua muncul sekaligus (smooth) setTimeout(() => createParticle(), i * 200); } -})(); \ No newline at end of file +})(); // IIFE (Immediately Invoked Function Expression) - langsung jalan \ No newline at end of file diff --git a/2048_Visual_Effects.js b/2048_Visual_Effects.js index e09c380..c0f2af4 100644 --- a/2048_Visual_Effects.js +++ b/2048_Visual_Effects.js @@ -1,23 +1,42 @@ -/* ------------------------ - 3. VISUAL EFFECTS - ------------------------ */ +/* ========================================== + 2048 VISUAL EFFECTS - ANIMATION SYSTEM + ========================================== + fungsi utama: + 1. triggerComboEffect() - Efek saat tile merge + 2. showComboPopup() - Popup combo text (x2, x3, x4+) + 3. createParticleBurst() - Ledakan partikel dari tile + 4. createScorePopup() - Score yang terbang ke atas + 5. getTileColor() - Warna sesuai nilai tile + ========================================== */ + +/* ========================================== + TRIGGER COMBO EFFECT - Main Visual Handler + ========================================== + Dipanggil dari move functions di 2048_Logic.js + ========================================== */ function triggerComboEffect(mergedCells, comboCount) { + // Guard: kalau nggak ada tile yang merge, skip if (mergedCells.length === 0) return; + // Loop setiap tile yang di-merge mergedCells.forEach(cell => { const tile = document.getElementById(`${cell.r}-${cell.c}`); if (!tile) return; + // Efek 1: Animasi "merge" (scale + glow) tile.classList.add('merge'); setTimeout(() => tile.classList.remove('merge'), 300); + // Efek 2: Ledakan partikel createParticleBurst(tile); + // Efek 3: Box shadow glow tile.style.boxShadow = '0 0 40px currentColor'; setTimeout(() => { - tile.style.boxShadow = ''; + tile.style.boxShadow = ''; // Reset setelah 300ms }, 300); + // Efek 4: Score popup yang terbang ke atas const rect = tile.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; @@ -25,42 +44,54 @@ function triggerComboEffect(mergedCells, comboCount) { createScorePopup(centerX, centerY, tileValue); }); + // Efek 5: Combo popup kalau merge ≥2 tile sekaligus if (comboCount >= 2) { showComboPopup(comboCount); } } +/* ========================================== + COMBO POPUP - Text "COMBO x2!", "AMAZING x3!", dst + ========================================== */ function showComboPopup(comboCount) { const board = document.getElementById('board'); if (!board) return; + // Hitung posisi tengah board const rect = board.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; + // Buat element popup const popup = document.createElement('div'); popup.className = 'combo-popup'; popup.style.left = centerX + 'px'; popup.style.top = centerY + 'px'; popup.style.position = 'fixed'; popup.style.fontWeight = '900'; - popup.style.pointerEvents = 'none'; - popup.style.zIndex = '9999'; - popup.style.transform = 'translate(-50%, -50%)'; + popup.style.pointerEvents = 'none'; // Nggak bisa diklik + popup.style.zIndex = '9999'; // Di depan semua + popup.style.transform = 'translate(-50%, -50%)'; // Center alignment popup.style.textTransform = 'uppercase'; popup.style.letterSpacing = '3px'; + // Styling berbeda sesuai combo level if (comboCount === 2) { + // COMBO x2 - Hijau neon popup.textContent = 'COMBO x2!'; popup.style.fontSize = '36px'; popup.style.color = '#00ff99'; popup.style.textShadow = '0 0 30px rgba(0, 255, 153, 1), 0 0 50px rgba(0, 255, 153, 0.5)'; + } else if (comboCount === 3) { + // AMAZING x3 - Pink magenta popup.textContent = 'AMAZING x3!'; popup.style.fontSize = '42px'; popup.style.color = '#ff00ff'; popup.style.textShadow = '0 0 35px rgba(255, 0, 255, 1), 0 0 60px rgba(255, 0, 255, 0.6)'; + } else if (comboCount >= 4) { + // PERFECT x4+ - Gold popup.textContent = 'PERFECT x' + comboCount + '!'; popup.style.fontSize = '48px'; popup.style.color = '#ffd700'; @@ -69,6 +100,7 @@ function showComboPopup(comboCount) { document.body.appendChild(popup); + // Animasi: muncul → bounce → hilang popup.animate([ { transform: 'translate(-50%, -50%) scale(0.3) rotate(-10deg)', opacity: 0 }, { transform: 'translate(-50%, -50%) scale(1.3) rotate(5deg)', opacity: 1, offset: 0.3 }, @@ -76,54 +108,66 @@ function showComboPopup(comboCount) { { transform: 'translate(-50%, -50%) scale(0.8) rotate(0deg)', opacity: 0 } ], { duration: 1200, - easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' - }).onfinish = () => popup.remove(); + easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' // Bounce effect + }).onfinish = () => popup.remove(); // Auto cleanup } +/* ========================================== + PARTICLE BURST - Ledakan partikel dari tile + ========================================== */ function createParticleBurst(tileElement) { + // Ambil posisi tengah tile const rect = tileElement.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; + // Ambil warna tile sesuai nilainya const tileValue = parseInt(tileElement.textContent); const tileColor = getTileColor(tileValue); + // Jumlah partikel random (8-12) const particleCount = 8 + Math.floor(Math.random() * 5); + // Buat partikel dalam lingkaran (360°) for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.className = 'merge-particle'; particle.style.left = centerX + 'px'; particle.style.top = centerY + 'px'; - particle.style.background = tileColor; + particle.style.background = tileColor; // Warna sama dengan tile document.body.appendChild(particle); + // Hitung sudut & kecepatan untuk ledakan melingkar const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5; - const velocity = 60 + Math.random() * 40; - const tx = Math.cos(angle) * velocity; - const ty = Math.sin(angle) * velocity; + const velocity = 60 + Math.random() * 40; // 60-100px + const tx = Math.cos(angle) * velocity; // Posisi X + const ty = Math.sin(angle) * velocity; // Posisi Y + // Animasi: meledak keluar sambil mengecil particle.animate([ { transform: 'translate(0, 0) scale(1)', opacity: 1 }, { transform: `translate(${tx}px, ${ty}px) scale(0)`, opacity: 0 } ], { - duration: 500 + Math.random() * 200, - easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' - }).onfinish = () => particle.remove(); + duration: 500 + Math.random() * 200, // 500-700ms + easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' // Smooth ease-out + }).onfinish = () => particle.remove(); // Auto cleanup } } +/* ========================================== + SCORE POPUP - Angka score yang terbang ke atas + ========================================== */ function createScorePopup(x, y, score) { const popup = document.createElement('div'); popup.className = 'score-popup'; - popup.textContent = '+' + score; + popup.textContent = '+' + score; // Contoh: "+16", "+32" popup.style.left = x + 'px'; popup.style.top = y + 'px'; popup.style.position = 'fixed'; popup.style.fontSize = '24px'; popup.style.fontWeight = '900'; - popup.style.color = '#ffd700'; + popup.style.color = '#ffd700'; // Gold popup.style.textShadow = '0 0 20px rgba(255, 215, 0, 0.8)'; popup.style.pointerEvents = 'none'; popup.style.zIndex = '9999'; @@ -131,21 +175,36 @@ function createScorePopup(x, y, score) { document.body.appendChild(popup); + // Animasi: terbang ke atas sambil fade out popup.animate([ { transform: 'translate(-50%, -50%) scale(0.5)', opacity: 0 }, { transform: 'translate(-50%, -70px) scale(1.2)', opacity: 1, offset: 0.3 }, { transform: 'translate(-50%, -120px) scale(1)', opacity: 0 } ], { duration: 1000, - easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' + easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' // Bounce ease }).onfinish = () => popup.remove(); } +/* ========================================== + GET TILE COLOR - Warna sesuai nilai tile + ========================================== + Dipakai untuk partikel biar warnanya match + ========================================== */ function getTileColor(value) { const colors = { - 2: '#00eaff', 4: '#00ff99', 8: '#ff00ff', 16: '#ff0066', - 32: '#ffaa00', 64: '#ff0000', 128: '#5f00ff', 256: '#00ffea', - 512: '#ff00aa', 1024: '#00ffaa', 2048: '#ffd700' + 2: '#00eaff', // Cyan + 4: '#00ff99', // Green + 8: '#ff00ff', // Magenta + 16: '#ff0066', // Pink + 32: '#ffaa00', // Orange + 64: '#ff0000', // Red + 128: '#5f00ff', // Purple + 256: '#00ffea', // Cyan bright + 512: '#ff00aa', // Pink bright + 1024: '#00ffaa', // Green bright + 2048: '#ffd700' // Gold (winning tile!) }; + // Return warna sesuai value, default cyan kalau nggak ada return colors[value] || '#00eaff'; } \ No newline at end of file diff --git a/Animation_Homepage.js b/Animation_Homepage.js index 944e56c..c05a115 100644 --- a/Animation_Homepage.js +++ b/Animation_Homepage.js @@ -1,7 +1,20 @@ +/* ========================================== + ANIMATION HOMEPAGE - Homepage Controller + ========================================== + fitur utama: + 1. updateAuthButton() - Toggle LOGIN/LOGOUT button + 2. handleLogout() - Logout logic dengan PHP + 3. Event listeners untuk semua button & keyboard + 4. Smooth scroll & responsive handling + ========================================== */ + +// Animation Homepage.js (function() { 'use strict'; - // ==================== DOM ELEMENTS ==================== + /* ========================================== + DOM ELEMENTS - Cache semua element penting + ========================================== */ const elements = { logo: null, authBtn: null, @@ -13,33 +26,37 @@ logoutFailedOverlay: null }; - // ==================== INITIALIZE ==================== + /* ========================================== + INITIALIZE - Setup saat page load + ========================================== */ function init() { if (document.readyState === 'loading') { + // Kalau DOM belum ready, tunggu event document.addEventListener('DOMContentLoaded', initAll); } else { + // Kalau sudah ready, langsung init initAll(); } } function initAll() { try { - // Cache DOM elements + // Step 1: Cache semua DOM elements cacheElements(); - // Validate elements + // Step 2: Validasi element wajib ada if (!validateElements()) { console.error('Some required elements are missing'); return; } - // Setup event listeners + // Step 3: Setup event listeners setupEventListeners(); - // Initialize smooth scroll + // Step 4: Initialize smooth scroll initSmoothScroll(); - // Check login status and update button + // Step 5: Update button LOGIN/LOGOUT sesuai status updateAuthButton(); console.log('✅ Homepage initialized successfully'); @@ -48,7 +65,11 @@ } } - // ==================== CACHE ELEMENTS ==================== + /* ========================================== + CACHE ELEMENTS - Simpan reference ke variable + ========================================== + Kenapa? Biar nggak query DOM berkali-kali (performa) + ========================================== */ function cacheElements() { elements.logo = document.querySelector('.logo'); elements.authBtn = document.getElementById('auth-button'); @@ -60,50 +81,60 @@ elements.logoutFailedOverlay = document.getElementById('logout-failed-overlay'); } - // ==================== VALIDATE ELEMENTS ==================== + /* ========================================== + VALIDATE ELEMENTS - Cek element wajib ada + ========================================== */ function validateElements() { + // Element yang HARUS ada: logo, authBtn, playBtn const requiredElements = ['logo', 'authBtn', 'playBtn']; const missingElements = requiredElements.filter(key => !elements[key]); if (missingElements.length > 0) { console.warn('Missing elements:', missingElements); - return false; + return false; // Gagal validasi } - return true; + return true; // Semua element ada } - // ==================== UPDATE AUTH BUTTON ==================== + /* ========================================== + UPDATE AUTH BUTTON - Toggle LOGIN/LOGOUT + ========================================== + Cek dari localStorage & sessionStorage + ========================================== */ function updateAuthButton() { - // Cek dari localStorage (authToken & username) atau sessionStorage + // Cek apakah user sudah login + // 3 cara cek: authToken, username, atau loggedInUser const authToken = localStorage.getItem('authToken'); const username = localStorage.getItem('username'); const loggedInUser = sessionStorage.getItem('loggedInUser'); if (authToken || username || loggedInUser) { - // User is logged in - show LOGOUT button + // User SUDAH LOGIN → show LOGOUT button elements.authBtn.textContent = 'LOGOUT'; elements.authBtn.classList.add('logout-mode'); } else { - // User is not logged in - show LOGIN button + // User BELUM LOGIN → show LOGIN button elements.authBtn.textContent = 'LOGIN'; elements.authBtn.classList.remove('logout-mode'); } } - // ==================== EVENT LISTENERS ==================== + /* ========================================== + SETUP EVENT LISTENERS - Bind semua event + ========================================== */ function setupEventListeners() { - // Logo click - reload page + // Logo click → reload homepage if (elements.logo) { elements.logo.addEventListener('click', handleLogoClick); } - // Auth button (Login/Logout) + // Auth button → login/logout if (elements.authBtn) { elements.authBtn.addEventListener('click', handleAuthClick); } - // Play button + // Play button → go to game if (elements.playBtn) { elements.playBtn.addEventListener('click', handlePlayClick); } @@ -113,7 +144,7 @@ elements.leaderboardBtn.addEventListener('click', handleLeaderboardClick); } - // Keyboard shortcuts + // Keyboard shortcuts (P, L, B) document.addEventListener('keydown', handleKeyPress); // Window events @@ -121,54 +152,64 @@ window.addEventListener('beforeunload', handleBeforeUnload); } - // ==================== EVENT HANDLERS ==================== + /* ========================================== + EVENT HANDLERS + ========================================== */ + function handleLogoClick(e) { e.preventDefault(); window.location.href = 'Homepage.html'; } + /* ========================================== + HANDLE AUTH CLICK - Login atau Logout + ========================================== */ function handleAuthClick(e) { - // Cek dari localStorage atau sessionStorage + // Cek status login dari localStorage & sessionStorage const authToken = localStorage.getItem('authToken'); const username = localStorage.getItem('username'); const loggedInUser = sessionStorage.getItem('loggedInUser'); if (authToken || username || loggedInUser) { - // User is logged in - perform logout + // User sudah login → LOGOUT handleLogout(); } else { - // User is not logged in - go to login page + // User belum login → ke halaman LOGIN window.location.href = 'Login.html'; } } + /* ========================================== + HANDLE LOGOUT - Logout Logic dengan PHP + ========================================== */ async function handleLogout() { try { - // Panggil PHP untuk hapus session + // Step 1: Panggil PHP untuk hapus session di server const response = await fetch("http://localhost/Kelompok06_2048/Logout.php", { method: "POST" }); const data = await response.json(); - // Hapus token & username dari localStorage + // Step 2: Hapus token & username dari localStorage localStorage.removeItem("authToken"); localStorage.removeItem("username"); - // Hapus juga dari sessionStorage (jika ada) + // Step 3: Hapus juga dari sessionStorage sessionStorage.removeItem('loggedInUser'); sessionStorage.removeItem('showTutorial'); - // Show success modal + // Step 4: Show success modal if (elements.logoutOverlay) { elements.logoutOverlay.style.display = 'flex'; - // Auto close after 2 seconds and redirect to homepage + // Auto close setelah 2 detik & redirect setTimeout(() => { elements.logoutOverlay.style.display = 'none'; window.location.href = "Homepage.html"; }, 2000); } + } catch (error) { console.error('Logout failed:', error); @@ -182,7 +223,7 @@ if (elements.logoutFailedOverlay) { elements.logoutFailedOverlay.style.display = 'flex'; - // Auto close after 2.5 seconds and redirect anyway + // Auto close setelah 2.5 detik setTimeout(() => { elements.logoutFailedOverlay.style.display = 'none'; window.location.href = "Homepage.html"; @@ -192,31 +233,34 @@ } function handlePlayClick(e) { - // Allow default behavior (navigate to 2048.html) + // Allow default behavior (navigate ke 2048.html) console.log('Starting game...'); } function handleLeaderboardClick(e) { - // Allow default behavior (navigate to leaderboard.html) + // Allow default behavior (navigate ke leaderboard.html) console.log('Opening leaderboard...'); } + /* ========================================== + KEYBOARD SHORTCUTS + ========================================== */ function handleKeyPress(e) { - // Press 'P' to play + // Press 'P' → Play game if (e.key === 'p' || e.key === 'P') { if (elements.playBtn) { elements.playBtn.click(); } } - // Press 'L' for login/logout + // Press 'L' → Login/Logout if (e.key === 'l' || e.key === 'L') { if (elements.authBtn) { elements.authBtn.click(); } } - // Press 'B' for leaderboard + // Press 'B' → Leaderboard (Board) if (e.key === 'b' || e.key === 'B') { if (elements.leaderboardBtn) { elements.leaderboardBtn.click(); @@ -225,7 +269,7 @@ } function handleResize() { - // Handle responsive behavior if needed + // Handle responsive behavior (mobile, tablet, desktop) const width = window.innerWidth; if (width < 768) { @@ -238,11 +282,13 @@ } function handleBeforeUnload(e) { - // Cleanup before page unload + // Cleanup sebelum page unload console.log('Page unloading...'); } - // ==================== SMOOTH SCROLL ==================== + /* ========================================== + SMOOTH SCROLL - Untuk anchor links (#) + ========================================== */ function initSmoothScroll() { document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { @@ -262,7 +308,9 @@ }); } - // ==================== UTILITY FUNCTIONS ==================== + /* ========================================== + UTILITY FUNCTIONS + ========================================== */ function checkBrowserSupport() { const features = { localStorage: typeof(Storage) !== 'undefined', @@ -278,7 +326,9 @@ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } - // ==================== PUBLIC API ==================== + /* ========================================== + PUBLIC API - Fungsi yang bisa dipanggil dari luar + ========================================== */ window.Homepage = { init: initAll, isMobile: isMobile, @@ -286,9 +336,11 @@ updateAuthButton: updateAuthButton }; - // ==================== CLEANUP ==================== + /* ========================================== + CLEANUP - Remove semua event listener + ========================================== */ window.cleanupHomepage = function() { - // Remove event listeners + // Remove event listeners (untuk prevent memory leak) if (elements.logo) { elements.logo.removeEventListener('click', handleLogoClick); } @@ -312,7 +364,7 @@ console.log('Homepage cleaned up'); }; - // Start initialization + // Start initialization saat script load init(); -})(); \ No newline at end of file +})(); // IIFE - langsung execute \ No newline at end of file diff --git a/Animation_Login.js b/Animation_Login.js index 9a24f9f..2354766 100644 --- a/Animation_Login.js +++ b/Animation_Login.js @@ -1,81 +1,150 @@ -// Particle System -const particlesContainer = document.getElementById('particles'); -const particleCount = 150; -const particles = []; +/* ========================================== + ANIMATION LOGIN - PARTICLE BACKGROUND SYSTEM + ========================================== + fungsi: + - Class Particle: Objek partikel dengan posisi & kecepatan + - Even Distribution: Distribusi partikel merata (15x10 grid) + - Animate Loop: Pergerakan smooth dengan requestAnimationFrame + ========================================== */ + +/* ========================================== + SETUP CONTAINER & CONFIG + ========================================== */ +const particlesContainer = document.getElementById('particles'); +const particleCount = 150; // Total partikel yang dibuat +const particles = []; // Array untuk simpan semua partikel + +/* ========================================== + CLASS PARTICLE - Blueprint untuk setiap partikel + ========================================== + Properties: + - x, y: Posisi partikel + - vx, vy: Kecepatan horizontal & vertikal (velocity) + - size: Ukuran partikel (2-5px) + - color: Warna neon random + ========================================== */ class Particle { constructor() { + // Buat element DOM untuk partikel this.element = document.createElement('div'); this.element.className = 'particle'; - this.reset(); + this.reset(); // Initialize posisi & properti particlesContainer.appendChild(this.element); } + /* ========================================== + RESET - Set/reset properti partikel + ========================================== */ reset() { + // Posisi random di seluruh layar this.x = Math.random() * window.innerWidth; this.y = Math.random() * window.innerHeight; + + // Kecepatan random (-0.6 sampai +0.6 pixel/frame) this.vx = (Math.random() - 0.5) * 1.2; this.vy = (Math.random() - 0.5) * 1.2; + + // Ukuran random (2-5px) this.size = Math.random() * 3 + 2; + // Array warna neon untuk partikel const colors = [ - '#00d9ff', '#ff00ff', '#00ffff', - '#ff0080', '#9d00ff', '#00ff88' + '#00d9ff', // Cyan + '#ff00ff', // Magenta + '#00ffff', // Cyan bright + '#ff0080', // Pink + '#9d00ff', // Purple + '#00ff88' // Green ]; + // Pilih warna random dari array const color = colors[Math.floor(Math.random() * colors.length)]; this.element.style.background = color; - this.element.style.boxShadow = `0 0 15px ${color}`; + this.element.style.boxShadow = `0 0 15px ${color}`; // Glow effect this.element.style.width = `${this.size}px`; this.element.style.height = `${this.size}px`; - this.updatePosition(); + this.updatePosition(); // Update posisi di DOM } + /* ========================================== + UPDATE POSITION - Sync posisi ke DOM + ========================================== */ updatePosition() { this.element.style.left = `${this.x}px`; this.element.style.top = `${this.y}px`; } + /* ========================================== + MOVE - Update posisi berdasarkan velocity + ========================================== + Fitur: Wrap-around screen (partikel keluar = muncul sisi lain) + ========================================== */ move() { + // Update posisi berdasarkan kecepatan this.x += this.vx; this.y += this.vy; + // ♻️ WRAP-AROUND LOGIC + // Kalau keluar dari kiri → muncul dari kanan if (this.x < -10) this.x = window.innerWidth + 10; + // Kalau keluar dari kanan → muncul dari kiri if (this.x > window.innerWidth + 10) this.x = -10; + // Kalau keluar dari atas → muncul dari bawah if (this.y < -10) this.y = window.innerHeight + 10; + // Kalau keluar dari bawah → muncul dari atas if (this.y > window.innerHeight + 10) this.y = -10; - this.updatePosition(); + this.updatePosition(); // Sync ke DOM } } -// Create particles with even distribution -const cols = 15; -const rows = 10; -let particleIndex = 0; +/* ========================================== + EVEN DISTRIBUTION - Distribusi Partikel Merata + ========================================== + Konsep: Grid 15 kolom x 10 baris (total 150 partikel) + Biar nggak menumpuk di satu area + ========================================== */ +const cols = 15; // Jumlah kolom +const rows = 10; // Jumlah baris +let particleIndex = 0; // Counter partikel yang sudah dibuat +// Loop baris for (let i = 0; i < rows; i++) { + // Loop kolom for (let j = 0; j < cols; j++) { + // Guard: stop kalau sudah 150 partikel if (particleIndex >= particleCount) break; const particle = new Particle(); - // Even distribution + slight random offset + // ⚙️ DISTRIBUSI MERATA + RANDOM OFFSET + // Formula: (index / total) × lebar/tinggi layar particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100; particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100; particle.updatePosition(); - particles.push(particle); + particles.push(particle); // Simpan ke array particleIndex++; } if (particleIndex >= particleCount) break; } -// Animate +/* ========================================== + ANIMATE - Main Animation Loop + ========================================== + Menggunakan requestAnimationFrame untuk performa optimal + (60 FPS, smooth, GPU-accelerated) + ========================================== */ function animate() { + // Loop semua partikel dan gerakkan particles.forEach(p => p.move()); + + // Request next frame (recursive call) + // Browser otomatis sync dengan refresh rate monitor requestAnimationFrame(animate); } +// Start animation loop animate(); \ No newline at end of file