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