[ CAPÍTULO 2 ]
A Arte da Programação
No princípio era o Código

Quando um pintor se sente impelido a expressar suas ideias em uma tela ele utiliza de seu pincel, paleta e instinto artístico. Ao longo dos anos esse profissional aprendeu a usar, naturalmente, dois conceitos muito importantes em sua carreira artística, o de ferramenta e o de processo. De maneira simplificada, as ferramentas do pintor são o pincel e a paleta. Elas permitem que o artista deposite a tinta com traços longos ou finos, com uma textura leve ou carregada e com cores vibrantes ou brandas que adicionam múltiplas dimensões de riqueza visual. Essas ferramentas são externas ao pintor, mas com o tempo, o treino e a experiência, tornam-se uma extensão dele. O segundo conceito que ele usa é o de processo, constantemente confundido com instinto simplesmente por ser difícil de explicar de uma maneira metódica ou racional. Porém, o processo artístico pode ser decomposto em regras[1] e padrões internos que o pintor segue (mesmo que criadas inconscientemente por ele) para expor suas ideias. Por exemplo, ele sabe que objetos contra a luz projetam uma sombra que está na mesma direção dessa luz. Sabe também quais cores utilizar para expressar um espectro infinito de emoções e motivações (vermelho para paixão e energia ou azul para razão e serenidade) [2].

Acredite, o programador também utiliza desses mesmos conceitos.

Todavia, suas ferramentas são as estruturas de dados, como variáveis e classes, e as estruturas de controle da programação, como funções, condicionais e repetições. O seu processo é o algoritmo e os passos lógicos que ele racionaliza para atingir os seus objetivos. As únicas diferenças entre o programador e o artista são a natureza das ferramentas e o detalhamento do processo. O detalhamento é especialmente importante uma vez que o computador não é capaz de expressar conceitos artísticos abstratos sem que um humano o designe para isso. Nesse ponto, o programador não pode ser puramente intuitivo como o artista, pois ele deve explicar para a máquina, em minúcias, como ele deseja que as ferramentas sejam utilizadas. O primeiro passo, portanto, é conhecer bem as ferramentas que estão disponíveis para ele. Algumas delas você já viu, são elas as funções (como rect(), setup() e draw()). Neste capítulo você irá aprender os conceitos básicos da programação, sempre regados a exemplos que visam demonstrar suas aplicações na prática artística.

2.1 Variáveis

Vamos começar pelo conceito de variáveis que basicamente são estruturas para armazenamento e consulta de dados, tais como números, letras e símbolos. Elas permitem guardar essas informações e usá-las quando for proveitoso, sendo regularmente aplicadas na parametrização de elementos de código. Por exemplo, suponha que você deseje desenhar um quadrado através de quatro retas em vez de usar diretamente a função rect() do Processing. Para facilitar essa ação vamos definir que seu vértice superior esquerdo esteja no ponto (25,25) e que ele possua 50 pixels de lado. Com essas informações você poderia escrever o seguinte código para desenhar um quadrado como o da figura 2.1.

Código 2.1 - Desenhando um quadrado -

// Desenha um quadrado formado por quatro arestas (linhas):
line(25,25,75,25);
line(75,25,75,75);
line(75,75,25,75);
line(25,75,25,25);
Figura 2.2 - Coordenadas das linhas.
Figura 2.3 - Janela de saída.
Figura - 2.1 - Quadrado formado por quatro linhas.

Agora imagine que a posição inicial, ou até mesmo do tamanho do lado desse quadrado não ficou como você esperava, e que você gostaria de experimentar outros valores. Para isso você terá de recalcular todos os pontos que compõem essas linhas, além de reescrever os dezesseis números nas chamadas das funções line(). Se você quiser testar apenas um ou outro caso você pode até optar por esse caminho, mas e se quiser testar dez, ou vinte, ou cem outras combinações? Isso se torna rapidamente inviável. Felizmente as variáveis nos permitem armazenar e parametrizar valores que estão continuamente mudando e sendo usados no código, como a posição do quadrado ou o tamanho de seu lado.

Uma variável necessita de ser criada ou, no jargão da computação, declarada, que é feito escrevendo o tipo da variável seguido pelo seu nome. A inicialização da variável, que significa definir uma valor para ela, é feita escrevendo o nome da variável seguido por um sinal de igual (=) e o valor que se deseja que ela adquira. Veja os exemplos:

Código 2.2 -

// Declaração de uma variável do tipo “float”, batizada de “numero”:
float numero;
// Inicialização arbitrária da variável “numero”:
numero = 10;

Também é possível fazer uma declaração de variável que incorpore sua inicialização através da combinação dos dois métodos:

Código 2.3 -

// Declara a variável “numero”, e a inicializa com o valor 10:
float numero = 10;

Múltiplas variáveis de um mesmo tipo podem ser declaradas desde que separadas por vírgula e, após a última, finalizadas com um ponto e vírgula:

Código 2.4 -

// Declaradas três variáveis do tipo float e inicializada apenas uma:
float variavel_1, variavel_2 = 10, variavel_3;

O tipo da variável indica se ela é um número, letra, ou um dado de outra natureza. Você pode consultar os principais tipos de variáveis primitivas do Processing na tabela 2.1. No exemplo acima, a palavra float firma que a variável é do tipo ponto flutuante, podendo armazenar qualquer valor numérico, inclusive aqueles que possuam casas decimais.

Nome Tipo Exemplo Descrição
Inteiro int int num = 2; Define um número inteiro, sem casas decimais.
Ponto
flutuante
float float num = 2.1; Define um número de ponto flutuante, que pode ter casas decimais.
Booleano boolean boolean cond = true; Define uma variável booleana, que pode ser verdadeira (true) ou falsa (false). É utilizada em operações lógicas, condicionais e repetições.
Caractere char char carac = 'b'; Define um caractere Unicode como letras, números e símbolos. Note que um caractere com um número Unicode ainda é um caractere e não pode participar de operações matemáticas no sentido convencional. Um caractere deve ser único (só um) e declarado entre aspas simples.
Palavra String String frase = "Olá Mundo!"; Define uma palavra que é, efetivamente, uma sequência linear de caracteres. Diferentemente do caractere, pode possuir uma quantidade arbitrária de letras, números e símbolos. O declarador String deve ser escrito com um "S" maiúsculo e a palavra deve estar entre aspas duplas.
Tabela 2.1 - Tipos de variáveis no Processing.

O nome da variável é aquele que será usado sempre que você pretender consultar o valor dela, podendo ser qualquer frase, palavra ou letra que você quiser, desde que siga as seguintes regras:

  • Pode começar com subtraço (_), cifrão ($) ou qualquer letra (A - Z ou a - z), porém não pode começar com números.
  • O restante do nome pode conter qualquer uma das condições anteriores e ainda números (0 - 9).
  • Não pode conter caracteres acentuados, espaços, operadores ou qualquer outro símbolo ou palavra reservada da linguagem.
  • Os nomes são sensíveis a letras maiúsculas e minúsculas, implicando que var1 é diferente de Var1 ou de vAr1.

Em geral, o nome da variável é uma abreviação ou referência ao propósito dela. Por exemplo, se você quiser guardar a posição x de um objeto em uma variável você poderia usar alguns nomes como posicaoX, posicao_X ou posX. Este último é especialmente interessante, pois nomes curtos e explicativos reduzem o código e tornam sua leitura mais rápida e eficiente.

Utilizando o conceito de variáveis você pode reescrever o código 2.1 de maneira parametrizada. Agora as coordenadas do quadrado não são mais números fixos, veja a figura 2.4.

Código 2.5 - Desenhando um quadrado parametrizado -

// Declaração e inicialização das variáveis:
float posX = 25;
float posY = 25;
float ladoQ = 50;

line(posX, posY, posX + ladoQ, posY);
line(posX + ladoQ, posY, posX + ladoQ, posY + ladoQ);
line(posX + ladoQ, posY + ladoQ, posX, posY + ladoQ);
line(posX, posY + ladoQ, posX, posY);
Figura 2.4 - Desenhando um quadrado usando variáveis.

Desta forma, se você decidir que o lado de 50 pixels do quadrado não é mais apropriado, basta alterar o valor da variável ladoQ que todas as posições das retas serão recalculadas automaticamente, sem a necessidade de reescrever uma a uma.

2.1.1 Variáveis do sistema

A linguagem Processing conta com variáveis nativas que possuem informações úteis sobre a janela de saída e o programa em execução. Essas variáveis podem ser diretamente acessadas a qualquer momento através de seu nome, sem a necessidade de um declaração prévia. Algumas delas podem ser consultadas na tabela 2.2. Elas se sobressaem na medida em que você escrever códigos que dependam do seu ambiente de programação. Por exemplo, se você desejar desenhar um ponto centralizado na tela, poderá encontrar suas coordenadas x e y através das operações width/2 e height/2 independente do tamanho da janela de exibição.

Nome Tipo Descrição
width int Armazena o comprimento atual, em pixels, da janela de exibição.
height int Armazena a altura atual, em pixels, da janela de exibição.
mouseX float Fornece a posição X (horizontal) do mouse na janela de exibição.
mouseY float Fornece a posição Y (vertical) do mouse na janela de exibição.
pmouseX float Fornece a posição X do mouse na janela de exibição no frame anterior.
pmouseY float Fornece a posição Y do mouse na janela de exibição no frame anterior.
mousePressed boolean Retorna verdadeiro se o mouse estiver pressionado, ou falso caso contrário.
keyPressed boolean Retorna verdadeiro se se alguma tecla estiver pressionada, ou falso caso contrário.
frameCount int Armazena o número de frames que foram exibidos desde o início da execução do código. Esta variável é automaticamente incrementada a cada quadro.
Tabela 2.2 - Variáveis de sistema do Processing.

No tópico de exibição de variáveis, além da maneira gráfica de visualizá-las, como tamanho e posição de uma figura, você pode consultar o conteúdo delas através do console do PDE. Para isto se usa a função println() como mostrado no código abaixo:

Código 2.6 -

void setup() {
size(150,150);
}

void draw() {
// Exibe, no console, o comprimento e altura da janela de saída:
println(width);
println(height);

// Exibe, no console, a posição (x,y) do mouse:
println("Posição x do mouse:", mouseX);
println("Posição y do mouse:", mouseY);
}
Figura 2.5 - Valores da posição do mouse sendo mostrados no console.

