G GAMESRICH
Anti-Stress Games · Beginner

How to Build a Virtual Bubble Wrap Game: Pop Tenang

Create satisfying pop animations, pitch-dropping synthesis, multi-touch sweep gestures, and haptic vibrations.

By GamesRich Engineering 6 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME ↗

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:

Step-by-Step Implementation

1

Generate Responsive Bubble Grid

Compute the optimal bubble count based on screen width so bubbles fill the card without horizontal overflow.

Step 1 Implementation
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);
  }
}
2

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).

Step 2 Implementation
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);
}
3

Implement Continuous Drag/Sweep Popping

Users love sweeping their fingers across bubble wrap. Detect continuous hover targets using document.elementFromPoint.

Step 3 Implementation
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:

Complete Runnable Game (index.html)
<!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: