Issues with panning & checking for visibility on a translated canvas?

I originally posted this on Stack Overflow via the p5 tag, but figured this might be a better place for it.

I’m working on getting 2D zooming and panning working in a p5 project, a la this example code. I’m working in instance mode in node, so my version of this translate/scale works like the block below. I wasn’t using this method before, and was instead just panning the canvas without translating it, but I’m adding it now so that I can (eventually) get zoom working as well. zoom = 1, for now.

p.push();
p.translate(p.width/2, p.height/2);
p.scale(zoom);
p.translate(pan.x/zoom, pan.y/zoom);
// ... some code ...
p.pop();

I have a tile-based map that’s larger than the canvas, which I was drawing starting at untranslated (0,0), with canvas dimensions of (700, 500) and the map as (1300, 1300). So when I translate the first time, I’m turning (0,0) into (350, 250). If I set the panning at the start at (-350, -250) to compensate, and at least position the “camera” over the map, the values I have to use to constrain the user’s ability to pan are really weird.

// in setup()
p.B_CENTER = p5.Vector.mult(dim.view, 0.5);
p.B_PANNING = p5.Vector.mult(p.B_CENTER, -1);
p.B_MAXDIFF = p5.Vector.sub(dim.map, dim.view).add(p.B_CENTER).mult(0.5);

// in mouseDragged()
p.B_PANNING.add(p.createVector(p.mouseX - p.pmouseX, p.mouseY - p.pmouseY));
p.B_PANNING.x = p.round(p.constrain(p.B_PANNING.x, -p.B_MAXDIFF.x, -p.B_CENTER.x/2));
p.B_PANNING.y = p.round(p.constrain(p.B_PANNING.y, -p.B_MAXDIFF.y, -p.B_CENTER.y/2));

My problem is, even though all the tiles and points I have are positioning themselves correctly, the resulting relative position has no apparent relationship to the canvas dimensions. Before I added this translation code, I could check if the relative position was inside the bounds of the canvas easily, because a point panned to the top-left of the canvas would show a relative position of (0,0). My max and min constrained panning values are super weird because I’m translating to not the center of the map but the center of the canvas:

// effective max panning values
minX = -440 // furthest right,  -maxdiff.x
maxX = -175 // furthest left,   -center.x
minY = -580 // furthest bottom, -maxdiff.y
maxY = -125 // furthest top,    -center.y

// checking for visibility
function __isVisible(pos, bounds) {
	if (pos.x < 0 || pos.y < 0) {
		return false;
	}
	if (pos.x >= bounds.x || pos.y >= bounds.y) {
		return false;
	}
	return true;
}

And in the below image, you can see what this does to the relative position (sorry for the terrible debug colors). Each point on the map shows its id, then its fixed position relative to the size of the map, followed by its relative position with panning applied. But most of the relative positions don’t reflect where they actually are relative to the canvas. For example, point #7’s relative x value is way too far to the right to really be at 378px when the width of the canvas is 700px.

map position example

So, my question(s):

  1. What do I need to do to be able to check if these points are visible on the canvas? It’s important for muting sounds attached to points that go offscreen. Right now the panning makes the relative positions of these points make absolutely zero sense with that algorithm.
  2. Did I misunderstand how this translate/scale/translate thing is supposed to work?
  3. Is there an easier way to work with a 2D camera than this? Is there a way to control the camera’s actual position when I need to translate it to the center of the screen? Or should I just give up and use p5’s camera object?

Sorry if this is confusing, I’ve been racking my brain to solve it and haven’t been able to come up with anything.

Please cross-link.

It sounds like you have a viewfinder rectangle, and you want to know whether things collide with it or not – if they are in view. Is that correct?

And/or is it that you want to be able to map any world coordinate back to a screen coordinate?

the values I have to use to constrain the user’s ability to pan are really weird.

I don’t understand this statement. “Constrain” the user how?