2.1.2 Vetores de variáveis

A declaração de variáveis uma a uma, como feito nas seções passadas, é uma prática costumeira que satisfaz boa parte dos programas. Contudo, à medida que você adicionar complexidade em seus códigos, você perceberá que existem casos que requerem um número elevado de elementos. Eventualmente manter a estratégia de declará-las individualmente não será mais satisfatório, pois você terá de memorizar muitos nomes de diferentes variáveis, além de digitar consideravelmente mais. A solução para esses problemas está na possibilidade de agrupar variáveis de um mesmo tipo e, em vez de armazenar apenas um valor por estrutura, você pode armazenar diversos se esta for um vetor (ou do ingês, um array). A figura 2.6 ilustra esse conceito.

Figura 2.7 - Variáveis individuais.
Figura 2.8 - Vetor de variáveis.
Figura - 2.6 - Armazenamento de dados.

Um vetor de variáveis pode ser de qualquer tipo, como os relatados anteriormente na tabela 2.2, e é declarado da seguinte maneira:

Código 2.7 -

tipoDoVetor[] nomeDoVetor = new tipoDoVetor[quantidadeDeValoresArmazenados];

Por exemplo:

Código 2.8 -

// Cria um vetor de variáveis que pode armazenar até 10 valores do tipo "float":
float[] numeros = new float[10];

Você pode perceber que essa estrutura é bem semelhante à declaração de uma variável normal. No lado esquerdo da declaração você só precisa adicionar os colchetes após o tipo, e no lado direito usar a palavra new (indicando que gostaria de criar um novo vetor) e o tipo da variável seguido de colchetes com a quantidade de elementos que se deseja armazenar. A atribuição de valores em um vetor deve ser feito individualmente por posição, sendo esta referenciada dentro dos colchetes:

Código 2.9 -

nomeDoVetor[indice] = valorDaVariavel;

E para acessá-los:

Código 2.10 -

nomeDoVetor[indice];

Tais como:

Código 2.11 -

// Cria um vetor de variáveis que pode armazenar até 10 valores do tipo "float":
float[] numeros = new float[10];

// Atribuição do valor de 4.998 para o índice 2 da variável “numeros”:
numeros[2] = 4.998;

// Acesso - Imprime 4.998 no console do PDE:
println(numeros[2]);

A seguir é realizada uma comparação de declarações e atribuições entre o método convencional e utilizando vetores. Também é mostrado que é possível declarar um vetor preenchido com valores, sem a necessidade de usar o qualificador new. Observe que todos os códigos abaixo são equivalentes:

Código 2.12 -

// Método convencional:
float num1 = 1;
float num2 = 10;
float num3 = 5;
float num4 = 22;

// Vetores - Declaração separada da atribuição:
float[] num = new float[4];
num[0] = 1;
num[1] = 10;
num[2] = 5;
num[3] = 22;

// Vetores - Declaração já com a atribuição:
float[] nums = {1,10,5,22};

Um ponto que deve ser evidenciado é que o índice do valor a ser acessado sempre começa do zero. Logo, se você declarar um vetor de 4 posições, os índices válidos serão 0, 1, 2 e 3, sempre indo de zero até o tamanho total do vetor subtraído de um. Acessar um índice que não seja válido (menor que zero, fracionário ou acima do tamanho total) irá causar uma falha durante a execução do seu programa:

Código 2.13 -

// Cria um vetor de quatro posições e inicializa apenas a primeira delas:
float[] nums = new float[4];
nums[0] = 1;

// Erro - Tentar acessar um índice maior que o tamanho do vetor:
println(nums[4]);

Por que você deve fazer o uso de vetores? O principal motivo é o fato de eles serem uma ótima forma de organizar e simplificar o seu código. Vamos a um caso concreto, imagine que você possua 50 figuras, todas com posições x e y na tela, e que você queira armazenar essas posições. Se você for declarar todas essas variáveis, somente para a posição x, terá:

Código 2.14 -

float posX01, posX02, posX03, posX04, posX05, posX06, posX07, posX08, posX09, posX10;
float posX11, posX12, posX13, posX14, posX15, posX16, posX17, posX18, posX19, posX20;
float posX21, posX22, posX23, posX24, posX25, posX26, posX27, posX28, posX29, posX30;
float posX31, posX32, posX33, posX34, posX35, posX36, posX37, posX38, posX39, posX40;
float posX41, posX42, posX43, posX44, posX45, posX46, posX47, posX48, posX49, posX50;

Agora, para as mesmas 50 figuras, mas incluindo ambas as posições, você pode declarar as variáveis usando vetores:

Código 2.15 -

float[] posX = new float[50];
float[] posY = new float[50];

É bem clara a vantagem de se utilizar vetores. Conforme você verá em diversos exemplos deste livro, eles são empregados estrategicamente para reduzir e estruturar o código. Antes de finalizar este tópico você deve saber que é possível consultar o número total de variáveis que um vetor previamente declarado pode armazenar. Isso é feito escrevendo o nome do vetor, seguido de um ponto e a palavra length. Veja essa propriedade no exemplo abaixo:

Código 2.16 -

float[] posX = new float[50];
float[] posY = new float[50];

// Exibe o tamanho do vetor (50) no console do PDE:
println("O tamanho de posX é:", posX.length);

2.1.3 Cores

Cores são elementos primordiais quando se almeja a elaborar mensagens visuais. Suas combinações podem causar sentimentos e emoções, e sua ausência pode ser capaz de transmitir mensagens através de contrastes acentuados. No Processing as cores são igualmente significativas e possuem um tipo próprio de variável chamada de color .

Cabe lembrar que computadores lidam apenas com números e não sabem o que é a cor vermelha ou azul, então como eles interpretam informações sobre elas? Eles trabalham com o que é especificado de canais de cores que são estruturas dedicadas ao armazenamento de uma imagem, definindo se ela é ou não colorida e se ela possui ou não transparência. O Processing permite dividir imagens em até quatro canais:

  • 1 Canal: Imagens formadas pelo preto, branco e tons de cinza.
  • 2 Canais: Imagens formadas pelo preto, branco, tons de cinza e com suporte para transparência.
  • 3 Canais: Imagens coloridas.
  • 4 Canais: Imagens coloridas e com suporte para transparência.

Um computador constrói uma imagem digital colorida básica como três grandes matrizes de cores, em que cada uma é uma cópia da imagem original, mas somente com valores referentes a uma das cores primárias: Vermelho, Verde ou Azul (Espaço RGB - Red, Green, Blue) [3]. Quando os canais são unidos ocorre a sobreposição das cores, revelando uma imagem colorida, veja a figura 2.9. Uma imagem em tons de cinza possui os mesmos valores RGB nos três canais e, portanto, um canal é o bastante para representá-la. A variável color do Processing armazena as cores com uma filosofia muito similar ao que acabamos de discutir. Ela pode ser declarada da seguinte maneira:

Código 2.17 -

// Declaração de uma variável "color", RGBA, genérica:
r = 100; // r = Canal R: Vermelho;
g = 220; // g = Canal G: Verde;
b = 220; // b = Canal B: Azul;
a = 255; // a = Canal A: Transparência.
color cor = color(r,g,b,a);
Figura 2.9 - Foto decomposta nos canais RGB.

Cada um dos argumentos da variável color emula um canal de cor (vermelho, verde e azul) cujo valor é um número inteiro que varia entre 0 e 255[4]. O número 0 indica a completa ausência da cor enquanto o número 255 indica uma total presença dela, podendo ser vistos na figura 2.10. Todas as outras cores são formadas através da combinação dessas três cores primárias que, no Processing, é um sistema aditivo de cores, ilustrado na figura 2.11. Uma cor RGB com valores color(255,0,0) indica o vermelho puro, e color(255,0,255) é a combinação do vermelho com azul, formando o violeta. As cores monocromáticas seguem essas mesmas regras, mas você pode especificar a cor em apenas um canal. Por exemplo, o preto é dado por color(0), o branco por color(255) e o cinza por qualquer valor entre eles, como color(127). Você pode usar a ferramenta seletora de cores do próprio Processing para identificar qual combinação de RGB leva a sua cor desejada. Ela pode ser acessada através do menu Ferramentas e depois Seletor de Côr..., e sua janela é mostrada na figura 2.12.

Figura 2.10 - Representação das cores de acordo com seus valores.
Figura 2.11 - Aditivo.
Figura 2.13 - Subtrativo.
Figura - 2.14 - Sistema de cores.
Figura 2.12 - Ferramenta Color Picker do Processing.

O canal relativo à transparência, muitas vezes chamado de alfa, funciona de maneira invertida. O número 0 indica uma cor completamente transparente (invisível) e 255 indica uma cor totalmente opaca (ou sem transparência). Você pode acompanhar alguns exemplos de como usar a variável do tipo color no código abaixo:

Código 2.18 -

void setup() {
size(325,175);
// Preenche o fundo da janela de branco:
background(255);

// Declara a cor vermelha como 255 apenas no canal R:
color vermelho = color(255,0,0);

// Preenche a figura com a cor vermelha:
fill(vermelho);
rect(25,50,75,75);

// Declara a cor azul como 255 apenas no canal B:
color azul = color(0,0,255);
fill(azul);
rect(125,50,75,75);

// Usa diretamente a cor violeta como 255 no canal R e no canal B:
fill(255,0,255);
rect(225,50,75,75);
}
Figura 2.15 - Figuras coloridas usando a variável color.

Ao incluir um valor de alfa, as figuras desenhadas serão em parte transparentes, permitindo que ocorra a combinação das cores durante a sobreposição de figuras, veja 2.16:

Código 2.19 -

void setup() {
size(175,175);
background(255);
// Remove a borda das figuras:
noStroke();

// Vermelho 50% transparente:
fill(255,0,0,127);
rect(25,25,75,75);

// Verde 50% transparente:
fill(0,255,0,127);
rect(50,50,75,75);

// Violeta 50% transparente:
fill(255,0,255,127);
rect(75,75,75,75);
}
Figura 2.16 - Figuras transparentes usando a variável color.

2.2 Operadores

O próximo tópico a ser discutido são as operações e operadores, ambos utilizados frequentemente na programação. Conforme você viu anteriormente, às vezes é preciso escrever uma variável em função de outras. Em 2.5 você definiu parte do vértice da figura como uma soma, (posX + ladoQ), usando o operador da adição, um dos muitos que existem. A maioria dos deles você já conhece do ensino médio e outros você usa no seu dia a dia sem perceber que são formalmente operadores. Eles podem ser divididos em quatro categorias intuitivas.

