2D matrix reverse screenX / screenY function (class)

I needed a reverse screenX/Y function, but found there wasn’t any (I only found the rare thread asking for the same).

I’d like to add that while I’ve used processing before now and then, I’m not too used to OOP (old spaghettimonster here. I’m getting there. I think). So maybe some methods aren’t as effective or elegant as they could have been (suggestions welcome!).

Anyway, I made this class that kind of substitute most 2D matrix operations (to keep track of them). Btw I’m not sure a class is the best way in this case as only one instance is needed. Maybe I should have used PVectors too, but for now I didn’t for some reason. It’s entirely possible I’ve “overthought” (overcomplicated) the whole thing, but hope I got it right and that I didn’t “reinvent the wheel” :slight_smile: It’s nothing special, but I figured it might come in handy.

// Object to keep track of matrix transforms
MatrixTracker2D matrix = new MatrixTracker2D();

// Matrix transform push/pop levels
final int levels = 3;

// Test ellipses, to show matrix transforms (translations / rotations)
EllipseThing ellipsoid[] = new EllipseThing[levels];

// mouse clicks make a test point that follows the following matrix "level"
final int testPointAtLevel = 2; // zero-based

// Test point coordinates
float tpx = 15;
float tpy = 20;

 
void setup() {
  // test Ellipses
  ellipsoid[0] = new EllipseThing( 300, 300,   0, 0,  50, 100, 0, 0.01);
  ellipsoid[1] = new EllipseThing( 200,   0,   0, 0,  25,  50, 0, 0.02);
  ellipsoid[2] = new EllipseThing(  50,  50, -10, 20, 25,  10, 0, 0.015);
  //ellipsoid[3] = new EllipseThing(  25,  -25,  20, 10, 0, 0.03);

  size(600, 600);
  ellipseMode(RADIUS);
}

 
void draw() {
  background(128);
  fill(255);

  for(int i=0; i<levels; i++) {
    stroke(0);
    strokeWeight(1);
    
    // track a new matrix transform
    matrix.push();
    
    // transform current matrix
    matrix.move(ellipsoid[i].x, ellipsoid[i].y);
    matrix.turn(ellipsoid[i].ang);
    
    // Indicate a matrix "level" with an ellipse
    ellipsoid[i].display();
    ellipsoid[i].update();
  
    // Test point that follows a matrix
    if (testPointAtLevel == i) testPoint();
    
    // Line from center of ellipse towards mouse pointer
    if (mousePressed) testLine(i);
  }
 
  // remove old matrix transforms before new frame
  for (int i=0; i<levels; i++) {
    matrix.pop();
  }
}




