JavaScript physics simulation

Modifying and explaining a JavaScript marble simulation.

You can query a model directly and test the results returned when using different parameter values with the Cloud console, or by calling the Vertex AI API directly.

Prompt Freeform

I have this JavaScript code that simulates falling marbles:

const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');

const marbles = [];
const obstacles = [];

const gravity = 0.1; // Adjust this value to change the speed of the simulation
const friction = 0.99;
const restitution = 0.8;

class Marble {
  constructor(x, y, radius, color) {
    Object.assign(this, { x, y, radius, color });
    this.dx = (Math.random() - 0.5) * 2;
    this.dy = 0;
  }

  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }

  update() {
    // Apply gravity to the vertical velocity
    this.dy += gravity; 
    // Apply friction to the horizontal velocity
    this.dx *= friction;
    // Update the marble's position based on its velocity
    this.x += this.dx; 
    this.y += this.dy; 

    // Check for collisions with the bottom of the canvas
    if (this.y + this.radius > canvas.height) {
      // Keep the marble within the canvas boundaries
      this.y = canvas.height - this.radius;
      // Reverse the vertical velocity and apply restitution for bouncing effect
      this.dy = -this.dy * restitution; 
    }

    // Check for collisions with the sides of the canvas
    if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
      // Reverse the horizontal velocity to make the marble bounce off the walls
      this.dx = -this.dx; 
    }

    // Check for collisions with obstacles
    obstacles.forEach(obstacle => {
      const { normalX, normalY, isColliding, penetrationDepth } = obstacle.checkCollision(this.x, this.y, this.radius);
      if (isColliding) {
        // Calculate the dot product of the velocity and the collision normal
        const dotProduct = this.dx * normalX + this.dy * normalY;
        // Reflect the velocity vector off the surface normal
        this.dx -= 2 * dotProduct * normalX; 
        this.dy -= 2 * dotProduct * normalY;
        // Apply restitution to the reflected velocity
        this.dx *= restitution; 
        this.dy *= restitution; 
        // Resolve the collision by moving the marble outside of the obstacle
        this.x += normalX * penetrationDepth; 
        this.y += normalY * penetrationDepth; 
      }
    });

    this.draw();
  }
}

class Obstacle {
  constructor(x, y, width, height, angle, color) {
    Object.assign(this, { x, y, width, height, angle, color });
  }

  draw() {
    ctx.save();
    ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
    ctx.rotate(this.angle * Math.PI / 180);
    ctx.fillStyle = this.color;
    ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
    ctx.restore();
  }

  checkCollision(mx, my, mr) {
    const cos = Math.cos(-this.angle * Math.PI / 180);
    const sin = Math.sin(-this.angle * Math.PI / 180);
    const dx = mx - (this.x + this.width / 2);
    const dy = my - (this.y + this.height / 2);
    const localX = cos * dx - sin * dy + this.width / 2;
    const localY = sin * dx + cos * dy + this.height / 2;
    const closestX = Math.max(0, Math.min(this.width, localX));
    const closestY = Math.max(0, Math.min(this.height, localY));
    const distanceX = localX - closestX;
    const distanceY = localY - closestY;
    const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);

    const isColliding = distance < mr;
    const normalX = cos * distanceX / distance + sin * distanceY / distance;
    const normalY = -sin * distanceX / distance + cos * distanceY / distance;
    const penetrationDepth = mr - distance;

    return { normalX, normalY, isColliding, penetrationDepth };
  }
}

for (let i = 0; i < 10; i++) {
  const radius = 10;
  const x = Math.random() * (canvas.width - radius * 2) + radius;
  const y = radius;
  const color = 'blue';
  marbles.push(new Marble(x, y, radius, color));
}

