Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Glass Tetris</title>
- <style>
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
- :root {
- --bg-glass: rgba(255, 255, 255, 0.1);
- --shadow-glass: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
- --border-glass: 1px solid rgba(255, 255, 255, 0.18);
- --text-color: #ffffff;
- }
- * {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- }
- body {
- min-height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
- font-family: 'Poppins', sans-serif;
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
- background-size: 400% 400%;
- animation: gradientBG 15s ease infinite;
- color: var(--text-color);
- overflow: hidden;
- }
- @keyframes gradientBG {
- 0% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- 100% { background-position: 0% 50%; }
- }
- /* Decorative background orbs to enhance glass effect */
- .orb {
- position: absolute;
- border-radius: 50%;
- filter: blur(80px);
- opacity: 0.4;
- z-index: -1;
- }
- .orb-1 {
- top: 10%; left: 20%; width: 300px; height: 300px; background: #ff00cc;
- }
- .orb-2 {
- bottom: 10%; right: 20%; width: 400px; height: 400px; background: #3333ff;
- }
- .game-container {
- display: flex;
- gap: 20px;
- padding: 30px;
- background: var(--bg-glass);
- backdrop-filter: blur(16px);
- -webkit-backdrop-filter: blur(16px);
- border-radius: 20px;
- border: var(--border-glass);
- box-shadow: var(--shadow-glass);
- }
- .main-board-container {
- position: relative;
- border-radius: 10px;
- overflow: hidden;
- background: rgba(0, 0, 0, 0.2);
- border: var(--border-glass);
- }
- #tetris {
- display: block;
- /* The canvas itself is transparent to let background show through */
- }
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- min-width: 150px;
- }
- .panel {
- padding: 15px;
- background: var(--bg-glass);
- border-radius: 15px;
- border: var(--border-glass);
- text-align: center;
- }
- .panel h2 {
- font-size: 1.2rem;
- font-weight: 300;
- opacity: 0.8;
- margin-bottom: 5px;
- }
- .panel .value {
- font-size: 1.8rem;
- font-weight: 600;
- text-shadow: 0 0 10px rgba(255,255,255,0.3);
- }
- #next-piece {
- margin: 0 auto;
- display: block;
- margin-top: 10px;
- }
- .controls-info {
- font-size: 0.8rem;
- opacity: 0.6;
- text-align: left;
- margin-top: auto;
- }
- .overlay {
- position: absolute;
- top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(0,0,0,0.6);
- backdrop-filter: blur(4px);
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- z-index: 10;
- }
- .overlay h1 {
- font-size: 3rem;
- margin-bottom: 20px;
- color: white;
- text-shadow: 0 0 20px rgba(255,255,255,0.5);
- }
- .btn {
- padding: 12px 30px;
- font-size: 1.2rem;
- border: none;
- background: rgba(255, 255, 255, 0.2);
- color: white;
- border-radius: 50px;
- cursor: pointer;
- transition: all 0.3s ease;
- border: 1px solid rgba(255,255,255,0.4);
- font-family: inherit;
- }
- .btn:hover {
- background: rgba(255, 255, 255, 0.4);
- box-shadow: 0 0 15px rgba(255,255,255,0.4);
- }
- .hidden {
- display: none !important;
- }
- </style>
- </head>
- <body>
- <!-- Background Orbs for visual depth -->
- <div class="orb orb-1"></div>
- <div class="orb orb-2"></div>
- <div class="game-container">
- <!-- Main Game Area -->
- <div class="main-board-container">
- <canvas id="tetris" width="300" height="600"></canvas>
- <!-- Start/Pause/Game Over Overlay -->
- <div id="overlay" class="overlay">
- <h1 id="overlay-title">Glass Tetris</h1>
- <button id="start-btn" class="btn">Start Game</button>
- </div>
- </div>
- <!-- Sidebar for stats -->
- <div class="sidebar">
- <div class="panel">
- <h2>Score</h2>
- <div id="score" class="value">0</div>
- </div>
- <div class="panel">
- <h2>Level</h2>
- <div id="level" class="value">1</div>
- </div>
- <div class="panel">
- <h2>Next</h2>
- <canvas id="next-piece" width="80" height="80"></canvas>
- </div>
- <div class="panel controls-info">
- <h3>Controls</h3>
- <p>← → : Move</p>
- <p>↑ : Rotate</p>
- <p>↓ : Soft Drop</p>
- <p>Space : Hard Drop</p>
- <p>P : Pause</p>
- </div>
- </div>
- </div>
- <script>
- const canvas = document.getElementById('tetris');
- const ctx = canvas.getContext('2d');
- const nextCanvas = document.getElementById('next-piece');
- const nextCtx = nextCanvas.getContext('2d');
- const scoreElement = document.getElementById('score');
- const levelElement = document.getElementById('level');
- const overlay = document.getElementById('overlay');
- const overlayTitle = document.getElementById('overlay-title');
- const startBtn = document.getElementById('start-btn');
- const ROW = 20;
- const COL = 10;
- const SQ = 30; // Square size
- const VACANT = "TRANSPARENT"; // Empty square color
- let board = [];
- let score = 0;
- let level = 1;
- let gameOver = false;
- let isPaused = false;
- let dropStart = Date.now();
- let dropSpeed = 1000; // initial drop speed in ms
- // The Tetrominoes and their colors (using RGB for easier alpha manipulation)
- const PIECES = [
- [Z, "rgb(239, 68, 68)"], // Red
- [S, "rgb(34, 197, 94)"], // Green
- [T, "rgb(168, 85, 247)"], // Purple
- [O, "rgb(234, 179, 8)"], // Yellow
- [L, "rgb(249, 115, 22)"], // Orange
- [I, "rgb(6, 182, 212)"], // Cyan
- [J, "rgb(59, 130, 246)"] // Blue
- ];
- // Shape Definitions
- function Z() { return [[[1,1,0],[0,1,1],[0,0,0]], [[0,0,1],[0,1,1],[0,1,0]], [[0,0,0],[1,1,0],[0,1,1]], [[0,1,0],[1,1,0],[1,0,0]]]; }
- function S() { return [[[0,1,1],[1,1,0],[0,0,0]], [[0,1,0],[0,1,1],[0,0,1]], [[0,0,0],[0,1,1],[1,1,0]], [[1,0,0],[1,1,0],[0,1,0]]]; }
- function T() { return [[[0,1,0],[1,1,1],[0,0,0]], [[0,1,0],[0,1,1],[0,1,0]], [[0,0,0],[1,1,1],[0,1,0]], [[0,1,0],[1,1,0],[0,1,0]]]; }
- function O() { return [[[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]]]; } // O only has one state effectively, but kept 4x4 for consistency if needed, simplified here to avoid rotation issues. Actually O doesn't rotate.
- function L() { return [[[0,0,1],[1,1,1],[0,0,0]], [[0,1,0],[0,1,0],[0,1,1]], [[0,0,0],[1,1,1],[1,0,0]], [[1,1,0],[0,1,0],[0,1,0]]]; }
- function I() { return [[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], [[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]], [[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]], [[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]]; }
- function J() { return [[[1,0,0],[1,1,1],[0,0,0]], [[0,1,1],[0,1,0],[0,1,0]], [[0,0,0],[1,1,1],[0,0,1]], [[0,1,0],[0,1,0],[1,1,0]]]; }
- // Fix O shape rotation by giving it identical states if we use the standard rotation logic,
- // or just handle it separately. Simplest is to just give it 1 state and not rotate it.
- // Redefining O for simplicity in uniform rotation calls:
- const O_Piece = [[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]]];
- // Init Board
- function initBoard() {
- for (let r = 0; r < ROW; r++) {
- board[r] = [];
- for (let c = 0; c < COL; c++) {
- board[r][c] = VACANT;
- }
- }
- }
- // Draw a single glass square
- function drawSquare(x, y, color, context = ctx, size = SQ) {
- if (color === VACANT) {
- // We don't draw vacant squares to keep transparency
- return;
- }
- const px = x * size;
- const py = y * size;
- // 1. Base semi-transparent fill
- // Convert rgb to rgba for transparency if it's not already VACANT
- let baseColor = color.replace('rgb', 'rgba').replace(')', ', 0.5)');
- context.fillStyle = baseColor;
- context.clearRect(px, py, size, size); // Clear first to avoid alpha stacking
- context.fillRect(px, py, size, size);
- // 2. Inner gradient for "glass" Shine
- let gradient = context.createLinearGradient(px, py, px + size/2, py + size/2);
- gradient.addColorStop(0, "rgba(255, 255, 255, 0.5)");
- gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
- context.fillStyle = gradient;
- context.fillRect(px, py, size, size);
- // 3. Glass borders (bevel effect)
- context.lineWidth = 1;
- // Top & Left (Highlight)
- context.beginPath();
- context.moveTo(px, py + size);
- context.lineTo(px, py);
- context.lineTo(px + size, py);
- context.strokeStyle = "rgba(255, 255, 255, 0.6)";
- context.stroke();
- // Bottom & Right (Shadow)
- context.beginPath();
- context.moveTo(px + size, py);
- context.lineTo(px + size, py + size);
- context.lineTo(px, py + size);
- context.strokeStyle = "rgba(0, 0, 0, 0.2)";
- context.stroke();
- }
- // Draw Board
- function drawBoard() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- for (let r = 0; r < ROW; r++) {
- for (let c = 0; c < COL; c++) {
- drawSquare(c, r, board[r][c]);
- }
- }
- // Optional: draw faint grid lines
- ctx.strokeStyle = "rgba(255,255,255,0.05)";
- ctx.lineWidth = 1;
- for (let r=0; r<=ROW; r++) {
- ctx.beginPath(); ctx.moveTo(0, r*SQ); ctx.lineTo(COL*SQ, r*SQ); ctx.stroke();
- }
- for (let c=0; c<=COL; c++) {
- ctx.beginPath(); ctx.moveTo(c*SQ, 0); ctx.lineTo(c*SQ, ROW*SQ); ctx.stroke();
- }
- }
- // Piece Object
- class Piece {
- constructor(tetromino, color) {
- this.tetromino = tetromino;
- this.color = color;
- this.tetrominoN = 0; // rotation index
- this.activeTetromino = this.tetromino[this.tetrominoN];
- // Center the piece initially
- this.x = 3;
- this.y = -2; // Start slightly above board
- }
- fill(color) {
- for (let r = 0; r < this.activeTetromino.length; r++) {
- for (let c = 0; c < this.activeTetromino.length; c++) {
- if (this.activeTetromino[r][c]) {
- drawSquare(this.x + c, this.y + r, color);
- }
- }
- }
- }
- draw() {
- this.fill(this.color);
- }
- unDraw() {
- // We don't actually need to undraw explicitly if we redraw the whole board every frame,
- // but for optimization in some engines you might. Here we redraw board + piece in loop.
- }
- moveDown() {
- if (!this.collision(0, 1, this.activeTetromino)) {
- this.y++;
- } else {
- this.lock();
- if(!gameOver) {
- spawnNextPiece();
- }
- }
- }
- moveRight() {
- if (!this.collision(1, 0, this.activeTetromino)) {
- this.x++;
- }
- }
- moveLeft() {
- if (!this.collision(-1, 0, this.activeTetromino)) {
- this.x--;
- }
- }
- rotate() {
- let nextPattern = this.tetromino[(this.tetrominoN + 1) % this.tetromino.length];
- let kick = 0;
- // Simple wall kick: if rotation collides, try moving left/right once
- if (this.collision(0, 0, nextPattern)) {
- if (this.x > COL/2) { // it's on the right side, try kicking left
- if (!this.collision(-1, 0, nextPattern)) {
- this.x -= 1;
- kick = 1;
- }
- } else { // on left side, try kicking right
- if (!this.collision(1, 0, nextPattern)) {
- this.x += 1;
- kick = 1;
- }
- }
- }
- if (!this.collision(0, 0, nextPattern)) {
- this.tetrominoN = (this.tetrominoN + 1) % this.tetromino.length;
- this.activeTetromino = this.tetromino[this.tetrominoN];
- }
- }
- hardDrop() {
- while (!this.collision(0, 1, this.activeTetromino)) {
- this.y++;
- }
- this.lock();
- if(!gameOver) {
- spawnNextPiece();
- }
- }
- lock() {
- for (let r = 0; r < this.activeTetromino.length; r++) {
- for (let c = 0; c < this.activeTetromino.length; c++) {
- // Skip vacant squares
- if (!this.activeTetromino[r][c]) continue;
- // Game Over if locking above board
- if (this.y + r < 0) {
- endGame();
- return;
- }
- // Lock piece into board
- board[this.y + r][this.x + c] = this.color;
- }
- }
- // Clear lines
- let rowsCleared = 0;
- for (let r = 0; r < ROW; r++) {
- let isRowFull = true;
- for (let c = 0; c < COL; c++) {
- if (board[r][c] === VACANT) {
- isRowFull = false;
- break;
- }
- }
- if (isRowFull) {
- rowsCleared++;
- // Move all rows above down
- for (let y = r; y > 0; y--) {
- for (let c = 0; c < COL; c++) {
- board[y][c] = board[y - 1][c];
- }
- }
- // Top row is now vacant
- for (let c = 0; c < COL; c++) {
- board[0][c] = VACANT;
- }
- }
- }
- if (rowsCleared > 0) {
- // Classic scoring
- const lineScores = [0, 40, 100, 300, 1200];
- score += lineScores[rowsCleared] * level;
- scoreElement.innerText = score;
- // Level up every 10 lines or roughly every 1000 points for simplicity here
- // Let's do score based leveling
- if (score > level * 1000) {
- level++;
- levelElement.innerText = level;
- dropSpeed = Math.max(100, 1000 - (level - 1) * 100);
- }
- }
- }
- collision(x, y, piece) {
- for (let r = 0; r < piece.length; r++) {
- for (let c = 0; c < piece.length; c++) {
- // If the square is empty, we skip it
- if (!piece[r][c]) {
- continue;
- }
- // Coordinates of the piece after movement
- let newX = this.x + c + x;
- let newY = this.y + r + y;
- // Conditions
- if (newX < 0 || newX >= COL || newY >= ROW) {
- return true;
- }
- // Skip checking board if y is still above it (negative)
- if (newY < 0) {
- continue;
- }
- // Check if there is a locked piece already
- if (board[newY][newX] !== VACANT) {
- return true;
- }
- }
- }
- return false;
- }
- }
- // Game State Management
- let p, nextP;
- function randomPiece() {
- let r = Math.floor(Math.random() * PIECES.length);
- return new Piece(PIECES[r][0](), PIECES[r][1]);
- }
- function spawnNextPiece() {
- p = nextP;
- nextP = randomPiece();
- drawNextPiece();
- }
- function drawNextPiece() {
- nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
- // Center the next piece in the small canvas
- // Assume standard 4x4 max size, tiles are smaller here usually to fit
- let previewSize = 20;
- let shape = nextP.activeTetromino;
- // Calculate offset to center
- let w = shape[0].length * previewSize;
- let h = shape.length * previewSize;
- let offX = (nextCanvas.width - w) / 2;
- let offY = (nextCanvas.height - h) / 2;
- for(let r=0; r<shape.length; r++){
- for(let c=0; c<shape.length; c++){
- if(shape[r][c]){
- drawSquare(c, r, nextP.color, nextCtx, previewSize);
- // Adjust positions because drawSquare uses grid coordinates * size
- // We need to manually offset standard drawSquare for the preview canvas if we want it centered nicely without rewriting drawSquare entirely.
- // Actually, let's just use a quick custom draw for preview to handle offsets easily.
- let px = offX + c * previewSize;
- let py = offY + r * previewSize;
- nextCtx.fillStyle = nextP.color.replace('rgb', 'rgba').replace(')', ', 0.8)');
- nextCtx.fillRect(px, py, previewSize-1, previewSize-1);
- nextCtx.strokeStyle = "rgba(255,255,255,0.5)";
- nextCtx.strokeRect(px,py,previewSize-1,previewSize-1);
- }
- }
- }
- }
- // Game Loop
- function drop() {
- if(gameOver || isPaused) return;
- let now = Date.now();
- let delta = now - dropStart;
- if (delta > dropSpeed) {
- p.moveDown();
- dropStart = Date.now();
- }
- drawBoard();
- p.draw();
- requestAnimationFrame(drop);
- }
- // Controls
- document.addEventListener("keydown", CONTROL);
- function CONTROL(event) {
- if(gameOver) return;
- // Pause (P key)
- if(event.keyCode == 80) {
- togglePause();
- return;
- }
- if(isPaused) return;
- if(event.keyCode == 37) {
- p.moveLeft();
- dropStart = Date.now(); // slight delay after user move before gravity kicks in again
- } else if(event.keyCode == 38) {
- p.rotate();
- dropStart = Date.now();
- } else if(event.keyCode == 39) {
- p.moveRight();
- dropStart = Date.now();
- } else if(event.keyCode == 40) {
- p.moveDown();
- // Don't reset dropStart here, let them hold it to soft drop fast
- } else if(event.keyCode == 32) {
- p.hardDrop();
- }
- // Redraw immediately on input for responsiveness
- drawBoard();
- p.draw();
- }
- function togglePause() {
- isPaused = !isPaused;
- if(isPaused) {
- overlayTitle.innerText = "Paused";
- startBtn.innerText = "Resume";
- overlay.classList.remove("hidden");
- startBtn.onclick = togglePause; // Resume on click
- } else {
- overlay.classList.add("hidden");
- drop(); // Restart loop
- }
- }
- function endGame() {
- gameOver = true;
- overlayTitle.innerText = "Game Over";
- startBtn.innerText = "Play Again";
- overlay.classList.remove("hidden");
- startBtn.onclick = resetGame;
- }
- function resetGame() {
- initBoard();
- score = 0;
- level = 1;
- dropSpeed = 1000;
- gameOver = false;
- isPaused = false;
- scoreElement.innerText = score;
- levelElement.innerText = level;
- overlay.classList.add("hidden");
- nextP = randomPiece();
- spawnNextPiece();
- dropStart = Date.now();
- drop();
- }
- // Initial Setup
- startBtn.addEventListener('click', () => {
- if(gameOver || overlayTitle.innerText === "Glass Tetris") {
- resetGame();
- } else {
- togglePause();
- }
- });
- initBoard();
- drawBoard(); // Draw empty board initially
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment