How to Draw the Path of an Object AND Make a Ball Bounce Left/Right

I am working on a project and need to do 2 things.

I have a number of objects that can be dropped from the user via mouse click and the movements of the ball with gravity will create a sort of "art" I have used the gravity mechanism from this tutorial on Processing: https://processing.org/examples/arraylistclass.html

I currently have a ball object that drops from where the mouse is clicked to the ground, bouncing several times with gravity like in real life before disappearing after a while.

The 2things I need to do are:

1) create a colored line/curve that fixed to the center of the ball and basically marks the path of the ball as it bounces up/down/left/right. this line should stay permanently on the screen while the ball disappears

2) have the ball bounce left or right. currently the ball only bounces up and down. I want the ball, after it first hits the ground to continue bouncing up and down while also moving left or right before coming to a stop

Help is appreciated.

Code below:

`ArrayList<Shape> shapes;

void setup() {
  size(2000, 1000);
  noStroke();
  shapes = new ArrayList<Shape>();
  background(255);
}

void draw() {
  //For objects to disappear after some time
  //background(255);

  //For a "stream" of shapes
  //if (mousePressed) {
  //  shapes.add(new Ball(mouseX, mouseY, random(5, 40)));
  //}

  for (int i = shapes.size()-1; i >= 0; i--) { 
    Shape ball = shapes.get(i);
    ball.move();
    ball.display();
    if (ball.finished()) {
      shapes.remove(i);
    }
  }
}

//For single shapes

void mousePressed() {
  shapes.add(new Ball(mouseX, mouseY, random(5, 40)));
}

//----------------------------------------------------------------
//Shape Superclass

class Shape {
  float xPos;
  float yPos;
  float speed;
  float gravity;
  float size;
  float life;

  Shape(float xPos, float yPos, float size) {
    this.xPos = xPos;
    this.yPos = yPos;
    this.size = size;
    speed = 0;
    gravity = 0.6;
    life = 255;
  }

  void move() {
    speed = speed + gravity;
    yPos = yPos + speed;
    if (yPos > height) {
      // Dampening
      speed = speed * -0.8;
      yPos = height;
    }
  }

  boolean finished() {
    life--;
    if (life < 0) {
      return true;
    } else {
      return false;
    }
  }

  void display() {
    fill(255);
  }
}

//----------------------------------------------------------------
//Ball Class

class Ball extends Shape {

  Ball(float xPos, float yPos, float size) {
    super(xPos, yPos, size);
  }

  void display() {
    fill(random(0, 255));
    ellipse(xPos, yPos, size, size);
  }
}

//----------------------------------------------------------------
// Rectangle Class

class Rectangle extends Shape {

  Rectangle(float xPos, float yPos, float size) {
    super(xPos, yPos, size);
  }

  void display() {
    fill(random(0, 255));
    rect(xPos, yPos, random(size-20, size+20), random(size-10, size+10));
  }
}

//----------------------------------------------------------------
// Square Class

class Square extends Shape {

  Square(float xPos, float yPos, float size) {
    super(xPos, yPos, size);
  }

  void display() {
    fill(random(0, 255));
    rect(xPos, yPos, size, size);
  }
}

//----------------------------------------------------------------
// Letters Class

class Letters extends Shape {
  PFont f;
  float angle;
  char letter;

  Letters(float xPos, float yPos, float size) {
    super(xPos, yPos, size);
    f = createFont("Arial",size+30,true);
    letter = (char) int(random(65, 90));

  }

  void display() {
    fill(random(0, 255));
    textFont(f);
    //translate(xPos,xPos);
    //rotate(angle); 
    textAlign(CENTER);
    text(letter, xPos , yPos);
    //angle += 0.05;  
  }
}`
Tagged:

