From 8f46f381c59d8595387527d582c07fff88bafadb Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Tue, 25 Mar 2025 15:57:27 -0400 Subject: [PATCH] Add mobile optimization with touch controls and responsive design --- index.html | 11 +- script.js | 308 +++++++++++++++++++++++++++++++++++++++++++++++++---- style.css | 196 ++++++++++++++++++++++++++++++++++ 3 files changed, 494 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index 52783b6..8e546ee 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,9 @@ - + + + 3D Tetris @@ -97,6 +99,13 @@ +
+ + +
diff --git a/script.js b/script.js index 8426e03..127b174 100644 --- a/script.js +++ b/script.js @@ -9,6 +9,10 @@ const BLOCK_SIZE = 30; const EMPTY = 'black'; const PREVIEW_BLOCK_SIZE = 25; +// Mobile detection +const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +let touchControls = false; + // Set canvas dimensions to match game board canvas.width = COLS * BLOCK_SIZE; canvas.height = ROWS * BLOCK_SIZE; @@ -33,6 +37,7 @@ const optionsModal = document.getElementById('options-modal'); const toggle3DEffects = document.getElementById('toggle-3d-effects'); const toggleSpinAnimations = document.getElementById('toggle-spin-animations'); const animationSpeedSlider = document.getElementById('animation-speed'); +const toggleMobileControls = document.getElementById('toggle-mobile-controls'); // Controller elements const controllerStatus = document.getElementById('controller-status'); @@ -57,6 +62,7 @@ let showShadow = true; // Toggle shadow display let enable3DEffects = true; let enableSpinAnimations = true; let animationSpeed = 0.05; +let forceMobileControls = false; // Controller variables let gamepadConnected = false; @@ -1686,49 +1692,51 @@ function applyOptions() { enable3DEffects = toggle3DEffects.checked; enableSpinAnimations = toggleSpinAnimations.checked; animationSpeed = parseFloat(animationSpeedSlider.value); + forceMobileControls = toggleMobileControls.checked; - // If 3D effects are disabled, also disable spin animations - if (!enable3DEffects) { - toggleSpinAnimations.disabled = true; - } else { - toggleSpinAnimations.disabled = false; + // Apply mobile controls if checked or if on mobile device + if (forceMobileControls) { + if (!touchControls) { + initTouchControls(); + touchControls = true; + } + document.body.classList.add('mobile-mode'); + } else if (!isMobile) { + document.body.classList.remove('mobile-mode'); } - // Save options in localStorage + // Save options saveOptions(); } // Save options to localStorage function saveOptions() { - const options = { + localStorage.setItem('tetris3DOptions', JSON.stringify({ enable3DEffects, enableSpinAnimations, - animationSpeed - }; - - localStorage.setItem('tetris3dOptions', JSON.stringify(options)); + animationSpeed, + forceMobileControls + })); } // Load options from localStorage function loadOptions() { - const savedOptions = localStorage.getItem('tetris3dOptions'); - + const savedOptions = localStorage.getItem('tetris3DOptions'); if (savedOptions) { const options = JSON.parse(savedOptions); - // Apply saved options - enable3DEffects = options.enable3DEffects; - enableSpinAnimations = options.enableSpinAnimations; - animationSpeed = options.animationSpeed; + enable3DEffects = options.enable3DEffects !== undefined ? options.enable3DEffects : true; + enableSpinAnimations = options.enableSpinAnimations !== undefined ? options.enableSpinAnimations : true; + animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05; + forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false; // Update UI controls toggle3DEffects.checked = enable3DEffects; toggleSpinAnimations.checked = enableSpinAnimations; animationSpeedSlider.value = animationSpeed; - // Update UI state - if (!enable3DEffects) { - toggleSpinAnimations.disabled = true; + if (toggleMobileControls) { + toggleMobileControls.checked = forceMobileControls; } } } @@ -1756,6 +1764,12 @@ function init() { // Listen for keyboard events document.addEventListener('keydown', control); + // Add resize listener for responsive layout + window.addEventListener('resize', handleResize); + + // Initial resize to set correct dimensions + handleResize(); + // Button event listeners startBtn.addEventListener('click', resetGame); pauseBtn.addEventListener('click', togglePause); @@ -1777,6 +1791,24 @@ function init() { applyOptions(); }); + if (toggleMobileControls) { + toggleMobileControls.addEventListener('change', function() { + applyOptions(); + + // Force resize to update layout + window.dispatchEvent(new Event('resize')); + }); + } + + // Apply mobile mode if forced or on mobile device + if (forceMobileControls || isMobile) { + if (!touchControls) { + initTouchControls(); + touchControls = true; + } + document.body.classList.add('mobile-mode'); + } + // Initialize controller support initControllerSupport(); @@ -1784,6 +1816,53 @@ function init() { updateControllerStatus(); } +// Handle window resize for responsive layout +function handleResize() { + const gameContainer = document.querySelector('.game-container'); + const gameWrapper = document.querySelector('.game-wrapper'); + + if (isMobile) { + // Scale the canvas to fit mobile screen + const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth); + const viewportHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); + + // Calculate optimal scale while maintaining aspect ratio + const scaleWidth = (viewportWidth * 0.85) / (COLS * BLOCK_SIZE); + const scaleHeight = (viewportHeight * 0.6) / (ROWS * BLOCK_SIZE); + const scale = Math.min(scaleWidth, scaleHeight, 1); // Don't scale up beyond 1 + + // Apply scale transform to canvas + canvas.style.transform = `scale(${scale})`; + canvas.style.transformOrigin = 'top left'; + + // Adjust container width based on scaled canvas + if (gameWrapper) { + gameWrapper.style.width = `${(COLS * BLOCK_SIZE) * scale}px`; + gameWrapper.style.height = `${(ROWS * BLOCK_SIZE) * scale}px`; + } + + // Scale next piece preview + if (nextPieceCanvas) { + nextPieceCanvas.style.transform = `scale(${scale})`; + nextPieceCanvas.style.transformOrigin = 'top left'; + } + + // Show touch controls + document.body.classList.add('mobile-mode'); + } else { + // Reset to desktop layout + canvas.style.transform = 'none'; + if (gameWrapper) { + gameWrapper.style.width = ''; + gameWrapper.style.height = ''; + } + if (nextPieceCanvas) { + nextPieceCanvas.style.transform = 'none'; + } + document.body.classList.remove('mobile-mode'); + } +} + // Toggle shadow display function toggleShadow() { showShadow = !showShadow; @@ -2045,9 +2124,198 @@ function clearPreviousPiecePosition() { } } +// Touch control variables +let touchStartX = 0; +let touchStartY = 0; +let touchStartTime = 0; +const SWIPE_THRESHOLD = 30; +const TAP_THRESHOLD = 200; // milliseconds +const DOUBLE_TAP_THRESHOLD = 300; // milliseconds +let lastTapTime = 0; + +// Initialize touch controls +function initTouchControls() { + // Add touch event listeners to the canvas + canvas.addEventListener('touchstart', handleTouchStart, false); + canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); + canvas.addEventListener('touchend', handleTouchEnd, false); + + // Create on-screen control buttons + createTouchControlButtons(); +} + +// Handle touch start event +function handleTouchStart(event) { + if (gameOver || paused) return; + event.preventDefault(); + + // Store the initial touch position + const touch = event.touches[0]; + touchStartX = touch.clientX; + touchStartY = touch.clientY; + touchStartTime = Date.now(); +} + +// Handle touch move event +function handleTouchMove(event) { + if (gameOver || paused) return; + event.preventDefault(); + + if (!event.touches.length) return; + + const touch = event.touches[0]; + const diffX = touch.clientX - touchStartX; + const diffY = touch.clientY - touchStartY; + + // Detect horizontal swipe for movement + if (Math.abs(diffX) > SWIPE_THRESHOLD) { + if (diffX > 0) { + p.moveRight(); + } else { + p.moveLeft(); + } + + // Reset touch start to allow for continuous movement + touchStartX = touch.clientX; + } + + // Detect downward swipe for soft drop + if (diffY > SWIPE_THRESHOLD) { + p.moveDown(); + touchStartY = touch.clientY; + } +} + +// Handle touch end event +function handleTouchEnd(event) { + if (gameOver || paused) return; + event.preventDefault(); + + const touchEndTime = Date.now(); + const touchDuration = touchEndTime - touchStartTime; + + // Check for tap (quick touch) + if (touchDuration < TAP_THRESHOLD) { + // Check for double tap (for hard drop) + if (touchEndTime - lastTapTime < DOUBLE_TAP_THRESHOLD) { + p.hardDrop(); + lastTapTime = 0; // Reset to prevent triple-tap detection + } else { + // Single tap rotates piece + p.rotate('right'); + lastTapTime = touchEndTime; + } + } +} + +// Create on-screen control buttons for mobile +function createTouchControlButtons() { + const controlsContainer = document.createElement('div'); + controlsContainer.className = 'touch-controls-container'; + document.body.appendChild(controlsContainer); + + // Create left button + const leftBtn = document.createElement('button'); + leftBtn.className = 'touch-btn left-btn'; + leftBtn.innerHTML = '←'; + leftBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + const moveInterval = setInterval(function() { + if (!gameOver && !paused) p.moveLeft(); + }, 100); + + leftBtn.addEventListener('touchend', function() { + clearInterval(moveInterval); + }, { once: true }); + }); + + // Create right button + const rightBtn = document.createElement('button'); + rightBtn.className = 'touch-btn right-btn'; + rightBtn.innerHTML = '→'; + rightBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + const moveInterval = setInterval(function() { + if (!gameOver && !paused) p.moveRight(); + }, 100); + + rightBtn.addEventListener('touchend', function() { + clearInterval(moveInterval); + }, { once: true }); + }); + + // Create down button + const downBtn = document.createElement('button'); + downBtn.className = 'touch-btn down-btn'; + downBtn.innerHTML = '↓'; + downBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + const moveInterval = setInterval(function() { + if (!gameOver && !paused) p.moveDown(); + }, 100); + + downBtn.addEventListener('touchend', function() { + clearInterval(moveInterval); + }, { once: true }); + }); + + // Create rotate button + const rotateBtn = document.createElement('button'); + rotateBtn.className = 'touch-btn rotate-btn'; + rotateBtn.innerHTML = '↻'; + rotateBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + if (!gameOver && !paused) p.rotate('right'); + }); + + // Create 3D buttons + const rotate3DXBtn = document.createElement('button'); + rotate3DXBtn.className = 'touch-btn rotate3d-x-btn'; + rotate3DXBtn.innerHTML = 'W'; + rotate3DXBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + if (!gameOver && !paused) p.rotate3DX(); + }); + + const rotate3DYBtn = document.createElement('button'); + rotate3DYBtn.className = 'touch-btn rotate3d-y-btn'; + rotate3DYBtn.innerHTML = 'X'; + rotate3DYBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + if (!gameOver && !paused) p.rotate3DY(); + }); + + // Create hard drop button + const hardDropBtn = document.createElement('button'); + hardDropBtn.className = 'touch-btn hard-drop-btn'; + hardDropBtn.innerHTML = '⤓'; + hardDropBtn.addEventListener('touchstart', function(e) { + e.preventDefault(); + if (!gameOver && !paused) p.hardDrop(); + }); + + // Add all buttons to the container + controlsContainer.appendChild(leftBtn); + controlsContainer.appendChild(rightBtn); + controlsContainer.appendChild(downBtn); + controlsContainer.appendChild(rotateBtn); + controlsContainer.appendChild(rotate3DXBtn); + controlsContainer.appendChild(rotate3DYBtn); + controlsContainer.appendChild(hardDropBtn); +} + // Start the game window.onload = function() { init(); update(); // Start the fireworks update loop draw(); // Start the drawing loop + + // Initialize touch controls if on mobile device + if (isMobile) { + initTouchControls(); + touchControls = true; + + // Force resize to ensure proper mobile layout + window.dispatchEvent(new Event('resize')); + } }; \ No newline at end of file diff --git a/style.css b/style.css index 91b3ab8..c9bdbd3 100644 --- a/style.css +++ b/style.css @@ -486,4 +486,200 @@ input[type=range]::-moz-range-thumb { background: linear-gradient(45deg, #ff00dd, #00ddff); cursor: pointer; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); +} + +/* Mobile-specific styles */ +.mobile-mode { + overflow: hidden; + overscroll-behavior: none; + touch-action: none; +} + +.mobile-mode .game-container { + flex-direction: column; + align-items: center; + gap: 20px; + padding: 15px; + max-width: 100vw; + box-sizing: border-box; + margin-top: 70px; +} + +.mobile-mode .game-title { + font-size: 24px; + top: 10px; +} + +.mobile-mode .game-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; +} + +.mobile-mode #next-piece-preview { + position: relative; + top: auto; + left: auto; + margin: 10px auto; + width: 120px; +} + +.mobile-mode .score-container { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 10px; + padding: 10px; +} + +.mobile-mode .score-container p { + margin: 5px; + font-size: 12px; +} + +.mobile-mode .game-btn { + margin: 5px; + padding: 8px 12px; + font-size: 10px; +} + +.mobile-mode .controls-info { + display: none; +} + +/* Touch control buttons */ +.touch-controls-container { + display: none; + position: fixed; + bottom: 20px; + left: 0; + right: 0; + z-index: 100; +} + +.mobile-mode .touch-controls-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} + +.touch-btn { + width: 60px; + height: 60px; + border-radius: 50%; + background: rgba(51, 51, 51, 0.7); + border: 2px solid rgba(0, 255, 255, 0.5); + color: #00ffff; + font-size: 24px; + display: flex; + justify-content: center; + align-items: center; + -webkit-tap-highlight-color: transparent; + user-select: none; + touch-action: manipulation; + box-shadow: 0 0 10px rgba(0, 255, 255, 0.3); +} + +.touch-btn:active { + background: rgba(0, 255, 255, 0.3); + transform: scale(0.95); +} + +/* Button positioning */ +.left-btn, .right-btn, .down-btn { + position: relative; +} + +.rotate-btn, .rotate3d-x-btn, .rotate3d-y-btn, .hard-drop-btn { + background: rgba(255, 0, 255, 0.2); + border-color: rgba(255, 0, 255, 0.5); + color: #ff00ff; +} + +/* Portrait vs landscape adjustments */ +@media (orientation: portrait) { + .mobile-mode .game-container { + padding-top: 60px; + } + + .touch-controls-container { + padding: 0 10px 10px; + } +} + +@media (orientation: landscape) and (max-width: 900px) { + .mobile-mode .game-container { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + margin-top: 50px; + } + + .mobile-mode .score-container { + width: auto; + min-width: 200px; + } + + .mobile-mode .game-wrapper { + margin: 0; + } + + .mobile-mode #next-piece-preview { + position: absolute; + top: 0; + left: 100%; + margin-left: 10px; + } + + .touch-controls-container { + flex-direction: row; + bottom: 10px; + } + + .touch-btn { + width: 50px; + height: 50px; + font-size: 20px; + } +} + +/* Smaller devices */ +@media (max-width: 400px) { + .mobile-mode .game-title { + font-size: 20px; + } + + .touch-btn { + width: 50px; + height: 50px; + font-size: 20px; + } + + .mobile-mode .score-container p { + font-size: 10px; + } +} + +/* Tablet optimization */ +@media (min-width: 768px) and (max-width: 1024px) { + .mobile-mode .game-container { + flex-direction: row; + flex-wrap: wrap; + gap: 30px; + } + + .mobile-mode .game-title { + font-size: 28px; + } + + .touch-btn { + width: 70px; + height: 70px; + font-size: 28px; + } } \ No newline at end of file