2.2.1 Aritméticos

Operadores aritméticos são os operadores básicos da matemática que você aprende na escola, como soma, subtração, multiplicação e divisão. Você pode ver uma lista completa na tabela 2.3.

Nome Operador Exemplo Resultado
Adição + num = 1+2; num será 3.
Subtração - num = 10-3; num será 7.
Multiplicação * num = 4*5; num será 20.
Divisão / num = 16/8; num será 2.
Módulo % num = 8%3; num será o resto da divisão inteira de 8 por 3, ou seja, 2.
Incremento ++ num++; num terá seu valor aumentado em 1.
Decremento -- num--; num terá seu valor diminuído em 1.
Negação - num = -num; num terá seu valor multiplicado por -1.
Tabela 2.3 - Operadores aritméticos no Processing.

Ao escrever operações com números ou variáveis é possível usar parênteses para controlar a precedência de todos os tipos de avaliações. Veja alguns exemplos:

Código 2.20 -

int num1 = 10, num2 = 5, num3 = 3;

int result1 = (num1 + num2) * num3;
println(result1); // Será igual a 15*3, ou 45.

int result2 = (num1 * num3) / (num2 * num3);
println(result2); // Será igual a 30/15, ou 2.

num1++;
println(num1); // Exibirá 11, que é o resultado de 10 + 1.

num2--;
println(num2); // Exibirá 4, que é o resultado de 5 - 1.

println(-num3); // Exibirá -3, que é o resultado de -1*3.

2.2.2 Atribuição

O segundo tipo, operadores de atribuição, são aqueles em que também são realizadas operações matemáticas, mas o sujeito da operação é a própria variável em que foi chamado o operador. Sendo assim, a atribuição do resultado é imediata. Estes operadores estão listados na tabela 2.4.

Nome Operador Exemplo Resultado
Atribuição = num = 1; num terá seu valor alterado para 1.
Atribuição por Adição += num += 5; num terá seu valor aumentado em 5.
Atribuição por Subtração -= num -= 5; num terá seu valor subtraído em 5.
Atribuição por Multiplicação *= num *= 5; num terá seu valor multiplicado por 5.
Atribuição por Divisão /= num /= 5; num terá seu valor dividido por 5.
Tabela 2.4 - Operadores de atribuição no Processing.

Sempre que se faz uma operação de atribuição é importante ter em mente que o valor da variável irá mudar, afetando qualquer operação futura. Veja o exemplo abaixo:

Código 2.21 -

int num1 = 2;
println(num1 += 3); // num1 = (2 + 3) = 5
println(num1 *= 4); // num1 = (5 * 4) = 20
println(num1 /= 10); // num1 = (20/10) = 2
println(num1 -= 2); // num1 = (2 - 2) = 0

2.2.3 Relação

Os operadores de relação são os responsáveis por comparar variáveis. Ao utilizá-los, você poderá dizer se uma variável é maior, menor, igual ou diferente de outra. O resultado de uma operação de relação será sempre verdadeiro (true) ou falso (false), ou seja, uma variável do tipo booleana. A lista completa está na tabela 2.5.

Nome Operador Exemplo Resultado
Maior > println(4 > 5); False. O número 4 não é maior que o número 5.
Menor < println(4 < 5); True. O número 4 é menor que o número 5.
Igual a == println(4 == 4); True. O número 4 é igual ao número 4.
Diferente de != println(4 != 4); False. O número 4 não é diferente do número 4.
Maior ou Igual >= println(4 >= 4); True. O número 4 não é maior que 4, mas é igual a 4 então a comparação é verdadeira.
Menor ou Igual <= println(5 <= 4); False. O número 5 não é menor que 4 nem igual a 4, portanto a comparação é falsa.
Tabela 2.5 - Operadores de relação no Processing.

2.2.4 Lógicos

Os últimos, mas não menos importantes, são chamados de operadores lógicos, que realizam operações sobre variáveis booleanas. Na prática eles serão utilizados para concatenar diversas operações relacionais. Isso será muito bem vindo nos condicionais e estruturas de repetição conforme você verá mais adiante. Estes operadores podem ser consultados na tabela 2.6.

Nome Operador Exemplo Resultado
E (AND) && println((4<5) && (5>1)); True. O operador E retorna verdadeiro se, e somente se, todas as condições forem simultaneamente verdadeiras.
OU (OR) || println((4<5) || (5>6)); True. O operador OU retorna verdadeiro se pelo menos uma de todas as condições forem verdadeiras.
NÃO (NOT) ! println(!(4>5)); True. O operador NÃO inverte o resultado booleano. Como 4 não é maior que 5, o resultado é falso, porém o operador NÃO inverte esse resultado tornando-o verdadeiro.
Tabela 2.6 - Operadores lógicos no Processing.

Um exemplo contendo múltiplas comparações:

Código 2.22 -

boolean comp = ((4 > 5) || (5 <= 6)) && !(10 < 20);

// Por partes:
// (4 > 5) -> False
// (5 <= 6) -> True
// (False || True) -> True
// (10 < 20) -> True, mas temos uma negação, logo !(10 < 20) -> False
// Portanto:
// (True && False) -> False
println(comp);

2.2.5 Operadores e cores

Um pequeno alerta sobre operadores e a variável color. Se você quiser aplicar operadores de qualquer natureza sobre esse tipo de variável é importante que você realize as operações individualmente por canal e posteriormente os transforme em argumentos de uma variável color. Algo do tipo:

Código 2.23 -

int r = 2*30;
int g = 100;
int b = 255/5;
color cor = color(r,g,b);

Realizar as operações diretamente sobre uma variável do tipo color não é uma violação na programação, mas não faz sentido do ponto de vista da matemática uma vez que ela é composta por outras variáveis (até 4 canais). Além disso, você deve ter em mente que os valores mínimos e máximos por canal são, respectivamente, 0 e 255. Qualquer número fora desse intervalo será reajustado para dentro do mesmo no ato da definição da variável.

2.2.6 Funções matemáticas

Os operadores são a base de uma quantidade infinita de algoritmos e operações matemáticas e serão usados em quase todo código que você escrever. No entanto eles não cobrem diretamente algumas funções matemáticas úteis que são recorrentes em algoritmos. O Procesing possui as mais comuns programadas e cabe a você apenas chamá-las. Resta a observação que elas são funções e não operadores, mas foram colocados nesta seção devido à abordagem matemática da mesma. Parte delas podem ser vistas na tabela 2.7 e as demais consultadas na referência.

Função Descrição
round(num) Arredonda um número do tipo float, que possui casas decimais, para um número inteiro. Se a parte decimal for menor que 0.5, o número é arredondado para baixo, e se for maior ou igual a 0.5, para cima.
ceil(num) Arredonda o número entre parênteses para cima independente de sua parte decimal.
floor(num) Arredonda o número entre parênteses para baixo independente de sua parte decimal.
max(num1,num2) Retorna o maior número entre os dois fornecidos.
min(num1,num2) Retorna o menor número entre os dois fornecidos.
pow(num1,num2) Realiza a exponenciação de num1 por num2.
sqrt(num1) Calcula a raiz quadrada do número fornecido.
Tabela 2.7 - Funções matemáticas implementadas no Processing.

Existe ainda uma função capaz de converter o valor de uma variável que possua sua escala entre um mínimo e máximo original, para um novo mínimo e máximo. Sua chamada é feita da seguinte maneira:

Código 2.24 -

float resultado = map(valor,originalMin,originalMax,novoMin,novoMax);

A função map() é particularmente útil, pois ela permite mapear, ou transformar, um valor em outro, veja a figura 2.17 para uma explicação visual. Em uma aplicação concreta, suponha que você queira que, ao movimentar o mouse horizontalmente pela janela de saída, a sua figura mude da cor preta para a vermelha. Neste caso você quer que uma cor seja o resultado de um mapeamento da posição do mouse. A posição horizontal do mouse (variável mouseX) possui valores entre zero e o tamanho da janela (variável width). Simultaneamente, o canal referente a cor vermelha, está contido entre 0 e 255, indicando a completa ausência do vermelho (restando o preto) até o vermelho puro. Sabendo dos máximos e mínimos da escala da variável mapeada (mouseX) e da variável de saída (canal vermelho), é possível usar a função map() para causar uma transição de cores na sua figura. Veja o código a seguir:

Código 2.25 -

void draw() {
background(255);

// Posição do mouse (mouseX) vai de 0 ao tamanho da janela (width).
// Cor vermelha vai de 0 (preto) a 255 (vermelho).
float r = map(mouseX,0,width,0,255);
color cor = color(r,0,0);
fill(cor);
ellipse(50,50,50,50);
}
Figura 2.17 - Representação visual da função map().
Figura 2.18 - Resultado do mapeamento do mouse para uma cor.

2.3 Funções

O conceito de função foi explicado superficialmente na seção 1.4 e agora será abordada a parte técnica sobre como você pode declarar e usar suas próprias funções. Um função, script ou sub-rotina pode ser definida como um conjunto de instruções agrupadas em um bloco, executadas sempre que você referenciá-la pelo seu nome. Suas vantagens incluem organização e redução de linhas de código, assim como reutilização em programas futuros. Vamos justificar e apreciar o poder das funções através de um estudo de caso. Você usou as linhas de código 2.5, repetidas aqui por conveniência, para desenhar um único quadrado:

Código 2.26 -

float posX = 25;
float posY = 25;
float ladoQ = 50;

line(posX, posY, posX + ladoQ, posY);
line(posX + ladoQ, posY, posX + ladoQ, posY + ladoQ);
line(posX + ladoQ, posY + ladoQ, posX, posY + ladoQ);
line(posX, posY + ladoQ, posX, posY);

E se você quisesse desenhar um número maior, na ordem de dezenas? Seria necessário repetir essas mesmas linhas dezenas de vezes, tornando seu código denso, e sem agregar nenhum valor. Conforme estudado o quadrado é formado por quatro linhas desenhadas de maneira parametrizada caso você possua o ponto de um vértice desse quadrado e o tamanho de seu lado. Seria muito conveniente se você pudesse fornecer apenas esses três dados, posição x, posição y e lado, e o computador desenhasse essa figura para você. As funções servem exatamente para isso, para executar uma série de instruções que são repetidas ao longo do código. Assim como uma variável, uma função precisa ser declarada antes que possa ser usada e isso deverá ser feito fora do setup(), draw() ou de qualquer outra função. A declaração de uma função genérica é feita seguindo o modelo abaixo:

