Skip to content

Balloon Viewer Component

An animated balloon component that grows when receiving click events and pops when reaching maximum size.

Overview

Component Name: balloon-viewerLatest Version: 1.0.0Author: BoardAPI Team License: MIT

The Balloon Viewer displays an animated balloon that responds to action:click events by growing larger. When it reaches the maximum size, it pops with an animation and resets.

Use Cases

  • Visual feedback for interactive games
  • Progress visualization (clicks = progress)
  • Event-driven animations
  • Tutorial demonstrations
  • Gamification elements

Component Preview

┌─────────────────────┐
│                     │
│        ☁️           │
│     (balloon)       │
│                     │
│   Size: 5/10        │
│                     │
└─────────────────────┘

States:

  • Small (0 clicks): Tiny balloon
  • Growing (1-9 clicks): Balloon gets progressively larger
  • Max Size (10 clicks): Full-size balloon
  • Pop! (auto): Balloon pops and resets to small

Props Schema

PropTypeRequiredDefaultDescription
balloonColorstringNo#f56565Balloon color in hex format
maxSizenumberNo10Maximum size before balloon pops

Props Examples

Red balloon (default):

json
{
  "balloonColor": "#f56565",
  "maxSize": 10
}

Blue balloon:

json
{
  "balloonColor": "#4299e1",
  "maxSize": 15
}

Fast-popping yellow balloon:

json
{
  "balloonColor": "#ffd93d",
  "maxSize": 5
}

Events

Listened Events

action:click

The balloon grows by one step when it receives this event.

Expected Event Structure:

javascript
{
  type: 'board-event',
  event: 'action:click',
  data: {
    clickCount: 5,    // Current click count (optional)
    timestamp: 1700000000
  }
}

Behavior:

  • Each action:click increments balloon size by 1
  • Size is capped at maxSize prop value
  • When size reaches maxSize, balloon pops automatically

Emitted Events

action:pop

Fired when the balloon reaches maximum size and pops.

Event Structure:

javascript
{
  type: 'component-event',
  event: 'action:pop',
  data: {
    finalSize: 10,        // Size when popped (= maxSize)
    totalClicks: 10,      // Total clicks received
    timestamp: 1700000000 // Unix timestamp
  }
}

Example Usage:

javascript
// In another component - listen for pop event
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'action:pop') {

    console.log('Balloon popped!', event.data.data);

    // Trigger celebration animation, play sound, etc.
    celebratePop();
  }
});

Dimensions

  • Width: 280px
  • Height: 360px
  • Resizable: No

manifest.json

json
{
  "name": "balloon-viewer",
  "version": "1.0.0",
  "description": "Animated balloon that grows on click events and pops",
  "author": "BoardAPI Team",
  "license": "MIT",
  "entry": "index.html",
  "component": {
    "width": 280,
    "height": 360,
    "resizable": false,
    "schema": {
      "type": "object",
      "properties": {
        "balloonColor": {
          "type": "string",
          "default": "#f56565",
          "description": "Balloon color (hex format)"
        },
        "maxSize": {
          "type": "number",
          "default": 10,
          "description": "Maximum size before pop"
        }
      }
    }
  }
}

Implementation Example

index.html (Simplified)

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      margin: 0;
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
    }

    .balloon-container {
      position: relative;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .balloon {
      width: 60px;
      height: 80px;
      border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
      position: relative;
      transition: transform 0.3s ease;
      animation: float 2s ease-in-out infinite;
    }

    .balloon::before {
      content: '';
      position: absolute;
      width: 15px;
      height: 20px;
      background: inherit;
      border-radius: 50%;
      bottom: -8px;
      left: 50%;
      transform: translateX(-50%);
    }

    .balloon-string {
      width: 2px;
      height: 50px;
      background: rgba(255, 255, 255, 0.6);
      margin-top: 2px;
    }

    @keyframes float {
      0%, 100% { transform: translateY(0px); }
      50% { transform: translateY(-15px); }
    }

    .balloon.popping {
      animation: pop 0.4s ease-out forwards;
    }

    @keyframes pop {
      0% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.3); opacity: 0.7; }
      100% { transform: scale(0); opacity: 0; }
    }

    .size-indicator {
      margin-top: 20px;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <div class="balloon-container">
    <div class="balloon" id="balloon"></div>
    <div class="balloon-string"></div>
  </div>
  <div class="size-indicator" id="sizeIndicator">Size: 0/10</div>

  <script>
    // Get props from parent
    let props = window.componentProps || {};
    const maxSize = props.maxSize || 10;
    const balloonColor = props.balloonColor || '#f56565';

    // State
    let currentSize = 0;

    // Set balloon color
    const balloon = document.getElementById('balloon');
    balloon.style.background = balloonColor;

    // Update size indicator
    function updateSizeIndicator() {
      document.getElementById('sizeIndicator').textContent =
        `Size: ${currentSize}/${maxSize}`;
    }

    // Grow balloon
    function growBalloon() {
      if (currentSize >= maxSize) return;

      currentSize++;
      updateSizeIndicator();

      // Scale balloon based on size (60px base + 15px per step)
      const scale = 1 + (currentSize / maxSize) * 1.5;
      balloon.style.transform = `scale(${scale})`;

      // Check if max size reached
      if (currentSize >= maxSize) {
        setTimeout(popBalloon, 300);
      }
    }

    // Pop balloon
    function popBalloon() {
      balloon.classList.add('popping');

      // Emit pop event
      window.parent.postMessage({
        type: 'component-event',
        event: 'action:pop',
        data: {
          finalSize: currentSize,
          totalClicks: currentSize,
          timestamp: Date.now()
        }
      }, '*');

      // Reset after animation
      setTimeout(() => {
        balloon.classList.remove('popping');
        currentSize = 0;
        balloon.style.transform = 'scale(1)';
        updateSizeIndicator();
      }, 400);
    }

    // Listen for click events from other components
    window.addEventListener('message', (event) => {
      if (event.data.type === 'board-event' &&
          event.data.event === 'action:click') {
        growBalloon();
      }
    });

    // Initialize
    updateSizeIndicator();
  </script>
</body>
</html>

Usage on a Board

Adding via API

bash
curl -X POST http://localhost:4000/api/v1/boards/{boardId}/objects \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "objectId": "balloon-1",
    "type": "external-component",
    "position": { "x": 300, "y": 100 },
    "width": 280,
    "height": 360,
    "data": {
      "componentUrl": "http://localhost:4000/components/balloon-viewer@1.0.0/index.html",
      "props": {
        "balloonColor": "#f56565",
        "maxSize": 10
      }
    }
  }'

