Transformações do sistema de coordenandas
O py5 tem funções embutidas que tornam relativamene fácil você mover, girar, crescer ou encolher objetos por meio da manipulação do sistema de coordenadas. São as funções translate()
, rotate()
, e scale()
, apresentadas nesta págima. Também serão apresentadas as funções, de grande importância, que permitem ‘guardar’ e ‘devolver’ o estado anterior do sistema de coordenadas, são elas: push_matrix()
e pop_matrix()
.
Essas funções mencionadas, em conjunto, tornam possível, entre outras coisas, desenhar um retângulo inclinado na tela. Note que até agoras, usando apenas a função rect()
, só podemos desenhar retângulos com os lados alinhados ao sistema de coordenadas. Uma forma possível de se desenhar um retângulo inclinado é elencar as coordenadas dos vértices do retângulo e calcular a posição girada de cada um deles, usando seno e cosseno, para por fim desenhar um polígono com as novas posições usando begin_shape()
, vertex()
e end_shape()
, o que pode ser interessante também, mas costuma ser mais trabalhoso.
Começando com a rotação, para ver como as coisas são estranhas
Suponha que queremos desenhar um quadrado no centro da tela, inclinado em 10 graus. Vejamos o que acontece quando usamos a função rotate()
que gira o sistema de coordenadas.
Para termos um elemento inicial de comparação, vamos primeiro desenhar um quadrado sem girar, usando as coordendadas da metade da largura e da altura da área de desenho (vamos chamar este de “quadrado 0”).
Então, vamos usar a função rotate()
, e em seguida vamos desenhar um quadrado com os mesmos argumentos novamente (“quadrado 1”). Por fim, vamos repetir tanto a rotação como o a chamada de função que desenha o quadrado mais uma vez (“quadrado 2”).
Nota: No py5 quando uma função pede um ângulo como argumento, espera que você informe esse ângulo em radianos, por isso, se você pensa em graus, use
radians(angulo_em_graus)
para converter.
def setup():
size(500, 500)
rect_mode(CENTER)
no_fill()
square(250, 250, 200) # quadrado 0
rotate(radians(10))
square(250, 250, 200) # quadrado 1
rotate(radians(10))
square(250, 250, 200) # quadrado 2
O resultado é o seguinte.
Você percebe o que está acontecendo? Pense nestas questões:
- Em primeiro lugar, onde está o centro de rotação?
- Será que é possível escolhermos o centro da rotação?
- Por qual motivo o último pedido de rotação e desenho, sendo exatamente igual ao anterior, não faz o “quadrado 2” cair sobre o “quadrado 1”?
As respostas para essas perguntas são:
- A rotação está acontecento em torno da origem, o ponto de referência inicial do sistema de coordenadas, onde X e Y valem zero. Imagine um papel milimetrado com uma tachinha fixada na origem e estamos então girando o papel em torno desse ponto.
- É sim possível escolher a origem usando
translate()
mara mover a origem. Podemos tirar a tachinha, mover o papel e fixá-la novamente. - As operações de transformação do sistema de coordenadas, como a rotação com a função
rotate()
, são cumulativas, e isso vai ser um problema a ser resolvido um pouco mais a frente.
Resolvendo a primeira parte, a escolha do centro da rotação, usando a translação
Se movermos a origem para o ponto no centro da área de desenho, usando translate()
, com os argumentos 250, 250
, conseguiremos girar o sitema de coordenadas em torno de um novo centro.
def setup():
size(500, 500)
rect_mode(CENTER)
no_fill()
square(250, 250, 200)
translate(250, 250)
rotate(radians(10))
square(0, 0, 200)
rotate(radians(10))
square(0, 0, 200)
Note que o segundo e terceiro quadrados são desenhados com square(0, 0, 200)
, nas novas coordenadas do centro da tela após o translate(250, 250)
, e não mais em square(250, 250, 200)
.
A sutil questão da acumulação de translate()
e rotate()
A segunda parte do problema, que se manifestou sutilmente até agora, é de que as transformações do sistema de coordenadas não cumulativas. Como mover e girar o mesmo papel milimetrado sucessivamente.
Suponha que queremos desenhar uma fila de quadrados girados, e veja este exemplo ingênuo de uma função quadrado_girado_errado()
que desenha, bem, um quadrado girado. O código a seguir, usando a função quadrado_girado_errado()
falha horrívelmente na missão de desenhar uma fila com os quadrados alinhados com Y valendo 100, como parecem indicar as coordenadas passadas como argumentos (100, 100), (250, 100) e (400, 100).
def setup():
size(500, 500)
rect_mode(CENTER)
no_fill()
def quadrado_girado_errado(x, y, lado, rot):
translate(x, y)
rotate(rot)
square(0, 0, lado)
def draw():
background(200)
quadrado_girado_errado(100, 100, 100, radians(10))
quadrado_girado_errado(250, 100, 100, radians(10))
quadrado_girado_errado(400, 100, 100, radians(10))
Cada chamada a função quadrado_girado_errado()
empurra a origem do sistema de coordenadas mais um pouco, e também gira 10 graus, então o segundo quadrado girado cai mais longe e mais girado, o terceiro já fica para fora da tela, mais abaixo à direita!
A solução com push_matrix e pop_matrix
É possível fazer uma espécie “backup” do atual sistema de coordenadas, usando a função push_matrix()
depois de feito o desenho que precisamos com as coordenadas alteradas, pop_matrix()
devolve ao sketch o estado anterior do sistema de coordenadas. Com esta versão modificada da função quadrado_girado()
conseguimos posicionar à vontade nossos quadrados girados.
def quadrado_girado(x, y, lado, rot):
push_matrix() # guarda a matriz atual do sistema de coordenadas
translate(x, y)
rotate(rot)
square(0, 0, lado)
pop_matrix() # recupera a matriz do sistema de coordenadas anterior
Mudando a escala
Além da translação e rotação é possível também escalar o sistema de coordenada com scale()
e entortar com shear_x()
e shear_y()
. São transformações um pouco mais difíceis de imaginar com a metáfora do papel milimetrado, que precisaria ser de uma borracha mágica, mas, vejamos um exemplo curto só com a transformação da mudança de escala.
def setup():
size(500, 500)
rect_mode(CENTER)
no_fill()
no_loop()
square(100, 100, 50)
scale(1.5)
square(100, 100, 50)
scale(1.5)
square(100, 100, 50)
scale(1.5)
square(100, 100, 50)
scale(1.5)
Repare no exemplo acima como a aplicação do fator de escala acontece baseada na origem do sistema de coordanadas, que não foi modificada, e a transformação de escala é cumulativa. Note também como a escala afeta a espessura de linha do traço, o atributo gráfico que pode ser controlado com stroke_weight()
.
Você consegue imaginar qual o código que gera a imagem a seguir?
Pense em como você escreveria o código e depois clique para a resposta
def setup(): size(500, 500) rect_mode(CENTER) no_fill() no_loop() translate(250, 250) square(0, 0, 50) scale(1.5) square(0, 0, 50) scale(1.5) square(0, 0, 50) scale(1.5) square(0, 0, 50) scale(1.5)
A matriz de transformação e a origem de push_matrix e pop_matrix
Na matemática temos a ideia de matriz, que são objetos formados por linhas e colunas de números. Sempre que você faz uma rotação, translação ou mudança de escala, as informações necessárias para essa transformação são armazenadas em uma matriz de 3 colunas e duas linhas, e é por isso que as funções push_matrix()
e pop_matrix()
têm essa palavra matrix no nome.
Você não precisa interagir diretamante com essa “matriz de transformações” se não quiser, mas por curiosidade veja o exemplo abaixo que exibe os valores dela com print_matrix()
.
def setup():
size(500, 500)
print('estado inicial')
print_matrix()
translate(250, 250)
print('depois de translate(250, 250)')
print_matrix()
rotate(radians(10))
print('depois de rotate(radians(10))')
print_matrix()
O resultado no console:
estado inicial
1.0000 0.0000 0.0000
0.0000 1.0000 0.0000
depois de translate(250, 250)
001.0000 000.0000 250.0000
000.0000 001.0000 250.0000
depois de rotate(radians(10))
000.9848 -000.1736 250.0000
000.1736 000.9848 250.0000
Já a parte push e pop dos nomes vêm de uma estrutura de dados muito comum na computação conhecida como pilha (stack). Imagine uma pilha de livros, e considere que se você acrescenta um livro na pilha ele vai por cima, e se acrescentar mais um ele vai por cima do anterior. Já na hora de tirar livros o mais natural é remover o mais de cima antes do seguinte, e assim por diante.
Tradicionalmente, adicionamos itens em uma pilha com instruções nomeadas push
e removemos com instruções nomeadas pop
. A influência dessa nomenclatura é tão grande que, no Python, usamos .pop()
para acessar e remover o último item de uma lista, e, no JavaScript, .push()
é usado para acrescentar itens em um array (semelhante ao .append()
para listas no Python).
De maneira parecida então, push_matrix()
coloca a descrição do estado atual do sistema de coordenadas no topo de uma pilha na memória, e pop_matrix()
remove e restaura a última descrição de estado da pilha.
Uma forma alternativa de uso do push_matrix
No py5 é possível usar a sintaxe do que chamamos “gerenciadores de contexto”, a linha with push_matrix():
indica o início de um bloco de código onde vai acontecer uma transformação do sistema de coordenadas, e quando a indentação acaba, acontece um pop_matrix()
“automático”.
def quadrado_girado(x, y, lado, rot):
with push_matrix():
translate(x, y)
rotate(rot)
square(0, 0, lado)
Notas:
- Sempre execute
push_matrix()
epop_matrix()
em pares ou use o gerenciador de contextowith push_matrix():
senão você vai encontrar erros. Um dos erros é basicamente uma proteção antes do famoso estouro ou transbordamento da pilha, stack overflow,push_matrix() cannot use push more than 32 times
. O outro erro é o aviso de que está faltando um push anterior, e a pilha está vazia,missing a push_matrix() to go with that pop_matrix()
.- No py5, como no Processing, o sistema de coordenadas é restaurado ao seu estado original (origem na parte superior esquerda da janela, sem rotação e sem mudança de escala) toda vez que a função
draw()
é executada. É possível também voltar para o estado inicial o sistema de coordenadas comreset_matrix()
.
Transformações tridimensionais
Se você estiver trabalhando em três dimensões, poderá chamar a função translate()
com três argumentos para as distâncias x, y, e z, a função scale()
pode ser chamada também com três argumentos e as funções rotate_x()
, rotate_y()
e rotate_z()
, que recebem um argumento em radianos e fazem a rotação em torno de cada eixo. Vej o código que produz esta imagem na página sobre desenho em 3D.
Assuntos relacionados
- Um pouco de ângulos, com seno, cosseno e arco tangente
- Desenhando em 3D: Primeiros passos com
size(…, …, P3D)
- Páginas externas: Tutorial 2D Transformations de J. David Eisenberg (versão traduzida em português)