@manufosela/convgame-object

@manufosela/convgame-object

Game object wrapper with physics properties and collision detection for canvas-based games

convgame-object Demo

A physics-enabled game object component with collision detection, forces, and boundary handling.

Ball 1 Position

0, 0

Ball 1 Velocity

0, 0

Collision Count

0

Boundary Hits

0

Event Log

Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<h1>convgame-object Demo</h1>
  <p class="description">A physics-enabled game object component with collision detection, forces, and boundary handling.</p>

  <div class="controls">
    <button id="startBtn">Start Simulation</button>
    <button id="pauseBtn">Pause</button>
    <button id="resetBtn">Reset</button>
    <button id="debugBtn">Toggle Debug</button>
    <button id="addBallBtn">Add Ball</button>
    <button id="applyForceBtn">Apply Random Force</button>
  </div>

  <div class="game-container" id="gameContainer">
    <convgame-object
      id="ball1"
      class="ball"
      x="100"
      y="100"
      width="40"
      height="40"
      velocity-x="3"
      velocity-y="2"
      mass="1"
      friction="0.995"
      restitution="0.9"
    ></convgame-object>

    <convgame-object
      id="ball2"
      class="ball"
      x="400"
      y="200"
      width="60"
      height="60"
      velocity-x="-2"
      velocity-y="3"
      mass="2"
      friction="0.995"
      restitution="0.9"
    ></convgame-object>

    <convgame-object
      id="obstacle"
      class="obstacle"
      x="350"
      y="350"
      width="100"
      height="30"
      is-static
    ></convgame-object>
  </div>

  <div class="stats">
    <div class="stat-card">
      <h3>Ball 1 Position</h3>
      <div class="stat-value" id="ball1Pos">0, 0</div>
    </div>
    <div class="stat-card">
      <h3>Ball 1 Velocity</h3>
      <div class="stat-value" id="ball1Vel">0, 0</div>
    </div>
    <div class="stat-card">
      <h3>Collision Count</h3>
      <div class="stat-value" id="collisionCount">0</div>
    </div>
    <div class="stat-card">
      <h3>Boundary Hits</h3>
      <div class="stat-value" id="boundaryCount">0</div>
    </div>
  </div>

  <h3>Event Log</h3>
  <div class="event-log" id="eventLog"></div>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

* {
      box-sizing: border-box;
    }

    h1 {
      color: var(--accent);
      margin-bottom: 10px;
    }

    .description {
      color: var(--text-muted);
      margin-bottom: 20px;
    }

    .game-container {
      position: relative;
      width: 800px;
      height: 500px;
      background: var(--bg-elevated);
      border: 2px solid var(--accent);
      border-radius: 8px;
      overflow: hidden;
      margin-bottom: 20px;
    }

    .controls {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }

    button {
      padding: 10px 20px;
      font-size: 14px;
      background: var(--accent);
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      transition: background 0.2s;
    }

    button:hover {
      background: var(--accent-strong);
    }

    button.active {
      background: #2ecc71;
    }

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 15px;
      margin-bottom: 20px;
    }

    .stat-card {
      background: var(--bg-elevated);
      padding: 15px;
      border-radius: 8px;
      border: 1px solid var(--border);
    }

    .stat-card h3 {
      margin: 0 0 10px 0;
      color: var(--accent);
      font-size: 14px;
    }

    .stat-value {
      font-size: 24px;
      font-weight: bold;
    }

    .event-log {
      background: var(--bg-panel);
      padding: 15px;
      border-radius: 8px;
      max-height: 200px;
      overflow-y: auto;
      font-family: monospace;
      font-size: 12px;
    }

    .event-log p {
      margin: 5px 0;
      padding: 5px;
      background: var(--bg-elevated);
      border-radius: 4px;
    }

    .event-log p.collision {
      border-left: 3px solid #e74c3c;
    }

    .event-log p.boundary {
      border-left: 3px solid #f39c12;
    }

    .event-log p.position {
      border-left: 3px solid #2ecc71;
    }

    /* Custom theming for game objects */
    convgame-object.ball {
      --convgame-object-debug-color: lime;
    }

    convgame-object.obstacle {
      --convgame-object-debug-color: orange;
    }
