Search code examples
processing

How can add interaction and animation to shapes drawn in Processing?


I'm trying to code a canvas full of shapes(houses) and animate them in processing.

Here's an example of shape:

void house(int x, int y) { 
  pushMatrix(); 
  translate(x, y); 
  fill(0, 200, 0); 
  triangle(15, 0, 0, 15, 30, 15); 
  rect(0, 15, 30, 30); 
  rect(12, 30, 10, 15); 
  popMatrix();
}

By animation I mean moving them in random directions.

I would also like to add basic interaction: when hovering over a house it's colour would change.

At the moment I've managed to render a canvas full of houses:

void setup() { 
  size(500, 500); 
  background(#74F5E9); 
  for (int i = 30; i < 500; i = i + 100) { 
    for (int j = 30; j < 500; j = j + 100) { 
      house(i, j);
    }
  }
} 
void house(int x, int y) { 
  pushMatrix(); 
  translate(x, y); 
  fill(0, 200, 0); 
  triangle(15, 0, 0, 15, 30, 15); 
  rect(0, 15, 30, 30); 
  rect(12, 30, 10, 15); 
  popMatrix();
}

Solution

  • Without seeing source code: your attempted sketch it's very hard to tell.

    They can be animated in many ways and it's unclear what you mean. For example, is that the position/rotation/scale of each square, is it the corners/vertices of each square, both ?

    You might have a clear idea in your mind, but the current form of the question is ambiguous. We also don't know you're comfort level with various notions such as classes/objects/PVector/PShape/etc. If you were to 'story board' this animation what would it look like ? Breaking the problem down and explaining it in a way that anyone can understand might actually help you figure out a solution on your own as well.

    Processing has plenty of examples. Here are a few I find relevant based on what my understanding is of your problem.

    You can have a look at the Objects and Create Shapes examples:

    1. File > Examples > Basics > Objects > Objects: Demonstrates grouping drawing/animation (easing, damping). You can tweak this example draw a single square and once you're happy with the look/motion you can animate multiple using an array or ArrayList
    2. File > Examples > Topics > Create Shapes > PolygonPShapeOOP3: Great example using PShape to animate objects.
    3. File > Examples > Topics > Create Shapes > WigglePShape: This example demonstrates how to access and modify the vertices of a PShape

    For reference I'm simply copy/pasting the examples mentioned above here as well:

    Objects

    /**
     * Objects
     * by hbarragan. 
     * 
     * Move the cursor across the image to change the speed and positions
     * of the geometry. The class MRect defines a group of lines.
     */
    
    MRect r1, r2, r3, r4;
     
    void setup()
    {
      size(640, 360);
      fill(255, 204);
      noStroke();
      r1 = new MRect(1, 134.0, 0.532, 0.1*height, 10.0, 60.0);
      r2 = new MRect(2, 44.0, 0.166, 0.3*height, 5.0, 50.0);
      r3 = new MRect(2, 58.0, 0.332, 0.4*height, 10.0, 35.0);
      r4 = new MRect(1, 120.0, 0.0498, 0.9*height, 15.0, 60.0);
    }
     
    void draw()
    {
      background(0);
      
      r1.display();
      r2.display();
      r3.display();
      r4.display();
     
      r1.move(mouseX-(width/2), mouseY+(height*0.1), 30);
      r2.move((mouseX+(width*0.05))%width, mouseY+(height*0.025), 20);
      r3.move(mouseX/4, mouseY-(height*0.025), 40);
      r4.move(mouseX-(width/2), (height-mouseY), 50);
    }
     
    class MRect 
    {
      int w; // single bar width
      float xpos; // rect xposition
      float h; // rect height
      float ypos ; // rect yposition
      float d; // single bar distance
      float t; // number of bars
     
      MRect(int iw, float ixp, float ih, float iyp, float id, float it) {
        w = iw;
        xpos = ixp;
        h = ih;
        ypos = iyp;
        d = id;
        t = it;
      }
     
      void move (float posX, float posY, float damping) {
        float dif = ypos - posY;
        if (abs(dif) > 1) {
          ypos -= dif/damping;
        }
        dif = xpos - posX;
        if (abs(dif) > 1) {
          xpos -= dif/damping;
        }
      }
     
      void display() {
        for (int i=0; i<t; i++) {
          rect(xpos+(i*(d+w)), ypos, w, height*h);
        }
      }
    }
    

    PolygonPShapeOOP3:

    /**
     * PolygonPShapeOOP. 
     * 
     * Wrapping a PShape inside a custom class 
     * and demonstrating how we can have a multiple objects each
     * using the same PShape.
     */
    
    
    // A list of objects
    ArrayList<Polygon> polygons;
    
    // Three possible shapes
    PShape[] shapes = new PShape[3];
    
    void setup() {
      size(640, 360, P2D);
      
      shapes[0] = createShape(ELLIPSE,0,0,100,100);
      shapes[0].setFill(color(255, 127));
      shapes[0].setStroke(false);
      shapes[1] = createShape(RECT,0,0,100,100);
      shapes[1].setFill(color(255, 127));
      shapes[1].setStroke(false);
      shapes[2] = createShape();  
      shapes[2].beginShape();
      shapes[2].fill(0, 127);
      shapes[2].noStroke();
      shapes[2].vertex(0, -50);
      shapes[2].vertex(14, -20);
      shapes[2].vertex(47, -15);
      shapes[2].vertex(23, 7);
      shapes[2].vertex(29, 40);
      shapes[2].vertex(0, 25);
      shapes[2].vertex(-29, 40);
      shapes[2].vertex(-23, 7);
      shapes[2].vertex(-47, -15);
      shapes[2].vertex(-14, -20);
      shapes[2].endShape(CLOSE);
    
      // Make an ArrayList
      polygons = new ArrayList<Polygon>();
      
      for (int i = 0; i < 25; i++) {
        int selection = int(random(shapes.length));        // Pick a random index
        Polygon p = new Polygon(shapes[selection]);        // Use corresponding PShape to create Polygon
        polygons.add(p);
      }
    }
    
    void draw() {
      background(102);
    
      // Display and move them all
      for (Polygon poly : polygons) {
        poly.display();
        poly.move();
      }
    }
    
    // A class to describe a Polygon (with a PShape)
    
    class Polygon {
      // The PShape object
      PShape s;
      // The location where we will draw the shape
      float x, y;
      // Variable for simple motion
      float speed;
    
      Polygon(PShape s_) {
        x = random(width);
        y = random(-500, -100); 
        s = s_;
        speed = random(2, 6);
      }
      
      // Simple motion
      void move() {
        y+=speed;
        if (y > height+100) {
          y = -100;
        }
      }
      
      // Draw the object
      void display() {
        pushMatrix();
        translate(x, y);
        shape(s);
        popMatrix();
      }
    }
    

    WigglePShape:

    /**
     * WigglePShape. 
     * 
     * How to move the individual vertices of a PShape
     */
    
    
    // A "Wiggler" object
    Wiggler w;
    
    void setup() {
      size(640, 360, P2D);
      w = new Wiggler();
    }
    
    void draw() {
      background(255);
      w.display();
      w.wiggle();
    }
    
    // An object that wraps the PShape
    
    class Wiggler {
      
      // The PShape to be "wiggled"
      PShape s;
      // Its location
      float x, y;
      
      // For 2D Perlin noise
      float yoff = 0;
      
      // We are using an ArrayList to keep a duplicate copy
      // of vertices original locations.
      ArrayList<PVector> original;
    
      Wiggler() {
        x = width/2;
        y = height/2; 
    
        // The "original" locations of the vertices make up a circle
        original = new ArrayList<PVector>();
        for (float a = 0; a < radians(370); a += 0.2) {
          PVector v = PVector.fromAngle(a);
          v.mult(100);
          original.add(new PVector());
          original.add(v);
        }
        
        // Now make the PShape with those vertices
        s = createShape();
        s.beginShape(TRIANGLE_STRIP);
        s.fill(80, 139, 255);
        s.noStroke();
        for (PVector v : original) {
          s.vertex(v.x, v.y);
        }
        s.endShape(CLOSE);
      }
    
      void wiggle() {
        float xoff = 0;
        // Apply an offset to each vertex
        for (int i = 1; i < s.getVertexCount(); i++) {
          // Calculate a new vertex location based on noise around "original" location
          PVector pos = original.get(i);
          float a = TWO_PI*noise(xoff,yoff);
          PVector r = PVector.fromAngle(a);
          r.mult(4);
          r.add(pos);
          // Set the location of each vertex to the new one
          s.setVertex(i, r.x, r.y);
          // increment perlin noise x value
          xoff+= 0.5;
        }
        // Increment perlin noise y value
        yoff += 0.02;
      }
    
      void display() {
        pushMatrix();
        translate(x, y);
        shape(s);
        popMatrix();
      }
    }
    

    Update

    Based on your comments here's an version of your sketch modified so the color of the hovered house changes:

    // store house bounding box dimensions for mouse hover check
    int houseWidth  = 30;
    // 30 px rect height + 15 px triangle height
    int houseHeight = 45;
    
    void setup() { 
      size(500, 500); 
    }
    
    void draw(){
      background(#74F5E9); 
      for (int i = 30; i < 500; i = i + 100) { 
        for (int j = 30; j < 500; j = j + 100) {
          // check if the cursor is (roughly) over a house
          // and render with a different color
          if(overHouse(i, j)){
            house(i, j, color(0, 0, 200));
          }else{
            house(i, j, color(0, 200, 0));
          }
        }
      }
    }
    
    void house(int x, int y, color fillColor) { 
      pushMatrix(); 
      translate(x, y); 
      fill(fillColor); 
      triangle(15, 0, 0, 15, 30, 15); 
      rect(0, 15, 30, 30); 
      rect(12, 30, 10, 15); 
      popMatrix();
    }
    
    // from Processing RollOver example
    // https://processing.org/examples/rollover.html
    boolean overRect(int x, int y, int width, int height) {
      if (mouseX >= x && mouseX <= x+width && 
          mouseY >= y && mouseY <= y+height) {
        return true;
      } else {
        return false;
      }
    }
    
    // check if the mouse is within the bounding box of a house
    boolean overHouse(int x, int y){
      // offset half the house width since the pivot is at the tip of the house 
      // the horizontal center
      return overRect(x - (houseWidth / 2), y, houseWidth, houseHeight);
    }
    

    The code is commented, but here are the main takeaways:

    • the house() function has been changed so you can specify a color
    • the overRect() function has been copied from the Rollover example
    • the overHouse() function uses overRect(), but adds a horizontal offset to take into account the house is drawn from the middle top point (the house tip is the shape's pivot point)

    Regarding animation, Processing has tons of examples:

    Processing examples: Motion / Simulate / Vectors screenshot of the bottom of the page which includes thumbnails

    Let's start take sine motion as an example. The sin() function takes an angle (in radians by default) and returns a value between -1.0 and 1.0

    Since you're already calculating positions for each house within a 2D grid, you can offset each position using sin() to animate it. The nice thing about it is cyclical: no matter what angle you provide you always get values between -1.0 and 1.0. This would save you the trouble of needing to store the current x, y positions of each house in arrays so you can increment them in a different directions.

    Here's a modified version of the above sketch that uses sin() to animate:

    // store house bounding box dimensions for mouse hover check
    int houseWidth  = 30;
    // 30 px rect height + 15 px triangle height
    int houseHeight = 45;
    
    void setup() { 
      size(500, 500); 
    }
    
    void draw(){
      background(#74F5E9); 
      for (int i = 30; i < 500; i = i + 100) { 
        for (int j = 30; j < 500; j = j + 100) {
          
          // how fast should each module move around a circle (angle increment)
          // try changing i with j, adding i + j or trying other mathematical expressions
          // also try changing 0.05 to other values
          float phase = (i + frameCount) * 0.05;
          // try changing amplitude to other values
          float amplitude = 30.0;
          // map the sin() result from it's range to a pixel range (-30px to 30px for example)
          float xOffset = map(sin(phase), -1.0, 1.0, -amplitude, amplitude);
          // offset each original grid horizontal position (i) by the mapped sin() result
          float x = i + xOffset;
          // check if the cursor is (roughly) over a house
          // and render with a different color
          if(overHouse(i, j)){
            house(x, j, color(0, 0, 200));
          }else{
            house(x, j, color(0, 200, 0));
          }
        }
      }
    }
    
    void house(float x, float y, color fillColor) { 
      pushMatrix(); 
      translate(x, y); 
      fill(fillColor); 
      triangle(15, 0, 0, 15, 30, 15); 
      rect(0, 15, 30, 30); 
      rect(12, 30, 10, 15); 
      popMatrix();
    }
    
    // from Processing RollOver example
    // https://processing.org/examples/rollover.html
    boolean overRect(int x, int y, int width, int height) {
      if (mouseX >= x && mouseX <= x+width && 
          mouseY >= y && mouseY <= y+height) {
        return true;
      } else {
        return false;
      }
    }
    
    // check if the mouse is within the bounding box of a house
    boolean overHouse(int x, int y){
      // offset half the house width since the pivot is at the tip of the house 
      // the horizontal center
      return overRect(x - (houseWidth / 2), y, houseWidth, houseHeight);
    }
    

    Read through the comments and try to tweak the code to get a better understanding of how it works and have fun coming up with different animations.

    The main changes are:

    • modifying the house() function to use float x,y positions (instead of int): this is to avoid converting float to int when using sin(), map() and get smoother motions (instead of motion that "snaps" to whole pixels)
    • Mapped sine to positions which can be used to animate

    Wrapping the 3 instructions that calculate the x offset into a reusable function would allow you do further experiment. What if you used a similar technique the y position of each house ? What about both x and y ?

    Go through the code step by step. Try to understand it, change it, break it, fix it and make new sketches reusing code.