obstacles.push(new Obstacle(100, 500, 150, 20, -20, 'black'));
obstacles.push(new Obstacle(600, 400, 150, 20, -20, 'black'));
obstacles.push(new Obstacle(200, 200, 100, 20, 45, 'black'));
obstacles.push(new Obstacle(400, 150, 120, 20, -45, 'black'));
obstacles.push(new Obstacle(500, 500, 200, 20, 15, 'black'));
obstacles.push(new Obstacle(800, 300, 150, 20, -15, 'black'));

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  marbles.forEach(marble => marble.update());
  obstacles.forEach(obstacle => obstacle.draw());
  requestAnimationFrame(animate);
}

animate();

Response

This code is a good starting point for a marble simulation. However, there are a few areas for improvement in terms of efficiency, accuracy, and robustness:

1. Inefficient Collision Detection: The current collision detection with obstacles checks all obstacles for every marble in every frame. For a large number of marbles and obstacles, this becomes computationally expensive. A spatial partitioning data structure like a quadtree or a grid would significantly improve performance.

2. Potential for Stack Overflow (Recursion): While not directly present in this code, recursive collision resolution (where a marble collides with multiple obstacles simultaneously) could lead to a stack overflow if not handled carefully. The current iterative approach is safer.

3. Inaccurate Collision Response: The collision response uses a simple reflection based on the normal vector. This works reasonably well for simple shapes, but it can lead to inaccuracies and jittering, especially when marbles collide with each other or at sharp angles. A more robust approach would involve impulse-based collision resolution, which considers the masses and velocities of the colliding objects.

4. Lack of Marble-Marble Collisions: The simulation currently only handles marble-obstacle collisions. Adding marble-marble collision detection and resolution would make the simulation more realistic.

5. Missing canvas element: The code assumes a canvas element with the ID simulationCanvas exists in the HTML. You need to include this in your HTML file.

Here's an improved version addressing some of these issues:

const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');

const gravity = 0.1;
const friction = 0.99;
const restitution = 0.8;

class Marble {
  constructor(x, y, radius, color) {
    Object.assign(this, { x, y, radius, color });
    this.dx = (Math.random() - 0.5) * 2;
    this.dy = 0;
    this.mass = 1; // Add mass for more realistic collisions
  }

  // ... (draw method remains the same) ...

  update() {
    this.dy += gravity;
    this.dx *= friction;
    this.x += this.dx;
    this.y += this.dy;

    this.handleCollisions();
    this.draw();
  }

  handleCollisions() {
    // Bottom collision
    if (this.y + this.radius > canvas.height) {
      this.y = canvas.height - this.radius;
      this.dy = -this.dy * restitution;
    }

    // Side collisions
    if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
      this.dx = -this.dx * restitution;
    }

    // Obstacle collisions (iterative approach)
    obstacles.forEach(obstacle => {
      const collisionData = obstacle.checkCollision(this.x, this.y, this.radius);
      if (collisionData.isColliding) {
        this.resolveCollision(collisionData);
      }
    });
  }

  resolveCollision(collisionData) {
    const { normalX, normalY, penetrationDepth } = collisionData;
    const dotProduct = this.dx * normalX + this.dy * normalY;
    this.dx -= 2 * dotProduct * normalX * restitution;
    this.dy -= 2 * dotProduct * normalY * restitution;
    this.x += normalX * penetrationDepth;
    this.y += normalY * penetrationDepth;
  }
}

class Obstacle {
  // ... (Obstacle class remains largely the same) ...
}

// ... (marble and obstacle creation remains the same) ...

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  marbles.forEach(marble => marble.update());
  obstacles.forEach(obstacle => obstacle.draw());
  requestAnimationFrame(animate);
}

animate();

Remember to include the <canvas id="simulationCanvas"></canvas> element in your HTML file. This improved version adds mass to the marbles and simplifies the collision resolution slightly. For more advanced features (marble-marble collisions, spatial partitioning), you'll need to implement more complex algorithms. Consider researching techniques like broad-phase and narrow-phase collision detection for better performance with many objects.

Model: gemini-1.5-flash-002
Temperature: 0.2
Max output tokens: 8192
TopK: 40.0
TopP: 0.95