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