How to play a short animation upon interaction (rotate a shape)

Hi all!

I want to rotate a cube about a particular axis through a particular angle every time the user presses the RIGHTARROW key. I want the user to see the rotation happen, starting at its original position and rotating through until it reaches its final position over the course of a couple of seconds. I can do all the necessary math, I just can’t figure out a nice way to have a short animation like that play upon a keyPressed. Is this a bad question? Do I need to be more clear and/or specific?

Oh, I’m doing this in p5.js.

Thank you,

Jack

1 Like

Hi
Hope this helps

Thank you jafal!

I’m super familiar with these functions, but how would I get an animation like this to start upon a key press and then stop a predetermined number of seconds later? I also need to update some variables at the end of each round of animation.

I’m going to try some simple versions of this on my own so I have code to share, but I hope that clears up my question.

1 Like

Think of it as running in a state machine with a variable that represents your current state. Yours is simple with just two states – stopped or rotating – so you could just represent that with a boolean. You also need to keep track of where you are along the rotation while in the rotating state, which you can do with a counter that starts at however many frames you want the animation to take and tics down by one each frame. When it hits zero, you’re done animating and switch back to the stopped state.

So, in keyPressed(), you would set your state variable to rotating (true) and set the counter to the number of frames to animate, and set up whatever math you want for your start and end orientations. In draw(), if your rotating state variable is true, you set the rotation based on the interpolated rotation based on the counter, decrease the counter, and if it hits 0, set the state back to false and update your other variables for the new orientation.

In your case, you could even just use the counter as the state variable. If it’s greater than 0, then you are in the rotating state. 0 means you are stopped. counter / total number of frames gives you your interpolation variable from 1 to 0 between the start and end orientations.

thank you scudly! i am going to take a closer look at this response tomorrow and see what i can come up with. much appreciated.

1 Like

I think the basic information here is that Processing

  • updates the screen only once at the end of draw().

This means that

  • using a for-loop for an animation won’t work since the screen is not updated throughout but only at the end of draw() - and then your for-loop is already over.

  • So, don’t use a for-loop or while-loop for animations but make use of the fact that draw() loops in itself 60 times per second automatically, which is perfect for animations.

void draw() {
  background(0); 
  if(stateVariableAnimate) {
    angle+=0.022; 
  }//if
  showCube(); 
}//func

Easing

We could also use easing, which is a simple technique to slow down an action / animation. In our case we would apply the easing to the change of the angle, here is the example for a ball following the mouse but slowly: Easing / Examples / Processing.org. Here the easing is for the change of the position I guess.

Chrisir

1 Like

here is an easing example, the easing happens with the two angles

// "Demo for Easing"

final String title    = "Demo for Easing";
final String helpText = "Use right mouse button (click or drag). Use Escape key.";

final color RED    = color(255, 0, 0);
final color GREEN  = color(0, 255, 0);
final color BLUE   = color(0, 0, 255);
final color YELLOW = color(240, 240, 0);
final color PINK   = color(0, 240, 240);

// for the graphical representation, 2 pairs coupled with easing
float angleXThatIs; // current angles, used for displaying the cube
float angleYThatIs;
float angleXDesired; // the angles entered by the mouse (requested by the user)
float angleYDesired;

// --------------------------------------------------------------------
// Core functions

void setup() {
  size(1300, 700, P3D);
}

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

  // read mouse to change angles angleXDesired and angleYDesired ----
  if (mousePressed && mouseButton==RIGHT) {
    angleXDesired=map(mouseY, 0, height, PI, -PI);
    angleYDesired=map(mouseX, 0, width, -PI, PI);
  }
  // calc the current angle angleXThatIs and angleYThatIs with easing (this is done throughout)
  angleXThatIs += (angleXDesired-angleXThatIs)*0.08;
  angleYThatIs += (angleYDesired-angleYThatIs)*0.08;

  // Show the cube/scene -----------------
  showCube();

  // HUD ---------------------------------
  HUD(); // The Texts
}