Combining with Other Components

The balloon-viewer is designed to work with event-emitting components like clicker-button.

Example Setup:

json
{
  "objects": [
    {
      "objectId": "clicker-btn-1",
      "type": "external-component",
      "position": { "x": 70, "y": 100 },
      "data": {
        "componentUrl": ".../clicker-button@1.0.0/index.html"
      }
    },
    {
      "objectId": "balloon-1",
      "type": "external-component",
      "position": { "x": 290, "y": 100 },
      "data": {
        "componentUrl": ".../balloon-viewer@1.0.0/index.html",
        "props": {
          "balloonColor": "#4299e1",
          "maxSize": 10
        }
      }
    }
  ]
}

See: Balloon Game Tutorial

Customization Ideas

Multiple Balloon Colors

Create a rainbow of balloons with different colors:

  • Red: #f56565
  • Blue: #4299e1
  • Yellow: #ffd93d
  • Green: #48bb78
  • Purple: #9f7aea

Custom Pop Animation

Replace the scale animation with a burst effect:

css
@keyframes pop {
  0% {
    transform: scale(1);
    opacity: 1;
    filter: blur(0px);
  }
  50% {
    transform: scale(1.5);
    opacity: 0.5;
    filter: blur(2px);
  }
  100% {
    transform: scale(2);
    opacity: 0;
    filter: blur(5px);
  }
}

Sound Effects

Add pop sound using Web Audio API:

javascript
function playPopSound() {
  const audioContext = new AudioContext();
  const oscillator = audioContext.createOscillator();
  const gainNode = audioContext.createGain();

  oscillator.connect(gainNode);
  gainNode.connect(audioContext.destination);

  // Short burst sound
  oscillator.frequency.value = 600;
  oscillator.type = 'sine';
  gainNode.gain.setValueAtTime(0.5, audioContext.currentTime);
  gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);

  oscillator.start(audioContext.currentTime);
  oscillator.stop(audioContext.currentTime + 0.15);
}

Progress Bar Addition

Show visual progress bar below balloon:

html
<div class="progress-bar">
  <div class="progress-fill" id="progressFill"></div>
</div>

<style>
.progress-bar {
  width: 100px;
  height: 8px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 4px;
  margin-top: 10px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: white;
  transition: width 0.3s ease;
  width: 0%;
}
</style>

<script>
function updateProgressBar() {
  const progressPercent = (currentSize / maxSize) * 100;
  document.getElementById('progressFill').style.width = `${progressPercent}%`;
}
</script>

Testing

Unit Test (Jest)

javascript
describe('Balloon Viewer Component', () => {
  test('grows balloon on action:click event', () => {
    const initialSize = currentSize;

    // Simulate click event from board
    window.postMessage({
      type: 'board-event',
      event: 'action:click',
      data: { clickCount: 1 }
    }, '*');

    expect(currentSize).toBe(initialSize + 1);
  });

  test('emits action:pop when maxSize reached', (done) => {
    const postMessageSpy = jest.spyOn(window.parent, 'postMessage');

    // Trigger 10 clicks
    for (let i = 0; i < 10; i++) {
      window.postMessage({
        type: 'board-event',
        event: 'action:click'
      }, '*');
    }

    setTimeout(() => {
      expect(postMessageSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'component-event',
          event: 'action:pop',
          data: expect.objectContaining({
            finalSize: 10
          })
        }),
        '*'
      );
      done();
    }, 400);
  });
});

Download


Next: Score Display Component