Código 2.27 - Estrutura de uma função genérica -

tipoDaVariavelDeRetorno nomeDaFuncao(tipoVar1 var1, tipoVar2 var2, ..., tipoVarN varN) {

// Corpo da função;

return variavelDeRetorno;
}

Elas podem ser usadas no código ao escrevermos o nome da rotina seguido de parênteses contendo todos os argumentos de entrada da mesma:

Código 2.28 -

nomeDaFuncao(arg1, arg2, ..., argN);

A estrutura completa de uma função pode ser intimidadora em um primeiro momento, mas ela sempre segue essa mesma forma. Uma vez que você aprender sobre cada um dos elementos que a compõe, você estará pronto para criar as suas próprias funções. Abaixo você pode consultar, com mais detalhes, cada um deles:

  • Tipo da variável de retorno: Se uma função realiza tarefas e devolve uma variável qualquer, o tipo de retorno deverá igual ao tipo dessa variável. Por exemplo, se devolver um número inteiro, o tipo de retorno será int, ponto flutuante, ele será float, e assim por diante de acordo com as variáveis que você estudou (tabela 2.1). A função map() é um exemplo cujo tipo de retorno é float, já que ela retorna um valor transformado que é o resultado de um cálculo. Por outro lado, se a função não retornar nada, o tipo dela será void . Isso é comum em funções que só exibem algo na tela ou no console, como rect() e println().
  • Nome: É o nome que você usará para chamar a função no programa. Utiliza-se das mesmas regras de nomeação de variáveis.
  • Parâmetros: Muitas vezes as instruções executadas pelas funções dependem de valores que são externos a elas. No ato da declaração da função, as variáveis que fazem a interface entre a parte interna dela e o código principal do programa são chamadas de parâmetros. No ato de chamada da função no código o nome do termo muda e o correto é falar que passamos argumentos para a função. Alguns padrões de escrita de código sugerem que os nomes dos parâmetros sejam precedidos pelo subtraço (_), mas isso não é obrigatório.
  • Corpo: É o código que compõe a função em si. Ele pode ser composto por uma quantidade arbitrária de linhas de código contendo funções pré declaradas (suas e do Processing), variáveis, condicionais, repetições e demais estruturas de programação. Em geral, tudo que você pode usar fora de uma função você pode usar dentro dela, com uma diferença importante, variáveis declaradas dentro de uma função são perdidas após a execução da mesma.
  • Retorno: O campo return permite que a função devolva uma variável ao programa principal. Ele deve estar na última linha do corpo de uma função e é obrigatório apenas se a mesma retornar alguma variável. Neste caso, a variável de retorno deverá ser colocada na frente da palavra return no corpo da função e seguido de um ponto e vírgula. Funções precedidas da palavra void não precisam ter esse campo visto que não retornam nenhuma variável.

Você pode usar essas informações para construir qualquer função personalizada. Um bom exercício para fixar esse conhecimento é migrar um código que escrevemos previamente, como o 2.5, referente ao desenho de um quadrado. O fluxo ideal para projetar uma função é analisar as ações que você quer que ela realize e listá-las em tópicos para um entendimento mais claro dos objetivos. Veja o exemplo:

Projeto de uma função para desenhar um quadrado:

  • A função deve ser autoexplicativa: A função deve ter um nome esclarecedor, que remete ao seu propósito, tal como quadrado().
  • A função apenas exibirá um quadrado: O desenho da figura será feito através de quatro instruções line(). Ela não irá retornar nenhum valor ou variável e, portanto, é precedida pelo tipo void e não possuirá o campo return.
  • Sua posição é determinada por um vértice: O desenho do quadrado será a partir do seu canto superior esquerdo, com coordenadas x e y informadas pelo usuário, implicando em dois argumentos de entrada da função.
  • Seus lados não serão fixos: O tamanho dos lados do quadrado também será definido pelo usuário. Como um quadrado possuí todos os lados com o mesmo tamanho, será necessário informar apenas um valor, sendo este o terceiro argumento de entrada da função.

Com isso definimos o nome da sub-rotina, suas funcionalidades (corpo) assim como tipo e o retorno e, por fim, os seus argumentos de entrada (ou parâmetros na declaração). Uma vez que todos os pré-requisitos foram satisfeitos, você pode escrevê-la usando modelo apresentado anteriormente. Para chamá-la no programa, basta que você utilize o nome dela e passe a posição e tamanho do lado do quadrado que deseja desenhar:

Código 2.15 -

void quadrado(float _posX, float _posY, float _lado) {
// Desenha um quadrado composto por quatro arestas:
line(_posX, _posY, _posX + _lado, _posY);
line(_posX + _lado, _posY, _posX + _lado, _posY + _lado);
line(_posX + _lado, _posY + _lado, _posX, _posY + _lado);
line(_posX, _posY + _lado, _posX, _posY);
}

void setup() {
// Chamada da função:
quadrado(25,25,50);
}

Finalmente, é pertinente citar que você é livre para declarar duas ou mais funções com o mesmo nome[5] , desde que o número dos argumentos de entrada delas sejam diferentes entre si. O código abaixo demonstra que não ocorrem erros ao executar o programa:

Código 2.29 -

void quadrado(float _posX, float _posY, float _lado) {
// Desenha um quadrado composto por quatro arestas:
line(_posX, _posY, _posX + _lado, _posY);
line(_posX + _lado, _posY, _posX + _lado, _posY + _lado);
line(_posX + _lado, _posY + _lado, _posX, _posY + _lado);
line(_posX, _posY + _lado, _posX, _posY);
}

void quadrado(float _posX, float _posY) {
// Exibe as variáveis passadas para a função no console:
println("Variável 1:", _posX, "Variável 2 Y", _posY);
}

void quadrado() {
// Desenha um quadrado de lado e posição fixa:
rect(0,0,50,50);
}

void setup() {
quadrado(25,25,50);
quadrado(25,25);
quadrado();
}

2.3.1 Interrupções

O Processing possui uma grande base de funções que podem ser usadas durante a execução de seus programas. A maioria delas deve ser chamada explicitamente e sequencialmente, significando que elas seguem a ordem de execução em que elas foram escritas no código. Em contrapartida, existe uma outra classe de funções, nomeadas de interrupções, que são invocadas assincronamente sempre que um evento específico ocorrer, disparando sua execução. Elas estão usualmente relacionadas a acontecimentos externos ao programa, como intervenções através do clique do mouse ou do pressionar de uma tecla. As interrupções são habituais quando se espera algum tipo de interação com o usuário do programa. No Processing, elas se comportam como funções pré-declaradas, mas não executam nenhuma ação. Você pode customizá-las e adicionar instruções que você quer que elas realizem através do incremento do corpo da mesma, assim como você faz com setup() ou draw(). Elas podem ser consultadas na tabela 2.8.

Função Descrição
mousePressed() Executa a interrupção sempre que algum botão do mouse for pressionado.
mouseReleased() Executa a interrupção sempre que algum botão do mouse for solto.
mouseWheel() Executa a interrupção sempre que a roda do mouse (scroll) for girada.
mouseDragged() Executa a interrupção sempre que o mouse for arrastado e algum botão dele estiver pressionado.
keyPressed() Executa a interrupção sempre que alguma tecla for pressionada.
keyReleased() Executa a interrupção sempre que alguma tecla for solta.
Tabela 2.8 - Interrupções nativas do Processing.

O exemplo abaixo mostra como usar interrupções para desenhar figuras na tela sempre que o mouse for arrastado, o resultado é mostrado na figura 2.19.

Código 2.30 -

void setup() {
size(400,200);
background(255);
}

void draw() {
}

// Interrupção - Mouse pressionado e arrastado:
void mouseDragged() {
float raio = dist(mouseX,mouseY,pmouseX,pmouseY);
ellipse(mouseX,mouseY,raio,raio);
}

A linha de código que contém a função dist() serve para calcular a distância euclidiana entre dois pontos. Ao usarmos a posição do mouse atual e a do frame passado como argumentos de entrada temos como resultado uma medida do quão rápido o mouse foi movimentado entre um frame e outro. Se a velocidade de arraste é elevada, então são desenhados círculos grandes, se não, círculos pequenos. O único ponto que merece maior destaque no código é o fato de ter sido incluída a função draw() vazia. Isso se faz necessário para que o programa entre em modo de animação, caso contrário seria gerado apenas um quadro, congelando qualquer interação com o usuário.

Figura 2.19 - Desenho na janela ao arrastar o mouse.

2.4 Condicionais

A próxima ferramenta que você irá aprender são os condicionais. Eles são definidos como estruturas de controle de fluxo de um programa que verificam se uma determinada condição é verdadeira ou falsa, e executam linhas de código baseadas nesse julgamento. Sua forma geral é dada por:

Código 2.31 -

se (condição) {
// Se a condição for verdadeira, execute estas linhas de código.
}

caso contrário {
// Se a condição for falsa, execute estas linhas de código.
}

No Processing temos o par de palavras chave if/else

Código 2.32 -

if (condicao) {
// Se “condicao” for verdadeiro (true), execute estas linhas de código.
}

else {
// Se “condicao” for falso (false), execute estas linhas de código
}

O campo da condição neste tipo de estrutura deve ser, obrigatoriamente, verdadeiro ou falso. Nada impede que ele seja uma variável ou uma série de operações sobre variáveis contanto que, na avaliação final, o campo tenha um resultado do tipo booleano. Você já estudou sobre esse tipo de variável nas seções passadas, então vamos brevemente relembrar algumas condições que são e não válidas para esse campo:

  • true ou false: Válido, essas são variáveis booleanas que indicam verdadeiro ou falso.
  • (2 + 3): Não válido, o resultado de uma soma é um número da mesma natureza, por exemplo float ou int.
  • (2 + 3) > 1: Válido, o resultado da comparação 5 > 1 é verdadeiro (true) que é uma variável booleana.
  • (2 + 3) > 7: Válido, o resultado da comparação 5 > 7 é falso (false) que é uma variável booleana.