// --------------------------------------------------------------------
// Input functions

// (mouse is read in draw())

void keyPressed() {
  if (key==ESC) {
    // reset view
    angleXDesired=0;
    angleYDesired=0;
    key=0; // kill Esc
  }
}

// --------------------------------------------------------------------
// Other functions

void showCube() {
  //
  pushMatrix();
  translate (width/2, height/2+98);

  rotateX(angleXThatIs); // use angles / ESC resets
  rotateY(angleYThatIs);

  fill(RED);
  stroke(111);
  box(100); // main box

  // 4 decorations cubes
  smallCubeRelativePos(0, -65, 0, GREEN); // Top
  smallCubeRelativePos(0, 65, 0, PINK); // Bottom

  smallCubeRelativePos(65, 0, 0, BLUE); // right
  smallCubeRelativePos(-65, 0, 0, YELLOW); // left

  popMatrix();
}

void smallCubeRelativePos (float x_, float y_, float z_,
  color col_) {

  // draws a small Cube from a Relative Pos x,y,z and color col

  pushMatrix();
  translate(x_, y_, z_);
  fill(col_);
  box(28);
  popMatrix();
}

void HUD() {
  // Head-up-Display (HUD)
  camera() ;
  noLights();

  // TOP Text
  // Title
  textSize(32);
  textAlign(CENTER, CENTER);
  fill(0);
  text (title, width / 2, 30);

  // Help
  textSize(14);
  text (helpText, width / 2, 90);

  // reset
  textAlign(LEFT);
}
//


New version

New version, in which first angle x is handled, and afterwards angle y

  • Visually, this makes it easier to understand what’s going on.

  • It’s especially good when clicking Escape key or click the mouse right button

  • It’s less good when dragging the mouse since the rotation is not complete



// "Demo for Easing"

final String title    = "Demo for Easing";
final String helpText = "Use right mouse button (click or drag). Use Escape key.";

final color RED    = color(255, 0, 0);
final color GREEN  = color(0, 255, 0);
final color BLUE   = color(0, 0, 255);
final color YELLOW = color(240, 240, 0);
final color PINK   = color(0, 240, 240);

// for the graphical representation, 2 pairs coupled with easing
float angleXThatIs;   // current angles, used for displaying the cube
float angleYThatIs;
float angleXDesired;  // the angles entered by the mouse (requested by the user)
float angleYDesired;

// --------------------------------------------------------------------
// Core functions

void setup() {
  size(1300, 700, P3D);
}

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

  // read mouse to change angles angleXDesired and angleYDesired ----
  if (mousePressed && mouseButton==RIGHT) {
    angleXDesired=map(mouseY, 0, height, PI, -PI);
    angleYDesired=map(mouseX, 0, width, -PI, PI);
  }

  // calc the current angles
  // we start with angle X
  // println(abs(angleXDesired-angleXThatIs));
  // calc the current angle angleXThatIs with easing (this is done throughout)
  angleXThatIs += (angleXDesired-angleXThatIs)*0.08;
  // below a certain value we stop x
  if (abs(angleXDesired-angleXThatIs)<0.02)
    angleXThatIs = angleXDesired;

  // when x is done, we start y
  if (abs(angleXDesired-angleXThatIs) == 0.0) {
    // calc the current angle angleYThatIs with easing (this is done throughout)
    angleYThatIs += (angleYDesired-angleYThatIs)*0.08;
    // below a certain value we stop y
    if (abs(angleYDesired-angleYThatIs)<0.02)
      angleYThatIs = angleYDesired;
  }

  // Show the cube/scene -----------------
  showCube();

  // HUD ---------------------------------
  HUD(); // The Texts
}

// --------------------------------------------------------------------
// Input functions

// (mouse is read in draw())

void keyPressed() {
  if (key==ESC) {
    // reset view
    angleXDesired=0;
    angleYDesired=0;
    key=0; // kill Esc
  }
}

// --------------------------------------------------------------------
// Other functions

