How to do mouseover after scale and translation

edited March 2014 in Questions about Code

Good day dear Processing folks!

I'm pretty new to Processing and need some assistance on a small project I'm currently working on. In this project I want to position some ellipses on the canvas. These ellipses should be zoomable via mouse wheel and it should be possible to translate the canvas coordinate system (and all ellipses with it) via mouse drag. Those things kinda work but I've got one more requirement which sadly cost me plenty of time already without working results: I need mouseovers for the ellipses which stop working correctly after scaling and translating. Take a look at the following example:


    color bg;
    color fg;
    
    boolean pressed;
    
    float pressedX;
    float pressedY;
    
    float transPressedX;
    float transPressedY;
    
    float sval;
    float radius;
    
    float transX;
    float transY;
    
    float maxZoom = 7.0;
    float minZoom = 0.1;
    
    float coords[][];
    
    void setup() {
      size(1600, 900);
      bg = color(0,0,0);
      fg = color(#55ACEE);  
      background(bg);
      
      pressed = false;
      
      transX = width/2;
      transY = height/2;
      
      sval = 1.0;
      radius = 10;
      
      coords = new float[50][2];
      
      int counter = 0;
      outerLoop:
      while (counter < coords.length) {
        float x = random(0, width);
        float y = random(0, width);
        
        for(int i = 0; i < counter; i++) {
          if(dist(coords[i][0], coords[i][1], x, y) < (radius * 5)) {
            continue outerLoop;
          }
        }
        
        coords[counter][0] = x;
        coords[counter][1] = y;
        
        counter++;
      }
      
    }
    
    void draw() {
      background(bg);
      fill(fg);
      
      translate(-transX, -transY);
      scale(sval);
      translate(transX, transY);
    
      for(int i = 0; i < coords.length; i++ ) {
        drawEllipse(coords[i][0], coords[i][1], radius);
      }
    }
    
    void drawEllipse(float x, float y, float radius) {
      if(dist(mouseX/sval, mouseY/sval, x, y) < radius) {
        ellipse(x, y, radius * 2.5, radius * 2.5);
      } else {
        ellipse(x, y, radius * 2, radius * 2);
      }
    }
    
    void mousePressed() {  
      if(!pressed) {
        pressedX = mouseX;
        pressedY = mouseY;
        transPressedX = transX;
        transPressedY = transY;
      }
      pressed = true;
    }
    
    void mouseReleased() {
      pressed = false;
    }
    
    void mouseDragged() {
      if(pressed) {
        float diffX = (max(mouseX, pressedX) - min(mouseX, pressedX));
        float diffY = (max(mouseY, pressedY) - min(mouseY, pressedY));
    
        if(sval<1) {
          diffX *= -((1/minZoom)/2);
          diffY *= -((1/minZoom)/2);
        } else {
          diffX *= ((maxZoom/sval)/2);
          diffY *= ((maxZoom/sval)/2);
        }
    
        if(mouseX < pressedX) {
          transX = (transPressedX - diffX);
        } else {
          transX = (transPressedX + diffX);
        }
    
        if(mouseY < pressedY) {
          transY = (transPressedY - diffY);
        } else {
          transY = (transPressedY + diffY);
        }
      }
    }
    
    void zoomIn(float step) {
      sval = constrain(sval + step, minZoom, maxZoom);
    }
    
    void zoomOut(float step) {
      sval = constrain(sval - step, minZoom, maxZoom);
    }
    
    void mouseWheel(MouseEvent event) {
      float e = event.getAmount();
      if(e < 0) {
        zoomOut(sval*0.075);
      }
      else if(e > 0) {
        zoomIn(sval*0.075);
      }
    }

The project is build with Processing.js 1.4.1 but I have stripped it down to the problematic chunks and put it inside Processing 2.1.1. It should run fine there and the only real difference is the handling of mouse wheel since Processing.js uses a different way to do that.

When you run the sketch you can see 25 ellipses randomly distributed on the canvas. Zooming in and out works fine but translation is weird: when scale factor is 1 it doesn't work at all and when the factor shrinks or expands the translation gets faster the farther away the factor gets from 0. That's awkward but not a deal breaker for now, but if you've got any suggestions to fix that, I'd appreciate it!

But back to the main topic: as long as scale factor is 1 everything is okay, but as soon as it shrinks/expands the mouse overs stop working. My problem here is that I don't really get how to achieve that in Processing. Is there any way to transform single points as usually possible in CG? If anyone can help me I would be very grateful!

Best regards, eldahar

Answers

  • Got an example of using mouseWheel() in this thread:

    http://forum.processing.org/two/discussion/598/mousewheel-syntax-question-solved

    Perhaps it might help while you wait for specific answers! :D

  • edited March 2014 Answer ✓

    Matrix transformation settings like translate(), scale(), rotate(), etc. are really cool!
    However, finding out an object's coordinates after they're transformed isn't elementary math for sure! X_X

    Rather than using scale(), you may try render the ellipses w/ radius * scale.
    And rather than translate(), change the ellipses' coordinates themselves!

    This way, it's much easier to match mouseX & mouseY, which aren't affected by transformations, to the ellipses' coords.! :-B

  • Thanks for the input, GoToLoop!

    Rather than using scale(), you may try render the ellipses w/ radius * scale. And rather than translate(), change the ellipses' coordinates themselves!

    Seems like a viable solution to me. Will see what I can do and post back the results.

  • Thanks for the help! It now works fine for the test sketch including the fixed awkward behaviour of translation. In case anyone wonders here's how to do it for that case (should work for other cases as well):

        color bg;
        color fg;
        
        boolean pressed;
        
        float pressedX;
        float pressedY;
        
        float transPressedX;
        float transPressedY;
        
        float sval;
        float radius;
        
        float transX;
        float transY;
        
        float maxZoom = 7.0;
        float minZoom = 0.1;
        
        float coords[][];
        
        void setup() {
          size(1600, 900);
          bg = color(0,0,0);
          fg = color(#55ACEE);  
          background(bg);
          
          pressed = false;
          
          transX = width/2;
          transY = height/2;
          
          sval = 1.0;
          radius = 10;
          
          coords = new float[25][2];
          
          int counter = 0;
          outerLoop:
          while (counter < coords.length) {
            float x = random(radius*3, width - (radius*3));
            float y = random(radius*3, height - (radius*3));
            
            for(int i = 0; i < counter; i++) {
              if(dist(coords[i][0], coords[i][1], x, y) < (radius * 5)) {
                continue outerLoop;
              }
            }
            
            coords[counter][0] = x;
            coords[counter][1] = y;
            
            counter++;
          }  
        }
        
        void draw() {
          background(bg);
          fill(fg);
          
          for(int i = 0; i < coords.length; i++ ) {
            drawEllipse(coords[i][0], coords[i][1], radius);
          }
        }
        
        void drawEllipse(float x, float y, float radius) {
          x *= sval;
          y *= sval;
          radius *= sval;
          
          x += transX;
          y +=transY; 
          
          if(dist(mouseX, mouseY, x, y) < radius) {
            ellipse(x, y, radius * 2.5, radius * 2.5);
          } else {
            ellipse(x, y, radius * 2, radius * 2);
          }
        }
        
        void mousePressed() {  
          if(!pressed) {
            pressedX = mouseX;
            pressedY = mouseY;
            transPressedX = transX;
            transPressedY = transY;
          }
          pressed = true;
        }
        
        void mouseReleased() {
          pressed = false;
        }
        
        void mouseDragged() {
          if(pressed) {
            float diffX = (max(mouseX, pressedX) - min(mouseX, pressedX));
            float diffY = (max(mouseY, pressedY) - min(mouseY, pressedY));
        
            if(sval<1) {
              diffX *= -1;
              diffY *= -1;
            } else {
              diffX *= -1;
              diffY *= -1;
            }
        
            if(mouseX < pressedX) {
              transX = (transPressedX + diffX);
            } else {
              transX = (transPressedX - diffX);
            }
        
            if(mouseY < pressedY) {
              transY = (transPressedY + diffY);
            } else {
              transY = (transPressedY - diffY);
            }
          }
        }
        
        void zoomIn(float step) {
          sval = constrain(sval + step, minZoom, maxZoom);
        }
        
        void zoomOut(float step) {
          sval = constrain(sval - step, minZoom, maxZoom);
        }
        
        void mouseWheel(MouseEvent event) {
          float e = event.getAmount();
          if(e > 0) {
            zoomOut(sval*0.075);
          }
          else if(e < 0) {
            zoomIn(sval*0.075);
          }
        }
    

    Now I'll try to bring it back into my project.

  • Answer ✓

    It is obvious that you are trying to zoom in/out about the centre of the screen and that is not working properly.

    The trick here is to move the screen origin from the top-left corner to the centre of the display before drawing the ellipses. It means that the 'world' origin (0,0) is now at the centre of the display and ellipse x/y coordinates should be calculated from this new origin.

    The trick then is to simply convert the mouse coordinates to 'world' coordinates and compare these to the ellipse coordinates.

    Anyway the code below does zooms in and out of the display centre and the ellipses enlarge when the mouse is over them. I have not touched the mousePressed/Released/Dragged methods I leave that to you but think in terms of worldX and worldY rather than mouseX and mouseY.

    color bg;
    color fg;
    
    boolean pressed;
    
    float pressedX;
    float pressedY;
    
    float transPressedX;
    float transPressedY;
    
    float sval;
    float radius;
    
    float transX;
    float transY;
    
    float maxZoom = 7.0;
    float minZoom = 0.1;
    
    float coords[][];
    
    float worldX, worldY;
    
    void setup() {
      size(1600, 900);
      bg = color(0, 0, 0);
      fg = color(#55ACEE); 
      background(bg);
    
      pressed = false;
    
      transX = width/2;
      transY = height/2;
    
      sval = 1.0;
      radius = 10;
    
      coords = new float[50][2];
    
      int counter = 0;
    outerLoop:
      while (counter < coords.length) {
        float x = random(-transX, transX);
        float y = random(-transY, transY);
    
        for (int i = 0; i < counter; i++) {
          if (dist(coords[i][0], coords[i][1], x, y) < (radius * 5)) {
            continue outerLoop;
          }
        }
        coords[counter][0] = x;
        coords[counter][1] = y;
        counter++;
      }
    }
    
    void calcMouseWorldPos() {
      worldX = (mouseX - transX) / sval;
      worldY = (mouseY - transY) / sval;
    }
    
    void draw() {
      background(bg);
      fill(fg);
    
      translate(transX, transY);
      scale(sval);
    //  translate(transX, transY);
      calcMouseWorldPos();
    
      for (int i = 0; i < coords.length; i++ ) {
        drawEllipse(coords[i][0], coords[i][1], radius);
      }
    }
    
    void drawEllipse(float x, float y, float radius) {
      if (dist(worldX, worldY, x, y) < radius) {
        ellipse(x, y, radius * 2.5, radius * 2.5);
      } 
      else {
        ellipse(x, y, radius * 2, radius * 2);
      }
    }
    
    void mousePressed() { 
      if (!pressed) {
        pressedX = mouseX;
        pressedY = mouseY;
        transPressedX = transX;
        transPressedY = transY;
      }
      pressed = true;
    }
    
    void mouseReleased() {
      pressed = false;
    }
    
    void mouseDragged() {
      if (pressed) {
        float diffX = (max(mouseX, pressedX) - min(mouseX, pressedX));
        float diffY = (max(mouseY, pressedY) - min(mouseY, pressedY));
    
        if (sval<1) {
          diffX *= -((1/minZoom)/2);
          diffY *= -((1/minZoom)/2);
        } 
        else {
          diffX *= ((maxZoom/sval)/2);
          diffY *= ((maxZoom/sval)/2);
        }
    
        if (mouseX < pressedX) {
          transX = (transPressedX - diffX);
        } 
        else {
          transX = (transPressedX + diffX);
        }
    
        if (mouseY < pressedY) {
          transY = (transPressedY - diffY);
        } 
        else {
          transY = (transPressedY + diffY);
        }
      }
    }
    
    void zoomIn(float step) {
      sval = constrain(sval + step, minZoom, maxZoom);
    }
    
    void zoomOut(float step) {
      sval = constrain(sval - step, minZoom, maxZoom);
    }
    
    void mouseWheel(MouseEvent event) {
      float e = event.getAmount();
      if (e < 0) {
        zoomOut(sval*0.075);
      }
      else if (e > 0) {
        zoomIn(sval*0.075);
      }
    }
    
  • Thanks, quark!

    That was exactly the answer I was looking for!

Sign In or Register to comment.