Essa estrutura é bem vinda quando se deseja alterar o comportamento do programa baseado em condições atuais da execução. Considere o caso em que você queira preencher de preto a respectiva metade da janela de exibição de acordo com a posição do seu cursor. Esse cenário é ideal para o uso de uma estrutura if/else já que é necessária uma condição (mouse do lado direito ou esquerdo da tela) para realizar uma ação (preencher a metade de preto), veja a figura 2.20. Utilizando condicionais você poderia escrever o código abaixo:

Código 2.30 -

void setup() {
size(300,150);
}

void draw() {
background(210);
fill(0);
// Se o mouse estiver na parte esquerda da janela ela é preenchida com um retângulo
// preto. Caso contrário, o mouse estará na parte direita, e ela é preenchida.
if(mouseX < width/2) {
rect(0,0,width/2,height);
}
else {
rect(width/2,0,width/2,height);
}
}

A condição if(mouseX < width/2) verifica se a posição horizontal do mouse está na metade esquerda da janela. Veja a figura 2.21 como exemplo.

Figura 2.20 - Posição do mouse e preenchimento da janela de exibição.
Figura 2.21 - Divisão da janela de saída e coordenadas do eixo x.

Sempre que você quiser verificar uma condição na direção horizontal da janela (direito/esquerdo) você precisará testar apenas o eixo coordenado na direção x, como foi o caso. Se você quisesse verificar uma condição na direção vertical (cima/baixo), bastaria fazer os testes com o eixo y. Focando na estrutura do condicional, note que só é necessário testar se o cursor está no lado esquerdo da janela pois, caso contrário (else), ele estará do lado direito. Cabe evidenciar que o computador só avaliará as condições do seu programa dentro dos limites dele, isto é, posições do cursor fora da janela de exibição serão ignoradas. Neste caso, o valor de mouseX permanecerá congelado na sua última posição conhecida.

2.4.1 Múltiplos condicionais

Na seção passada você escreveu um código que testava apenas duas condições, atendidas completamente por uma única estrutura if/else. De maneira equivalente, é possível fazer múltiplos testes inserindo mais condicionais dentro de estruturas desse próprio tipo. Isto é chamado de aninhamento de condicionais ou nested conditionals. Considere o código em que foi criada uma interrupção que testa qual letra foi pressionada pelo usuário (variável do sistema key ) e desenha uma figura baseada nela. Pressionar a tecla 'a' desenha um quadrado, 's' um círculo, 'd' um triângulo, 'f' um losango e qualquer outra tecla limpa a tela, veja a figura 2.22.

Figura 2.23 - 'a'.
Figura 2.24 - 's'.
Figura 2.25 - 'd'.
Figura 2.26 - 'f'.
Figura - 2.22 - Comportamento do programa baseado nas teclas pressionadas.

Código 2.33 - Aninhamento de condicionais completo -

void draw() {
}

void keyPressed() {
background(255);
noStroke();

if (key == 'a') {
fill(255, 0, 0);
rect(25, 25, 50, 50);
}
else {
if (key == 's') {
fill(0, 255, 0);
ellipse(50, 50, 50, 50);
}
else {
if (key == 'd') {
fill(0, 0, 255);
triangle(50, 25, 25, 75, 75, 75);
}
else {
if (key == 'f') {
fill(0, 0, 0);
quad(50, 25, 25, 50, 50, 75, 75, 50);
}
}
}
}
}

Note que dentro da cada estrutura else é criada uma outra if/else para continuar as comparações da tecla pressionada com uma das letras específicas para o desenho. Esse código também pode ser visto em forma de fluxograma como mostrado na figura 2.27.

Figura 2.27 - Fluxograma referente ao código 2.33.

Infelizmente o código 2.33 sofre de um problema típico de aninhamentos, a perda da legibilidade. Quanto mais comparações forem feitas, mais confusa ficará a escrita do código, principalmente se as instruções a serem executadas forem compostas por várias linhas. É possível contornar esse inconveniente reescrevendo o código de forma a separar completamente as comparações. Considere o exemplo:

Código 2.34 - Aninhamento de condicionais separados -

void draw() {
}

void keyPressed() {
background(255);
noStroke();

if (key == 'a') {
fill(255, 0, 0);
rect(25, 25, 50, 50);
}

if (key == 's') {
fill(0, 255, 0);
ellipse(50, 50, 50, 50);
}

if (key == 'd') {
fill(0, 0, 255);
triangle(50, 25, 25, 75, 75, 75);
}

if (key == 'f') {
fill(0, 0, 0);
quad(50, 25, 25, 50, 50, 75, 75, 50);
}
}

É bem mais agradável analisar o código acima, mas veja que ele é acometido por um outro contratempo. Imagine que você pressione a tecla 'a', que é o primeiro teste de ambos os códigos, 2.33 e 2.34. Em 2.33 o teste será realizado e dado como verdadeiro, desta forma nenhum dos outros condicionais serão processados, pois eles se encontram dentro da estrutura else. Já em 2.34 a primeira condição será dada como verdadeira e será desenhado um quadrado, no entanto os outros condicionais não estão contidos dentro de um else e, por isso, também serão testados, ilustrado no fluxograma 2.28. Neste caso, enquanto o código 2.33 realizou apenas um teste, o código 2.34 realizou quatro, sendo bem menos eficiente do ponto de vista computacional. Claro que cabe ao programador balancear esses pontos, pois algumas vezes esse impacto será tão insignificante que é mais vantajoso manter um código limpo do que implementar micro otimizações de baixo ganho.

Figura 2.28 - Fluxograma referente ao código 2.34.

Uma alternativa para quem deseja manter a eficiência e uma interface limpa é utilizar a estrutura switch cuja forma é mostrada abaixo:

Código 2.35 -

switch(variavel) {

case teste1:
// Código a ser executado se a comparação: (variavel == teste1) for verdadeira.
break;

//...

case testeN:
// Código a ser executado se a comparação: (variavel == testeN) for verdadeira.
break;
}

Os códigos 2.33 e 2.34 convertidos para esse novo modelo seriam dados por:

Código 2.36 -

void draw() {
}

void keyPressed() {
background(255);
noStroke();

switch (key) {
case 'a':
fill(255, 0, 0);
rect(25, 25, 50, 50);
break;

case 's':
fill(0, 255, 0);
ellipse(50, 50, 50, 50);
break;

case 'd':
fill(0, 0, 255);
triangle(50, 25, 25, 75, 75, 75);
break;

case 'f':
fill(0, 0, 0);
quad(50, 25, 25, 50, 50, 75, 75, 50);
break;
}
}

Uma grande limitação do elemento switch é que seus testes requerem as chamadas expressões constantes, ou seja, apenas comparações de igualdade (==) são válidas. Testes que usem de outros operadores (>, <, >=, <=, !=) ou cujas variáveis de teste não sejam int, String, char ou enum são considerados como erros durante a compilação.

Esta subseção, mais do que apresentar sobre outras formas de escrever condicionais, foi importante do ponto de vista de desenvolvimento de código. Ela foi desdobrada com o intuito de mostrar a você como pequenas escolhas na escrita de um bloco de instruções podem impactar antes mesmo da execução do programa, em sua legibilidade e, ao mesmo tempo, em sua performance durante a execução, relativo ao tempo de processamento. Apesar dessas informações serem mais voltadas para a parte técnica da programação, você deve ficar atento a esses tipos de detalhes, pois podem ocorrer não só em condicionais, mas em qualquer outro tipo de estrutura de código.

2.5 Repetições

O último tópico que compõe a formação básica de um programador são as repetições ou loops . Basicamente esse tipo de estrutura permite que parte de seu código seja repetido um número arbitrário de vezes em uma fração das linhas que seriam necessárias. Até agora, se você quisesse desenhar um número elevado de figuras, digamos mil, seria necessário repetir mil vezes as linhas para desenhar essas figuras. Imagine quanto tempo você demoraria para chamar mil funções rect() e depois percebesse que esse número era excessivo e que, na verdade, você gostaria de 500 retângulos. Você iria ficar no mínimo frustrado por ter escrito o dobro do que realmente precisava, além de ter perdido um tempo precioso. As repetições foram criadas para evitar essa e outras situações semelhantes e ainda permitir um engrandecimento de seu programa por possibilitar criar mais conteúdo com menos linhas de código. Estruturas de repetição têm a seguinte forma:

Código 2.37 -

enquanto (condição) {
// Código a ser repetido.
}

que no Processing se reflete com o uso do while

Código 2.38 -

while(condicao) {
// Código a ser repetido.
}

O campo de condição do while funciona de maneira muito semelhante ao da estrutura if/else, de tal forma que se a condição dentro dos parênteses for verdadeira (true) todo código dentro das chaves será executado. A diferença é que enquanto essa condição for verdadeira, o código continuará a ser repetido. É necessário tomar um pouco de cuidado com qual condição se coloca nessa estrutura, pois caso contrário pode ser que a execução do seu código fique presa para sempre em uma repetição. Ela deve ser projetada para executar apenas um número finito de iterações. É usual criar uma variável de controle que é verificada no condicional do while e incrementada no corpo da repetição. Por exemplo, se você quisesse desenhar 50 quadrados centralizados na tela, como na figura 2.29, poderia escrever o seguinte código:

Código 2.39 - Figuras concêntricas -

void setup() {
size(300,300);
background(255);
noFill();

float x = 0, y = 0, lado = 0;

int contador = 0;

while(contador < 50) {
lado = lado + 5;
x = width/2 - lado/2;
y = height/2 - lado/2;

rect(x,y,lado,lado);

contador++;
}
}
Figura 2.29 - Desenho de quadrados usando repetições.

Vamos analisar com mais detalhes alguns pontos desse código:

Código 2.40 -

float x = 0, y = 0, lado = 0;

As variáveis de posicionamento e tamanho do quadrado são criadas e inicializadas. A estratégia é fazer o quadrado começar com um tamanho de lado desprezível e aumentá-lo gradativamente.

Código 2.41 -

int contador = 0;

Neste ponto é definida a variável que irá armazenar quantas vezes a repetição foi executada. É ela quem faz o controle do loop e provocará uma condição de parada da estrutura de repetição que, caso contrário, continuaria para sempre.

Código 2.42 -

while(contador < 50)

Aqui é declarado o início de uma repetição e sua condição de parada. Note que o código interno (dentro das chaves do while) será executado enquanto a condição (contador < 50) for verdadeira.

