Detect if over object when object is zoomed/scaled

This bascially comes from an attempt at zoom and pan (not using peasyCam ) it’s part of a much bigger project.
I have a load of ‘cells’ that are clickable and I want to be able to scale them to ‘simulate’ a zoom in 2D, all cells are in thier own PGraphics buffer.
Scaling them is no real bother, but any attempt I’ve made at getting their new position after being scaled has failed totally.
I’m sure I’m missing something very obvious here, but I can’t figure out how to find the new x and y positions.
Can any point me in the right direction please ?

Simplified code below uses just up and down arrows for simple zoom and ‘r’ to reset.

/* ************* 2D zoom & click question ****************
 ***---->> ZOOM IN : PRESS DOWN ARROW <<----***
 ***---->> ZOOM OUT : PRESS UP ARROW  <<----***
 ***---->> RESET VIEW : PRESS 'r'     <<----***
 *********************************************/

final int cell_size = 66;
final int padding = 2; // just to keep rect stroke within buffer
final int cell_space = 6;
final int startGrid_space = cell_size+cell_space;

int startX = startGrid_space;
int startY = startGrid_space;
float zoom = 1.0;
int cell_count = 100;
ArrayList<Cell> Cells = new ArrayList<Cell>();
int liveCell;
//
boolean bufferDebug = true; 
int nbrBufferUpdates = 0;


void setup() {
  size(864, 864);

  for (int i = 0; i < cell_count; i++) {
    Cells.add(new Cell(this, i));
  }
}


void draw() {
  background(0);

  if (bufferDebug) {
    surface.setTitle(int(frameRate) + " fps" +" BufferUpdates :" +nbrBufferUpdates);
  }

  for (Cell cell : Cells) {
    cell.display();
  }
}


void mouseMoved() {
  liveCell = -1;
  for (Cell cell : Cells) {
    if (cell.isOver()) {
      liveCell = Cells.indexOf(cell);
    }
  }
}

void mouseReleased() {
  for (Cell cell : Cells) {
    if (Cells.indexOf(cell) == liveCell) {
      cell.clicked();
    }
  }
}

void keyReleased() {
  if (key == CODED) {
    switch(keyCode) {

    case UP : //zoom out
      zoom += 0.1;
      zoom = constrain(zoom, 0.5, 1.0);
      break;

    case DOWN : // zoom in
      zoom -= 0.1;
      zoom = constrain(zoom, 0.5, 1.0);
      break;
    }
  } else if (key == 'r') { //reset all cells to start up size & position
    zoom = 1.0;
    for (Cell cell : Cells) {
      cell.setPosition(startX +(cell.index/10)*startGrid_space, startY +(cell.index%10)*startGrid_space);
    }
  }
  println("Zoom :"+zoom);
}


//----------------------------->> CELL CLASS
public class Cell { 
  PGraphics buffer;
  float x, y, w, h;
  boolean invalidBuffer, Over, pre_Over, Active;
  int index;

  Cell(PApplet app, int t_index) {
    index = t_index;
    x = startX + (index/10)*startGrid_space;
    y = startY + (index%10)*startGrid_space; 
    w = cell_size;
    h = cell_size;
    buffer = app.createGraphics(int(w+padding), int(h+padding)); 
    invalidBuffer = true;
  }

  public boolean isOver() {
    if (mouseX >x && mouseX< x+w && mouseY>y && mouseY<y+h ) { // Edit for changed position when zoomed  ?
      Over = true;
    } else {
      Over = false;
    }
    if (Over != pre_Over) {
      invalidBuffer = true;
      pre_Over = Over;
    }
    return Over;
  }

  public void clicked() {
    Active = !Active;
    invalidBuffer = true;
  }

  public void setPosition(float t_X, float t_Y) {
    x = t_X;
    y = t_Y;
  }

  public void update() {
    invalidBuffer = true;
  }

