/** * PIXEL SLIDE — 15-Puzzle Image Game with PixiJS v8 * Complete game logic: Image Slicing, Puzzle Engine, PixiJS Rendering, * Web Audio FX, Leaderboard, and UI Controllers. */ // ============================================================================ // 1. CONFIGURATION & CONSTANTS // ============================================================================ const LEVELS = { debug: { key: "debug", grid: 3, shuffleMoves: 5, label: "Debug", gridText: "3×3", tilesText: "8 Tiles + 1 Blank", desc: "Trivial dev mode guaranteed solvable in ≤5 moves.", badge: "🐛 Dev Mode", color: "#f59e0b", glow: "rgba(245, 158, 11, 0.4)" }, easy: { key: "easy", grid: 3, shuffleMoves: 30, label: "Easy", gridText: "3×3", tilesText: "8 Tiles + 1 Blank", desc: "Quick 3×3 warm-up puzzle for beginners.", badge: "Beginner", color: "#10b981", glow: "rgba(16, 185, 129, 0.4)" }, medium: { key: "medium", grid: 4, shuffleMoves: 100, label: "Medium", gridText: "4×4", tilesText: "15 Tiles + 1 Blank", desc: "The classic 15-puzzle standard challenge.", badge: "Standard", color: "#00e5ff", glow: "rgba(0, 229, 255, 0.4)" }, hard: { key: "hard", grid: 5, shuffleMoves: 200, label: "Hard", gridText: "5×5", tilesText: "24 Tiles + 1 Blank", desc: "24 sliding tiles for advanced puzzle solvers.", badge: "Expert", color: "#9d4edd", glow: "rgba(157, 78, 221, 0.4)" }, insane: { key: "insane", grid: 6, shuffleMoves: 400, label: "Insane", gridText: "6×6", tilesText: "35 Tiles + 1 Blank", desc: "Extreme 35-tile challenge for true masters.", badge: "Extreme", color: "#f43f5e", glow: "rgba(244, 63, 94, 0.4)" } }; const LEADERBOARD_KEY = "puzzle15_leaderboard_v1"; const SOUND_MUTE_KEY = "puzzle15_sound_muted"; const CANVAS_BOARD_SIZE = 600; // Internal Pixi resolution const TILE_GAP = 3; // Pixel gap between tiles // ============================================================================ // 2. AUDIO SYNTHESIZER (Web Audio API) // ============================================================================ // 2. AUDIO SYNTHESIZER & SOUNDTRACK (Web Audio API & Audio Tracks) // ============================================================================ class SoundController { constructor() { this.ctx = null; this.muted = localStorage.getItem(SOUND_MUTE_KEY) === "true"; this.userInteracted = false; // Background soundtrack this.bgMusic = new Audio("assets/sounds/backsound.mp3"); this.bgMusic.loop = true; this.bgMusic.volume = 0.35; // Winner fanfare track this.winAudio = new Audio("assets/sounds/winner.mp3"); this.winAudio.volume = 0.75; } init() { this.userInteracted = true; if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) { const AudioCtx = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioCtx(); } if (this.ctx && this.ctx.state === "suspended") { this.ctx.resume(); } if (!this.muted && this.bgMusic.paused) { this.playBGM(); } } playBGM() { if (this.muted) return; this.bgMusic.play().catch(() => { // Handled silently if autoplay restricted before interaction }); } pauseBGM() { this.bgMusic.pause(); } resumeBGM() { if (!this.muted && this.userInteracted) { this.playBGM(); } } toggleMute() { this.muted = !this.muted; localStorage.setItem(SOUND_MUTE_KEY, this.muted); if (this.muted) { this.pauseBGM(); this.winAudio.pause(); } else { if (this.userInteracted) { this.playBGM(); } } return this.muted; } playSlide() { if (this.muted) return; this.init(); if (!this.ctx) return; const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = "sine"; osc.frequency.setValueAtTime(180, now); osc.frequency.exponentialRampToValueAtTime(320, now + 0.08); gain.gain.setValueAtTime(0.08, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.1); } playInvalid() { if (this.muted) return; this.init(); if (!this.ctx) return; const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = "sawtooth"; osc.frequency.setValueAtTime(120, now); osc.frequency.linearRampToValueAtTime(90, now + 0.1); gain.gain.setValueAtTime(0.06, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.12); } playClick() { if (this.muted) return; this.init(); if (!this.ctx) return; const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = "triangle"; osc.frequency.setValueAtTime(440, now); gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.04); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.04); } playVictory() { if (this.muted) return; this.init(); // Pause backsound so winner fanfare is crystal clear this.pauseBGM(); // Play winner mp3 audio this.winAudio.currentTime = 0; const playPromise = this.winAudio.play(); if (playPromise !== undefined) { playPromise.catch((e) => { console.warn("winner.mp3 playback failed, playing synth fallback:", e); this.playSynthVictoryFallback(); }); } } playSynthVictoryFallback() { if (!this.ctx) return; const notes = [523.25, 659.25, 783.99, 1046.50, 1318.51]; const now = this.ctx.currentTime; notes.forEach((freq, i) => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const start = now + i * 0.1; osc.type = "triangle"; osc.frequency.setValueAtTime(freq, start); gain.gain.setValueAtTime(0, start); gain.gain.linearRampToValueAtTime(0.12, start + 0.03); gain.gain.exponentialRampToValueAtTime(0.001, start + 0.4); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(start); osc.stop(start + 0.45); }); } } const sounds = new SoundController(); // ============================================================================ // 3. PRESET PROCEDURAL ART GENERATORS // ============================================================================ function createPresetArtworks() { const presets = []; // Preset 1: Cyberpunk Neon Skyline const canvas1 = document.createElement("canvas"); canvas1.width = 600; canvas1.height = 600; const ctx1 = canvas1.getContext("2d"); const grad1 = ctx1.createLinearGradient(0, 0, 0, 600); grad1.addColorStop(0, "#08071a"); grad1.addColorStop(0.5, "#2b0938"); grad1.addColorStop(1, "#03001e"); ctx1.fillStyle = grad1; ctx1.fillRect(0, 0, 600, 600); // Cyber grid ctx1.strokeStyle = "rgba(0, 229, 255, 0.35)"; ctx1.lineWidth = 1.5; for (let y = 350; y <= 600; y += 25) { ctx1.beginPath(); ctx1.moveTo(0, y); ctx1.lineTo(600, y); ctx1.stroke(); } for (let x = -200; x <= 800; x += 50) { ctx1.beginPath(); ctx1.moveTo(300, 320); ctx1.lineTo(x, 600); ctx1.stroke(); } // Neon Sun const sunGrad = ctx1.createRadialGradient(300, 300, 20, 300, 300, 140); sunGrad.addColorStop(0, "#ff007f"); sunGrad.addColorStop(0.7, "#ffaa00"); sunGrad.addColorStop(1, "rgba(255, 170, 0, 0)"); ctx1.fillStyle = sunGrad; ctx1.beginPath(); ctx1.arc(300, 300, 140, 0, Math.PI * 2); ctx1.fill(); // Skyline buildings ctx1.fillStyle = "#0c0824"; const bldg = [ [40, 280, 50, 200], [100, 220, 60, 260], [170, 260, 50, 220], [230, 180, 70, 300], [310, 200, 65, 280], [390, 250, 55, 230], [460, 210, 60, 270], [530, 290, 45, 190] ]; bldg.forEach(([x, y, w, h]) => { ctx1.fillRect(x, y, w, h); ctx1.fillStyle = "rgba(0, 229, 255, 0.7)"; for (let wy = y + 15; wy < 400; wy += 20) { for (let wx = x + 8; wx < x + w - 8; wx += 14) { if (Math.sin(wx * wy) > 0) ctx1.fillRect(wx, wy, 6, 8); } } ctx1.fillStyle = "#0c0824"; }); presets.push({ id: "cyberpunk", name: "Neon City", canvas: canvas1 }); // Preset 2: Cosmic Nebula Galaxy const canvas2 = document.createElement("canvas"); canvas2.width = 600; canvas2.height = 600; const ctx2 = canvas2.getContext("2d"); ctx2.fillStyle = "#020208"; ctx2.fillRect(0, 0, 600, 600); // Stars for (let i = 0; i < 200; i++) { const x = Math.random() * 600; const y = Math.random() * 600; const r = Math.random() * 1.8; ctx2.fillStyle = Math.random() > 0.3 ? "#ffffff" : "#00e5ff"; ctx2.beginPath(); ctx2.arc(x, y, r, 0, Math.PI * 2); ctx2.fill(); } // Nebula clouds const nebula1 = ctx2.createRadialGradient(240, 260, 10, 240, 260, 220); nebula1.addColorStop(0, "rgba(157, 78, 221, 0.75)"); nebula1.addColorStop(0.5, "rgba(58, 134, 255, 0.4)"); nebula1.addColorStop(1, "rgba(0,0,0,0)"); ctx2.fillStyle = nebula1; ctx2.fillRect(0, 0, 600, 600); const nebula2 = ctx2.createRadialGradient(380, 360, 20, 380, 360, 180); nebula2.addColorStop(0, "rgba(0, 229, 255, 0.65)"); nebula2.addColorStop(0.6, "rgba(255, 0, 128, 0.3)"); nebula2.addColorStop(1, "rgba(0,0,0,0)"); ctx2.fillStyle = nebula2; ctx2.fillRect(0, 0, 600, 600); // Glowing core const core = ctx2.createRadialGradient(300, 300, 0, 300, 300, 60); core.addColorStop(0, "rgba(255, 255, 255, 0.95)"); core.addColorStop(0.3, "rgba(0, 229, 255, 0.6)"); core.addColorStop(1, "rgba(0, 229, 255, 0)"); ctx2.fillStyle = core; ctx2.beginPath(); ctx2.arc(300, 300, 60, 0, Math.PI * 2); ctx2.fill(); presets.push({ id: "nebula", name: "Deep Galaxy", canvas: canvas2 }); // Preset 3: Geometric Synth Crystals const canvas3 = document.createElement("canvas"); canvas3.width = 600; canvas3.height = 600; const ctx3 = canvas3.getContext("2d"); const bg3 = ctx3.createLinearGradient(0, 0, 600, 600); bg3.addColorStop(0, "#0f172a"); bg3.addColorStop(1, "#020617"); ctx3.fillStyle = bg3; ctx3.fillRect(0, 0, 600, 600); const colors = ["#00e5ff", "#3a86ff", "#7928ca", "#ff007f", "#10b981", "#f59e0b"]; for (let r = 240; r >= 30; r -= 35) { ctx3.strokeStyle = colors[(r / 35) % colors.length]; ctx3.lineWidth = 4; ctx3.beginPath(); for (let a = 0; a < Math.PI * 2; a += Math.PI / 3) { const px = 300 + Math.cos(a + r * 0.02) * r; const py = 300 + Math.sin(a + r * 0.02) * r; if (a === 0) ctx3.moveTo(px, py); else ctx3.lineTo(px, py); } ctx3.closePath(); ctx3.stroke(); } presets.push({ id: "synth", name: "Hex Prism", canvas: canvas3 }); // Preset 4: Emerald Forest Nature const canvas4 = document.createElement("canvas"); canvas4.width = 600; canvas4.height = 600; const ctx4 = canvas4.getContext("2d"); const bg4 = ctx4.createLinearGradient(0, 0, 0, 600); bg4.addColorStop(0, "#06281e"); bg4.addColorStop(0.6, "#0a4433"); bg4.addColorStop(1, "#03140f"); ctx4.fillStyle = bg4; ctx4.fillRect(0, 0, 600, 600); // Emerald mountain layers & golden moon const moon = ctx4.createRadialGradient(420, 150, 10, 420, 150, 70); moon.addColorStop(0, "#fff5d0"); moon.addColorStop(0.5, "#ffd166"); moon.addColorStop(1, "rgba(255, 209, 102, 0)"); ctx4.fillStyle = moon; ctx4.beginPath(); ctx4.arc(420, 150, 70, 0, Math.PI * 2); ctx4.fill(); const drawMountain = (color, pts) => { ctx4.fillStyle = color; ctx4.beginPath(); ctx4.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) ctx4.lineTo(pts[i][0], pts[i][1]); ctx4.lineTo(600, 600); ctx4.lineTo(0, 600); ctx4.closePath(); ctx4.fill(); }; drawMountain("rgba(16, 185, 129, 0.35)", [[0, 360], [150, 240], [300, 340], [450, 200], [600, 330]]); drawMountain("rgba(5, 150, 105, 0.65)", [[0, 420], [180, 310], [340, 400], [500, 280], [600, 390]]); drawMountain("#032e22", [[0, 480], [120, 380], [260, 470], [420, 360], [600, 450]]); presets.push({ id: "emerald", name: "Aurora Peak", canvas: canvas4 }); return presets; } // ============================================================================ // 4. MAIN GAME STATE & APP // ============================================================================ class PixelSlideGame { constructor() { this.presets = createPresetArtworks(); this.selectedImageCanvas = this.presets[0].canvas; // default image this.currentLevel = LEVELS.medium; this.board = []; // 1D array of tile indices (null for blank) this.sprites = []; // array of { id, sprite, textContainer, targetX, targetY, currX, currY } this.blankTileSprite = null; // Sprite for the solved state blank tile // Game stats this.moves = 0; this.startTime = null; this.elapsedTimeMs = 0; this.timerInterval = null; this.isGameStarted = false; this.isGameActive = false; // true when tiles are interactive & timer can run this.isPaused = false; this.isVictory = false; this.isAnimatingMove = false; // View toggles this.showNumbers = false; this.showGhostHint = false; // Pixi App this.pixiApp = null; this.boardContainer = null; this.tilesContainer = null; this.numbersContainer = null; // DOM cache this.dom = { // Screens screenHome: document.getElementById("screen-home"), screenLevelSelect: document.getElementById("screen-level-select"), screenGame: document.getElementById("screen-game"), screenLeaderboard: document.getElementById("screen-leaderboard"), // Overlays overlayPause: document.getElementById("overlay-pause"), overlayVictory: document.getElementById("overlay-victory"), shuffleOverlay: document.getElementById("shuffle-overlay"), ghostOverlay: document.getElementById("ghost-overlay"), // Home elements fileInput: document.getElementById("file-input"), uploadDropzone: document.getElementById("upload-dropzone"), presetGallery: document.getElementById("preset-gallery"), // Level select elements previewCanvas: document.getElementById("preview-canvas"), previewGridOverlay: document.getElementById("preview-grid-overlay"), levelCardsGrid: document.getElementById("level-cards-grid"), btnChangeImage: document.getElementById("btn-change-image"), btnBackToHome: document.getElementById("btn-back-to-home"), // Game HUD elements hudTimer: document.getElementById("hud-timer"), hudMoves: document.getElementById("hud-moves"), hudLevelBadge: document.getElementById("hud-level-badge"), pixiMount: document.getElementById("pixi-canvas-mount"), btnGamePause: document.getElementById("btn-game-pause"), btnToggleNumbers: document.getElementById("btn-toggle-numbers"), btnToggleHint: document.getElementById("btn-toggle-hint"), btnGameRestartQuick: document.getElementById("btn-game-restart-quick"), // Pause Modal elements pauseModalTime: document.getElementById("pause-modal-time"), pauseModalMoves: document.getElementById("pause-modal-moves"), btnPauseResume: document.getElementById("btn-pause-resume"), btnPauseRestart: document.getElementById("btn-pause-restart"), btnPauseChangeLevel: document.getElementById("btn-pause-change-level"), btnPauseQuit: document.getElementById("btn-pause-quit"), // Victory Modal elements victoryTime: document.getElementById("victory-time"), victoryMoves: document.getElementById("victory-moves"), victoryDifficulty: document.getElementById("victory-difficulty"), playerNameInput: document.getElementById("player-name-input"), victoryForm: document.getElementById("victory-form"), btnSaveScore: document.getElementById("btn-save-score"), saveFeedback: document.getElementById("save-feedback"), btnVictoryReplay: document.getElementById("btn-victory-replay"), btnVictoryLevels: document.getElementById("btn-victory-levels"), btnVictoryLeaderboard: document.getElementById("btn-victory-leaderboard"), confettiCanvas: document.getElementById("confetti-canvas"), // Leaderboard elements leaderboardTabs: document.getElementById("leaderboard-tabs"), leaderboardTbody: document.getElementById("leaderboard-tbody"), leaderboardEmpty: document.getElementById("leaderboard-empty"), btnClearLeaderboard: document.getElementById("btn-clear-leaderboard"), btnBackFromLeaderboard: document.getElementById("btn-back-from-leaderboard"), // Header controls btnNavHome: document.getElementById("btn-nav-home"), btnSoundToggle: document.getElementById("btn-sound-toggle"), soundIcon: document.getElementById("sound-icon"), btnHeaderLeaderboard: document.getElementById("btn-header-leaderboard") }; } // ========================================================================== // INITIALIZATION // ========================================================================== async init() { this.setupSoundUI(); this.renderPresetGallery(); this.renderLevelCards(); this.bindEvents(); this.updatePreviewCanvas(); this.renderLeaderboardTable("all"); // Auto-focus home screen this.showScreen("home"); } setupSoundUI() { this.dom.soundIcon.textContent = sounds.muted ? "🔇" : "🔊"; } // ========================================================================== // EVENT BINDINGS // ========================================================================== bindEvents() { // Nav Home this.dom.btnNavHome.addEventListener("click", () => { sounds.playClick(); this.pauseTimer(); this.showScreen("home"); }); // Sound toggle this.dom.btnSoundToggle.addEventListener("click", () => { const isMuted = sounds.toggleMute(); this.dom.soundIcon.textContent = isMuted ? "🔇" : "🔊"; if (!isMuted) sounds.playClick(); }); // Header Leaderboard this.dom.btnHeaderLeaderboard.addEventListener("click", () => { sounds.playClick(); this.pauseTimer(); this.renderLeaderboardTable("all"); this.showScreen("leaderboard"); }); // File Upload & Dropzone this.dom.uploadDropzone.addEventListener("click", () => { this.dom.fileInput.click(); }); this.dom.fileInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (file) this.handleImageUpload(file); }); ["dragenter", "dragover"].forEach((eventName) => { this.dom.uploadDropzone.addEventListener(eventName, (e) => { e.preventDefault(); this.dom.uploadDropzone.classList.add("drag-over"); }); }); ["dragleave", "drop"].forEach((eventName) => { this.dom.uploadDropzone.addEventListener(eventName, (e) => { e.preventDefault(); this.dom.uploadDropzone.classList.remove("drag-over"); }); }); this.dom.uploadDropzone.addEventListener("drop", (e) => { const files = e.dataTransfer.files; if (files && files.length > 0) { this.handleImageUpload(files[0]); } }); // Level Select Back & Change Image this.dom.btnBackToHome.addEventListener("click", () => { sounds.playClick(); this.showScreen("home"); }); this.dom.btnChangeImage.addEventListener("click", () => { sounds.playClick(); this.showScreen("home"); }); // Game HUD Controls this.dom.btnGamePause.addEventListener("click", () => { this.openPauseModal(); }); this.dom.btnToggleNumbers.addEventListener("click", () => { sounds.playClick(); this.toggleNumbers(); }); this.dom.btnToggleHint.addEventListener("click", () => { sounds.playClick(); this.toggleHint(); }); this.dom.btnGameRestartQuick.addEventListener("click", () => { sounds.playClick(); this.restartCurrentGame(); }); // Pause Modal actions this.dom.btnPauseResume.addEventListener("click", () => { this.closePauseModal(); }); this.dom.btnPauseRestart.addEventListener("click", () => { this.closePauseModal(); this.restartCurrentGame(); }); this.dom.btnPauseChangeLevel.addEventListener("click", () => { this.closePauseModal(); this.showScreen("level-select"); }); this.dom.btnPauseQuit.addEventListener("click", () => { this.closePauseModal(); this.showScreen("home"); }); // Victory Form & Actions this.dom.victoryForm.addEventListener("submit", (e) => { e.preventDefault(); this.saveVictoryScore(); }); this.dom.btnVictoryReplay.addEventListener("click", () => { this.closeVictoryModal(); this.restartCurrentGame(); }); this.dom.btnVictoryLevels.addEventListener("click", () => { this.closeVictoryModal(); this.showScreen("level-select"); }); this.dom.btnVictoryLeaderboard.addEventListener("click", () => { this.closeVictoryModal(); this.renderLeaderboardTable(this.currentLevel.key); this.showScreen("leaderboard"); }); // Leaderboard actions this.dom.btnBackFromLeaderboard.addEventListener("click", () => { sounds.playClick(); this.showScreen("home"); }); this.dom.leaderboardTabs.addEventListener("click", (e) => { const btn = e.target.closest(".tab-btn"); if (!btn) return; sounds.playClick(); this.dom.leaderboardTabs.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active")); btn.classList.add("active"); const level = btn.getAttribute("data-level"); this.renderLeaderboardTable(level); }); this.dom.btnClearLeaderboard.addEventListener("click", () => { if (confirm("Are you sure you want to clear all high scores?")) { sounds.playClick(); localStorage.removeItem(LEADERBOARD_KEY); const activeTab = this.dom.leaderboardTabs.querySelector(".tab-btn.active"); const level = activeTab ? activeTab.getAttribute("data-level") : "all"; this.renderLeaderboardTable(level); } }); // Keyboard controls (Arrow keys / WASD, Esc, M, N, H, R) window.addEventListener("keydown", (e) => { if (this.dom.screenGame.classList.contains("active") && !this.isPaused && !this.isVictory) { if (e.key === "ArrowUp" || e.key === "w" || e.key === "W") { e.preventDefault(); this.moveBlankDirection("down"); // Move tile down into blank = slide tile below blank upwards } else if (e.key === "ArrowDown" || e.key === "s" || e.key === "S") { e.preventDefault(); this.moveBlankDirection("up"); } else if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") { e.preventDefault(); this.moveBlankDirection("right"); } else if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") { e.preventDefault(); this.moveBlankDirection("left"); } else if (e.key === "Escape") { this.openPauseModal(); } else if (e.key === "h" || e.key === "H") { this.toggleHint(); } else if (e.key === "n" || e.key === "N") { this.toggleNumbers(); } else if (e.key === "r" || e.key === "R") { this.restartCurrentGame(); } } else if (this.isPaused && e.key === "Escape") { this.closePauseModal(); } }); } // ========================================================================== // IMAGE PROCESSING // ========================================================================== handleImageUpload(file) { if (!file.type.startsWith("image/")) { alert("Please upload a valid image file."); return; } const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { this.selectedImageCanvas = this.cropImageToSquare(img); this.updatePreviewCanvas(); sounds.playClick(); this.showScreen("level-select"); }; img.src = e.target.result; }; reader.readAsDataURL(file); } cropImageToSquare(img) { const canvas = document.createElement("canvas"); canvas.width = CANVAS_BOARD_SIZE; canvas.height = CANVAS_BOARD_SIZE; const ctx = canvas.getContext("2d"); const minDim = Math.min(img.width, img.height); const sx = (img.width - minDim) / 2; const sy = (img.height - minDim) / 2; ctx.drawImage(img, sx, sy, minDim, minDim, 0, 0, CANVAS_BOARD_SIZE, CANVAS_BOARD_SIZE); return canvas; } selectPreset(preset) { this.selectedImageCanvas = preset.canvas; this.updatePreviewCanvas(); sounds.playClick(); this.showScreen("level-select"); } renderPresetGallery() { this.dom.presetGallery.innerHTML = ""; this.presets.forEach((preset) => { const card = document.createElement("div"); card.className = "preset-thumb-card"; card.title = `Choose ${preset.name}`; const img = document.createElement("img"); img.src = preset.canvas.toDataURL(); img.alt = preset.name; const badge = document.createElement("div"); badge.className = "preset-badge"; badge.textContent = preset.name; card.appendChild(img); card.appendChild(badge); card.addEventListener("click", () => this.selectPreset(preset)); this.dom.presetGallery.appendChild(card); }); } updatePreviewCanvas() { const pCanvas = this.dom.previewCanvas; pCanvas.width = CANVAS_BOARD_SIZE; pCanvas.height = CANVAS_BOARD_SIZE; const ctx = pCanvas.getContext("2d"); ctx.drawImage(this.selectedImageCanvas, 0, 0); // Also update ghost overlay this.dom.ghostOverlay.style.backgroundImage = `url(${this.selectedImageCanvas.toDataURL()})`; } renderLevelCards() { this.dom.levelCardsGrid.innerHTML = ""; Object.values(LEVELS).forEach((level) => { const card = document.createElement("div"); card.className = "level-card"; card.style.setProperty("--card-color", level.color); card.style.setProperty("--card-glow", level.glow); card.innerHTML = `
${level.desc}