void showCube() {
  //
  pushMatrix();
  translate (width/2, height/2+98);

  rotateX(angleXThatIs); // use angles / ESC resets
  rotateY(angleYThatIs);

  fill(RED);
  stroke(111);
  box(100); // main box

  // 4 decorations cubes
  smallCubeRelativePos(0, -65, 0, GREEN); // Top
  smallCubeRelativePos(0, 65, 0, PINK); // Bottom

  smallCubeRelativePos(65, 0, 0, BLUE); // right
  smallCubeRelativePos(-65, 0, 0, YELLOW); // left

  popMatrix();
}

void smallCubeRelativePos (float x_, float y_, float z_,
  color col_) {

  // draws a small Cube from a Relative Pos x,y,z and color col

  pushMatrix();
  translate(x_, y_, z_);
  fill(col_);
  box(28);
  popMatrix();
}

void HUD() {
  // Head-up-Display (HUD)
  camera() ;
  noLights();

  // TOP Text
  // Title
  textSize(32);
  textAlign(CENTER, CENTER);
  fill(0);
  text (title, width / 2, 30);

  // Help
  textSize(14);
  text (helpText, width / 2, 90);

  // reset
  textAlign(LEFT);
}
//

1 Like

How does this look? It does what I want, but is it clunky? Is there anything to clean up? An important feature I need is that the square rotates through an additional PI/4 every time the right arrow is pressed, hence the ‘rotations’ variable keeping track of how many times it’s been pressed.

function setup() {
  createCanvas(400, 400);
  countdown = 100;
  rotations = 0;
  animate = false;
}

function keyPressed() {
  if (keyCode === RIGHT_ARROW) {
    animate = true;
    countdown = 100;
  }
}

function draw() {
  background(220);
  push();
  translate(width / 2, height / 2);
  if (animate == true) {
    rotate((rotations + (100 - countdown) / 100) * (PI / 4));
    rect(0, 0, 100, 100);
    countdown--;
  } else {
    rotate(rotations * (PI / 4));
    rect(0, 0, 100, 100);
  }
  pop();
  if (countdown == 0) {
    animate = false;
    rotations++;
    countdown = 100;
  }
}

One caution any time you are doing division is whether you want your result to be an integer or a float. If you divide an integer by another integer, the result will be an integer. In this case, though, you want

(100 - countdown) / 100

to be a float between 0.0 and 1.0. If the calculation is done on integers, you can only get a value of 0 or 1, but nothing in between. To ensure a float result, you want at least one of those values in the expression to be a float. You don’t show your declaration for countdown. If it’s a float, you’ll be good. Alternatively, leave it as an int and change the 100s to 100.0.