Just to warn you, it is very hard to get good debugging feedback on a complex problem for code that you don’t share. You would be better off making a simple sketch on the p5.j5 web editor demonstrating one concrete aspect of your problem, then sharing a link to that.

Here is an example in Processing(Java) to quickly share some concepts.

/**
 *  Simple Panning and Zooming View
 * 2019-06-14 - Processing 3.4 - Jeremy Douglass
 * discourse.processing.org/t/issues-with-panning-checking-for-visibility-on-a-translated-canvas/12006
 *
 * Use mouse and +/- to pan/zoom.
 * The sketch enters a pan-zoom view, where it draws using normal world coordinates.
 * Viewport bounds are retrieved using a simple function
 * Tiles are compared to the viewport bounds using rect-rect collision detection – both rects are expressed in world coordinates.
 * The sketch exits the view, then uses the bounds to draw a view locator overlay box in red.
**/

// zoom and pan
float offx, offy;
float scalex, scaley;
float zoom=1.0;
float[] bounds;

void setup() {
  size(400, 400);
}

void draw() {
  background(128);
  noStroke();

  // define view
  offx=-width/2.0;  // default origin
  offy=-height/2.0; // detault origin
  offx+=mouseX;     // ...shifted by mouse
  offy+=mouseY;     // ...shifted by mouse
  scalex = zoom;
  scaley = zoom;

  pushMatrix();

  // pan and zoom
  scale(scalex, scaley);
  translate(-offx*scalex, -offy*scaley);  // negative!

  // get current viewport bounds in world space
  bounds = getBounds(offx, offy, scalex, scaley);
  // display in console
  for (int i=0; i<4; i++) {
    print(bounds[i], ' ');
  }
  println();

  // draw a random world map while inside the pan-zoom
  randomSeed(0);
  int grid=10;
  float step=width/(float)grid;
  int rim = 10;  // when view culling works we can't see it working.
                 // for debug, add a rim that culls objects still in view
  for (int y=0; y<grid; y++) {
    for (int x=0; x<grid; x++) {
      fill((int)random(255));
      // will this rect be inside the viewport rect? if so, draw
      boolean viewable = rectRect(bounds[0]+rim, bounds[1]+rim, 
        bounds[2]-(2*rim), bounds[3]-(2*rim), x*step, y*step, step, step);
      if (viewable) {
        rect(x*step, y*step, step, step);
      }
    }
  }
  // outline the world map
  stroke(0, 0, 255);
  noFill();
  rect(0, 0, width, height);

  popMatrix();

  // outside the view, draw a view marker
  // to show where on the map we are.
  noFill();
  stroke(255, 0, 0);
  rect(bounds[0], bounds[1], bounds[2], bounds[3]);
  text("mouse to pan\n+/- to zoom", 5, 10);
}

// for a defined zoom-pan, find its xywh bounds in world coordinates -- when unpanned / unzoomed
float[] getBounds(float xoff, float yoff, float xscale, float yscale) {
  float[] bounds = new float[4];
  bounds[0] = xoff * xscale;
  bounds[1] = yoff * yscale;
  bounds[2] = width/xscale;
  bounds[3] = height/yscale;
  return bounds;
}

// check if object is in view with rect-rect collision detection
// www.jeffreythompson.org/collision-detection/rect-rect.php
boolean rectRect(float r1x, float r1y, float r1w, float r1h, float r2x, float r2y, float r2w, float r2h) {
  if (r1x + r1w >= r2x && r1x <= r2x + r2w &&
    r1y + r1h >= r2y && r1y <= r2y + r2h) {
    return true;
  }
  return false;
}

// zoom with keyboard
void keyReleased() {
  if (key=='+' || key=='=') {
    zoom=zoom*1.1;
  }
  if (key=='-' || key=='_') {
    zoom=zoom*0.9;
  }
}

I believe it’s a range of values like those used by function constrain(). :stuck_out_tongue:

1 Like