Código 2.43 -

lado = lado + 5;
x = width/2 - lado/2;
y = height/2 - lado/2;

A primeira tarefa que é feita dentro do loop é aumentar o tamanho do lado do quadrado. Assim, cada vez que uma iteração da repetição for executada, o quadrado se torna maior criando um padrão formado por figuras concêntricas. Logo em seguida são calculadas as novas posições x e y que serão passadas para a função rect(). É necessário atualizar essas posições uma vez que essa função usa o canto superior esquerdo para desenhar a figura. A centralização é feita com base no centro da janela de exibição, ponto (width/2, height/2), subtraída da metade do lado do quadrado. Essa é a mesma filosofia aplicada na seção 1.5. É de suma importância que essas atualizações sejam feitas dentro da estrutura de repetição para que o quadrado cresça a cada iteração.

Código 2.44 -

rect(x,y,lado,lado);

Uma vez que as coordenadas foram atualizadas, a figura é desenhada.

Código 2.45 -

contador++;

Nesta linha a variável contador é incrementada em um. Isso significa que após a primeira repetição o valor de contador será 1, após a segunda ele será 2 e assim sucessivamente até atingir 50. Quando isso acontecer, o condicional da estrutura de repetição se tornará falso e o código interno ao while deixará de ser executado.

Veja o poder incrível dessa nova ferramenta. Em vez de escrever mais de 70 linhas de código, você conseguiu reduzir para 22. E se você quisesse agora mais ou menos quadrados? Seria necessário alterar apenas a condição da repetição de (contador < 50) para outra como (contador < 5000), sem adicionar nenhuma linha de código a mais. A grande vantagem dessa estrutura é que você pode focar na construção de apenas um elemento e depois repeti-lo para formar um padrão complexo. Na figura 2.30 foi criada uma única linha e depois repetida ao longo da janela de exibição.

Figura 2.30 - Múltiplas repetições da curva em vermelho.

2.5.1 Repetições - for

A estrutura while é conveniente quando você está aprendendo repetições, necessita de mais flexibilidade ou quando opta por conjunções mais elaboradas. Ela é acometida pelo inconvenientes da necessidade de declarar variáveis controle fora do loop e manualmente incrementá-las. Uma estrutura correspondente, compacta e mais popular é a for . Veja sua forma abaixo:

Código 2.46 -

for(variavel; condicao; incremento) {
// Código a ser repetido.
}

Esta estrutura possuí em uma única linha, respectivamente, a declaração e inicialização da variável de controle, a condição de parada da repetição e o incremento a cada iteração. A seguir é mostrada uma comparação entre essas duas formas de repetição:

Código 2.47 -

int contador = 0;
while(contador < 50) {
// Código a ser repetido.
contador++;
}

O for equivalente é dado por:

Código 2.48 -

for(int contador = 0; contador < 50; contador++) {
// Código a ser repetido.
}

A grande vantagem dessa notação é que você pode se dedicar diretamente ao código a ser repetido e não precisa alterar a variável de controle (contador) manualmente em cada iteração. Neste livro é dado foco a estrutura for justamente por esses fatores.

2.5.2 Múltiplas repetições

As estruturas de repetição são ideais para economizar linhas de códigos ou transcorrer um grande volume de dados, mas na arte computacional elas também são usadas para criar ilusões e padrões. Para exemplificar esse conceito vamos criar um padrão de Truchet, cuja base é formada pelo revestimento quadriculado de um espaço. Imagine a janela de saída como uma tela dividida em blocos e considere, a princípio, apenas uma linha dessa janela, ilustrada na figura 2.31.

Figura 2.31 - Janela de saída como uma fileira de blocos.

A quantidade de blocos necessários para preencher toda a janela horizontalmente pode ser calculado utilizando a informação do tamanho dela e do bloco, que é arbitrariamente definido:

Código 2.49 -

void setup() {
size(600,200);

// Tamanho do bloco, em pixels, da divisão da janela de saída:
float lado = 50;

// Número de blocos necessários para preencher a janela horizontalmente:
float numBlocosHoriz = width/lado;

println(numBlocosHoriz);
}

Dependendo do tamanho do bloco que você escolher, como por exemplo 70 pixels, os cálculos acima podem indicar que você precisa de um número fracionário de figuras (8.57 blocos) para preencher a tela. Isso não é um impeditivo, mas pode deixar o padrão irregular, com aparência de cortado. Essa situação é evitada se você escolher um tamanho de bloco que seja um divisor de resto zero do comprimento e altura da janela de saída. Agora que você possui o número e tamanho dos blocos é preciso descobrir como desenhá-los lado a lado, de maneira sequencial. Esta será uma operação padronizada com diversos elementos, sendo uma boa candidata para se utilizar de repetições. Considere como os blocos se ajustam um ao lado do outro, figura 2.32.

Figura 2.32 - Coordenadas dos blocos que dividem a tela.

Os blocos serão desenhados pela função nativa do Processing, rect(), com o intuito de simplificar o código. É preciso lembrar que essa função recebe a posição do canto superior esquerdo de cada retângulo na sequência, assim como seu tamanho. Pela figura 2.32 você pode perceber que a posição do primeiro bloco é a origem, ou seja zero. A do segundo é igual a posição horizontal do primeiro, somada do lado do primeiro bloco, lado. A do terceiro é igual a posição inicial do segundo somado de lado, totalizando 2*lado a partir do ponto inicial. Se você continuar com esse raciocínio para obter a posição de todos os outros blocos perceberá que cada posição horizontal será igual ao número do bloco atual subtraído de um[6], que multiplica o tamanho do lado do bloco, variável lado. Sendo assim, poderia escrever o código abaixo para desenhar uma linha de quadrados:

Código 2.50 -

void setup() {
size(600,200);
background(255);

// Tamanho do bloco da divisão da janela de saída - Em pixels:
float lado = 20;

// Calcula o número de blocos na horizontal:
float numBlocosH = width/lado;

for(int i = 0; i < numBlocosH; i++) {
rect(i*lado,height/2 - lado/2,lado,lado);
}
}

Neste exemplo, a posição vertical dos blocos é uma constante apenas para centralizá-los na janela de exibição e permitir uma visualização clara do que está ocorrendo. Quanto à estrutura for, veja que a parametrização da posição horizontal dos blocos foi feita pela variável i, contida no intervalo de zero até o número total de blocos para preencher a tela. Você pode ter estranhado o uso da variável i, sem um significado concreto, como índice da repetição, mas isso é uma prática habitual para compactação do código. A variável i é apenas um contador e seu nome é insignificante no escopo do programa. Execute o código e veja como é gerada uma linha de blocos que preenche toda extensão horizontal da tela.

Você pode utilizar uma abordagem similar para preencher toda extensão vertical da tela. A única diferença é que desta vez a posição vertical de cada um dos blocos é atualizada em vez de permanecer uma constante. Os padrões de Truchet são ainda mais elaborados, formados pela divisão total da tela em blocos. Esta é uma tarefa repetitiva na qual se preenche completamente uma linha horizontal, em seguida toma-se um passo na vertical e novamente preenche-se uma linha na horizontal. Esse ritmo é mantido até que se termine de estampar a tela em sua totalidade, veja a figura 2.33.

Figura 2.33 - Preenchimento horizontal e vertical da janela.

O preenchimento sequencial em linhas e colunas requer um tipo especial de loop, em que uma estrutura de repetição está contida dentro de outra. Essa estratégia é chamada de aninhamento de repetições, ou nested loops, e é evidenciada no código abaixo:

Código 2.51 - Janela quadriculada -

void setup() {
size(600,180);
background(255);

// Tamanho do bloco da divisão da janela de saída - Em pixels:
float lado = 20;

// Calcula o número de blocos na horizontal:
float numBlocosH = width/lado;

// Calcula o número de blocos na vertical:
float numBlocosV = height/lado;

for(int i = 0; i < numBlocosH; i++) {
for(int j = 0; j < numBlocosV; j++) {
rect(i*lado,j*lado,lado,lado);
}
}
}

Em estruturas aninhadas, as repetições internas serão reiniciadas sempre que houver um passo na repetição externa. Por exemplo, no código 2.51, o for mais interno desenhará uma quantidade de retângulos igual ao valor da variável numBlocosV. Neste momento será dado um "passo" na estrutura do for externo, alterando i de zero para um e causando a repetição das linhas internas dele, novamente a estrutura for de índice j. O ciclo termina quando i atingir numBlocosH e j atingir numBlocosV, o que terá feito com que o Processing desenhasse um número de retângulos igual a multiplicação[7] dos limites das repetições, um total de (numBlocosH*numBlocosV).

Finalizado o preenchimento completo da tela com quadrados, você deve estar se perguntando o porquê de tudo isso. Realmente, até agora não há nada de interessante do ponto de vista artístico, mas a divisão da tela é importante em uma série de padrões visuais. Por exemplo, e se em vez de desenhar um bloco, você desenhasse apenas uma das diagonais dele? Você pode implementar isso substituindo a função rect() pela função line(). Após alterar o código sua janela será dividida igual a figura 2.34.

Código 2.52 - Preenchimento da tela com diagonais -

void setup() {
size(600,180);
background(255);

// Tamanho do bloco da divisão da janela de saída - Em pixels:
float lado = 30;

// Calcula o número de blocos na horizontal:
float numBlocosH = width/lado;

// Calcula o número de blocos na vertical:
float numBlocosV = height/lado;

for(int i = 0; i < numBlocosH; i++) {
for(int j = 0; j < numBlocosV; j++) {
line(i*lado,j*lado,i*lado + lado,j*lado + lado);
}
}
}
Figura 2.34 - Padrões de linhas em vez de blocos.

É possível ainda adicionar condicionais para criar padrões visuais variados. Você pode alternar entre desenhar uma ou outra diagonal de cada bloco dependendo se os contadores das repetições forem pares ou ímpares. Substitua a linha:

Código 2.53 -

line(i*lado,j*lado,i*lado + lado,j*lado + lado);

por este condicional:

Código 2.54 -

if(i%2 == 0) { //Se i é par, desenha uma diagonal:
line(i*lado,j*lado,i*lado + lado,j*lado + lado);
}
else { //Se i é ímpar, desenha a outra diagonal:
line(i*lado + lado,j*lado,i*lado,j*lado + lado);
}