Answers

  • Edit post, highlight code, press ctrl-o

  • Ok, my code is formatted. Sorry it is my first post. Can someone help?

  • You could either use an ArrayList of fixed points to trace the ball's movement, or use PGraphics (https://processing.org/tutorials/rendering/) . Also, you could have the shapes randomly choose a direction when they hit the ground.

  • edited September 2016

    Re: "2) have the ball bounce left or right."

    The simplest way is to extend the .move() behavior of your Shape class. If I understand right, you want it to fall straight, but to drift left or right at random on each bounce -- like a rubber high-bounce ball.

    Add the three lines marked * into your class Shape class:

    class Shape {
      float xPos, yPos;
      float speed, gravity, size, life;
      float drift; //// *
    
      Shape(float xPos, float yPos, float size) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.size = size;
        speed = 0;
        gravity = 0.6;
        life = 255;
        drift = 0; //// *
      }
    
      void move() {
        speed = speed + gravity;
        yPos = yPos + speed;
        xPos = xPos + drift;
        if (yPos > height) {
          // Dampening
          speed = speed * -0.8;
          yPos = height;
          drift = random(-3,3); ////*
        }
      }
    

    Now, as @BleuBox suggested, each time a shape strike the ground (your if statement), it will be assigned a new drift in a random left/right direction.

    bounce-with-drift

  • edited September 2016 Answer ✓

    One simple way to add path-drawing to your Shapes is:

    1. create a list of points inside each shape
    2. when move is called, write down each new position in the list of points
    3. create a function called "path" that draws a line through all the points.

    Now when you call shape.path() from draw it renders a curved bouncing line.

    Here is the new Shape class, with path related changes marked //// + :

    class Shape {
      float xPos, yPos;
      float speed, gravity, size, life;
      float drift; //// *
      ArrayList<PVector> path; //// +
    
      Shape(float xPos, float yPos, float size) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.size = size;
        speed = 0;
        gravity = 0.6;
        life = 255;
        drift = 0; //// *
        path = new ArrayList<PVector>(); //// +
      }
    
      void move() {
        speed = speed + gravity;
        yPos = yPos + speed;
        xPos = xPos + drift;
        path.add(new PVector(xPos, yPos)); //// +
        if (yPos > height) {
          // Dampening
          speed = speed * -0.8;
          yPos = height;
          drift = random(-3,3); ////*
        }
      }
    
      boolean finished() {
        life--;
        if (life < 0) {
          return true;
        } else {
          return false;
        }
      }
    
      void display() {
        fill(255);
      }
    
      void path() { //// +
        pushStyle();
          stroke(0);
          strokeWeight(1);
          for(int i = 1; i<path.size(); i++) {
            line(path.get(i-1).x, path.get(i-1).y, path.get(i).x, path.get(i).y);
          }
        popStyle();
      }
    }
    

    Then call it from draw:

    ball.display();
    ball.path();
    

    bounce-with-drift-and-path

  • Thanks for all your help! It works great! But is there a way to make the path stay even after the shape disappears?

    I guess you could make Path a separate class object and pass in the ball's X and Y to it.

  • edited September 2016 Answer ✓

    @Archlefirth -- you are correct -- you can make the Path a separate class object.

    Another easy approach is to just leave the path in each ball, but never remove balls, just change what you draw.

    Only .move and .display balls that aren't .finished. Show the .path for all balls, even ones that you aren't displaying because they are finished.

    Below I reorganized your draw loop, putting move and display inside the conditional and dropping the remove.

      for (int i = shapes.size()-1; i >= 0; i--) { 
        Shape ball = shapes.get(i);
        ball.path();
        if (!ball.finished()) {
          ball.move();
          ball.display();
        }
      }
    

    This is easier to see working if you uncomment background(255);

  • That makes more sense than a separate class.

    However, not removing might cause the program to lag since every ball created still exists.

    I'll try implementing a seperate class and comparing that to the modified loop

    Thanks for your help!

  • edited September 2016

    Yes, you should definitely coordinate a separate Path class if you plan to do many different things with those paths. Try it out. Just note that, as things stand, separate paths won't reduce your number of objects.

    • 100 ball paths in 20 active balls / 80 inactive balls = 100 objects
    • 100 separate paths + 20 active balls = 120 objects (more, not less)

    If you want thousands or millions of paths, then you might want to draw each path then onto a PImage or PGraphics and render it with image(). Then you only have one object (the image layer you are drawing the paths on) and you can remove balls whenever you want without needing the path data for rendering.

  • How would you add everything to a PGraphic while removing all the balls and adding all the paths?

  • Think of a PGraphics object as like a separate transparent sheet that you draw on.

    https://processing.org/reference/PGraphics.html

    See the example. So you create an PGraphics pgPaths object for drawing your paths, and instead of drawing your paths onto the main sketch canvas with line(...) you instead draw your paths onto that object with pgPaths.line(...). In your draw loop, display your pgPaths as an image with image(pgPaths,0,0);

    Depending on whether your paths image is drawn before or after your balls (behind or in front of) you may need to make sure that it is a transparent layer so it doesn't hide the balls completely.

  • Putting image inside the loop caused the program to become even more slow, I think for the same principle you mentioned earlier about adding more objects.

    So I put the image as well as the begin/endDraw() outside the loop and that solved the lag issue.

    However, the path color does not match the color of the shape that draws it. I have a color slider (worked perfectly before pgGraphics) that the user can slide over. The color of the path is always the initial value of the slider.

    I used pg.push/pop Style in the path drawing method but to no avail

  • /**  OBJECT LIST
     *    - Balls
     *    - Capital Letters
     *    - Squares
     *    - Chairs?
     */
    
    ArrayList<Shape> shapes;
    
    //GUI vars
    float rectX, rectY;      // Position of square button
    float circleX, circleY;  // Position of circle button
    float letterX, letterY;  // Position of letter button
    float rectSize = 80;     // Diameter of rect
    float circleSize = 80;   // Diameter of circle
    float letterW = 30;      // Width of letter "L" button
    float letterH = 60;      // Height of letter "L" button
    float gap = 30;          // Space btw buttions
    boolean ballOver = false;
    boolean squareOver = false;
    boolean letterOver = false;
    boolean ballClick = false;
    boolean squareClick = false;
    boolean letterClick = false;
    color base = color(76);
    color high = color(0);
    color select = color(150, 20, 30);
    
    // GUI slider vars;
    float barWidth = 300;
    float slidePos = barWidth/2;
    float hueVal = map (slidePos, 0, barWidth, 0, 255);
    float slider1X = 20;
    float slider1Y = 20;
    float sliderWidth = 35;
    boolean slider1Click = false;
    boolean slider1Over = false;
    
    //Contains all the paths
    PGraphics pgPaths;
    
    
    void setup() {
      size(2200, 1000);
      pgPaths = createGraphics(2200, 1000);
      noStroke();
      shapes = new ArrayList<Shape>();
      colorMode(HSB);
    
      //GUI var setup
      rectX = slider1X;
      rectY = slider1Y + gap + sliderWidth;
      circleX = rectX + (.5*rectSize);
      circleY = rectY + rectSize +.5*circleSize + gap;
      letterX = circleX - .5*circleSize + 10;
      letterY = circleY + .5*circleSize + gap;
    }
    //draw() method
    //--------------------------------------------------------------------------------
    
    void draw() {
      //Update buttons
      update();
      //For objects to disappear after some time
      background(254);
      //Object Color Slider
      hueVal = drawSlider(slider1X, slider1Y, barWidth, sliderWidth, hueVal);
    
      //Shape Selectors
      //-------------
      //Ball Button
      if (ballOver) {
        fill(high);
      } else if (ballClick) { 
        fill(select);
      } else {
        fill(base);
      }
      noStroke();
      ellipse(circleX, circleY, circleSize, circleSize);
      //-------------
    
      //Square Button
      if (squareOver) {
        fill(high);
      } else if (squareClick) { 
        fill(select);
      } else {
        fill(base);
      }
      noStroke();
      rect(rectX, rectY, rectSize, rectSize);
      //-------------
      //Letter Button
      if (letterOver) {
        fill(high);
      } else if (letterClick) {
        fill(select);
      } else {
        fill(base);
      }
      noStroke();
      rect(letterX, letterY, letterW/1.5, letterH);
      rect(letterX, letterY + letterH, letterH, letterW/1.5);
    
      // Code to draw() 
      //-----------------------------------------------------------------
      pgPaths.beginDraw();
      for (int i = shapes.size()-1; i >= 0; i--) {
        Shape s = shapes.get(i);
        s.path();
        if (!s.finished()) {
          s.move();
          s.display();
        } else {
          if (!(s instanceof Fragment) && s.fragoff == false) {
            for (int q = 0; q < (int) random(2, 4); q++) {
              shapes.add(new Fragment(s.xPos + random(-1, 1), s.yPos + random(-1, 1), s.size, s.colorVal, s.speed, s.drift));
              s.fragoff = true;
            }
            shapes.remove(i);
          }
          shapes.remove(i);
    
        }
      }
      pgPaths.endDraw();
      image(pgPaths, 0, 0);
      //-----------------------------------------------------------------
    }
    
    //For single shapes
    
    void mousePressed() {
      if (ballOver) {
        ballClick = true;
        squareClick = false;
        letterClick = false;
        slider1Click = false;
        slider1Over = false;
      }
      if (squareOver) {
        squareClick = true;
        ballClick = false;
        letterClick = false;
        slider1Click = false;
        slider1Over = false;
      }
      if (letterOver) {
        letterClick = true;
        squareClick = false;
        ballClick = false;
        slider1Click = false;
        slider1Over = false;
      }
      if (slider1Over) {
        slider1Click = true;
      }
      if (mouseX > 100 || mouseY > 380) {
        if (slider1Over == false) {
          if (!ballOver) {
            if (ballClick) {
              shapes.add(new Ball(mouseX, mouseY, random(30, 50), hueVal, 0, 0));
            }
          }
          if (!squareOver) {
            if (squareClick) {
              shapes.add(new Square(mouseX, mouseY, random(30, 50), hueVal, 0, 0));
            }
          }
          if (!letterOver) {
            if (letterClick) {
              shapes.add(new Letters(mouseX, mouseY, random(30, 50), hueVal, 0, 0));
            }
          }
        }
      }
    }
    
    boolean overSquare(float x, float y, float width, float height) {
      if (mouseX >= x && mouseX <= x+width && 
        mouseY >= y && mouseY <= y+height) {
        return true;
      } else {
        return false;
      }
    }
    
    boolean overBall(float x, float y, float diameter) {
      float disX = x - mouseX;
      float disY = y - mouseY;
      if (sqrt(sq(disX) + sq(disY)) < diameter/2 ) {
        return true;
      } else {
        return false;
      }
    }
    
    boolean overLetter(float x, float y, float w, float h) {
      if (overSquare(x, y, w/1.5, h) || overSquare(x, y+h, h, w/1.5)) {
        return true;
      } else {
        return false;
      }
    }
    
    void update() {
      if (overBall(circleX, circleY, circleSize) ) {
        ballOver = true;
        squareOver = false;
        letterOver = false;
      } else if (overSquare(rectX, rectY, rectSize, rectSize) ) {
        squareOver = true;
        ballOver = false;
        letterOver = false;
      } else if (overLetter(letterX, letterY, letterW, letterH)) {
        letterOver = true;
        ballOver = false;
        squareOver = false;
      } else {
        ballOver = squareOver = letterOver = false;
      }
    }
    
    //Color Picker Slider
    //-------------------------------------------------------------------------------------
    float drawSlider(float xPos, float yPos, float sWidth, float sHeight, float hueVal) {
      fill(250);
      noStroke();
      rect(xPos-5, yPos-10, sWidth+10, sHeight+20);  //draw white background behind slider
    
      float sliderPos=map(hueVal, 0, 255, 0, sWidth); //find the current sliderPosition from hueVal
    
      for (int i=0; i<sWidth; i++) {  //draw 1 line for each hueValue from 0-255
        hueVal=map(i, 0, sWidth, 0, 255.0);  //get hueVal for each position in the bar
        stroke(hueVal, 255, 255);
        line(xPos+i, yPos, xPos+i, yPos+sHeight);
      }
      if (mouseX>xPos && mouseX<(xPos+sWidth) && mouseY>yPos && mouseY <yPos+sHeight) {
        slider1Over = true;
        if (mousePressed) {
          sliderPos=mouseX-xPos;
        }
      } else {
        slider1Over = false;
      }
      stroke(50);
      hueVal=map(sliderPos, 0, sWidth, 0, 255.0);
      fill(hueVal, 255, 255);
      rect(sliderPos+xPos-3, yPos-5, 15, sHeight+10);
      rect(sWidth+40, yPos, sHeight, sHeight);
      return hueVal;
    }
    
    //-----------------------------------------------------------------------------------------------------------------------
    //Shape Superclass
    
    class Shape {
      float xPos, yPos;
      float speed;
      float gravity;
      float size; 
      float life;
      float drift;
      int fragNum;
      ArrayList<PVector> path;
      float colorVal;
      boolean fragoff = false;
    
      Shape(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.size = size;
        this.speed = speed;
        gravity = .7;
        life = random(155, 255);
        this.drift = drift;
        path = new ArrayList<PVector>();
        this.colorVal = colorVal;
      }
    
      void move() {
        speed = speed + gravity;
        yPos = yPos + speed;
        xPos = xPos + drift;
        path.add(new PVector(xPos, yPos));
        if (yPos > height) {
          // Dampening
          speed = speed * -0.8;
          yPos = height;
          drift = random(-3, 3);
        }
        if (xPos >= width) {
          speed = speed * -0.8;
          xPos = width;
          drift = random(-3, 3);
        }
        if (xPos <= 0) {
          speed = speed * -0.8;
          xPos = 0;
          drift = random(-3, 3);
        }
      }
    
      boolean finished() {
        life--;
        if (life < 0) {
          return true;
        } else {
          return false;
        }
      }
    
      void display() {
        fill(colorVal, 255, 255);
      }
    
      void path() {
        //pgPaths.pushStyle();
        for (int i = 1; i<path.size(); i++) {
          pgPaths.pushStyle();
          pgPaths.stroke(colorVal, 255, 255);
          pgPaths.strokeWeight(3);
          pgPaths.line(path.get(i-1).x, path.get(i-1).y, path.get(i).x, path.get(i).y);
          pgPaths.popStyle();
        }
        //pgPaths.popStyle();
    
      }
    }
    
    //----------------------------------------------------------------
    //Ball Class
    
    class Ball extends Shape {
    
      Ball(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        super(xPos, yPos, size, colorVal, speed, drift);
      }
    
      void display() {
        fill(colorVal, 255, 255);
        ellipse(xPos, yPos, size, size);
      }
    }
    
    //----------------------------------------------------------------
    // Square Class
    
    class Square extends Shape {
    
      Square(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        super(xPos, yPos, size, colorVal, speed, drift);
      }
    
      void display() {
        fill(colorVal, 255, 255);
        rect(xPos, yPos, size, size);
      }
    }
    
    //----------------------------------------------------------------
    // Letters Class
    
    class Letters extends Shape {
      PFont f;
      float angle;
      char letter;
    
      Letters(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        super(xPos, yPos, size, colorVal, speed, drift);
        f = createFont("Georgia", size+30, true);
        letter = (char) int(random(65, 90));
      }
    
      void display() {
        fill(colorVal, 255, 255);
        textFont(f);
        //translate(xPos,xPos);
        //rotate(angle); 
        textAlign(CENTER);
        text(letter, xPos, yPos);
        //angle += 0.05;
      }
    }
    
    //----------------------------------------------------------------
    // Fragment Class
    
    class Fragment extends Shape {
    
    
      Fragment(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        super(xPos, yPos, size, colorVal, speed, drift);
      }
    
      void display() {
        fill(colorVal, 255, 255);
        rect(xPos, yPos, random((size/2)-1, (size/2)), random((size/2)-1, (size/2)));
      }
    }
    
  • edited September 2016 Answer ✓

    I'm not sure why your sketch has serious performance problems -- there is a lot going on in it, and it is also rendering to a large screen area (twice).

    However, the purpose of the PGraphics for paths is to eliminate keeping a large ArrayList<PVector> path; list that is being built up in every Shape object, and, more importantly, to simplify the Shape.path draw routine so that you can avoid looping through the entire list for every object every time you draw each shape.

    So, if you have 10 objects that have been on the screen for 40 path segments each, then draw() will have to draw 400 segments every single time the draw loop runs (e.g. 60 fps). Instead, if you eliminate ArrayList<PVector> path and instead only keep your previous positions in each object float xPosLast; float yPosLast; then you are only drawing 10 segments (one per object) each draw, and you aren't constantly growing arrays. The drawn line segments accumulate as pixel data on the PGraphics surface rather than being constantly recomputed, and they are shown with each image() call at the end of draw.

    class Shape {
      float xPos, yPos;
      float speed;
      float gravity;
      float size; 
      float life;
      float drift;
      int fragNum;
      float xPosLast, yPosLast;
      float colorVal;
      boolean fragoff = false;
    
      Shape(float xPos, float yPos, float size, float colorVal, float speed, float drift) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.xPosLast = xPos;
        this.yPosLast = yPos;
        this.size = size;
        this.speed = speed;
        gravity = .7;
        life = random(155, 255);
        this.drift = drift;
        this.colorVal = colorVal;
      }
    
      void move() {
        speed = speed + gravity;
        xPosLast = xPos;
        yPosLast = yPos;
        yPos = yPos + speed;
        xPos = xPos + drift;
        if (yPos > height) {
          // Dampening
          speed = speed * -0.8;
          yPos = height;
          drift = random(-3, 3);
        }
        if (xPos >= width) {
          speed = speed * -0.8;
          xPos = width;
          drift = random(-3, 3);
        }
        if (xPos <= 0) {
          speed = speed * -0.8;
          xPos = 0;
          drift = random(-3, 3);
        }
      }
    
      boolean finished() {
        life--;
        if (life < 0) {
          return true;
        } else {
          return false;
        }
      }
    
      void display() {
        fill(colorVal, 255, 255);
      }
    
      void path() {
        pgPaths.beginDraw();
        pgPaths.stroke(colorVal, 255, 255);
        pgPaths.strokeWeight(3);
        pgPaths.line(xPosLast, yPosLast, xPos, yPos);
        pgPaths.endDraw();
      }
    }
    

    Still, the number one thing that affects performance on my machine regardless of other factors is your sketch size being 2200, 1000. But I can get choppiness for even simple bouncing balls at that size.

    You can test this by displaying frameRate, for example adding println(frameRate); to the end of your draw loop and watching the console. For example, on my machine at 500,500 I'm able to create as many shapes and paths as I want and the sketch stays at between 58-61 fps. At 2100,1000 I start at around 40fps and the sketch dives to ~5fps as I add shapes.

  • Thanks for the input. Changing the window did really help as did changing button size.

    But I still have the problem where path will only use the initial Color Value and not the shape's color.

    I tried using push/pop style but to no avail

  • I fixed everything. All I had to do was change the color mode of the pgGraphics object.

    With a smaller window size and pgGraphics, the preformace is much much better now.

  • Glad to hear it! So it sounds like pgPaths.colorMode(HSB); fixed your last problem?

Sign In or Register to comment.