  public void display() {
    if (invalidBuffer) {
      updateBuffer();
    }
    pushMatrix();
    scale(zoom);
    translate(x, y);
    image(buffer, 0, 0);
    popMatrix();
  }

  public void updateBuffer()
  {
    if (bufferDebug) {
      nbrBufferUpdates ++;
    }
    buffer.beginDraw();
    buffer.clear() ; 
    buffer.rectMode(CENTER);
    buffer.strokeWeight(2);
    buffer.noFill();
    buffer.stroke(200, 200, 200);
    if (Active) {
      buffer.stroke(0, 200, 0);
    } else if (Over) {
      buffer.stroke(253, 114, 63);
    }
    buffer.rect(w/2, h/2, w-padding, h-padding, 15);  
    buffer.endDraw();
    invalidBuffer = false;
  }
}
1 Like

Hi!
Nice Project!

look at this formula
Dilatasi-pusat-O-e1511319524357

let say you want

translate(width/2, height/2);
scale(scale);
circle(x, y);

so

xAfter = x * scale + width/2;
yAfter = y * scale + height/2;

if you scale before translate it

scale(scale);
translate(width/2, height/2);
circle(x, y);

it will be

xAfter = (x + width/2) * scale;
yAfter = (y + height/2) * scale;

See the implementation

void setup() {
  size(500, 500);
  originX = width/2;
  originY = height/2;
  rectMode(CENTER);
}

float originX, originY;
float scale = 1;
float d = 30;
float x = 30;
float y = 30;
void draw() {
  background(51);


  pushMatrix();
  translate(originX, originY);
  scale(scale);
  fill(255, 100);
  noStroke();
  ellipse(x, y, d, d);
  popMatrix();

  float onScreenX = x * scale + originX;
  float onScreenY = y * scale + originY;
  stroke(0, 255, 0);
  drawCoordinates(onScreenX, onScreenY);
  scale = map(mouseX, 0, width, -5, 5);

  fill(255);
  text("Scale: " + scale, 50, 50);
  
}

void mousePressed() {
  originX = mouseX;
  originY = mouseY;
}

void drawCoordinates(float px, float py) {
  strokeWeight(1);
  stroke(255, 0, 0);
  line(-width, 0, width, 0);
  line(0, -height, 0, height);
  line(0, py, px, py);
  line(px, 0, px, py);
  fill(0, 255, 0);
  strokeWeight(5);
  textAlign(CENTER);
  
  text("Origin", originX, originY - 20);
  point(originX, originY);
  text("Click to change transformation origin", width/2, 40);
  textAlign(LEFT);
  text("(" + int(px) + ", " +  int(py) + ")", px, py);
  
  text("Postion after transformation", width/2, height-60);
  text("X: " + px, width/2, height-50);
  text("Y: " + py, width/2, height - 40);
}
3 Likes

@humayung Thank you so much that was a great help and explained very well !

I do have one further question :

If you look at my original example and watch the zoom value printed to console… when this zoom value is between approx. 0.75 - 9.90. The framerate drops considerably from 60fps to as low as 37fps sometimes.
What could be causing this only in that specific range ?
The exact same issue occurs when I update my code with your suggestion above, so it is nothing to do with how the values for the scale & translation are arrived at, but a problem of scaling using that range.

im not sure how that could happened.
But i suspected such thing caused by the var zoom itself.

Zoom :0.9
Zoom :0.79999995
Zoom :0.6999999
Zoom :0.5999999

it is quite like doing expensive calculation with these long decimal in the scale() function itself (im not very sure)
try to floor to its second decimal

