processingprocessing-ide

Processing - Multiple Image-Buttons That will Shape a Circle Together


I have several buttons that I created like this:

In the setup()

PImage[] imgs1 = {loadImage("AREA1_1.png"),loadImage("AREA1_2.png"),loadImage("AREA1_3.png")};
cp5.addButton("AREA_1")  // The button
.setValue(128)
.setPosition(-60,7)     // x and y relative to the group
.setImages(imgs1)
.updateSize()
.moveTo(AreaRingGroup);   // add it to the group 
; 

After draw()

void AREA_1(){
  println("AREA_1");
  if (port != null){ 
      port.write("a\n");
  }

Now, I have to add about 24 small buttons from images that are going to be seen as 1 circle. But instead of adding them using cp5.addButton like above, I thought I should create a class.

I want to extend the existing CP5 button class because I want to be able to reach the cp5 parameters; like using .setImages() for 3 mouseClick conditions in the button images.

My problem is, I couldn't figure out

Here is my silly attempt:

class myButton extends Button
 {
   myButton(ControlP5 cp5, String theName)
   {
     super(cp5, cp5.getTab("default"), theName, 0,0,0, autoWidth, autoHeight);
   }
   
  PVector pos = new PVector (0,0);
  PVector center = new PVector(500,500);
  PImage[] img = new PImage[3];
  String lable;
  int sizeX = 10, sizeY=10;
  color imgColor;
  Boolean pressed = false;
  Boolean clicked = false;
  
  //Constructor
  myButton(float a, String theName)
  {
    
    //Angle between the center I determined and the position of the 
    button. 
    a = degrees (PVector.angleBetween(center, pos));

    //Every button will have 3 images for the 3 mouseClick conditions.
    for (int i = 0; i < 2; ++i)
        (img[i] = loadImage(lable + i + ".png")).resize(sizeX, sizeY);

  }
}

Solution

  • Laancelot's idea isn't bad, however, due to controlP5's OOP architecture it might not be possible, or at least trivial, to extend the controlP5's Button class to achieve your goal from a controlP5 sketch.

    I'm making the assumption this is connected to other LED ring related questions you posted around this time (such as this one).

    It might be wrong (and I'd be interested in seeing a solution), but without forking/modifying the controlP5 library itself, the inheritance chain gets a bit tricky.

    Ideally you'd just need to extend Button and be done with it, however the method used to tell states (if a button over/pressed/etc.) is the Controller superclass's inside() method. To work with your image you'd need override this method to and swap the logic so it takes into account the alpha transparency of your png button skin (e.g. if the pixel under the cursor is opaque then it's inside otherwise it's not)

    Here's a rough illustration you were constrained to using a Processing sketch:

    public abstract class CustomController< T > extends Controller< T> {
      
      public CustomController( ControlP5 theControlP5 , String theName ) {
        super( theControlP5 , theControlP5.getDefaultTab( ) , theName , 0 , 0 , autoWidth , autoHeight );
        theControlP5.register( theControlP5.papplet , theName , this );
      }
      
      protected CustomController( final ControlP5 theControlP5 , final ControllerGroup< ? > theParent , final String theName , final float theX , final float theY , final int theWidth , final int theHeight ) {
        super(theControlP5, theParent, theName, theX, theY, theWidth, theHeight);
      }
      
      boolean inside( ) {
        /* constrain the bounds of the controller to the dimensions of the cp5 area, required since PGraphics as render
         * area has been introduced. */
        //float x0 = PApplet.max( 0 , x( position ) + x( _myParent.getAbsolutePosition( ) ) );
        //float x1 = PApplet.min( cp5.pgw , x( position ) + x( _myParent.getAbsolutePosition( ) ) + getWidth( ) );
        //float y0 = PApplet.max( 0 , y( position ) + y( _myParent.getAbsolutePosition( ) ) );
        //float y1 = PApplet.min( cp5.pgh , y( position ) + y( _myParent.getAbsolutePosition( ) ) + getHeight( ) );
        //return ( _myControlWindow.mouseX > x0 && _myControlWindow.mouseX < x1 && _myControlWindow.mouseY > y0 && _myControlWindow.mouseY < y1 );
        // FIXME: add alpha pixel logic here
        return true;
      }
      
    }
    
    public class CustomButton<Button> extends CustomController< Button > {
      
      protected CustomButton( ControlP5 theControlP5 , ControllerGroup< ? > theParent , String theName , float theDefaultValue , int theX , int theY , int theWidth , int theHeight ) {
        super( theControlP5 , theParent , theName , theX , theY , theWidth , theHeight );
        _myValue = theDefaultValue;
        _myCaptionLabel.align( CENTER , CENTER );
      }
     
      // isn't this fun ?
      //public CustomButton setImage( PImage theImage ) {
      //  return super.setImage( theImage , DEFAULT );
      //}
      
    }
    

    I would suggest a simpler workaround: simply add smaller buttons in a ring layout (using polar to cartesian coordinate conversion):

    import controlP5.*;
    
    ControlP5 cp5;
    
    void setup(){
      size(600, 600);
      background(0);
      
      cp5 = new ControlP5(this);
      
      int numLEDs = 24;
      float radius = 240;
      for(int i = 0; i < numLEDs; i++){
        float angle = (TWO_PI / numLEDs * i) - HALF_PI;
        cp5.addButton(nf(i, 2))
           .setPosition(width * 0.5 + cos(angle) * radius, height * 0.5 + sin(angle) * radius)
           .setSize(30, 30);
      } 
    }
    
    void draw(){}
    

    enter image description here

    Sure, not as pretty, but much much simpler. Hopefully rendering the ring image drawn behind as a background and changing the button colour might be enough to get the point across.

    It might be simpler to skip ControlP5 altogether and make your own button class for such a custom behaviour.

    Let's say you want to have a square button in a ring layout that also rotates along the ring. This rotation will make checking the bounds tricker, but not impossible. You could hold track of the button's 2D transformation matrix using PMatrix2D) to render the button, but use the inverse of this transformation matrix to convert between Processing's global coordinate system to the button's local coordinate system. This would make checking if the button hovered trivial again (as the mouse converted to local coordinates would be within the button's bounding box). Here's an example:

    TButton btn;
    
    void setup(){
      size(600, 600);
      btn = new TButton(0, 300, 300, 90, 90, radians(45));
    }
    void draw(){
      background(0);
      btn.update(mouseX, mouseY, mousePressed);
      btn.draw();
    }
    class TButton{
      
      PMatrix2D transform = new PMatrix2D();
      PMatrix2D inverseTransform;
      
      int index;
      int x, y, w, h;
      float angle;
      
      boolean isOver;
      boolean isPressed;
      
      PVector cursorGlobal = new PVector();
      PVector cursorLocal = new PVector();
      
      color fillOver = color(192);
      color fillOut  = color(255);
      color fillOn   = color(127);
      
      TButton(int index, int x, int y, int w, int h, float angle){
        this.index = index;
        this.x     = x;
        this.y     = y;
        this.w     = w;
        this.h     = h;
        this.angle = angle;
        
        transform.translate(x, y);
        transform.rotate(angle);
        inverseTransform = transform.get();
        inverseTransform.invert();
        
      }
      
      
      
      void update(int mx, int my, boolean mPressed){
        cursorGlobal.set(mx, my);
        inverseTransform.mult(cursorGlobal, cursorLocal);
        isOver = ((cursorLocal.x >= 0 && cursorLocal.x <= w) &&
                  (cursorLocal.y >= 0 && cursorLocal.y <= h));
        isPressed = mPressed && isOver;
      }
      
      void draw(){
        pushMatrix();
        applyMatrix(transform);
        pushStyle();
        if(isOver) fill(fillOver);
        if(isPressed) fill(fillOn);
        rect(0, 0, w, h);
        popStyle();
        popMatrix();
      }
      
      
    }
    

    If you can draw one button, you can draw many buttons, in a ring layout:

    int numLEDs = 24;
    TButton[] ring = new TButton[numLEDs];
    
    TButton selectedLED;
    
    void setup(){
      size(600, 600);
      float radius = 240;
      float ledSize= 30;
      float offsetX = width * 0.5;
      float offsetY = height * 0.5;
      for(int i = 0 ; i < numLEDs; i++){
        float angle = (TWO_PI / numLEDs * i) - HALF_PI;
        float x = offsetX + (cos(angle) * radius);
        float y = offsetY + (sin(angle) * radius);
        ring[i] = new TButton(i, x, y, ledSize, ledSize, angle);
      }
      println("click to select");
      println("click and drag to change colour");
    }
    
    void draw(){
      background(0);
      for(int i = 0 ; i < numLEDs; i++){
        ring[i].update(mouseX, mouseY);
        ring[i].draw();
      }
    }
    
    void onButtonClicked(TButton button){
      selectedLED = button;
      println("selected", selectedLED); 
    }
    
    void mouseReleased(){
      for(int i = 0 ; i < numLEDs; i++){
        ring[i].mouseReleased();
      }
    }
    
    void mouseDragged(){
      if(selectedLED != null){
        float r = map(mouseX, 0, width, 0, 255);
        float g = map(mouseY, 0, height, 0, 255);
        float b = 255 - r;
        selectedLED.fillColor = color(r, g, b);
      }
    }
    
    class TButton{
      
      PMatrix2D transform = new PMatrix2D();
      PMatrix2D inverseTransform;
      
      int index;
      float x, y, w, h;
      float angle;
      
      boolean isOver;
      
      PVector cursorGlobal = new PVector();
      PVector cursorLocal = new PVector();
      
      color fillColor = color(255);
      
      TButton(int index, float x, float y, float w, float h, float angle){
        this.index = index;
        this.x     = x;
        this.y     = y;
        this.w     = w;
        this.h     = h;
        this.angle = angle;
        
        transform.translate(x, y);
        transform.rotate(angle);
        inverseTransform = transform.get();
        inverseTransform.invert();
        
      }
      
      
      
      void update(int mx, int my){
        cursorGlobal.set(mx, my);
        inverseTransform.mult(cursorGlobal, cursorLocal);
        isOver = ((cursorLocal.x >= 0 && cursorLocal.x <= w) &&
                  (cursorLocal.y >= 0 && cursorLocal.y <= h));
      }
      
      void mouseReleased(){
        if(isOver) onButtonClicked(this);
      }
      
      void draw(){
        pushMatrix();
        applyMatrix(transform);
        pushStyle();
        if(isOver) {
          fill(red(fillColor) * .5, green(fillColor) * .5, blue(fillColor) * .5);
        }else{
          fill(fillColor);
        }
        rect(0, 0, w, h);
        popStyle();
        popMatrix();
      }
      
      String toString(){
        return "[TButton index=" + index + "]";
      }
      
    }
    

    This isn't bad, but it easily gets complicated, right ? What is you apply a global transformation in draw(): that may need to be handled by the button ? What if the button is nested in a bunch of pushMatrix()/popMatrix() calls when .draw() is called ? etc...

    Could this be simpler ? Sure, the LEDs appear as rotated squares on a ring layout, however the important part that lights up is circular in shape. A rotated circle looks the same regardless of the angle. Checking if the mouse is over is trivial, as you can see in the Processing RollOver example : simply check if the distance between a circle's position and the cursor is smaller than the circle's radius.

    Here's an example using circular buttons (removing the need for handling coordinate space transformations):

    int numLEDs = 24;
    CButton[] ring = new CButton[numLEDs];
    
    CButton selectedLED;
    
    void setup(){
      size(600, 600);
      float radius = 240;
      float ledSize= 30;
      float offsetX = width * 0.5;
      float offsetY = height * 0.5;
      for(int i = 0 ; i < numLEDs; i++){
        float angle = (TWO_PI / numLEDs * i) - HALF_PI;
        float x = offsetX + (cos(angle) * radius);
        float y = offsetY + (sin(angle) * radius);
        ring[i] = new CButton(i, x, y, ledSize);
      }
      println("click to select");
      println("click and drag to change colour");
    }
    
    void draw(){
      background(0);
      for(int i = 0 ; i < numLEDs; i++){
        ring[i].update(mouseX, mouseY);
        ring[i].draw();
      }
    }
    
    void onButtonClicked(CButton button){
      selectedLED = button;
      println("selected", selectedLED); 
    }
    
    void mouseReleased(){
      for(int i = 0 ; i < numLEDs; i++){
        ring[i].mouseReleased();
      }
    }
    
    void mouseDragged(){
      if(selectedLED != null){
        float r = map(mouseX, 0, width, 0, 255);
        float g = map(mouseY, 0, height, 0, 255);
        float b = 255 - r;
        selectedLED.fillColor = color(r, g, b);
      }
    }
    class CButton{
      
      int index;
      float x, y, radius, diameter;
      
      boolean isOver;
      
      color fillColor = color(255);
      
      CButton(int index, float x, float y, float radius){
        this.index = index;
        this.x     = x;
        this.y     = y;
        this.radius= radius;
        
        diameter = radius * 2;
      }
      
      void update(int mx, int my){
        isOver = dist(x, y, mx, my) < radius;
      }
      
      void mouseReleased(){
        if(isOver) onButtonClicked(this);
      }
      
      void draw(){
        pushMatrix();
        pushStyle();
        if(isOver) {
          fill(red(fillColor) * .5, green(fillColor) * .5, blue(fillColor) * .5);
        }else{
          fill(fillColor);
        }
        ellipse(x, y, diameter, diameter);
        popStyle();
        popMatrix();
      }
      
      String toString(){
        return "[CButton index=" + index + "]";
      }
      
    }
    

    enter image description here

    Using these circular buttons on top of your NeoPixel ring image and perhaps with a bit of glow might look pretty good and simpler to achieve in a Processing sketch than taking the ControlP5 route. Don't get me wrong, it's a great library, but for custom behaviours it sometimes makes more sense to use your own custom code.

    One other aspect to think is the overall interaction. If this interface would be used to set the colors of an LED ring once, then it might ok, although it would take numLEDs * 3 interactions to set all the LEDs with a custom colour. If this was for a custom frame by frame LED animation tool then the interaction would get exhausting after a few frames.