JS (js)
import "https://esm.sh/@manufosela/convgame-object";

    const container = document.getElementById('gameContainer');
    const eventLog = document.getElementById('eventLog');
    const ball1 = document.getElementById('ball1');
    const ball2 = document.getElementById('ball2');
    const obstacle = document.getElementById('obstacle');

    let running = false;
    let animationId = null;
    let collisionCount = 0;
    let boundaryCount = 0;
    let ballCounter = 2;

    // Get all game objects
    const getGameObjects = () => Array.from(container.querySelectorAll('convgame-object'));

    // Set bounds for all objects
    const setBounds = () => {
      const bounds = {
        minX: 0,
        maxX: container.offsetWidth,
        minY: 0,
        maxY: container.offsetHeight
      };
      getGameObjects().forEach(obj => {
        obj.bounds = bounds;
      });
    };

    // Custom render functions
    const renderBall = (ctx, width, height) => {
      ctx.beginPath();
      ctx.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, Math.PI * 2);
      ctx.fillStyle = '#4a90d9';
      ctx.fill();
      ctx.strokeStyle = '#2d5a8a';
      ctx.lineWidth = 2;
      ctx.stroke();
    };

    const renderObstacle = (ctx, width, height) => {
      ctx.fillStyle = '#e74c3c';
      ctx.fillRect(0, 0, width, height);
      ctx.strokeStyle = '#c0392b';
      ctx.lineWidth = 2;
      ctx.strokeRect(0, 0, width, height);
    };

    // Apply render functions
    ball1.renderFn = renderBall;
    ball2.renderFn = renderBall;
    obstacle.renderFn = renderObstacle;

    // Initialize bounds
    setBounds();

    // Log event helper
    const logEvent = (message, type = 'position') => {
      const p = document.createElement('p');
      p.className = type;
      p.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
      eventLog.insertBefore(p, eventLog.firstChild);
      if (eventLog.children.length > 50) {
        eventLog.removeChild(eventLog.lastChild);
      }
    };

    // Event listeners for all objects
    const setupEventListeners = (obj, name) => {
      obj.addEventListener('collision', (e) => {
        collisionCount++;
        document.getElementById('collisionCount').textContent = collisionCount;
        logEvent(`${name} collided with another object`, 'collision');
      });

      obj.addEventListener('boundary-hit', (e) => {
        boundaryCount++;
        document.getElementById('boundaryCount').textContent = boundaryCount;
        logEvent(`${name} hit ${e.detail.boundary} boundary`, 'boundary');
      });
    };

    setupEventListeners(ball1, 'Ball 1');
    setupEventListeners(ball2, 'Ball 2');

    // Update stats display
    const updateStats = () => {
      document.getElementById('ball1Pos').textContent =
        `${ball1.x.toFixed(1)}, ${ball1.y.toFixed(1)}`;
      document.getElementById('ball1Vel').textContent =
        `${ball1.velocityX.toFixed(2)}, ${ball1.velocityY.toFixed(2)}`;
    };

    // Game loop
    const gameLoop = () => {
      if (!running) return;

      const objects = getGameObjects();

      // Update all objects
      objects.forEach(obj => obj.update());

      // Check collisions between all pairs
      for (let i = 0; i < objects.length; i++) {
        for (let j = i + 1; j < objects.length; j++) {
          if (objects[i].collidesWith(objects[j])) {
            objects[i].resolveCollision(objects[j]);
          }
        }
      }

      updateStats();
      animationId = requestAnimationFrame(gameLoop);
    };

    // Control handlers
    document.getElementById('startBtn').addEventListener('click', () => {
      running = true;
      document.getElementById('startBtn').classList.add('active');
      gameLoop();
      logEvent('Simulation started', 'position');
    });

    document.getElementById('pauseBtn').addEventListener('click', () => {
      running = false;
      document.getElementById('startBtn').classList.remove('active');
      if (animationId) {
        cancelAnimationFrame(animationId);
      }
      logEvent('Simulation paused', 'position');
    });

    document.getElementById('resetBtn').addEventListener('click', () => {
      running = false;
      document.getElementById('startBtn').classList.remove('active');
      if (animationId) {
        cancelAnimationFrame(animationId);
      }

      // Reset positions
      ball1.setPosition(100, 100);
      ball1.setVelocity(3, 2);
      ball2.setPosition(400, 200);
      ball2.setVelocity(-2, 3);

      // Remove extra balls
      getGameObjects().forEach(obj => {
        if (!['ball1', 'ball2', 'obstacle'].includes(obj.id)) {
          obj.remove();
        }
      });

      collisionCount = 0;
      boundaryCount = 0;
      ballCounter = 2;
      document.getElementById('collisionCount').textContent = '0';
      document.getElementById('boundaryCount').textContent = '0';

      updateStats();
      logEvent('Simulation reset', 'position');
    });

    document.getElementById('debugBtn').addEventListener('click', () => {
      getGameObjects().forEach(obj => {
        obj.debug = !obj.debug;
      });
    });

    document.getElementById('addBallBtn').addEventListener('click', () => {
      ballCounter++;
      const newBall = document.createElement('convgame-object');
      newBall.id = `ball${ballCounter}`;
      newBall.className = 'ball';
      newBall.x = Math.random() * 700 + 50;
      newBall.y = Math.random() * 400 + 50;
      newBall.width = 30 + Math.random() * 40;
      newBall.height = newBall.width;
      newBall.velocityX = (Math.random() - 0.5) * 8;
      newBall.velocityY = (Math.random() - 0.5) * 8;
      newBall.mass = newBall.width / 40;
      newBall.friction = 0.995;
      newBall.restitution = 0.9;
      newBall.renderFn = renderBall;

      const bounds = {
        minX: 0,
        maxX: container.offsetWidth,
        minY: 0,
        maxY: container.offsetHeight
      };
      newBall.bounds = bounds;

      setupEventListeners(newBall, `Ball ${ballCounter}`);
      container.appendChild(newBall);

      logEvent(`Added Ball ${ballCounter}`, 'position');
    });

    document.getElementById('applyForceBtn').addEventListener('click', () => {
      getGameObjects().forEach(obj => {
        if (!obj.isStatic) {
          const forceX = (Math.random() - 0.5) * 20;
          const forceY = (Math.random() - 0.5) * 20;
          obj.applyForce(forceX, forceY);
        }
      });
      logEvent('Applied random forces', 'position');
    });

    // Initial stats update
    updateStats();