2048 update

This commit is contained in:
Jevinca Marvella 2025-11-30 22:39:18 +07:00
parent b08d1b5ce7
commit cd9955bfa8
3 changed files with 451 additions and 57 deletions

237
2048.css
View File

@ -1274,3 +1274,240 @@ h1 {
font-weight: 500;
}
}
/* ==========================
ADVANCED SOUND CONTROL
========================== */
.sound-control-container {
position: fixed;
top: clamp(10px, 2vh, 20px);
left: clamp(10px, 2vw, 20px);
z-index: 100;
}
/* Main Sound Button */
.btn-sound-main {
width: clamp(40px, 6.5vw, 52px);
height: clamp(40px, 6.5vw, 52px);
padding: 0;
background: rgba(30, 0, 50, 0.9);
backdrop-filter: blur(15px);
border: 2px solid rgba(0, 217, 255, 0.5);
border-radius: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 5px 20px rgba(0, 0, 0, 0.4),
0 0 25px rgba(0, 217, 255, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative;
}
.btn-sound-main svg {
width: clamp(20px, 3.5vw, 26px);
height: clamp(20px, 3.5vw, 26px);
color: rgba(0, 234, 255, 0.95);
transition: all 0.3s ease;
position: absolute;
}
.btn-sound-main:hover {
background: rgba(0, 234, 255, 0.18);
border-color: rgba(0, 234, 255, 0.85);
box-shadow:
0 7px 28px rgba(0, 0, 0, 0.5),
0 0 40px rgba(0, 234, 255, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.btn-sound-main:hover svg {
color: rgba(0, 234, 255, 1);
transform: scale(1.12);
}
.btn-sound-main:active {
transform: translateY(0);
}
/* Muted State - Red */
.btn-sound-main.all-muted {
background: rgba(60, 0, 10, 0.9) !important;
border-color: rgba(255, 60, 60, 0.7) !important;
box-shadow:
0 5px 20px rgba(0, 0, 0, 0.4),
0 0 25px rgba(255, 60, 60, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}
.btn-sound-main.all-muted svg {
color: rgba(255, 80, 80, 0.95) !important;
}
.btn-sound-main.all-muted:hover {
background: rgba(255, 60, 60, 0.2) !important;
border-color: rgba(255, 80, 80, 0.9) !important;
}
/* Volume Panel */
.volume-panel {
position: absolute;
top: clamp(46px, 8vh, 60px);
left: 0;
background: linear-gradient(145deg, rgba(20, 0, 40, 0.98), rgba(30, 0, 50, 0.98));
backdrop-filter: blur(25px);
border: 2px solid rgba(0, 217, 255, 0.4);
border-radius: 18px;
padding: 20px 18px;
min-width: clamp(240px, 30vw, 280px);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.7),
0 0 50px rgba(0, 217, 255, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
animation: slideDown 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Volume Item */
.volume-item {
margin-bottom: 18px;
}
.volume-item:last-child {
margin-bottom: 0;
}
.volume-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.volume-icon {
width: 18px;
height: 18px;
color: rgba(0, 234, 255, 0.8);
}
.volume-label {
font-size: 13px;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
flex: 1;
}
.volume-value {
font-size: 12px;
font-weight: 700;
color: rgba(0, 234, 255, 0.9);
min-width: 40px;
text-align: right;
}
/* Volume Slider */
.volume-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
outline: none;
cursor: pointer;
transition: all 0.2s;
}
.volume-slider::-webkit-slider-track {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #00eaff 0%, #0099ff 100%);
border-radius: 50%;
cursor: pointer;
box-shadow:
0 2px 10px rgba(0, 234, 255, 0.5),
0 0 20px rgba(0, 234, 255, 0.3);
transition: all 0.2s;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow:
0 4px 15px rgba(0, 234, 255, 0.7),
0 0 30px rgba(0, 234, 255, 0.5);
}
.volume-slider::-moz-range-track {
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.volume-slider::-moz-range-thumb {
width: 18px;
height: 18px;
background: linear-gradient(135deg, #00eaff 0%, #0099ff 100%);
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow:
0 2px 10px rgba(0, 234, 255, 0.5),
0 0 20px rgba(0, 234, 255, 0.3);
transition: all 0.2s;
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.2);
}
/* Slider Fill Effect */
.volume-slider {
background: linear-gradient(to right,
rgba(0, 234, 255, 0.3) 0%,
rgba(0, 234, 255, 0.3) var(--value, 0%),
rgba(255, 255, 255, 0.1) var(--value, 0%),
rgba(255, 255, 255, 0.1) 100%);
}
/* Mobile Responsive */
@media (max-width: 768px) {
.volume-panel {
min-width: clamp(220px, 50vw, 260px);
padding: 16px 14px;
}
.volume-item {
margin-bottom: 14px;
}
.volume-label {
font-size: 12px;
}
.volume-value {
font-size: 11px;
}
}

View File

@ -16,42 +16,72 @@
<div class="cursor-light" aria-hidden="true"></div>
<!-- SOUND CONTROLS - KIRI ATAS -->
<div class="sound-controls">
<!-- BG Music Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-bg" data-sound="bg" title="Toggle Background Music">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<path d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/>
</svg>
</button>
<!-- SOUND CONTROL - KIRI ATAS -->
<div class="sound-control-container">
<!-- Main Sound Button -->
<button class="icon-btn btn-sound-main" id="btn-sound-main" title="Sound Settings">
<!-- Icon Full Sound (all ON) -->
<svg class="sound-full" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14"/>
</svg>
<!-- Icon Medium Sound (some muted) -->
<svg class="sound-medium" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
</svg>
<!-- Icon Low Sound (mostly muted) -->
<svg class="sound-low" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
</svg>
<!-- Icon Muted (all OFF) -->
<svg class="sound-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<line x1="23" y1="9" x2="17" y2="15"/>
<line x1="17" y1="9" x2="23" y2="15"/>
</svg>
</button>
<!-- Pop SFX Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-pop" data-sound="pop" title="Toggle Pop Sound">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12h8M12 8v8"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<circle cx="12" cy="12" r="10"/>
</svg>
</button>
<!-- Volume Sliders Panel (Hidden by default) -->
<div class="volume-panel" id="volume-panel" style="display: none;">
<!-- Music Volume -->
<div class="volume-item">
<div class="volume-header">
<svg class="volume-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18V5l12-2v13M9 18c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3zm12-2c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"/>
</svg>
<span class="volume-label">Music</span>
<span class="volume-value" id="vol-music-display">25%</span>
</div>
<input type="range" class="volume-slider" id="vol-music" min="0" max="100" value="25">
</div>
<!-- Merge SFX Toggle -->
<button class="icon-btn btn-sound" id="btn-sound-merge" data-sound="merge" title="Toggle Merge Sound">
<svg class="sound-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<svg class="mute-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display: none;">
<line x1="2" y1="2" x2="22" y2="22"/>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</button>
<!-- Pop SFX Volume -->
<div class="volume-item">
<div class="volume-header">
<svg class="volume-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12h8M12 8v8"/>
</svg>
<span class="volume-label">Pop SFX</span>
<span class="volume-value" id="vol-pop-display">90%</span>
</div>
<input type="range" class="volume-slider" id="vol-pop" min="0" max="100" value="90">
</div>
<!-- Merge SFX Volume -->
<div class="volume-item">
<div class="volume-header">
<svg class="volume-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<span class="volume-label">Merge SFX</span>
<span class="volume-value" id="vol-merge-display">100%</span>
</div>
<input type="range" class="volume-slider" id="vol-merge" min="0" max="100" value="100">
</div>
</div>
</div>
<!-- Top Right Controls -->
<div class="top-controls">
<button class="icon-btn btn-tutorial" id="btn-tutorial" title="How to Play">

175
2048.js
View File

@ -608,38 +608,165 @@ function hideGameOver() {
/* =============================================
SOUND CONTROLS
============================================= */
function toggleSound(soundType) {
soundState[soundType] = !soundState[soundType];
localStorage.setItem('sound_' + soundType, soundState[soundType]);
updateAudioVolumes();
/* =============================================
ADVANCED VOLUME CONTROL SYSTEM
============================================= */
const button = document.getElementById('btn-sound-' + soundType);
if (button) {
updateSoundButtonState(button, soundState[soundType]);
// Volume State (0-100 for each sound)
let volumeState = {
music: parseInt(localStorage.getItem('vol_music')) || 25,
pop: parseInt(localStorage.getItem('vol_pop')) || 90,
merge: parseInt(localStorage.getItem('vol_merge')) || 100
};
// Apply volumes on load
function initVolumeControl() {
// Set audio volumes
audio.bg.volume = volumeState.music / 100;
audio.pop.volume = volumeState.pop / 100;
audio.merge.volume = volumeState.merge / 100;
// Update sliders
const musicSlider = document.getElementById('vol-music');
const popSlider = document.getElementById('vol-pop');
const mergeSlider = document.getElementById('vol-merge');
if (musicSlider) {
musicSlider.value = volumeState.music;
updateSliderFill(musicSlider, volumeState.music);
document.getElementById('vol-music-display').textContent = volumeState.music + '%';
}
if (soundType === 'bg') {
if (soundState.bg) {
tryPlayBg();
if (popSlider) {
popSlider.value = volumeState.pop;
updateSliderFill(popSlider, volumeState.pop);
document.getElementById('vol-pop-display').textContent = volumeState.pop + '%';
}
if (mergeSlider) {
mergeSlider.value = volumeState.merge;
updateSliderFill(mergeSlider, volumeState.merge);
document.getElementById('vol-merge-display').textContent = volumeState.merge + '%';
}
updateMainSoundIcon();
// Event listeners for sliders
if (musicSlider) {
musicSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
volumeState.music = val;
audio.bg.volume = val / 100;
localStorage.setItem('vol_music', val);
document.getElementById('vol-music-display').textContent = val + '%';
updateSliderFill(e.target, val);
updateMainSoundIcon();
// Auto-play BG music if volume > 0
if (val > 0 && audio.bg.paused) {
tryPlayBg();
} else if (val === 0) {
audio.bg.pause();
}
});
}
if (popSlider) {
popSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
volumeState.pop = val;
audio.pop.volume = val / 100;
localStorage.setItem('vol_pop', val);
document.getElementById('vol-pop-display').textContent = val + '%';
updateSliderFill(e.target, val);
updateMainSoundIcon();
});
}
if (mergeSlider) {
mergeSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
volumeState.merge = val;
audio.merge.volume = val / 100;
localStorage.setItem('vol_merge', val);
document.getElementById('vol-merge-display').textContent = val + '%';
updateSliderFill(e.target, val);
updateMainSoundIcon();
});
}
// Toggle panel visibility
const btnSoundMain = document.getElementById('btn-sound-main');
const volumePanel = document.getElementById('volume-panel');
if (btnSoundMain && volumePanel) {
btnSoundMain.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = volumePanel.style.display === 'block';
volumePanel.style.display = isVisible ? 'none' : 'block';
});
// Close panel when clicking outside
document.addEventListener('click', (e) => {
if (!volumePanel.contains(e.target) && !btnSoundMain.contains(e.target)) {
volumePanel.style.display = 'none';
}
});
// Prevent panel click from closing
volumePanel.addEventListener('click', (e) => {
e.stopPropagation();
});
}
}
// Update slider fill effect
function updateSliderFill(slider, value) {
slider.style.setProperty('--value', value + '%');
}
// Update main sound icon based on volumes
function updateMainSoundIcon() {
const btnMain = document.getElementById('btn-sound-main');
if (!btnMain) return;
const iconFull = btnMain.querySelector('.sound-full');
const iconMedium = btnMain.querySelector('.sound-medium');
const iconLow = btnMain.querySelector('.sound-low');
const iconMuted = btnMain.querySelector('.sound-muted');
// Calculate total volume average
const totalVolume = volumeState.music + volumeState.pop + volumeState.merge;
const avgVolume = totalVolume / 3;
// Hide all icons first
iconFull.style.display = 'none';
iconMedium.style.display = 'none';
iconLow.style.display = 'none';
iconMuted.style.display = 'none';
// Show appropriate icon based on average
if (totalVolume === 0) {
iconMuted.style.display = 'block';
btnMain.classList.add('all-muted');
} else {
btnMain.classList.remove('all-muted');
if (avgVolume >= 60) {
iconFull.style.display = 'block';
} else if (avgVolume >= 30) {
iconMedium.style.display = 'block';
} else {
audio.bg.pause();
iconLow.style.display = 'block';
}
}
}
function updateSoundButtonState(button, isEnabled) {
if (!button) return;
if (isEnabled) {
button.classList.remove('muted');
button.querySelector('.sound-icon').style.display = 'block';
button.querySelector('.mute-icon').style.display = 'none';
} else {
button.classList.add('muted');
button.querySelector('.sound-icon').style.display = 'none';
button.querySelector('.mute-icon').style.display = 'block';
}
}
// Initialize on DOM load (add this to your existing DOMContentLoaded)
document.addEventListener("DOMContentLoaded", () => {
// ... existing code ...
initVolumeControl(); // ADD THIS LINE
});
/* =============================================
COMBO EFFECTS