Architecture & Core Concept
Virtual bubble wrap is fundamentally an exercise in tactile sensory feedback. The sheet is generated as an array of round buttons in CSS Grid. To allow players to pop bubbles by dragging their finger across the sheet: 1. When pointerdown occurs, set isPressing = true 2. On pointermove, call document.elementFromPoint(e.clientX, e.clientY) 3. If the element is an unpopped bubble, trigger the pop! Each pop triggers: - CSS pop-now pulse animation - Web Audio sine wave that rapidly drops from 180 Hz to 70 Hz in 80ms - Mobile vibration: navigator.vibrate(12)
Key Game Mechanics Taught:
- Responsive CSS Grid bubble sheet layout
- Sweep popping with pointermove and document.elementFromPoint
- Frequency-dropping exponential pitch synthesis for realistic pops
- Mobile device haptic feedback via navigator.vibrate()
- Encouragement message cycling and infinite refill state
Step-by-Step Implementation
Generate Responsive Bubble Grid
Compute the optimal bubble count based on screen width so bubbles fill the card without horizontal overflow.
function populateSheet() {
const total = window.innerWidth < 430 ? 60 : 70;
sheet.innerHTML = '';
for (let i = 0; i < total; i++) {
const bubble = document.createElement('button');
bubble.className = 'bubble';
bubble.setAttribute('aria-label', `Bubble ${i + 1}`);
bubble.addEventListener('pointerdown', e => {
isPressing = true;
e.preventDefault();
popBubble(bubble);
});
sheet.append(bubble);
}
}
Synthesize the Classic Pitch-Drop Pop Tone
A bubble wrap pop is characterized by a quick, deep downward pitch drop (frequency ramp down from 180Hz to 70Hz).
function playPopSound() {
audioCtx ||= new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const now = audioCtx.currentTime;
osc.type = 'sine';
osc.frequency.setValueAtTime(170 + Math.random() * 50, now);
osc.frequency.exponentialRampToValueAtTime(70, now + 0.08); // Rapid drop
gain.gain.setValueAtTime(0.045, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
osc.connect(gain).connect(audioCtx.destination);
osc.start();
osc.stop(now + 0.1);
}
Implement Continuous Drag/Sweep Popping
Users love sweeping their fingers across bubble wrap. Detect continuous hover targets using document.elementFromPoint.
sheet.addEventListener('pointermove', e => {
if (isPressing) {
const target = document.elementFromPoint(e.clientX, e.clientY);
if (target && target.classList.contains('bubble')) {
popBubble(target);
}
}
});
window.addEventListener('pointerup', () => isPressing = false);
window.addEventListener('pointercancel', () => isPressing = false);
Complete Standalone Source Code
Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Bubble Wrap Pop Demo</title>
<style>
body { margin:0; background:#f0fdf4; display:flex; flex-direction:column; align-items:center; padding:20px; font-family:sans-serif; touch-action:none; }
.sheet { display:grid; grid-template-columns:repeat(8, 44px); gap:10px; padding:20px; background:#dcfce7; border-radius:16px; }
.bubble { width:44px; height:44px; border-radius:50%; border:none; background:linear-gradient(135deg, #86efac, #22c55e); box-shadow:inset 0 3px 6px rgba(255,255,255,0.7), 0 3px 6px rgba(0,0,0,0.1); cursor:pointer; transition:transform 0.1s; }
.bubble.popped { background:#bbf7d0; box-shadow:inset 0 2px 4px rgba(0,0,0,0.15); transform:scale(0.92); opacity:0.6; }
</style></head><body>
<h2>Pop Tenang Demo</h2>
<div class="sheet" id="sheet"></div>
<script>
const sheet = document.querySelector('#sheet');
let audio, pressing = false;
function pop(b) {
if(b.classList.contains('popped')) return;
b.classList.add('popped');
audio ||= new (window.AudioContext || window.webkitAudioContext)();
const o = audio.createOscillator(), g = audio.createGain(), now = audio.currentTime;
o.type = 'sine'; o.frequency.setValueAtTime(180, now); o.frequency.exponentialRampToValueAtTime(70, now + 0.08);
g.gain.setValueAtTime(0.04, now); g.gain.exponentialRampToValueAtTime(0.001, now + 0.09);
o.connect(g).connect(audio.destination); o.start(); o.stop(now + 0.1);
navigator.vibrate?.(10);
}
for(let i=0;i<48;i++) {
const b = document.createElement('button');
b.className = 'bubble';
b.onpointerdown = e => { pressing = true; pop(b); };
sheet.append(b);
}
sheet.onpointermove = e => {
if(!pressing) return;
const el = document.elementFromPoint(e.clientX, e.clientY);
if(el && el.classList.contains('bubble')) pop(el);
};
window.onpointerup = () => pressing = false;
</script>
</body></html>
Ideas to Expand Your Game
Now that you have the core mechanics running, consider adding:
- Local High Scores: Use
localStorage.setItem()andlocalStorage.getItem()to persist personal bests. - Visual Polish: Add screen shake on impacts by displacing
ctx.translate(shakeX, shakeY). - Mobile Touch Controls: Bind on-screen directional buttons using
onpointerdownandonpointerup.