// Mouse click makes a point that follows current "level" of matrix transforms
void testPoint() {
    if (mousePressed) {
      tpx = matrix.posX(mouseX, mouseY);  // reverse screenX
      tpy = matrix.posY(mouseX, mouseY);  // reverse screenY
    }
    strokeWeight(3);
    stroke(#FF0000);
    point(tpx, tpy);
}


// A line from center of an ellipsis towards the mouse pointer (just because)
void testLine(int i) {
  matrix.push();
  matrix.move(ellipsoid[i].xt, ellipsoid[i].yt);  // testing off-center ellipses

  // Turn towards mouse pointer
  float mAng = matrix.getAngle(mouseX, mouseY, 0, 0);
  matrix.turn(mAng);
  
  // Ellipse radius at angle mAng (pointing at mouse pointer)
  float ra = ellipsoid[i].ra;
  float rb = ellipsoid[i].rb;
  float radiusAtAngle = (ra*rb)/sqrt(pow(ra*sin(mAng),2) + pow(rb*cos(mAng),2));
  
  strokeWeight(3);
  stroke(#FF0000);       // red line inside ellipse
  line(0,0,radiusAtAngle,0);
  strokeWeight(1);
  stroke(#0000FF);       // black line outside ellipse
  line(radiusAtAngle,0,200,0);
    
  matrix.pop();
}




// Test ellipse class, to indicate matrix transforms and rotations
class EllipseThing {
  float x, y;      // x, y matrix translation
  float xt, yt;    // x, y additional offset
  float ra, rb;    // radius A and B of ellipse
  float ang;       // ellipse (matrix) angle
  float turnSpeed; 
  
  EllipseThing( float ex, float ey, float ext, float eyt,
                float era, float erb, float angle, float ts) {
    x = ex;
    y = ey;
    xt = ext;
    yt = eyt;
    ra = era;
    rb = erb;
    ang = angle;
    turnSpeed = ts;
  }
  
  void display() {
    // Draw around offset from wherever origo (of matrix) is 
    ellipse(xt, yt, ra, rb);
  }
  
  void update() {
    ang += turnSpeed;
  }
}

And the class:

/** Class to keep track of 2D matrix transformations
   (to reverse-transform screen coordinates to matrix coordinates)

    To keep track of matrix transformations:
      Instead of:     Use:
      --------------  -----------------------------
      pushMatrix()    MatrixTracker2D.push()
      popMatrix()     MatrixTracker2D.pop()
      resetMatrix()   MatrixTracker2D.reset()
      translate(x,y)  MatrixTracker2D.move(x,y)
      rotate(angle)   MatrixTracker2D.turn(angle)
      
    Where "MatrixTracker2D" is an object of that type.

    To get a "matrix position" from a screen position (reverse of screenX / screenY):
  
      MatrixTracker2D.posX(x,y)
      MatrixTracker2D.posY(x,y)   where x,y = screen coordinates

    To get the angle between a screen position and a matrix position
    relative to the matrix X-axis, use:

      MatrixTracker2D.getAngle(sx, sy, mx, my)

    where:
      sx, sy is a screen position, and
      mx, my is a position in the current matrix


    2019.05.05 raron 
    (No guarantee that it actually works as intended)
*/

class MatrixTracker2D {
  int level = 0;
  // Processing allows a maximum of 32 pushMatrix()'es afaik
  // I'm including the "base" matrix here, totalling 33.
  final int max = 33;
  FloatList txList;
  FloatList tyList;
  FloatList angList;
  
  MatrixTracker2D() {
    // pre-allocating max matrix transformations
    txList = new FloatList(max);
    tyList = new FloatList(max);
    angList = new FloatList(max);
    // initialize one entry for the "base" matrix (assumed reset)
    txList.append(0);
    tyList.append(0);
    angList.append(0);
    level = txList.size(); // is 1 at instantiation
  }
  
  // Make space for new matrix transform data
  void push() {
    if (level>0 && level<max) {
      int i = level-1;
      txList.append(txList.get(i));
      tyList.append(tyList.get(i));
      angList.append(angList.get(i));
      level = txList.size();
      pushMatrix();
    }
  }
  
  // Translate matrix
  void move(float x, float y) {
    if (level > 0) {
      int i = level-1;
      float tempX = txList.get(i);
      float tempY = tyList.get(i);
      float ang = angList.get(i);
      txList.set(i, tempX + x*cos(ang) + y*cos(ang+PI/2));
      tyList.set(i, tempY + x*sin(ang) + y*sin(ang+PI/2));
      translate(x,y);
    }
  }
  
  // Rotate matrix
  void turn(float angle) {
    if (level > 0) {
      int i = level-1;
      angList.set(i, angList.get(i) + angle);
      rotate(angle);
    }
  }

  void reset() {
    txList.set(level-1, 0);
    tyList.set(level-1, 0);
    angList.set(level-1, 0);
    resetMatrix();
  }
  
  // remove last matrix and data
  void pop() {
    if (level>0) {
      txList.remove(level-1);
      tyList.remove(level-1);
      angList.remove(level-1);
      level = txList.size();
      popMatrix();
    }
  }

  // Get matrix X position from screen position
  // (reverse screenX)
  float posX(float x, float y) {
    int i = level-1;
    float tx = txList.get(i);
    float ty = tyList.get(i);
    float ang = atan2(y-ty, x-tx) - angList.get(i);
    float pDist = sqrt(pow(x-txList.get(i),2) + pow((y-tyList.get(i)),2));
    float mx = pDist * cos(ang);
    return mx;
  }
  
  // Get matrix Y position from screen position
  // (reverse screenY)
  float posY(float x, float y) {
    int i = level-1;
    float tx = txList.get(i);
    float ty = tyList.get(i);
    float ang = atan2(y-ty, x-tx) - angList.get(i);
    float pDist = sqrt(pow(x-txList.get(i),2) + pow((y-tyList.get(i)),2));
    float my = pDist * sin(ang);
    return my;
  }
  
  // Angle of line between a screen position and a matrix position
  // relative to the matrix X-axis.
  float getAngle(float sx, float sy, float mx, float my) {
    int i = level-1;
    float tx = screenX(mx, my);
    float ty = screenY(mx, my);
    float ang = atan2(sy-ty, sx-tx) - angList.get(i);
    // Also works (slower?)
    // float tx = posX(sx, sy);
    // float ty = posY(sx, sy);
    // float ang = atan2(ty-my, tx-mx);
    return ang;
  }
  
}

2 Likes

sorry if i understand you wrong,

yes , there can not be a reverse screenX
( would require a z info and view angles )

but no need to remember all 3D translation / scale / rotate …
you do prior, because there is a
https://processing.org/reference/modelX_.html
of any point ( or just the actual coordinate system origin ) modelX(0,0,0);
what gives back the absolute posx … ( also Y , Z )

@ kll
I think you got it.

Did a quick test just now with P2D, P3D and FX2D. I couldn’t get modelX / Y to work (I tried to set Z to 0, since it had to have 3 arguments, as you point out). Perhaps it will work if I use a 3D space and somehow line up a plane with the view, but that’s getting rather complicated I think.

Processing should have a 2D version of modelX (or matrixX? IE a reverse screenX. And Y). But no matter, I made my own for now.

2 Likes

Thanks for sharing

I have been looking for a reverseScreenX but in 3D space (not the opposite of modelX but of screenX).

I have the z Position and the camera Position and mouseX, mouseY and I am looking for the x and y pos in 3D pos (a new 3D-point I want to add).

Can your code do this?

Thanks a lot!

Regards, Chrisir

P.S.
To clarify things a bit: raron works in 2D and there’s no modelX and modelY for 2D.

In 3D modelX and modelY and modelZ store a point (e.g. 0,0,0) with the changes of the transformation matrix.

Another thing is screenX and screenY (there’s also screenZ which makes no sense):

  • The purpose is to know the 2D position on the screen of a point that’s drawn in 3D. It calculates the projection of 3D space onto the screen surface.

  • It is useful to register mouse clicks on a 3D point as in the example below.


ArrayList<SphereClass> list = new ArrayList();  

void setup() {
  size(1300, 900, P3D);

  // init all spheres 
  SphereClass newSphere;

  newSphere=new SphereClass(230, 230, -230);
  list.add(newSphere);

  newSphere=new SphereClass(530, 230, -230);
  list.add(newSphere);

  newSphere=new SphereClass(230, 230, 30);
  list.add(newSphere);
}

void draw() {
  background(0);
  lights(); 

  // loop over all spheres 
  for (SphereClass sphere : list) { 
    // show it
    sphere.display();
  }
} 

void mousePressed() {
  // loop over all spheres 
  for (SphereClass sphere : list) {
    // select / unselect depending on mouse pos 
    sphere.selectWhenMouseOver();
  }//for
}

// ===========================================================

class SphereClass {

  PVector pos; // 3D vector
  PVector screenPos=new PVector(0, 0); // 2D vector  

  boolean selected=false; 

  // constr
  SphereClass(float x, float y, float z) {
    pos = new PVector(x, y, z); // 3D vector
  }// constr

  void display() {
    // draw sphere at pos (x, y, z) coordinate and store 2D screen pos

    pushMatrix();
    translate(pos.x, pos.y, pos.z);
    noStroke(); 
    // we choose the color depending on selected 
    if (selected)
      fill(255, 0, 0); // red 
    else
      fill(0, 0, 255); // blue 
    // draw the sphere 
    sphere(50);
    // we monitor the 2D screen pos throughout and store it
    screenPos.set(screenX(0, 0, 0), screenY(0, 0, 0));
    popMatrix();

    // show the 2D screen pos
    if (keyPressed)
      drawSignX(screenPos);
  }

  void drawSignX( PVector pos ) { 
    // Draw a "X" sign
    // 
    float sizeHalf=60; 
    float x=pos.x;
    float y=pos.y;
    float z=pos.z;  
    stroke(255);
    line(x-sizeHalf, y-sizeHalf, z, x+sizeHalf, y+sizeHalf, z); 
    line(x+sizeHalf, y-sizeHalf, z, x-sizeHalf, y+sizeHalf, z);
  }

  void selectWhenMouseOver() {
    // select / unselect 
    if (mouseOver()) 
      selected=true;
    else 
    selected=false;
  }

  boolean mouseOver() {
    return dist(mouseX, mouseY, screenPos.x, screenPos.y) < 30;
  }
  //
}//class
//
1 Like

@Chrisir

Maybe? Not as is, but I don’t see why it can’t be expanded to 3D. Except I’m not sure how to deal with camera direction, FOV and such (my 3D algebra is a bit rusty. 2D too for that matter, I haven’t coped with shear and scale in my reverse-screen X/Y functions above).

I was also wondering about that screenZ function. Maybe for a future holographic screen? (Or VR?) :slight_smile:

I was thinking maybe making an invisible 3D helper object, that sort of traces the 2D mouse somehow, could be a work-around? I’m not sure though. I haven’t really used processing with 3D (yet), except for examples now and then.

1 Like

It turns out there already is a library that does the same thing. But only for the mouse position afaik. Also it has scaling as well, but not shear transforms (different scales for X and Y didn’t work too well though). It’s called “mouse 2D transformations” and can be installed from the Contributions Manager.

Here’s my test sketch changed to use that instead (just for completeness sake).

import mouse.transformed2D.*;

// mouse clicks make a test point that follows the following matrix "level"
final int testPointAtLevel = 2; // zero-based

// To track mouse coordinates in transformed matrices
MouseTransformed mouse = new MouseTransformed(this);

// Matrix transforms push/pop levels
final int levels = 3;

// Test figure, to show matrix transforms (translations / rotations)
TestFigure testFig[] = new TestFigure[levels];


// Initial test point coordinates
float tpx = 15;
float tpy = 20;

 
void setup() {
  // test figures
  testFig[0] = new TestFigure( 300, 300,   0, 0,  50, 100, 0, 0.01 );
  testFig[1] = new TestFigure( 200,   0,   0, 0,  25,  50, 0, 0.02 );
  testFig[2] = new TestFigure(  50,  50, -10, 20, 25,  10, 0, 0.015 );
  //testFig[3] = new TestFigure(  25,  -25,  0,  0, 20,  10, 0, 0.03);

  size(600, 600);
  ellipseMode(RADIUS);
}

 
void draw() {
  background(128);
  fill(255);

  for(int i=0; i<levels; i++) {
    stroke(0);
    strokeWeight(1);
    
    // track a new matrix transform
    mouse.pushMatrix();
    
    // transform current matrix
    mouse.translate(testFig[i].x, testFig[i].y);
    mouse.rotate(testFig[i].ang);
    mouse.scale(1.1, 1.1);

    // Indicate a matrix "level" with a figure
    testFig[i].display();
    testFig[i].update();
  
    // Test point that follows a matrix
    if (testPointAtLevel == i) testPoint();
    
  }
 
  // remove old matrix transforms before new frame
  for (int i=0; i<levels; i++) {
    mouse.popMatrix();
  }
}


// Mouse click makes a point that follows current "level" of matrix transforms
void testPoint() {
    if (mousePressed) {
      tpx = mouse.mouseX();  // reverse screenX
      tpy = mouse.mouseY();  // reverse screenY
    }
    strokeWeight(3);
    stroke(#FF0000);
    point(tpx, tpy);
}



// Test figure class, to show matrix transforms and rotations
class TestFigure {
  float x, y;      // x, y center position (matrix translation)
  float xt, yt;    // x, y additional offset
  float ra, rb;    // radius A and B (of figure)
  float ang;       // angle
  float turnSpeed; 
  
  TestFigure( float ex, float ey, float ext, float eyt,
              float era, float erb, float angle, float ts) {
    x = ex;
    y = ey;
    xt = ext;
    yt = eyt;
    ra = era;
    rb = erb;
    ang = angle;
    turnSpeed = ts;
  }
  
  void display() {
    // Draw around offset from wherever origo (of matrix) is 
    ellipse(xt, yt, ra, rb);
    //rect(xt-ra, yt-rb, 2*ra, 2*rb);
  }
  
  void update() {
    ang += turnSpeed;
  }
}

1 Like