From 021d61a1b090158664ecb7d5e27cafe27b7f2df3 Mon Sep 17 00:00:00 2001 From: Jevinca Marvella Date: Tue, 18 Nov 2025 19:42:10 +0700 Subject: [PATCH] 2048 --- 2048.css | 153 ++++++++++++++++++++++++ 2048.html | 5 +- 2048.js | 348 ++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 429 insertions(+), 77 deletions(-) diff --git a/2048.css b/2048.css index 88da4ad..8ade4f5 100644 --- a/2048.css +++ b/2048.css @@ -144,3 +144,156 @@ button:hover { .tile-1024 { background: #00ffaa55; box-shadow: 0 0 10px #00ffaa; } .tile-2048 { background: #ffd70066; box-shadow: 0 0 15px #ffd700; } +/* ======= ENHANCEMENTS: Animations, Particles, Glows ======= */ + +/* ensure board stacking context for absolute animations */ +#board { position: relative; z-index: 2; } + +.particles { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + + background: + radial-gradient(circle at 20% 30%, rgba(0,234,255,0.18), transparent 45%), + radial-gradient(circle at 80% 70%, rgba(255,0,90,0.18), transparent 45%), + radial-gradient(circle at 50% 50%, rgba(140,0,255,0.14), transparent 55%), + radial-gradient(circle at 10% 85%, rgba(0,255,200,0.12), transparent 55%), + radial-gradient(circle at 90% 15%, rgba(255,0,255,0.15), transparent 45%); + + filter: blur(55px) brightness(120%) saturate(130%); + animation: particlesFloat 18s ease-in-out infinite alternate; +} + +@keyframes particlesFloat { + 0% { transform: translateY(0px) translateX(0px); } + 50% { transform: translateY(-25px) translateX(10px); } + 100% { transform: translateY(-40px) translateX(-15px); } +} + + +.cursor-light { + position: absolute; + width: 240px; + height: 240px; + background: radial-gradient(circle, rgba(0,255,255,0.25), transparent 70%); + border-radius: 50%; + pointer-events: none; + transform: translate(-50%, -50%); + filter: blur(40px); + mix-blend-mode: screen; + opacity: 0.6; +} + + +/* gentle vertical float */ +@keyframes floatBg { 0% { transform: translateY(0);} 100% { transform: translateY(-14px);} } + +/* Tile move / slide illusion: tiles appear from direction */ +.tile { + transition: transform 0.14s cubic-bezier(.2,.8,.2,1), opacity 0.12s linear, box-shadow 0.12s; + will-change: transform, opacity, box-shadow; +} + +/* 'new' pop kept but slightly tuned */ +.tile.new { + animation: pop 0.22s cubic-bezier(.2,.9,.2,1); +} + +/* merge pop */ +.tile.merge { + animation: mergePop 0.24s ease-out; + z-index: 3; +} +@keyframes mergePop { + 0% { transform: scale(1); box-shadow: 0 0 6px rgba(255,255,255,0.1); } + 50% { transform: scale(1.18); box-shadow: 0 0 30px rgba(255,255,255,0.35); } + 100% { transform: scale(1); box-shadow: 0 0 6px rgba(255,255,255,0.1); } +} + +/* board shake when invalid move */ +#board.shake { + animation: shakeBoard 0.36s; +} +@keyframes shakeBoard { + 0%,100% { transform: translateX(0); } + 25% { transform: translateX(-8px); } + 75% { transform: translateX(8px); } +} + +/* ambient glow under board (keeps aesthetics) */ +#board::after { + content: ""; + position: absolute; + bottom: -36px; + left: 50%; + transform: translateX(-50%); + width: 340px; + height: 84px; + background: radial-gradient(ellipse at center, rgba(0,234,255,0.12), transparent 60%); + filter: blur(30px); + z-index: 0; +} + +/* special 2048 tile sparkle (adds shimmer) */ +.tile-2048 { + animation: goldShimmer 2.4s infinite; +} +@keyframes goldShimmer { + 0% { box-shadow: 0 0 10px #ffd70066; transform: translateZ(0); } + 50% { box-shadow: 0 0 26px #ffd700aa; transform: translateY(-2px); } + 100% { box-shadow: 0 0 10px #ffd70066; transform: translateZ(0); } +} + +/* small particle bits that appear for merge */ +.merge-particle { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + pointer-events: none; + opacity: 0.95; + will-change: transform, opacity; + filter: blur(1px) drop-shadow(0 0 6px rgba(255,255,255,0.08)); +} + +/* ensure base tile readability on animation */ +.tile { backface-visibility: hidden; -webkit-backface-visibility: hidden; } + +/* mobile touch hint (optional) */ +.touch-hint { + position: fixed; + right: 18px; + bottom: 18px; + background: rgba(0,0,0,0.45); + padding: 8px 10px; + border-radius: 10px; + font-size: 12px; + z-index: 4; + color: #cfefff; +} + +.starfield { + position: fixed; + inset: 0; + overflow: hidden; + pointer-events: none; + z-index: 1; +} + +.starfield span { + position: absolute; + width: 4px; + height: 4px; + background: rgba(0,255,255,0.8); + border-radius: 50%; + filter: blur(2px); + animation: starMove linear infinite; +} + +/* bintang bergerak random */ +@keyframes starMove { + 0% { transform: translateY(0); opacity: 0.8; } + 100% { transform: translateY(-700px); opacity: 0; } +} diff --git a/2048.html b/2048.html index a150f2e..c5c2f07 100644 --- a/2048.html +++ b/2048.html @@ -6,8 +6,11 @@ 2048 - + + +
+

2048

diff --git a/2048.js b/2048.js index 00072df..fce07f0 100644 --- a/2048.js +++ b/2048.js @@ -1,5 +1,13 @@ +/* 2048.js — Enhanced with animations, particles, and glows + Replace previous 2048.js content with this file. +*/ + +/* ------------------------ + State & audio (kept) + ------------------------ */ let board = []; let score = 0; +let lastMoveDir = null; // 'left','right','up','down' or null // --- Audio setup --- const audio = { @@ -7,19 +15,15 @@ const audio = { pop: new Audio("pop.mp3"), merge: new Audio("merge.wav") }; - -// lower default volumes audio.bg.volume = 0.25; audio.pop.volume = 0.9; audio.merge.volume = 0.9; audio.bg.loop = true; -// try to play background music; may be blocked until user interaction function tryPlayBg() { audio.bg.play().catch(() => { - // autoplay blocked — will try again on first user keypress or click const unlock = () => { - audio.bg.play().catch(() => {}); + audio.bg.play().catch(()=>{}); window.removeEventListener("keydown", unlock); window.removeEventListener("click", unlock); }; @@ -28,20 +32,21 @@ function tryPlayBg() { }); } -// --- DOM ready: setup board and initial tiles --- +/* ------------------------ + DOM ready + ------------------------ */ document.addEventListener("DOMContentLoaded", () => { setupBoard(); addNewTile(); addNewTile(); tryPlayBg(); - - // keyboard controls (also used to unlock audio) document.addEventListener("keydown", handleKey); + setupAmbientCursor(); }); -// ---------------------------- -// SETUP BOARD (render tiles ONCE) -// ---------------------------- +/* ------------------------ + Setup & rendering + ------------------------ */ function setupBoard() { board = []; score = 0; @@ -53,7 +58,6 @@ function setupBoard() { return; } - // empty container and create fixed 4x4 cells container.innerHTML = ""; for (let r = 0; r < 4; r++) { board[r] = []; @@ -61,40 +65,55 @@ function setupBoard() { board[r][c] = 0; const tile = document.createElement("div"); tile.id = `${r}-${c}`; - tile.className = "tile"; // base class only - // ensure box sizing not influenced by text nodes - tile.style.boxSizing = "border-box"; + tile.className = "tile"; container.appendChild(tile); } } } -// ---------------------------- -// UPDATE UI FOR SINGLE TILE -// ---------------------------- +/* update single tile visual with small entrance based on last move */ function updateTile(row, col, num) { const tile = document.getElementById(`${row}-${col}`); if (!tile) return; - // reset classes to base + // reset classes except base .tile tile.className = "tile"; - // force reflow to allow re-adding 'new' animation class reliably - void tile.offsetWidth; + // ensure previous transforms cleared + tile.style.transform = ""; + tile.style.opacity = ""; if (num > 0) { tile.textContent = num; - // add tile class for color (expects classes like tile-2, tile-4, ...) tile.classList.add("tile-" + num); - // make numbers visually white-neon glow if desired: - // ensure text is centered by CSS; no inline style needed + // slide-illusion: appear from direction of last move + if (lastMoveDir) { + let tx = 0, ty = 0; + const gap = 22; // small px offset for feel + if (lastMoveDir === "left") tx = gap; + else if (lastMoveDir === "right") tx = -gap; + else if (lastMoveDir === "up") ty = gap; + else if (lastMoveDir === "down") ty = -gap; + + // start slightly offset & transparent, then animate to 0 + tile.style.transform = `translate(${tx}px, ${ty}px)`; + tile.style.opacity = "0.0"; + // force reflow then animate back + void tile.offsetWidth; + tile.style.transition = "transform 0.14s cubic-bezier(.2,.8,.2,1), opacity 0.12s"; + tile.style.transform = ""; + tile.style.opacity = "1"; + // cleanup transition after done + setTimeout(() => { tile.style.transition = ""; }, 160); + } + } else { tile.textContent = ""; } } -// updates entire board DOM from board array +/* refresh whole board */ function refreshBoard() { for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { @@ -104,17 +123,13 @@ function refreshBoard() { updateScore(); } -// ---------------------------- -// SCORE UI -// ---------------------------- +/* score */ function updateScore() { const el = document.getElementById("score"); if (el) el.textContent = score; } -// ---------------------------- -// ADD NEW TILE (value 2, play pop sound + animation) -// ---------------------------- +/* add new tile with pop animation */ function addNewTile() { const empty = []; for (let r = 0; r < 4; r++) { @@ -131,65 +146,42 @@ function addNewTile() { const tile = document.getElementById(`${spot.r}-${spot.c}`); if (tile) { tile.classList.add("new"); - // play pop sound playSound(audio.pop); - // remove 'new' class after animation completes to keep DOM tidy tile.addEventListener("animationend", function handler() { tile.classList.remove("new"); tile.removeEventListener("animationend", handler); }); - // update text/color updateTile(spot.r, spot.c, 2); } else { - // fallback updateTile(spot.r, spot.c, 2); } return true; } -// helper to play sound safely +/* safe playSound */ function playSound(soundObj) { try { soundObj.currentTime = 0; - soundObj.play().catch(() => { - // suppressed (autoplay or other) - }); - } catch (e) { /* ignore */ } + soundObj.play().catch(() => {}); + } catch (e) {} } -// ---------------------------- -// INPUT HANDLING -// ---------------------------- -function handleKey(e) { - let moved = false; - if (e.key === "ArrowLeft") moved = moveLeft(); - else if (e.key === "ArrowRight") moved = moveRight(); - else if (e.key === "ArrowUp") moved = moveUp(); - else if (e.key === "ArrowDown") moved = moveDown(); - - if (moved) { - // after a successful move: add tile, refresh board, play bg or unlock - addNewTile(); - refreshBoard(); - } -} - -// ---------- MOVE HELPERS ---------- +/* ------------------------ + Movement helpers (logic preserved) + ------------------------ */ function filterZero(row) { return row.filter(n => n !== 0); } function slide(row) { - // row is an array of length 4 row = filterZero(row); let mergedThisMove = false; for (let i = 0; i < row.length - 1; i++) { if (row[i] === row[i + 1]) { row[i] = row[i] * 2; - // play merge sound and vibrate playSound(audio.merge); - if (navigator.vibrate) navigator.vibrate(30); + if (navigator.vibrate) navigator.vibrate(28); score += row[i]; row[i + 1] = 0; @@ -202,7 +194,6 @@ function slide(row) { return { row, merged: mergedThisMove }; } -// Compare arrays helper function arraysEqual(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]); } @@ -260,17 +251,44 @@ function moveDown() { return moved; } +/* after move: refresh and reset lastMoveDir after small delay */ function updateAfterMove() { - // update all tiles now (this keeps sizes stable) + // apply merge glow to merged tiles (scan for high values that were recently created) refreshBoard(); - // update score DOM updateScore(); - // small debounce is not required—moves are sequential + // schedule dropping lastMoveDir after small delay so new tiles animate in direction + setTimeout(() => { lastMoveDir = null; }, 180); } -// ---------------------------- -// RESTART & HOME -// ---------------------------- +/* ------------------------ + Input handling (adds lastMoveDir + invalid-move shake) + ------------------------ */ +function handleKey(e) { + let moved = false; + if (e.key === "ArrowLeft") { lastMoveDir = "left"; moved = moveLeft(); } + else if (e.key === "ArrowRight") { lastMoveDir = "right"; moved = moveRight(); } + else if (e.key === "ArrowUp") { lastMoveDir = "up"; moved = moveUp(); } + else if (e.key === "ArrowDown") { lastMoveDir = "down"; moved = moveDown(); } + + if (moved) { + // add tile + subtle delay so new tile animates from direction + setTimeout(() => { + addNewTile(); + refreshBoard(); + }, 70); + } else { + // show board shake + const b = document.getElementById("board"); + if (b) { + b.classList.add("shake"); + setTimeout(()=>b.classList.remove("shake"), 360); + } + } +} + +/* ------------------------ + Restart & home + ------------------------ */ function restartGame() { setupBoard(); addNewTile(); @@ -279,14 +297,13 @@ function restartGame() { } function goHome() { - // stops music to prevent continuing on homepage try { audio.bg.pause(); audio.bg.currentTime = 0; } catch (e) {} window.location.href = "Homepage.html"; } -// ---------------------------- -// OPTIONAL: touch swipe for mobile -// ---------------------------- +/* ------------------------ + Touch swipe + ------------------------ */ let touchStartX = 0; let touchStartY = 0; document.addEventListener("touchstart", function (e) { @@ -300,12 +317,191 @@ document.addEventListener("touchend", function (e) { const dx = t.clientX - touchStartX; const dy = t.clientY - touchStartY; if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 30) { - if (dx > 0) moveRight() && addNewTile() && refreshBoard(); - else moveLeft() && addNewTile() && refreshBoard(); + if (dx > 0) { lastMoveDir = "right"; moveRight() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } + else { lastMoveDir = "left"; moveLeft() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } } else if (Math.abs(dy) > 30) { - if (dy > 0) moveDown() && addNewTile() && refreshBoard(); - else moveUp() && addNewTile() && refreshBoard(); + if (dy > 0) { lastMoveDir = "down"; moveDown() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } + else { lastMoveDir = "up"; moveUp() && setTimeout(()=>{ addNewTile(); refreshBoard(); }, 70); } } }, { passive: true }); +/* ------------------------ + Ambient cursor light + merge particles + ------------------------ */ +function setupAmbientCursor() { + const container = document.querySelector(".particles"); + if (!container) return; + // create a subtle cursor-follow blob + const cursor = document.createElement("div"); + cursor.className = "cursor-light"; + container.appendChild(cursor); + + let lastX = window.innerWidth/2, lastY = window.innerHeight/2; + document.addEventListener("mousemove", (e) => { + lastX = e.clientX; lastY = e.clientY; + cursor.style.left = lastX + "px"; + cursor.style.top = lastY + "px"; + }); + + // small periodic motion for background + setInterval(() => { + cursor.style.opacity = (0.4 + Math.random()*0.35).toString(); + }, 900); +} + +/* spawn merge particles at tile center */ +function spawnMergeParticles(row, col, colorHex="#00eaff") { + const container = document.body; + const boardRect = document.getElementById("board").getBoundingClientRect(); + const tileEl = document.getElementById(`${row}-${col}`); + if (!tileEl) return; + + const tileRect = tileEl.getBoundingClientRect(); + const cx = tileRect.left + tileRect.width/2; + const cy = tileRect.top + tileRect.height/2; + + const particles = []; + const count = 10; + for (let i = 0; i < count; i++) { + const p = document.createElement("div"); + p.className = "merge-particle"; + p.style.background = colorHex; + p.style.left = (cx - 6) + "px"; + p.style.top = (cy - 6) + "px"; + p.style.opacity = "1"; + p.style.transform = "translate(0,0) scale(1)"; + document.body.appendChild(p); + particles.push(p); + + // random flight vector + const angle = Math.random() * Math.PI * 2; + const dist = 24 + Math.random()*36; + const tx = Math.cos(angle) * dist; + const ty = Math.sin(angle) * dist; + const rot = (Math.random() * 360)|0; + p.animate([ + { transform: `translate(0,0) rotate(0deg) scale(1)`, opacity: 1 }, + { transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(0.6)`, opacity: 0 } + ], { + duration: 420 + Math.random()*240, + easing: "cubic-bezier(.2,.8,.2,1)", + fill: "forwards" + }); + + // cleanup + setTimeout(()=>{ try{ p.remove(); }catch(e){} }, 800 + Math.random()*400); + } +} + +/* ------------------------ + Optional: call spawn on merges + We don't track exact merge positions in slide() local scope here, + but we can detect new larger tiles after move vs before and spawn particles. + ------------------------ */ +function spawnMergesFromDiff(prev, next) { + // prev & next are 4x4 arrays + for (let r = 0; r < 4; r++) { + for (let c = 0; c < 4; c++) { + if (next[r][c] > 0 && prev[r][c] !== next[r][c]) { + // if new value appears that wasn't same in prev -> likely merged or moved; if it's > 2 we spawn small effect + if (next[r][c] >= 4) { + spawnMergeParticles(r, c, chooseColorForValue(next[r][c])); + const tileEl = document.getElementById(`${r}-${c}`); + if (tileEl) { + tileEl.classList.add("merge"); + setTimeout(()=>tileEl.classList.remove("merge"), 260); + } + } + } + } + } +} + +/* choose nice color for particle based on value */ +function chooseColorForValue(n) { + if (n >= 2048) return "#ffd700"; + if (n >= 1024) return "#00ffaa"; + if (n >= 512) return "#ff00aa"; + if (n >= 128) return "#5f00ff"; + if (n >= 32) return "#ffaa00"; + return "#00eaff"; +} + +/* We'll wrap move functions to produce prev snapshot, then spawn particles for merges detected */ +function cloneBoard(b) { + const out = []; + for (let r = 0; r < 4; r++) out.push([...b[r]]); + return out; +} + +/* Override move functions to spawn particles after move */ +function moveLeft() { + const prev = cloneBoard(board); + let moved = false; + for (let r = 0; r < 4; r++) { + const { row: newRow } = slide(board[r]); + if (!arraysEqual(newRow, board[r])) moved = true; + board[r] = newRow; + } + if (moved) { + spawnMergesFromDiff(prev, board); + updateAfterMove(); + } + return moved; +} +function moveRight() { + const prev = cloneBoard(board); + let moved = false; + for (let r = 0; r < 4; r++) { + let reversed = [...board[r]].reverse(); + const { row: slid } = slide(reversed); + let newRow = slid.reverse(); + if (!arraysEqual(newRow, board[r])) moved = true; + board[r] = newRow; + } + if (moved) { + spawnMergesFromDiff(prev, board); + updateAfterMove(); + } + return moved; +} +function moveUp() { + const prev = cloneBoard(board); + let moved = false; + for (let c = 0; c < 4; c++) { + const col = [board[0][c], board[1][c], board[2][c], board[3][c]]; + const { row: newCol } = slide(col); + for (let r = 0; r < 4; r++) { + if (board[r][c] !== newCol[r]) moved = true; + board[r][c] = newCol[r]; + } + } + if (moved) { + spawnMergesFromDiff(prev, board); + updateAfterMove(); + } + return moved; +} +function moveDown() { + const prev = cloneBoard(board); + let moved = false; + for (let c = 0; c < 4; c++) { + const col = [board[3][c], board[2][c], board[1][c], board[0][c]]; + const { row: slid } = slide(col); + const newCol = slid.reverse(); + for (let r = 0; r < 4; r++) { + if (board[r][c] !== newCol[r]) moved = true; + board[r][c] = newCol[r]; + } + } + if (moved) { + spawnMergesFromDiff(prev, board); + updateAfterMove(); + } + return moved; +} + +/* ------------------------ + End of file + ------------------------ */