{"project":{"id":"iEsRkij","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Pop frenzy","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n</head>\n<body>\n<div class=\"game\">\n\t<canvas id=\"gameCanvas\" class=\"game__canvas\"></canvas>\n\t<div class=\"game__hud\">\n\t\t<div class=\"game__score-wrap\">\n\t\t\t<span class=\"game__label\">SCORE</span>\n\t\t\t<span class=\"game__score\" id=\"score\">0</span>\n\t\t</div>\n\t\t<div class=\"game__combo-wrap\">\n\t\t\t<span class=\"game__label\">COMBO</span>\n\t\t\t<span class=\"game__combo\" id=\"combo\">x1</span>\n\t\t</div>\n\t\t<div class=\"game__lives-wrap\">\n\t\t\t<span class=\"game__label\">LIVES</span>\n\t\t\t<span class=\"game__lives\" id=\"lives\">♥♥♥</span>\n\t\t</div>\n\t</div>\n\t<div class=\"game__overlay\" id=\"startScreen\">\n\t\t<div class=\"game__overlay-content\">\n\t\t\t<h1 class=\"game__title\">POP<span class=\"game__title--accent\">FRENZY</span></h1>\n\t\t\t<p class=\"game__subtitle\">Tap the orbs before they escape!</p>\n\t\t\t<p class=\"game__hint\">Chain combos for massive points<br>Miss 3 and it's game over</p>\n\t\t\t<button class=\"game__btn\" id=\"startBtn\">TAP TO PLAY</button>\n\t\t</div>\n\t</div>\n\t<div class=\"game__overlay game__overlay--hidden\" id=\"gameOverScreen\">\n\t\t<div class=\"game__overlay-content\">\n\t\t\t<h1 class=\"game__title game__title--small\">GAME OVER</h1>\n\t\t\t<p class=\"game__final-score\">Score: <span id=\"finalScore\">0</span></p>\n\t\t\t<p class=\"game__best-score\">Best: <span id=\"bestScore\">0</span></p>\n\t\t\t<button class=\"game__btn\" id=\"restartBtn\">PLAY AGAIN</button>\n\t\t</div>\n\t</div>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"(function() {\n\t'use strict';\n\n\tconst canvas = document.getElementById('gameCanvas');\n\tconst ctx = canvas.getContext('2d');\n\tconst scoreEl = document.getElementById('score');\n\tconst comboEl = document.getElementById('combo');\n\tconst livesEl = document.getElementById('lives');\n\tconst startScreen = document.getElementById('startScreen');\n\tconst gameOverScreen = document.getElementById('gameOverScreen');\n\tconst finalScoreEl = document.getElementById('finalScore');\n\tconst bestScoreEl = document.getElementById('bestScore');\n\tconst startBtn = document.getElementById('startBtn');\n\tconst restartBtn = document.getElementById('restartBtn');\n\n\tlet W, H, dpr;\n\tlet audioCtx;\n\n\tfunction initAudio() {\n\t\tif (!audioCtx) {\n\t\t\taudioCtx = new(window.AudioContext || window.webkitAudioContext)();\n\t\t}\n\t\tif (audioCtx.state === 'suspended') audioCtx.resume();\n\t}\n\n\tfunction playPop(comboLevel) {\n\t\tif (!audioCtx) return;\n\t\tconst baseFreq = 400 + Math.min(comboLevel, 10) * 60;\n\t\tconst osc = audioCtx.createOscillator();\n\t\tconst gain = audioCtx.createGain();\n\t\tosc.connect(gain);\n\t\tgain.connect(audioCtx.destination);\n\t\tosc.type = 'sine';\n\t\tosc.frequency.setValueAtTime(baseFreq, audioCtx.currentTime);\n\t\tosc.frequency.exponentialRampToValueAtTime(baseFreq * 2.5, audioCtx.currentTime + 0.08);\n\t\tgain.gain.setValueAtTime(0.25, audioCtx.currentTime);\n\t\tgain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);\n\t\tosc.start(audioCtx.currentTime);\n\t\tosc.stop(audioCtx.currentTime + 0.15);\n\t}\n\n\tfunction playCombo(level) {\n\t\tif (!audioCtx || level < 3) return;\n\t\tconst t = audioCtx.currentTime;\n\t\tconst osc = audioCtx.createOscillator();\n\t\tconst gain = audioCtx.createGain();\n\t\tosc.connect(gain);\n\t\tgain.connect(audioCtx.destination);\n\t\tosc.type = 'triangle';\n\t\tconst freq = 600 + level * 80;\n\t\tosc.frequency.setValueAtTime(freq, t);\n\t\tosc.frequency.exponentialRampToValueAtTime(freq * 1.5, t + 0.1);\n\t\tgain.gain.setValueAtTime(0.15, t);\n\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);\n\t\tosc.start(t);\n\t\tosc.stop(t + 0.2);\n\t}\n\n\tfunction playMiss() {\n\t\tif (!audioCtx) return;\n\t\tconst t = audioCtx.currentTime;\n\t\tconst osc = audioCtx.createOscillator();\n\t\tconst gain = audioCtx.createGain();\n\t\tosc.connect(gain);\n\t\tgain.connect(audioCtx.destination);\n\t\tosc.type = 'square';\n\t\tosc.frequency.setValueAtTime(200, t);\n\t\tosc.frequency.exponentialRampToValueAtTime(80, t + 0.25);\n\t\tgain.gain.setValueAtTime(0.15, t);\n\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);\n\t\tosc.start(t);\n\t\tosc.stop(t + 0.3);\n\t}\n\n\tfunction playGameOver() {\n\t\tif (!audioCtx) return;\n\t\tconst t = audioCtx.currentTime;\n\t\tconst notes = [400, 350, 300, 200];\n\t\tnotes.forEach((freq, i) => {\n\t\t\tconst osc = audioCtx.createOscillator();\n\t\t\tconst gain = audioCtx.createGain();\n\t\t\tosc.connect(gain);\n\t\t\tgain.connect(audioCtx.destination);\n\t\t\tosc.type = 'sawtooth';\n\t\t\tosc.frequency.setValueAtTime(freq, t + i * 0.18);\n\t\t\tgain.gain.setValueAtTime(0.12, t + i * 0.18);\n\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.18 + 0.3);\n\t\t\tosc.start(t + i * 0.18);\n\t\t\tosc.stop(t + i * 0.18 + 0.3);\n\t\t});\n\t}\n\n\tfunction playSpecialPop() {\n\t\tif (!audioCtx) return;\n\t\tconst t = audioCtx.currentTime;\n\t\t[800, 1000, 1200].forEach((freq, i) => {\n\t\t\tconst osc = audioCtx.createOscillator();\n\t\t\tconst gain = audioCtx.createGain();\n\t\t\tosc.connect(gain);\n\t\t\tgain.connect(audioCtx.destination);\n\t\t\tosc.type = 'sine';\n\t\t\tosc.frequency.setValueAtTime(freq, t + i * 0.06);\n\t\t\tosc.frequency.exponentialRampToValueAtTime(freq * 2, t + i * 0.06 + 0.12);\n\t\t\tgain.gain.setValueAtTime(0.18, t + i * 0.06);\n\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.06 + 0.18);\n\t\t\tosc.start(t + i * 0.06);\n\t\t\tosc.stop(t + i * 0.06 + 0.18);\n\t\t});\n\t}\n\tlet orbs = [];\n\tlet particles = [];\n\tlet floatingTexts = [];\n\tlet score = 0;\n\tlet combo = 0;\n\tlet comboTimer = 0;\n\tlet lives = 3;\n\tlet gameRunning = false;\n\tlet spawnTimer = 0;\n\tlet spawnInterval = 1.2;\n\tlet difficulty = 1;\n\tlet elapsed = 0;\n\tlet lastTime = 0;\n\tlet bestScore = parseInt(localStorage.getItem('popfrenzy_best') || '0');\n\tlet bgStars = [];\n\tlet shakeAmount = 0;\n\n\tconst ORB_COLORS = [{\n\t\t\tfill: '#ff2d78',\n\t\t\tglow: 'rgba(255,45,120,0.4)',\n\t\t\tpoints: 10\n\t\t},\n\t\t{\n\t\t\tfill: '#00e5ff',\n\t\t\tglow: 'rgba(0,229,255,0.4)',\n\t\t\tpoints: 10\n\t\t},\n\t\t{\n\t\t\tfill: '#39ff14',\n\t\t\tglow: 'rgba(57,255,20,0.4)',\n\t\t\tpoints: 10\n\t\t},\n\t\t{\n\t\t\tfill: '#ffe135',\n\t\t\tglow: 'rgba(255,225,53,0.4)',\n\t\t\tpoints: 10\n\t\t},\n\t\t{\n\t\t\tfill: '#ff6f00',\n\t\t\tglow: 'rgba(255,111,0,0.4)',\n\t\t\tpoints: 15\n\t\t},\n\t\t{\n\t\t\tfill: '#e040fb',\n\t\t\tglow: 'rgba(224,64,251,0.4)',\n\t\t\tpoints: 15\n\t\t},\n\t];\n\n\tconst SPECIAL_ORB = {\n\t\tfill: '#ffffff',\n\t\tglow: 'rgba(255,255,255,0.6)',\n\t\tpoints: 50\n\t};\n\n\tfunction resize() {\n\t\tdpr = Math.min(window.devicePixelRatio || 1, 2);\n\t\tW = window.innerWidth;\n\t\tH = window.innerHeight;\n\t\tcanvas.width = W * dpr;\n\t\tcanvas.height = H * dpr;\n\t\tcanvas.style.width = W + 'px';\n\t\tcanvas.style.height = H + 'px';\n\t\tctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\t\tinitStars();\n\t}\n\n\tfunction initStars() {\n\t\tbgStars = [];\n\t\tfor (let i = 0; i < 60; i++) {\n\t\t\tbgStars.push({\n\t\t\t\tx: Math.random() * W,\n\t\t\t\ty: Math.random() * H,\n\t\t\t\tr: Math.random() * 1.5 + 0.5,\n\t\t\t\talpha: Math.random() * 0.5 + 0.2,\n\t\t\t\tspeed: Math.random() * 0.3 + 0.1\n\t\t\t});\n\t\t}\n\t}\n\n\tclass Orb {\n\t\tconstructor() {\n\t\t\tconst isSpecial = Math.random() < 0.08;\n\t\t\tconst colorData = isSpecial ? SPECIAL_ORB : ORB_COLORS[Math.floor(Math.random() * ORB_COLORS.length)];\n\t\t\tthis.color = colorData.fill;\n\t\t\tthis.glow = colorData.glow;\n\t\t\tthis.points = colorData.points;\n\t\t\tthis.isSpecial = isSpecial;\n\n\t\t\tconst baseR = Math.random() * 12 + 22;\n\t\t\tthis.r = baseR;\n\t\t\tthis.maxR = baseR;\n\t\t\tthis.x = Math.random() * (W - baseR * 2) + baseR;\n\t\t\tthis.y = H + baseR + 20;\n\n\t\t\tconst speed = (Math.random() * 40 + 50) * (0.8 + difficulty * 0.15);\n\t\t\tthis.vy = -speed;\n\t\t\tthis.vx = (Math.random() - 0.5) * 30;\n\n\t\t\tthis.lifespan = (H + 200) / speed + Math.random() * 0.8;\n\t\t\tthis.age = 0;\n\t\t\tthis.alive = true;\n\t\t\tthis.popped = false;\n\t\t\tthis.wobble = Math.random() * Math.PI * 2;\n\t\t\tthis.wobbleSpeed = Math.random() * 3 + 2;\n\t\t\tthis.wobbleAmp = Math.random() * 8 + 4;\n\t\t\tthis.pulsePhase = Math.random() * Math.PI * 2;\n\t\t\tthis.scaleIn = 0;\n\t\t}\n\n\t\tupdate(dt) {\n\t\t\tthis.age += dt;\n\t\t\tthis.scaleIn = Math.min(1, this.age / 0.15);\n\t\t\tthis.y += this.vy * dt;\n\t\t\tthis.wobble += this.wobbleSpeed * dt;\n\t\t\tthis.x += Math.sin(this.wobble) * this.wobbleAmp * dt;\n\t\t\tthis.pulsePhase += dt * 4;\n\n\t\t\tif (this.x < this.r) this.x = this.r;\n\t\t\tif (this.x > W - this.r) this.x = W - this.r;\n\n\t\t\tif (this.y < -this.r - 40) {\n\t\t\t\tthis.alive = false;\n\t\t\t\tif (!this.popped) {\n\t\t\t\t\tloseLife();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdraw() {\n\t\t\tconst scale = this.scaleIn;\n\t\t\tconst pulse = 1 + Math.sin(this.pulsePhase) * 0.06;\n\t\t\tconst r = this.r * scale * pulse;\n\n\t\t\tctx.save();\n\t\t\tctx.translate(this.x, this.y);\n\n\t\t\t// Glow\n\t\t\tconst glowSize = r * (this.isSpecial ? 3 : 2.2);\n\t\t\tconst glowGrad = ctx.createRadialGradient(0, 0, r * 0.3, 0, 0, glowSize);\n\t\t\tglowGrad.addColorStop(0, this.glow);\n\t\t\tglowGrad.addColorStop(1, 'transparent');\n\t\t\tctx.fillStyle = glowGrad;\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(0, 0, glowSize, 0, Math.PI * 2);\n\t\t\tctx.fill();\n\n\t\t\t// Body\n\t\t\tconst bodyGrad = ctx.createRadialGradient(-r * 0.3, -r * 0.3, r * 0.1, 0, 0, r);\n\t\t\tbodyGrad.addColorStop(0, '#ffffff');\n\t\t\tbodyGrad.addColorStop(0.3, this.color);\n\t\t\tbodyGrad.addColorStop(1, this.darken(this.color, 0.4));\n\t\t\tctx.fillStyle = bodyGrad;\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(0, 0, r, 0, Math.PI * 2);\n\t\t\tctx.fill();\n\n\t\t\t// Highlight\n\t\t\tctx.fillStyle = 'rgba(255,255,255,0.35)';\n\t\t\tctx.beginPath();\n\t\t\tctx.ellipse(-r * 0.2, -r * 0.3, r * 0.35, r * 0.2, -0.5, 0, Math.PI * 2);\n\t\t\tctx.fill();\n\n\t\t\tif (this.isSpecial) {\n\t\t\t\tctx.strokeStyle = 'rgba(255,255,255,0.5)';\n\t\t\t\tctx.lineWidth = 2;\n\t\t\t\tctx.beginPath();\n\t\t\t\tconst sparkAngle = this.pulsePhase * 2;\n\t\t\t\tfor (let i = 0; i < 4; i++) {\n\t\t\t\t\tconst a = sparkAngle + (Math.PI / 2) * i;\n\t\t\t\t\tctx.moveTo(Math.cos(a) * (r + 4), Math.sin(a) * (r + 4));\n\t\t\t\t\tctx.lineTo(Math.cos(a) * (r + 10), Math.sin(a) * (r + 10));\n\t\t\t\t}\n\t\t\t\tctx.stroke();\n\t\t\t}\n\n\t\t\tctx.restore();\n\t\t}\n\n\t\tdarken(hex, amount) {\n\t\t\tconst num = parseInt(hex.slice(1), 16);\n\t\t\tconst r = Math.max(0, ((num >> 16) & 0xff) * (1 - amount)) | 0;\n\t\t\tconst g = Math.max(0, ((num >> 8) & 0xff) * (1 - amount)) | 0;\n\t\t\tconst b = Math.max(0, (num & 0xff) * (1 - amount)) | 0;\n\t\t\treturn `rgb(${r},${g},${b})`;\n\t\t}\n\n\t\thitTest(px, py) {\n\t\t\tconst dx = px - this.x;\n\t\t\tconst dy = py - this.y;\n\t\t\tconst touchR = this.r * 1.3; // generous touch area\n\t\t\treturn dx * dx + dy * dy <= touchR * touchR;\n\t\t}\n\t}\n\n\tclass Particle {\n\t\tconstructor(x, y, color) {\n\t\t\tthis.x = x;\n\t\t\tthis.y = y;\n\t\t\tthis.color = color;\n\t\t\tconst angle = Math.random() * Math.PI * 2;\n\t\t\tconst speed = Math.random() * 250 + 80;\n\t\t\tthis.vx = Math.cos(angle) * speed;\n\t\t\tthis.vy = Math.sin(angle) * speed;\n\t\t\tthis.r = Math.random() * 4 + 2;\n\t\t\tthis.life = 1;\n\t\t\tthis.decay = Math.random() * 1.5 + 1.5;\n\t\t\tthis.gravity = 200;\n\t\t}\n\n\t\tupdate(dt) {\n\t\t\tthis.x += this.vx * dt;\n\t\t\tthis.y += this.vy * dt;\n\t\t\tthis.vy += this.gravity * dt;\n\t\t\tthis.vx *= 0.98;\n\t\t\tthis.life -= this.decay * dt;\n\t\t}\n\n\t\tdraw() {\n\t\t\tif (this.life <= 0) return;\n\t\t\tctx.globalAlpha = this.life;\n\t\t\tctx.fillStyle = this.color;\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(this.x, this.y, this.r * this.life, 0, Math.PI * 2);\n\t\t\tctx.fill();\n\t\t\tctx.globalAlpha = 1;\n\t\t}\n\t}\n\n\tclass FloatingText {\n\t\tconstructor(x, y, text, color) {\n\t\t\tthis.x = x;\n\t\t\tthis.y = y;\n\t\t\tthis.text = text;\n\t\t\tthis.color = color;\n\t\t\tthis.life = 1;\n\t\t\tthis.vy = -80;\n\t\t}\n\n\t\tupdate(dt) {\n\t\t\tthis.y += this.vy * dt;\n\t\t\tthis.life -= dt * 1.2;\n\t\t}\n\n\t\tdraw() {\n\t\t\tif (this.life <= 0) return;\n\t\t\tctx.globalAlpha = this.life;\n\t\t\tctx.fillStyle = this.color;\n\t\t\tctx.font = `bold ${18 + (1 - this.life) * 8}px 'Lilita One', cursive`;\n\t\t\tctx.textAlign = 'center';\n\t\t\tctx.fillText(this.text, this.x, this.y);\n\t\t\tctx.globalAlpha = 1;\n\t\t}\n\t}\n\n\tfunction spawnOrb() {\n\t\torbs.push(new Orb());\n\t}\n\n\tfunction popOrb(orb, index) {\n\t\torb.popped = true;\n\t\torb.alive = false;\n\n\t\tcombo++;\n\t\tcomboTimer = 1.5;\n\n\t\tconst multiplier = Math.min(combo, 10);\n\t\tconst pts = orb.points * multiplier;\n\t\tscore += pts;\n\n\t\t// Sound\n\t\tif (orb.isSpecial) {\n\t\t\tplaySpecialPop();\n\t\t} else {\n\t\t\tplayPop(combo);\n\t\t}\n\t\tplayCombo(combo);\n\n\t\tscoreEl.textContent = score;\n\t\tcomboEl.textContent = 'x' + multiplier;\n\t\tcomboEl.classList.add('game__combo--pulse');\n\t\tsetTimeout(() => comboEl.classList.remove('game__combo--pulse'), 100);\n\n\t\t// Particles\n\t\tconst count = orb.isSpecial ? 25 : 14;\n\t\tfor (let i = 0; i < count; i++) {\n\t\t\tparticles.push(new Particle(orb.x, orb.y, orb.color));\n\t\t}\n\n\t\t// Floating text\n\t\tconst label = (multiplier > 1 ? `+${pts} x${multiplier}` : `+${pts}`);\n\t\tfloatingTexts.push(new FloatingText(orb.x, orb.y - 20, label, orb.color));\n\n\t\tshakeAmount = Math.min(8, combo * 1.5);\n\t}\n\n\tfunction loseLife() {\n\t\tlives--;\n\t\tcombo = 0;\n\t\tcomboEl.textContent = 'x1';\n\t\tplayMiss();\n\t\tlivesEl.textContent = '♥'.repeat(Math.max(0, lives)) + '♡'.repeat(Math.max(0, 3 - lives));\n\t\tshakeAmount = 10;\n\n\t\tif (lives <= 0) {\n\t\t\tgameOver();\n\t\t}\n\t}\n\n\tfunction gameOver() {\n\t\tgameRunning = false;\n\t\tplayGameOver();\n\t\tfinalScoreEl.textContent = score;\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tlocalStorage.setItem('popfrenzy_best', bestScore.toString());\n\t\t}\n\t\tbestScoreEl.textContent = bestScore;\n\t\tgameOverScreen.classList.remove('game__overlay--hidden');\n\t}\n\n\tfunction startGame() {\n\t\tscore = 0;\n\t\tcombo = 0;\n\t\tlives = 3;\n\t\tdifficulty = 1;\n\t\telapsed = 0;\n\t\tspawnInterval = 1.2;\n\t\tspawnTimer = 0;\n\t\torbs = [];\n\t\tparticles = [];\n\t\tfloatingTexts = [];\n\n\t\tscoreEl.textContent = '0';\n\t\tcomboEl.textContent = 'x1';\n\t\tlivesEl.textContent = '♥♥♥';\n\n\t\tstartScreen.classList.add('game__overlay--hidden');\n\t\tgameOverScreen.classList.add('game__overlay--hidden');\n\n\t\tinitAudio();\n\t\tgameRunning = true;\n\t\tlastTime = performance.now();\n\t\trequestAnimationFrame(loop);\n\t}\n\n\tfunction handleTap(px, py) {\n\t\tif (!gameRunning) return;\n\n\t\tlet hitAny = false;\n\t\t// Check from top (newest/closest) to bottom\n\t\tfor (let i = orbs.length - 1; i >= 0; i--) {\n\t\t\tconst orb = orbs[i];\n\t\t\tif (!orb.alive || orb.popped) continue;\n\t\t\tif (orb.hitTest(px, py)) {\n\t\t\t\tpopOrb(orb, i);\n\t\t\t\thitAny = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (!hitAny) {\n\t\t\tcombo = 0;\n\t\t\tcomboEl.textContent = 'x1';\n\t\t}\n\t}\n\n\tfunction drawBackground(dt) {\n\t\tfor (const star of bgStars) {\n\t\t\tstar.y -= star.speed * 20 * dt;\n\t\t\tif (star.y < -5) {\n\t\t\t\tstar.y = H + 5;\n\t\t\t\tstar.x = Math.random() * W;\n\t\t\t}\n\t\t\tctx.globalAlpha = star.alpha * (0.5 + 0.5 * Math.sin(elapsed * star.speed * 5));\n\t\t\tctx.fillStyle = '#ffffff';\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(star.x, star.y, star.r, 0, Math.PI * 2);\n\t\t\tctx.fill();\n\t\t}\n\t\tctx.globalAlpha = 1;\n\t}\n\n\tfunction loop(now) {\n\t\tif (!gameRunning) return;\n\n\t\tconst dt = Math.min((now - lastTime) / 1000, 0.05);\n\t\tlastTime = now;\n\t\telapsed += dt;\n\n\t\t// Difficulty ramp\n\t\tdifficulty = 1 + elapsed / 30;\n\t\tspawnInterval = Math.max(0.3, 1.2 - elapsed * 0.008);\n\n\t\t// Spawn\n\t\tspawnTimer += dt;\n\t\tif (spawnTimer >= spawnInterval) {\n\t\t\tspawnTimer -= spawnInterval;\n\t\t\tspawnOrb();\n\t\t\t// Double spawn after 45s\n\t\t\tif (elapsed > 45 && Math.random() < 0.4) spawnOrb();\n\t\t}\n\n\t\t// Combo timer\n\t\tif (comboTimer > 0) {\n\t\t\tcomboTimer -= dt;\n\t\t\tif (comboTimer <= 0) {\n\t\t\t\tcombo = 0;\n\t\t\t\tcomboEl.textContent = 'x1';\n\t\t\t}\n\t\t}\n\n\t\t// Shake decay\n\t\tshakeAmount *= Math.pow(0.05, dt);\n\n\t\t// Update\n\t\tfor (const orb of orbs) orb.update(dt);\n\t\tfor (const p of particles) p.update(dt);\n\t\tfor (const ft of floatingTexts) ft.update(dt);\n\n\t\t// Cleanup\n\t\torbs = orbs.filter(o => o.alive);\n\t\tparticles = particles.filter(p => p.life > 0);\n\t\tfloatingTexts = floatingTexts.filter(ft => ft.life > 0);\n\n\t\t// Draw\n\t\tctx.clearRect(0, 0, W, H);\n\n\t\tctx.save();\n\t\tif (shakeAmount > 0.5) {\n\t\t\tctx.translate(\n\t\t\t\t(Math.random() - 0.5) * shakeAmount,\n\t\t\t\t(Math.random() - 0.5) * shakeAmount\n\t\t\t);\n\t\t}\n\n\t\tdrawBackground(dt);\n\n\t\tfor (const orb of orbs) orb.draw();\n\t\tfor (const p of particles) p.draw();\n\t\tfor (const ft of floatingTexts) ft.draw();\n\n\t\tctx.restore();\n\n\t\trequestAnimationFrame(loop);\n\t}\n\n\t// Events\n\tcanvas.addEventListener('pointerdown', (e) => {\n\t\te.preventDefault();\n\t\tconst rect = canvas.getBoundingClientRect();\n\t\tconst px = e.clientX - rect.left;\n\t\tconst py = e.clientY - rect.top;\n\t\thandleTap(px, py);\n\t});\n\n\t// Support multi-touch for frantic tapping\n\tcanvas.addEventListener('touchstart', (e) => {\n\t\te.preventDefault();\n\t\tconst rect = canvas.getBoundingClientRect();\n\t\tfor (const touch of e.changedTouches) {\n\t\t\tconst px = touch.clientX - rect.left;\n\t\t\tconst py = touch.clientY - rect.top;\n\t\t\thandleTap(px, py);\n\t\t}\n\t}, {\n\t\tpassive: false\n\t});\n\n\tstartBtn.addEventListener('click', startGame);\n\trestartBtn.addEventListener('click', startGame);\n\twindow.addEventListener('resize', resize);\n\n\tresize();\n\n\t// Ambient animation on start screen\n\tfunction ambientLoop(now) {\n\t\tif (gameRunning) return;\n\t\tconst dt = lastTime ? Math.min((now - lastTime) / 1000, 0.05) : 0.016;\n\t\tlastTime = now;\n\t\telapsed += dt;\n\n\t\tctx.clearRect(0, 0, W, H);\n\t\tdrawBackground(dt);\n\n\t\trequestAnimationFrame(ambientLoop);\n\t}\n\tlastTime = performance.now();\n\trequestAnimationFrame(ambientLoop);\n})();"},{"name":"style.css","content":"@import url('https://fonts.googleapis.com/css2?family=Lilita+One&family=Nunito:wght@700;900&display=swap');\n\n:root {\n\t--bg-primary: #1b0a2e;\n\t--bg-secondary: #2d1250;\n\t--accent-pink: #ff2d78;\n\t--accent-cyan: #00e5ff;\n\t--accent-yellow: #ffe135;\n\t--accent-green: #39ff14;\n\t--text-primary: #ffffff;\n\t--text-muted: rgba(255, 255, 255, 0.6);\n\tcolor-scheme: dark;\n}\n\n* {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tpadding: 0;\n\t-webkit-tap-highlight-color: transparent;\n\tuser-select: none;\n}\n\nhtml, body {\n\theight: 100%;\n\toverflow: hidden;\n\ttouch-action: none;\n}\n\nbody {\n\tfont-family: 'Nunito', sans-serif;\n\tbackground: var(--bg-primary);\n}\n\n.game {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100dvh;\n\toverflow: hidden;\n\tbackground: radial-gradient(ellipse at 50% 80%, var(--bg-secondary) 0%, var(--bg-primary) 70%);\n}\n\n.game__canvas {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tz-index: 1;\n}\n\n.game__hud {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tz-index: 10;\n\tdisplay: flex;\n\tjustify: space-between;\n\talign-items: flex-start;\n\tpadding: 12px 16px;\n\tpadding-top: max(12px, env(safe-area-inset-top));\n\tbackground: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, transparent 100%);\n\tpointer-events: none;\n}\n\n.game__label {\n\tdisplay: block;\n\tfont-size: 9px;\n\tfont-weight: 700;\n\tletter-spacing: 2px;\n\tcolor: var(--text-muted);\n\ttext-transform: uppercase;\n}\n\n.game__score {\n\tfont-family: 'Lilita One', cursive;\n\tfont-size: 32px;\n\tcolor: var(--text-primary);\n\tline-height: 1;\n\ttext-shadow: 0 0 20px rgba(255, 45, 120, 0.5);\n}\n\n.game__combo {\n\tfont-family: 'Lilita One', cursive;\n\tfont-size: 24px;\n\tcolor: var(--accent-yellow);\n\tline-height: 1;\n\ttext-shadow: 0 0 15px rgba(255, 225, 53, 0.5);\n\ttransition: transform 0.1s;\n}\n\n.game__combo--pulse {\n\ttransform: scale(1.4);\n}\n\n.game__lives {\n\tfont-size: 20px;\n\tcolor: var(--accent-pink);\n\tline-height: 1;\n\ttext-shadow: 0 0 10px rgba(255, 45, 120, 0.6);\n}\n\n.game__score-wrap,\n.game__combo-wrap,\n.game__lives-wrap {\n\ttext-align: center;\n}\n\n.game__overlay {\n\tposition: absolute;\n\tinset: 0;\n\tz-index: 20;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: rgba(10, 4, 20, 0.88);\n\tbackdrop-filter: blur(12px);\n\tanimation: fadeIn 0.3s ease;\n}\n\n.game__overlay--hidden {\n\tdisplay: none;\n}\n\n@keyframes fadeIn {\n\tfrom {\n\t\topacity: 0;\n\t}\n\n\tto {\n\t\topacity: 1;\n\t}\n}\n\n.game__overlay-content {\n\ttext-align: center;\n\tpadding: 24px;\n}\n\n.game__title {\n\tfont-family: 'Lilita One', cursive;\n\tfont-size: clamp(48px, 14vw, 80px);\n\tcolor: var(--text-primary);\n\tline-height: 1;\n\tmargin-bottom: 12px;\n\ttext-shadow:\n\t\t0 0 30px rgba(255, 45, 120, 0.4),\n\t\t0 4px 0 rgba(0, 0, 0, 0.3);\n}\n\n.game__title--accent {\n\tdisplay: block;\n\tbackground: linear-gradient(135deg, var(--accent-pink), var(--accent-cyan));\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tbackground-clip: text;\n\tfilter: drop-shadow(0 4px 0 rgba(0, 0, 0, 0.3));\n}\n\n.game__title--small {\n\tfont-size: clamp(36px, 10vw, 56px);\n}\n\n.game__subtitle {\n\tfont-size: 18px;\n\tfont-weight: 700;\n\tcolor: var(--accent-cyan);\n\tmargin-bottom: 8px;\n}\n\n.game__hint {\n\tfont-size: 14px;\n\tcolor: var(--text-muted);\n\tline-height: 1.6;\n\tmargin-bottom: 32px;\n}\n\n.game__final-score {\n\tfont-family: 'Lilita One', cursive;\n\tfont-size: 28px;\n\tcolor: var(--accent-cyan);\n\tmargin-bottom: 4px;\n}\n\n.game__best-score {\n\tfont-size: 16px;\n\tcolor: var(--accent-yellow);\n\tfont-weight: 700;\n\tmargin-bottom: 32px;\n}\n\n.game__btn {\n\tfont-family: 'Lilita One', cursive;\n\tfont-size: 22px;\n\tcolor: var(--bg-primary);\n\tbackground: linear-gradient(135deg, var(--accent-yellow), #ffab00);\n\tborder: none;\n\tborder-radius: 50px;\n\tpadding: 16px 48px;\n\tcursor: pointer;\n\tbox-shadow:\n\t\t0 4px 0 #b89800,\n\t\t0 8px 30px rgba(255, 225, 53, 0.3);\n\ttransition: transform 0.1s, box-shadow 0.1s;\n\tletter-spacing: 1px;\n}\n\n.game__btn:active {\n\ttransform: translateY(4px);\n\tbox-shadow:\n\t\t0 0 0 #b89800,\n\t\t0 2px 15px rgba(255, 225, 53, 0.3);\n}"}],"folders":[]},"variants":null,"createdAt":"2026-02-28T13:40:55.692Z","updatedAt":"2026-03-04T16:30:47.883Z","hasThumbnail":true}}