e você terá imagens como a figura 2.35. No código acima o operador módulo (%) foi empregado pois ele retorna o resto da divisão de um número pelo seu dividendo. O resto de qualquer número par dividido por dois será zero e de qualquer número ímpar será um. Sendo assim, este operador permite identificar as duas classes de números (par ou ímpar) que podem ser usadas no condicional para alternar o padrão de desenho.

Figura 2.35 - Padrões de linhas alternadas.

Experimente trocar os testes do condicional e verá que muitos outros padrões surgem das disposições variadas das diagonais. Altere a condição:

Código 2.55 -

if(i%2 == 0)

por outras do tipo:

Código 2.56 -

if(i*j%2 == 0) // ou
if(i*j%3 == 0) // ou
if((i+j)%3 == 0) // ou
if(((i%2)+(j%3))%2 == 0)

e obterá resultados como os da figura 2.36.

Figura 2.37 - i*j%2
Figura 2.38 - i*j%3
Figura 2.39 - (i+j)%3
Figura 2.40 - ((i%2)+(j%3))%2
Figura - 2.36 - Padrões variados formados pelas diagonais dos blocos.

Esse gênero de padrões foi inspirado no trabalho do frade dominicano Sébastien Truchet, que desenvolveu um método[8] capaz de ordenar gravuras simples em disposições complexas. Os chamados padrões de Truchet costumam ser usados em uma série de aplicações estéticas e, em sua forma primordial, são o resultado de uma análise combinatória de quatro matrizes compostas por um triângulo retângulo inscrito em um quadrado, figura 2.41.

Figura 2.42
Figura 2.43
Figura 2.44
Figura 2.45
Figura - 2.41 - Matrizes de Truchet.

Dispostos sequencialmente, lado a lado, elas assumem uma forma unitária, sintetizando imagens como a figura 2.46.

Figura 2.46 - Padrões formados por triângulos.

Para criar esse efeito em seu código, localize as linhas que desenham as diagonais em 2.52:

Código 2.57 -

line(i*lado,j*lado,i*lado + lado,j*lado + lado);

e altere por estas, que desenham triângulos:

Código 2.58 -

