How can I center an object on my screen that moves?

Understanding the relationship between translate, scale, and the idea of a 2D “camera” can sometimes be a bit tricky. It is worth reviewing the coordinate system and 2D transformations tutorials:

However, in the end, there are three key transformations:

  1. translate to the center of the screen
  2. optionally scale the view, e.g. based on player speed or inter-player distance
  3. translate again to recenter the camera on the player – or on a selected point of view, the midpoint between two players, or the center of a selected group of objects et cetera.
/**
 * Two Player Camera 2D
 * 2019-06 - Jeremy Douglass - Processing 3.4
 * Keep two balls (players) in view by midpoint/distance.
 * Press any key to switch modes.
 * 
 * Switch between modes: FLAT, TRACK, TRACK_ZOOM.
 * 0 FLAT is a normal 2D view.
 * 1 TRACK centers view on midpoint between two balls.
 * 2 TRACK_ZOOM tracks + zooms distance between balls.
 */

Ball p1;
Ball p2;
float[] bounds;
PVector view;
int mode; // 0=FLAT, 1=TRACK, 2=TRACK_ZOOM

void setup() {
  size(200, 200);
  fill(0);

  // create a bounding box with two bouncing balls
  bounds = new float[]{1, height*.2, width-1, height*.6};
  p1 = new Ball(random(width), height/2, 2, 10, bounds);
  p2 = new Ball(random(width), height/2, 2, 10, bounds);
}

void draw() {
  background(128);

  pushMatrix();
  noFill();
  // update ball locations
  p1.move();
  p2.move();

  // view x,y is a world-space location halfway between two balls
  view = PVector.lerp(p1.pos, p2.pos, 0.5);
  // view.z is the recommended zoom based on inter-ball distance
  view.z = (width-20)/(20+p1.pos.dist(p2.pos));
  
  switch(mode){
    case 0: // FLAT
    break;
    case 1: // TRACK
    translate(width/2, height/2);  // center screen on 0,0
    translate(-view.x, -view.y);   // recenter screen on view
    break;
    case 2: // TRACK_ZOOM
    translate(width/2, height/2);  // center screen on 0,0
    scale(view.z);                 // scale screen to ball distance
    translate(-view.x, -view.y);   // recenter screen on view
    break;
  }

  // draw
  rect(bounds[0], bounds[1], bounds[2], bounds[3]);
  p1.render();
  p2.render();
  ellipse(view.x, view.y, 3, 3);
  popMatrix();

  // status
  String msg = "mode:" + mode + "  xy:" + (int)view.x + "," + (int)view.y + "  zoom:" + nf(view.z, 0, 1) + "\nPress any key";
  text(msg, 5, 15);
}

// simple bouncing ball with a bounding box
class Ball {
  float[] box;
  PVector pos;
  PVector speed;
  float rad;
  Ball(float x, float y, float speed, float rad, float[] box) {
    this.pos = new PVector(x, y);
    this.rad = rad;
    this.box = box;
    this.speed = PVector.random2D().setMag(speed);
  }
  void move() {
    if (pos.x > box[0]+box[2] || pos.x < box[0]) speed.x *= -1;
    if (pos.y > box[1]+box[3] || pos.y < box[1]) speed.y *= -1;
    pos.add(speed);
  }
  void render() {
    ellipse(pos.x, pos.y, 10, 10);
  }
}

// press any key to switch between modes 0, 1, 2
void keyReleased() {
  mode = (mode+1)%3;
}

0674 1375

These operations are order dependent if you are scaling: you must center your coordinate system first, then scale from the center, then translate to your view. If you try to scale first or last then bad things happen: either scaling happens from the top left, or the view scale does not match the drawing scale and they are misaligned.

2 Likes