Squash & Stretch simples (pré-objetos)

Alguns estudantes têm sentido dificuldades em compreender o princípio de um squash & stretch. O primeiro conceito a reter é a animação. Um squash & stretch precisa de uma deformação inicial (squash). Tipicamente esmagar. Seguido de uma animação que estica (para além do tamanho original). E, para terminar o loop, tem que voltar a esmagar para o tamanho original. Esta animação deve decorrer, pelo menos, uma vez.

Ora vejam em ação:

Então… como é que isto se traduz em código? De duas formas. A primeira, mais mecânica é usar um conjunto de ifs.

Vamos considerar um círculo (como se fosse um botão ou uma bola)

void setup(){
 size(500, 500);
 noStroke();
 
}

void draw() {
  background(200);
  
  fill(40);
  radar(300, 200, 50);
}

void radar(int x, int y, int w) {
  
  if(dist(mouseX, mouseY, x, y) < w/2 ) {
    fill(200, 0, 0);
    
    if (mousePressed) {
      w = w/2;
    }    
    
  }
  ellipse(x, y, w, w);
}

Esta bola, basicamente, responde à entrada do rato no seu interior mudando a cor. E, quando se clica no interior, ela já parece fazer o início da animação. A partir daqui precisamos de criar as condições para quando largarmos o rato ela expanda para além do limite original e regresse ao normal.

Atenção que ainda não estamos a usar objetos. Mas, os valores de posição e tamanho do desenho da bola estão a ser guardados em vetores (arrays). Então, o nosso programa, na realidade terá mais este aspeto:

int[][] bolas;
int n;

void setup(){
 size(500, 500);
 noStroke();
 n = 1;
 bolas = new int[n][3];
 
 for (int i = 0; i < n; i++) {
    bolas[i][0] = int( random(width) );
    bolas[i][1] = int( random(height) );
    bolas[i][2] = int( random(20, 50) );
  }
}

void draw() {
  background(200);
  
  fill(40);
  
  for (int i = 0; i < n; i++) {
    radar(bolas[i][0], bolas[i][1], bolas[i][2]);
  }
  
}

void radar(int x, int y, int w) {
  
  if(dist(mouseX, mouseY, x, y) < w/2 ) {
    fill(200, 0, 0);
    
    if (mousePressed) {
      w = w/2;
    }    
    
  }
  ellipse(x, y, w, w);
}

Se nós conseguimos fazer com o que o Processing se “lembre” de coisas como as posições e os tamanhos de cada bola, então só precisamos que ele também saiba se está a: 1) animar; 2) de squash para stretch; 3) do stretch para a posição original. Para isso, vamos acrescentar mais alguns valores ao vetor, ok? Podemos usar ints na mesma. Se estiver parado a animação = 0 e vice-versa. Isto permite controlar o arranque e o fim da animação. Depois mais um para se estiver [de squash para] stretch e mais outro para se estiver [de stretch para] a posição normal.

Isto para fazermos o seguinte. Se o rato estiver dentro e pressionado, a animação está pronta. Assim que soltarmos, inicia um ciclo de crescimento (por exemplo adicionando um incremento ao tamanho). Assim que a bola crescer demasiado inverte até voltar ao normal.

O exemplo é cru, mas já demonstra uma ligeira animação.

int[][] bolas;
int n;

void setup() {
  size(500, 500);
  noStroke();
  n = 1;
  bolas = new int[n][6];

  for (int i = 0; i < n; i++) {
    bolas[i][0] = int( random(width) );
    bolas[i][1] = int( random(height) );
    bolas[i][2] = int( random(20, 50) );

    // squash & stretch properties
    bolas[i][3] = 0; // animation
    bolas[i][4] = 0; // stretching
    bolas[i][5] = 0; // squashing
  }
}

void draw() {
  background(200);

  fill(40);

  for (int i = 0; i < n; i++) {
    radar(i, 
      bolas[i][0], bolas[i][1], bolas[i][2], 
      bolas[i][3], bolas[i][4], bolas[i][5]);
  }
}

void radar(int id, int x, int y, int w, int an, int sq, int st) {
  
  // check if the mouse is inside the ball
  if (dist(mouseX, mouseY, x, y) < w/2 ) {
    fill(200, 0, 0);

    // if it "clicks" inside the ball, squash it
    if (mousePressed) {
      w = w/2;
      bolas[id][3] = 1; // activate the "I am going to animate" property
    }
    
    // if the mouse is still inside, but is not pressed
    // check if it was pressed by also verifying if it started the animation
    if (!mousePressed && an == 1) {
      println("animation");
      
      // if it started animating
      if (sq == 0) {
        bolas[id][2] += 2;  // grow the size (stretch)
      }
      
      // if the size reaches a certain limit (here is sitll hardcoded…)
      if (bolas[id][2] >= 75) {  
        bolas[id][4] = 1;      // stop stretching. start squashing 
      }
      
      // if it is squashing
      if (sq == 1) {
        bolas[id][2] -= 2;    // shrink the size (squash)
      }
      
      // if the size reaches a certain limit (here is sitll hardcoded we need an original value backup…)
      if (bolas[id][2] <= 10) {
        bolas[id][5] = 1;      // stop squashing
      }
      
      // if it has stretched, and squashed… restore normality (add backup values in the array to restore correctly)
      if (st == 1) {
        bolas[id][3] = 0;
        bolas[id][4] = 0;
        bolas[id][5] = 0;
      }
    }
  }
  ellipse(x, y, w, w);
}