fill(0);
if(i%2 == 0 && j%2 == 0) {
triangle(i*lado,j*lado,i*lado + lado,j*lado,i*lado + lado,j*lado + lado);
if(i%2 != 0 && j%2 == 0) {
triangle(i*lado,j*lado,i*lado,j*lado + lado,i*lado + lado,j*lado + lado);
}
if(i%2 == 0 && j%2 != 0) {
triangle(i*lado + lado,j*lado,i*lado + lado,j*lado + lado,i*lado, j*lado + lado);
}
if(i%2 != 0 && j%2 != 0) {
triangle(i*lado + lado,j*lado,i*lado,j*lado,i*lado,j*lado + lado);
}

Usando esse mesmo código é possível criar dezenas de padrões apenas modificando os condicionais, veja a figura 2.47. Ao longo dos anos o método de Truchet vem sendo usado como guia para experimentação com regras similares, usualmente formadas por matrizes na qual uma figura não radialmente simétrica é inscrita em quadrados, triângulos ou hexágonos, veja 2.48.

Figura 2.49
Figura 2.50
Figura 2.51
Figura 2.52
Figura - 2.47 - Padrões de Truchet.
Figura 2.53
Figura 2.54
Figura 2.55
Figura - 2.48 - Padrões de Truchet.
Figura 2.56
Figura 2.57
Figura 2.58
Figura - 2.59 - Padrões de Truchet.
Figura 2.60 - Padrões de Truchet em uma grade hexagonal.

2.6 Tópicos avançados - classes e objetos

A seção final deste capítulo é dedicada à apresentação de um tópico extremamente popular na programação moderna, a Programação Orientada a Objetos (do inglês, Object Oriented Programming ou OOP). Na terceira parte deste livro existe um capítulo destinado exclusivamente ao uso deste tipo de abordagem, com um exemplo construído passo a passo. Se desejar, você pode postergar esta seção até lá, mas eu aconselho fortemente que você a siga e comece a se familiarizar com esta incrível metodologia de pensamento.

A Programação Orientada a Objetos é uma maneira de conduzir o design de softwares. Ela visa agrupar dados e código em uma abstração única, cujo objetivo é modelar um evento, um objeto ou ser, em fim, um elemento que faça parte do seu dia a dia. Muito mais do que uma série de regras, a OOP é uma abstração de conceitos e uma maneira diferente de abordar problemas, escrever códigos e conceber programas. No ponto central da OOP reside a classe . Você pode imaginar uma classe como sendo uma estrutura de dados altamente customizável que contém suas próprias variáveis e funções, também chamadas, respectivamente, de atributos e métodos. Isso facilita o seu entendimento uma vez que você estudou esses dois tópicos separadamente.

Você deve estar se perguntando o porquê das classes serem tão revolucionárias, então vamos para um cenário concreto. Suponha que você precisasse desenvolver um programa para modelar um animal de estimação, um cachorro por exemplo, qual tipo de variável usaria? Um cão seria uma String, identificado pelo seu nome? Ou um int que guardaria um registro no sistema? E se em um segundo momento você também quisesse que esse cão realizasse ações, como correr? Seus comportamentos seriam descritos por uma série de instruções e não variáveis. Onde você iria declarar a função correr(), no corpo do programa? De fato isto não é o ideal, pois você estaria misturando dados relativos ao objeto (cachorro) com a execução do programa principal que pode ser composto por muitos outros objetos e funções. As classes surgem para sistematizar, e de certa forma padronizar, essas questões levantadas.

Uma cão possui uma lista de características além do nome ou registro, como altura, peso, cor dos olhos e raça dentre outros, que formam os seus atributos. Ele também pode realizar uma série de ações próprias como correr, latir ou brincar, que seriam seus métodos. A classe é altamente versátil, pois ela permite agrupar essas variáveis e funções em uma única estrutura de dados. Um ponto importante é que a classe é uma espécie de "molde" geral, por exemplo, o animal cachorro, enquanto um membro específico da classe é chamado de objeto (ou instância da classe). Uma cachorrinha chamada Mia, com 14cm de altura, 5kg de peso, cor de olhos preto e raça Pinscher seria uma instância da classe geral cachorro. A comparação é mostrada na figura 2.61.

Figura 2.61 - Classe Cachorro e Instâncias Mia e Dukea.
a Todos os direitos reservados pelo fotógrafo Rafael Girundi.

No Processing, uma classe é definida da seguinte maneira:

Código 2.59 - Estrutura padrão de uma classe -

class NomeDaClasse {

// Variáveis ou atributos da classe.

// Construtor

NomeDaClasse(parametrosDoConstrutor) {
// Código do construtor.
}

// Funções ou métodos da classe.
}

Observe que a classe é uma estrutura complexa no sentido que é composta por variáveis e funções. Em um programa do Processing, antes mesmo de declarar um objeto de uma classe, é necessário que a mesma esteja programada, que é basicamente um bloco como o do código 2.59. Mais detalhadamente, as classes possuem os seguintes pontos importantes:

  • Declaração: Uma classe é precedida pela palavra class , e seu nome segue as mesmas restrições da nomenclatura de variáveis e funções. Devem ser declaradas sempre fora do setup(), draw() ou qualquer outra função. Uma boa prática é declará-las com a primeira letra maiúscula. As chaves que seguem após o nome delimitam todo o código referente à classe.
  • Construtores: São funções executadas quando é feita a instanciação do objeto da classe. Eles podem ser entendidos como uma função que é chamada sempre que você cria um novo objeto da classe. O propósito do construtor é servir como uma interface para passar informações pertinentes ou essenciais para a existência de uma instância. Sua restrição é que construtores devem ter exatamente o mesmo nome da classe e não possuir nenhum tipo de retorno (nem mesmo void).
  • Instanciação: A instanciação de um objeto da classe é feita de maneira muito semelhante à declaração e inicialização de uma nova variável, contudo é precedida pela palavra new seguida do nome da classe e dos argumentos do construtor, tal como:
    NomeDaClasse nomeDaInstancia = new NomeDaClasse(argumentosDoConstrutor);
  • Variáveis: São chamadas de atributos de uma classe e podem ser qualquer tipo de variável estudada até o momento, inclusive outras classes, sendo sujeitas as mesmas regras de nomenclatura e declaração. Essas variáveis são internas às classes e não é possível usá-las em um programa sem referenciar explicitamente o objeto ou classe de origem. Para acessar os atributos de um objeto no programa principal utiliza-se da chamada sintaxe por ponto:
    nomeDaInstancia.nomeDoAtributo;
  • Funções: Ou métodos de uma classe também seguem as mesmas regras das funções especificadas na seção 2.3. Assim como os atributos, elas são internas a classe e suas invocações também exigem a sintaxe por ponto:
    nomeDaInstancia.nomeDaFuncao(argumentosDaFuncao);

Confira no código abaixo como a classe genérica, citada anteriormente, modelaria a classe cachorro:

Código 2.60 -

// Agrupa atributos e métodos comuns a todos os cachorros:
class Cachorro {

// Atributos:
String nome, raca;
float peso, altura;

// Construtor:
// Inicializa os atributos essenciais para a instanciação do objeto:
Cachorro(String _nome, String _raca, float _peso, float _altura) {
nome = _nome;
raca = _raca;
peso = _peso;
altura = _altura;
}

// Métodos
void latir() {
println("Au Au!");;
}
}

// Programa principal:
void setup() {

// Declara um objeto da classe cachorro:
Cachorro cao1 = new Cachorro("Mia", "Pinscher", 3.2, 27);

// Exibe atributos do objeto cao1:
println("Nome:", cao1.nome);
println("Raça:", cao1.raca);

// Invoca um método do objeto cao1:
cao1.latir();
}

Isso concluí a parte técnica sobre classes. Os conceitos apresentados aqui permitem a principal aplicação das classes, exemplificada através de um estudo de caso na próxima seção.

2.6.1 Uma classe na prática

Professores de desenho costumam dizer que metade da habilidade de criar uma boa obra está na destreza da mão e a outra metade em uma mudança na forma de observar o mundo. Com a OOP é exatamente igual. Metade do necessário você acabou de aprender, relativo a como declarar uma classe, seus construtores, métodos e atributos. A outra metade consiste na mudança de como enxergar a separação e organização de seus programas. O entendimento dessa mudança de paradigma pode ser melhor ilustrado através de um exemplo. Considere o código abaixo que desenha círculos dispostos como a figura 2.62. Neste momento não estamos interessados na construção do padrão em si, o foco deve ser direcionado para a estrutura global da escrita do programa.

Código 2.61 -

void setup() {
size(700,200);
background(255);

// Desenho do padrão:
for(int i = 0; i < 31; i++) {
for(int j = 0; j < 10; j++) {
color corCirc = color(180-50*(j-2),50*(j-3),j*25);
fill(corCirc);
ellipse(i*22.3+15,j*25+12.5*(i%2),25-abs(i-15),25-abs(i-15));
}
}
}
Figura 2.62 - Padrão formado por círculos.

Em relação as funcionalidades, não há nenhum mistério até agora visto que você conhece todos os comandos que formam esse programa; mas e em relação ao código? E se você quisesse saber qual a posição, o tamanho do raio ou até mesmo as cores do primeiro círculo desenhado, ou do círculo número 42, seria possível? Da maneira que o código acima foi escrito não é possível acessar nenhuma dessas informações simplesmente porque elas não foram armazenadas. Guardar essas informações implica em criar variáveis para conter os atributos dos círculos, tais como posição x, posição y, valor do raio e cor. Vamos reescrever o código e aproveitar para utilizar de vetores como a maneira de armazenar esses dados, reduzindo assim o tamanho final do texto. Além disso, optamos por criar uma função separada para exibir esses círculos coloridos de forma a organizar o código.

Código 2.62 - Círculos - Programação Procedural -

// Variáveis para armazenamento de dados dos círculos:
float[] posX;
float[] posY;
float[] raio;
color[] corC;

void setup() {
size(700, 200);
background(255);

// Total de circulos: 31 (horiz) * 10 (vert):
posX = new float[310];
posY = new float[310];
raio = new float[310];
corC = new color[310];

// Índice identificador do número do círculo:
int indice = 0;

// Etapa de construção dos círculos:
for (int i = 0; i < 31; i++) {
for (int j = 0; j < 10; j++) {
color corCirc = color(180 - 50 * (j - 2), 50 * (j - 3), j * 25);

// Guarda dados dos círculos para consultas posteriores:
posX[indice] = i * 22.3 + 15;
posY[indice] = j * 25 + 12.5 * (i % 2);
raio[indice] = 25 - abs(i - 15);
corC[indice] = corCirc;
indice++;
}
}

// Etapa de exibição dos círculos:
for (int i = 0; i < indice - 1; i++) {
exibirCirculo(i);
}

println("Posição X,Y do círculo número 1:", posX[0], ",", posY[0]);
println("Tamanho do raio do círculo número 42 ", raio[41]);
}

void exibirCirculo(int _c) {
fill(corC[_c]);
ellipse(posX[_c], posY[_c], raio[_c], raio[_c]);
}

Veja como foi necessário declarar variáveis adicionais e preenchê-las de maneira cuidadosa de modo que correspondessem especificamente a cada círculo. Se mais informações sobre esses círculos forem adicionadas (diâmetro, área, perímetro, exibição como um arco, etc.), serão necessárias cada vez mais variáveis e funções para obter esses dados. Isso gera inconvenientes, como o aumento do número de variáveis que não tem relação com o programa principal (e sim com os círculos), poluição do código principal com funções específicas dos círculos e atenção para preencher e consultar os dados com os índices corretos. Grande parte desses problemas podem ser contornados se abordarmos o programa com a mentalidade da OOP.

Retorne ao código 2.62 e analise-o com calma. Você deve ter percebido que todos os círculos possuem variáveis comuns como posição do centro, valor de raio e cor, assim como uma função comum, a de exibição (todos eles são mostrados na tela). Portanto é possível considerar a forma "círculo" como uma estrutura de dados abstrata que agrupa as variáveis e funções comuns a todas essas figuras, sendo estas a base para a construção da classe Circulo. A representação individual de cada círculo será alcançada através da criação de uma instância (ou objeto) dessa classe. O código seguir demonstra a unificação das variáveis e funções comuns aos objetos na classe:

Código 2.63 - Círculos - OOP -

// Declaração da classe:
class Circulo {

// Atributos:
float posX, posY;
float raio;
color cor;

// Construtor:
Circulo(float _posX, float _posY, float _raio, color _cor) {
posX = _posX;
posY = _posY;
raio = _raio;
cor = _cor;
}

// Métodos:
void exibir() {
fill(cor);
ellipse(posX, posY, raio, raio);
}
}


// Programa Principal:

// Declara um vetor de objetos da classe "Circulo".
// Total de circulos: 31 (horiz) * 10 (vert):
Circulo[] circ = new Circulo[310];

void setup() {
size(700, 200);
background(255);

int indice = 0;

// Instanciação dos objetos da classe:
for (int i = 0; i < 31; i++) {
for (int j = 0; j < 10; j++) {
color corCirc = color(180 - 50 * (j - 2), 50 * (j - 3), j * 25);
circ[indice] = new Circulo(i * 22.3 + 15, j * 25 + 12.5 * (i % 2), 25 - abs(i - 15), corCirc);
indice++;
}
}

// Etapa de exibição dos círculos:
for (int i = 0; i < circ.length; i++) {
circ[i].exibir();
}

println("Posição X,Y do círculo número 1:", circ[0].posX, ",", circ[0].posY);
println("Tamanho do raio do círculo número 42:", circ[41].raio);
}

Note como esse código ficou muito mais limpo e organizado que o anterior. Agora a classe Circulo contém todas as informações referentes a essa figura e, se necessário consultá-las, basta acessar o respectivo atributo do objeto através da sintaxe por ponto. Essa característica tornam as classes uma estrutura de dados autocontida. No que diz respeito ao código em si, perceba que agora há uma distinção notável entre o programa e a estrutura de dados que ele utiliza. No programa principal o foco foi direcionado apenas para a criação de objetos do tipo Circulo e, posteriormente, sua exibição. Todas as peculiaridades relativas aos detalhes técnicos da declaração e atribuição minuciosa das variáveis ou da implementação da função de exibição foram abstraídas pela classe, que agora faz isso automaticamente para você. Um esquema visual holístico das diferenças entre os códigos 2.62 e 2.63 pode ser vista na figura 2.63.

Figura 2.63 - Diferenças entre abordagens Procedural e OOP.

Os tópicos apresentados nesta seção são uma introdução mínima as classes, mas suficiente para que você aplique esse conhecimento em quase a totalidade de seus programas. Se você quiser aprofundar mais na OOP, você pode procurar recursos que expliquem conceitos como polimorfismo, herança e encapsulamento que aumentam o reaproveitamento do código e a segurança de acesso as classes. Independente de suas outras funcionalidades, lembre-se que elas são uma de suas maiores aliadas na abordagem de problemas e no planejamento de soluções. A medida que você aumentar a complexidade de seus programas você irá querer organizá-los em componentes e subcomponentes, e nenhuma metodologia fornece um maneira mais natural e pragmática para atingir esse objetivo do que a OOP .

2.7 Sumário

Neste primeiro capítulo focado puramente no código você aprendeu sobre os componentes que formam a base de todos os programas que você irá escrever. Brevemente, você estudou sobre:

  • Variáveis: São estruturas de dados que armazenam informações que podem ser acessadas com um simples referenciar ao seu nome.
  • Operadores: Manipulam e estabelecem relações entre as variáveis. São elementos chave em todas as demais estruturas de programação.
  • Funções: Permite compactar uma infinidade de instruções em um único bloco ou script. São executadas sempre que conveniente através da invocação da função.
  • Condicionais: Principais responsáveis por direcionar o fluxo de um programa de acordo com condições pré-estabelecidas ou em execução.
  • Repetições: Possibilitam repetir blocos de código em uma composição reduzida, potencialmente economizando centenas de linhas além do precioso tempo do programador.
  • Classes: É a mais customizável e poderosa estrutura de dados na programação. Agrupa variáveis e métodos referentes a uma mesma abstração ou modelo. Permite acessar os dados de uma maneira organizada e segregá-los do programa principal.

Com isso você finaliza a primeira parte da fundação de um programador. Símbolos e nomes que antes não faziam sentido são prontamente decodificados por você. Mas calma, não deixe o poder subir à cabeça por que, na verdade, você é somente parte de um programador. Um programador não é alguém que memoriza todos os comandos, funções e sintaxes, e sim alguém que sabe usar o código a seu favor, criando e programando algoritmos para atingir um propósito. Saber a sintaxe e não ter um objetivo é como decorar um dicionário e não saber usar as palavras para formar frases.

Na segunda parte dessa jornada você será convidado a se aprofundar em tópicos que compõem a carteira criativa de um artista programador (ou programador artista, fica a seu critério). Você aprenderá um pouco mais sobre como usar propriedades e resultados matemáticos para estampar estéticas singulares nos seus trabalhos. Mais á frente você testemunhará como é importante saber aplicar conhecimentos que não se restringem a linguagens de programação, abrangendo conceitos universais da matemática e da natureza.

 

 


[1] Nada garante que essas regras estejam no reino da lógica e da razão. As vezes elas se fundem no que origina essencialmente o estilo do artista, como as composições de Piet Mondrian ou de Pollock.

[2] Esses são apenas exemplos simplórios. Existem campos da arte e da psicologia inteiramente dedicados ao estudo das cores que, dependendo do contexto, podem apresentar diversos significados.

[3] Existem outros espaços de cores muito populares, como o HSV (Hue, Saturation, Value) ou o CMYK (Cyan, Magenta, Yellow, Black) sendo que este é representado por quatro canais e usa um sistema de cores subtrativo.

[4] O modo padrão do Processing considera uma profundidade de cores de 24 bits chamada de Truecolor. Isso significa que existem 28 = 256 bits por canal, podendo representar um total de 16777216 de cores distintas.

[5] Isso é chamado de sobrecarga ou overload de métodos na programação.

[6] É possível evitar essa subtração se começarmos com um bloco de número zero em vez de um.

[7] Visto que o incremento de ambas as variáveis de controle, i e j, é igual a um.

[8] Douat, D. (1722). Méthode pour faire une infinité de desseins différens avec des carreaux mi-partis de deux couleurs par une ligne diagonale. Chez Florentin de Laulne.