Another problem is that you only want to check for the transition from rotating to stopping while you are rotating and not the rest of the time. That means your final check of if (countdown == 0) { should only be done inside the if block when animate == true. So move that whole second if block up to just below the countdown--; statement inside the first if block.

1 Like

this is great, thank you. it performs the same before and after i make the changes you suggest, so i guess p5 is treating countdown as a float. i guess also that, although the performance looks the same to me, p5 has much less work to do if it’s not checking whether countdown equals 0 every time it runs through the code.

again, thank you

Ah, right. You’re in javascript where every number is a float. I forgot.

The other reason you need to keep the countdown == 0 check inside the first if is that you increase the rotations variable each time, so your object would continue rotating by PI/4 every frame. Try drawing a triangle instead of a square to see what happens if you don’t move it.

I thought about that issue, which is why I changed countdown to 100 after I incremented rotations. But I see now that I don’t need to do that extra step if I keep that conditional inside the other conditional.

Here’s the finished product. RIGHTARROW rotates about one of the axes, UPARROW rotates about the other. Would love for you to try it out and tell me what you think.

function setup() {
  createCanvas(700, 700, WEBGL);

  //constants for drawing the tetrapod

  s = 50;
  l = sqrt(8 / 3) * s;

  //variables for animating

  vAnimate = false;
  eAnimate = false;
  vCountdown = 100;
  eCountdown = 100;
  vAxis = createVector(1, 1, 1);
  eAxis = createVector(1, 0, 0);

  //indexing parts of the tetrapod to use in for-loop

  hub = [
    [1, 1, 1],
    [-1, -1, 1],
    [1, -1, -1],
    [-1, 1, -1],
    [1, 1, -1],
    [1, -1, 1],
    [-1, 1, 1],
    [-1, -1, -1],
  ];
  feet = [
    [-1, -1, -1],
    [1, 1, -1],
    [-1, 1, 1],
    [1, -1, 1],
  ];

  //colors for the legs and feet

  colors = ["#F500D3", "#FFBC00", "#019AFF", "#41F501"];
}

//interaction

function keyPressed() {
  if (keyCode === RIGHT_ARROW) {
    vAnimate = true;
    vCountdown = 100;
  }

  if (keyCode === UP_ARROW) {
    eAnimate = true;
    eCountdown = 100;
  }
}

function draw() {
  background(220);
  orbitControl();

  push();

  // draw rotation axes
  strokeWeight(3);
  line(-500, 0, 500, 0);
  line(-500, -500, -500, 500, 500, 500);

  //rotate about vAxis

  if (vAnimate == true) {
    rotate((((100 - vCountdown) / 100.0) * 2 * PI) / 3, vAxis);
    for (i = 0; i <= 3; i++) {
      strokeWeight(1);
      //HUB
      fill(255);
      beginShape();
      vertex(s * hub[i][0], 0, 0);
      vertex(0, s * hub[i][1], 0);
      vertex(0, 0, s * hub[i][2]);
      endShape(CLOSE);

      //LEGS
      fill(color(colors[i]));
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(0, feet[i][1] * s, 0);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(0, feet[i][1] * s, 0);
      endShape(CLOSE);

      //FEET
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      endShape(CLOSE);
    }
    vCountdown--;
    if (vCountdown == 0) {
      vAnimate = false;
      colors = [colors[0], colors[3], colors[1], colors[2]];
    }
  }

  //rotate about eAxis
  else if (eAnimate == true) {
    rotate(((100 - eCountdown) / 100.0) * PI, eAxis);
    for (i = 0; i <= 3; i++) {
      strokeWeight(1);
      //HUB
      fill(255);
      beginShape();
      vertex(s * hub[i][0], 0, 0);
      vertex(0, s * hub[i][1], 0);
      vertex(0, 0, s * hub[i][2]);
      endShape(CLOSE);

      //LEGS
      fill(color(colors[i]));
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(0, feet[i][1] * s, 0);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(0, feet[i][1] * s, 0);
      endShape(CLOSE);

      //FEET
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      endShape(CLOSE);
    }
    eCountdown--;
    if (eCountdown == 0) {
      eAnimate = false;
      colors = [colors[2], colors[3], colors[0], colors[1]];
    }
  }

  //draw shape in current orientation
  else {
    for (i = 0; i <= 3; i++) {
      strokeWeight(1);
      //HUB
      fill(255);
      beginShape();
      vertex(s * hub[i][0], 0, 0);
      vertex(0, s * hub[i][1], 0);
      vertex(0, 0, s * hub[i][2]);
      endShape(CLOSE);

      //LEGS
      fill(color(colors[i]));
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(0, feet[i][1] * s, 0);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(feet[i][0] * s, 0, 0);
      endShape(CLOSE);

      beginShape();
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      vertex(0, 0, feet[i][2] * s);
      vertex(0, feet[i][1] * s, 0);
      endShape(CLOSE);

      //FEET
      beginShape();
      vertex(feet[i][0] * (s + l), feet[i][1] * l, feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * (s + l), feet[i][2] * l);
      vertex(feet[i][0] * l, feet[i][1] * l, feet[i][2] * (s + l));
      endShape(CLOSE);
    }
  }

  pop();
}

Another cube rotating example: :ice_cube:

1 Like

very helpful, thank you!

1 Like