Esta é uma forma de fazer. Com ifs. Não é muito eficiente. É “crua” e “dura”. E a animação é pouco orgânica. Mas, é uma das formas como se faz muita coisa. O Daniel Shiffman também explica esta abordagem para um exercício de bouncing ball

Mas há formas mais interessantes, ou pelo menos alternativas de resolver isto. No ano passado demonstrei isto com uma animação com um ciclo de rotação.

https://www.instagram.com/p/BxyFaEPhEGw/?utm_source=ig_web_button_share_sheet

Infelizmente, não encontro o código original, por isso aqui vai uma reconstrução rápida.

A ideia é ter uma forma que “reage ao click”. Quando o utilizador clica, o objeto usa um “rotação interna” para afetar o valor da largura e encolher, esticar e voltar a encolher. Isto consegue-se através das propriedades do Seno e do Coseno da trigonometria, como expliquei no último artigo. Se rodarem entre 45º e 315º ele encolhe, estica e encolhe.

int a;

void setup() {
  size(500, 500);
  a = 0;
  rectMode(CORNERS);
}

void draw() {
  background(200);
  
  noFill();
  stroke(40);
  ellipse(width/2, height/2, 300, 300);
  
  fill(40);
  noStroke();
  int tx = width/2 + int( cos( radians(a) ) * 300/2 );
  int ty = height/2 + int( sin( radians(a) ) * 300/2 );
  
  ellipse(tx, ty, 30, 30);
  
  fill(255);
  rect(tx, height/2-25, width/2, height/2+25); 
  a++;
}

Basta agora adaptar este pequeno pedaço de código (não deixando que os objetos encolham demais) ao nosso código anterior.

int[][] bolas;
int n;

void setup() {
  size(500, 500);
  noStroke();
  n = 10;
  bolas = new int[n][5];

  for (int i = 0; i < n; i++) {
    bolas[i][0] = int( random(100, width-100) );
    bolas[i][1] = int( random(100, height-100) );
    bolas[i][2] = int( random(30, 70) );

    // squash & stretch properties
    bolas[i][3] = 0; // animation
    bolas[i][4] = 0; // rotation
    
  }
}

void draw() {
  background(200);

  for (int i = 0; i < n; i++) {
    fill(40);
    radar(i, 
      bolas[i][0], bolas[i][1], bolas[i][2], 
      bolas[i][3], bolas[i][4]);
  }
}

void radar(int id, int x, int y, int w, int an, int rot) {
  
  // calculate a positive/negative width increment to the ball
  float inc =  w * cos( radians(rot) ) ;
  
  // check if the mouse is inside the ball
  if (dist(mouseX, mouseY, x, y) < w/2 ) {
    fill(200, 0, 0);

    // if it "clicks" inside the ball, squash it by rotating the increment to a specific angle
    if (mousePressed) {
      bolas[id][4] = 270;
      bolas[id][3] = 1; // activate the "I am going to animate" property
    }
    
    // if the mouse is still inside, but is not pressed
    // check if it was pressed by also verifying if it started the animation
    if (!mousePressed && an == 1) {
      println("animation "+inc);
      
      bolas[id][4]++;   // increment the rotation
      
      if (bolas[id][4] > 540) {  // if it has completed one or more cycles, restore the values
        bolas[id][3] = 0;
        bolas[id][4] = 0;
      }
      
    }
  }
  ellipse(x, y, w + inc, w + inc);
}

O código não ficou perfeito. Longe disso. Mas creio que serve para demonstrar o princípio. Ainda faltam alguns mecanismos para manter o tamanho e afetar apenas o incremento de forma correta, mas a hora é avançada e amanhã preciso de estar “fresco” para uma sessão síncrona logo pela manhã. E, até porque, amanhã vamos abordar este problema de forma semelhante, mas de forma mais organizada e eficiente criando e manipulando (finalmente!) objetos. Este tipo de abordagem orientada aos objetos vai permitir-nos controlar este tipo de propriedades individuais muito mais facilmente.

Deixe um comentário

O seu endereço de email não será publicado.