void keyReleased() {
  if (key == CODED) {
    switch(keyCode) {

    case UP : //zoom out
      zoom = constrain(zoom +0.1, 0.5, 1.0);
      zoom = floor((zoom * 10))/10f; 
      break;

    case DOWN : // zoom in
      zoom = constrain(zoom - 0.1, 0.5, 1.0);
      zoom = floor((zoom * 10))/10f;
      break;
    }
......
......

for another, since the scaling factor are the same for every cell, the calculation would be slightly faster if you scale it once for everything. put it in the draw(), not in the display function in the cell class. it will be executed once rather than do it for every cell, costs you lot.

Thanks again for the help.
I had also suspected the long decimal.
But having tried the floor() and moving the scale() to the draw, neither of these fix the problem.

I’m sure the clue is in the problem scale range 0.75 -> 0.9 , though what that means I’m not sure.

Ignoring fps hit for now, I’ve moved onto putting back in some functions from the larger project
The cells can be selected and moved about, they should snap to a ‘virtual grid’ and not be allowed to rest on top of another cell.

I have got a bit lost again with trying to track / update postions when zoomed, if you try the code below, it works well enough when scale/zoom is 1.0, but the lower the scale, the less the position keeps up with the cursor.
I’m not sure if this maybe made worse by my snap and “gridCheck” methods not being very efficient as well ?

/* ************* 2D scale & click question  MK2 ****************
 ---->> ZOOM IN/OUT : MOUSEWHEEL 
 ---->> PAN VIEW : LMB & DRAG
 ---->> RE CENTER VIEW : DOUBLE CLICK LMB
 ---->> SELECT/DESELECT CELLS : LMB CLICK
 ---->> DESELECT ALL CELLS : DOUBLE CLICK RMB
 ---->> MOVE SELECTED CELLS : HOLD 'ALT' + LMB & DRAG 
 --->> RESET CELLS TO START GRID : PRESS 'r'
 *********************************************/

final int cell_size = 66;
final int padding = 2; // just to keep cell stroke within buffer
final int cell_space = 6;
final int grid_space = cell_size+cell_space;
int cell_count = 100;
ArrayList<Cell> Cells = new ArrayList<Cell>();
int liveCell;
//
float scale; 
float originX, originY;
float vx, vy;
boolean altHeld = false;

//
boolean bufferDebug = true; 
int nbrBufferUpdates = 0;


void setup() {
  size(1200, 1200);

  originX = width/2;
  originY = height/2;

  scale = 1.0;

  for (int i = 0; i < cell_count; i++) {
    Cells.add(new Cell(this, i));
  }
}


void draw() {
  background(0);


  if (bufferDebug) {
    surface.setTitle(int(frameRate) + " fps" +" BufferUpdates :" +nbrBufferUpdates);
  }

  for (Cell cell : Cells) {
    cell.display();
  }
}


void mouseClicked(MouseEvent event) {
  if (mouseButton == LEFT) {
    if (event.getCount() == 2) { //re center view
      originX = width/2;
      originY = height/2;
    }
  }
  if (mouseButton == RIGHT) {
    if (event.getCount() == 2) { //de select all cells
      for (Cell cell : Cells) {
        cell.setActiveState(false);
      }
    }
  }
}

void mouseMoved() {
  liveCell = -1;
  for (Cell cell : Cells) {
    if (cell.isOver()) {
      liveCell = Cells.indexOf(cell);
    }
  }
}

void mouseReleased() {
  if (!altHeld) {
    for (Cell cell : Cells) {
      if (Cells.indexOf(cell) == liveCell) {
        cell.clicked();
      }
    }
  }
}

void mousePressed() {
  if (altHeld) { // record offset of cursor position to cell position
    for (Cell cell : Cells) {
      cell.setHeld(mouseX, mouseY);
    }
  }
}

void mouseDragged() { 
  if (altHeld) { //drag a cell to new position
    for (Cell cell : Cells) {
      cell.dragPosition();
    }
  } else { // Pan view
    vx = (pmouseX - mouseX); 
    vy = (pmouseY - mouseY); 
    originX -= vx;
    originY -= vy;
  }
}

void mouseWheel(MouseEvent event) { // scale/zoom view on mouse scroll 
  if (event.getCount()>0) {
    scale+= 0.1;
  } else {
    scale-=0.1;
  }
  scale = constrain(scale, 0.5, 1.0);
  scale = round((scale*10))/10f;
  println(scale);
}

void keyPressed() {
  if (key == CODED) {
    switch(keyCode) {
    case ALT:
      altHeld = true;
      break;
    }
  }
}

void keyReleased() {
  if (key == CODED) {
    switch(keyCode) {

    case ALT:
      altHeld = false;
      break;
    }
  } else {

    switch(key) {

    case 'r': //reset all cells to start up size & position
      scale = 1.0;
      for (Cell cell : Cells) {
        cell.reset();
      }
      break;
    }
  }
}


//----------------------------->> CELL CLASS
public class Cell { 
  PGraphics buffer;
  float x, y, hx, hy, xOffset, yOffset, w, h;
  boolean invalidBuffer, Over, pre_Over, Active, Held;
  int index;

  Cell(PApplet app, int t_index) {
    index = t_index;
    x = (originX-(grid_space*5)) + (index/10)*grid_space - originX;
    y = (originY-(grid_space*5))+ (index%10)*grid_space - originY; 
    w = cell_size;
    h = cell_size;
    buffer = app.createGraphics(int(w+padding), int(h+padding)); 
    invalidBuffer = true;
  }

  public boolean isOver() {
    if (mouseX >x*scale+originX && mouseX< x*scale+originX+(w*scale) && mouseY>y*scale+originY && mouseY<y*scale+originY+(h*scale) ) { 
      Over = true;
    } else {
      Over = false;
    }
    if (Over != pre_Over) {
      invalidBuffer = true;
      pre_Over = Over;
    }
    return Over;
  }

  public void clicked() {
    if (Held) {
      Held = false;
    }
    Active = !Active;
    invalidBuffer = true;
  }

  public void setHeld(float px, float py) {
    if (Active) {
      Held = true;
      hx = px-x;
      hy = py-y;
      invalidBuffer = true;
    }
  }

  public void dragPosition() { //does not follow cursor well when zoomed...
    float new_x = 0;
    float new_y = 0;

    if (Active && Held && altHeld) {
      new_x = (mouseX-hx);
      new_x -=new_x %grid_space;

      new_y = (mouseY-hy);
      new_y -=new_y %grid_space;
      //
      setPosition(new_x, new_y);
    }
  }

  public void setPosition(float t_x, float t_y) {
    if (checkGrid(index, t_x, t_y)) {
      x = t_x;
      y = t_y;
    }
  }

  public boolean checkGrid(int t_Index, float t_X, float t_Y) { // tests if grid-cell already occupied
    for (Cell cell : Cells) {
      if (cell.index != t_Index && t_X == cell.x && t_Y == cell.y) {
        return false;
      }
    }
    return true;
  }

  public void reset() {
    x = originX-(grid_space*5) + (index/10)*grid_space - originX;
    y = originY-(grid_space*5) + (index%10)*grid_space - originY;
  }

  public void setActiveState(boolean t_state) {
    Active = t_state;
    invalidBuffer = true;
  }

  public void update() {
    invalidBuffer = true;
  }

  public void display() {
    if (invalidBuffer) {
      updateBuffer();
    }
    pushMatrix();
    translate(x*scale+originX, y*scale+originY);
    scale(scale);
    image(buffer, 0, 0);
    popMatrix();
  }

  public void updateBuffer()
  {
    if (bufferDebug) {
      nbrBufferUpdates ++;
    }
    buffer.beginDraw();
    buffer.clear() ; 
    buffer.rectMode(CENTER);
    buffer.strokeWeight(2);
    buffer.noFill();
    buffer.stroke(200, 200, 200);
    if (Active) {
      buffer.stroke(0, 200, 0);
    } else if (Over) {
      buffer.stroke(253, 114, 63);
    }
    buffer.rect(w/2, h/2, w-padding, h-padding, 15);  
    buffer.endDraw();
    invalidBuffer = false;
  }
}