Introdução à programação
com Python em um contexto visual


L-System - Sistema de Lindenmayer

L-Systems são as estruturas e procedimentos criados por Aristide Lindenmayer para estudar o crescimento de algas e plantas, por meio da manipulação de sequências de símbolos. As sequências são geradas por sucessivas iterações da aplicação de regras de substituição.

A tradução computacional dessas estruturas foi discutida pela primeira vez na serie Lecture Notes in Biomathematics com o artigo Lindenmayer Systems, Fractals, and Plants por Przemyslaw Prusinkiewcz e James Hanan (DOI: 10.1007/978-1-4757-1428-9).

Mais referências:

Pré-requisitos

Para entender os exemplos apresentados mais à frente, é preciso familiaridade com algumas ideias do Python e da biblioteca py5:

Percorrendo e concatenando strings

Podemos percorrer uma palavra com um laço for.

>>> for letra in 'aeiou':
       print(letra)
a
e
i
o
u

Podemos concatenar novas palavras com o operador +. Um string vazio com '' pode ser concatenado sem alterar o resultado.

>>> 'a' + 'e' + 'i' + 'o' + 'u'
'aeiou'

>>> '' + 'a'
'a'  

Usando dicionários para fazer substituições em strings

Um dicionário é uma estrutura que guarda pares chave-valor. Os valores podem ser pesquisados na estrutura a partir de uma chave. Quando consultamos o dicionário com a sintaxe dos colchetes dicionario[chave], ele entrega o o valor, e se não houver a chave ocorre uma exceção KeyError. Usando o método .get(chave) é possível evitar essa excessãom e o valor especial None é devolvido, mas e usarmos a forma .get(chave, valor_para_chave_faltando) é possível escolher o que o dicionário devolve caso a chave não seja encontrada.

>>> ingles = {'maçã': 'apple', 'pêra': 'pear'}

>>> ingles['maçã']
'apple'

>>> fruta = 'cupuaçu'  # não tem 'cupuaçu' no dicionário

>>> ingles[fruta]   # a fruta é 'cupuaçu'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'cupuaçu'

>>> ingles.get(fruta, 'não sei') # .get() evita o erro
'não sei'

>>> ingles.get(fruta, fruta)  
'cupuaçu'

Como pode ser observado no exemplo acima, se usarmos a forma .get(chave, chave), obtemos a própria chave caso ela não seja encontrada no dicionário, e isso se mostra muito útil quando usamos o dicionário para indicar regras de substituição: a falta da chave indica que a própria chave deve ser usada sem nenhuma substituição.

No exemplo abaixo vamos armazenar algumas letras como chaves associadas a uma sequência de letras como valor para cada uma delas, usaremos o valores para substituir as letras das chaves encontradas em um laço for.

vogais = {'a': 'aaa', 'e': 'eeê', 'i': 'iih', 'o': 'oôo', 'u': 'üüü'}
palavra = 'anticonstitucionalissimamente'
nova_palavra = ''
for letra in palavra:
    nova_palavra = nova_palavra + vogais.get(letra, letra)
print(nova_palavra)

Resultado: aaantiihcoôonstiihtüüüciihoôonaaaliihssiihmaaameeênteeê

Desenhando linhas, como se arrastando uma caneta

Para simular o movimento de uma “caneta” fazendo linhas sucessivas, podemos mover a origem do sistema de coordenadas como translate() desenhar uma linha e em seguida mover a origem para o final da linha, para mudar a orientação dos traços é possível “girar o papel” em torno da origem com rotate(). Se salvarmos o estado do sistema de coordenadas com push_matrix() podemos desfazer transformações posteriores com pop_matrix(), trazendo a “caneta” para um ponto e orientação anterior.

Veja abaixo um exemplo e o resultado que gera.

def setup():
    size(400, 400)
    translate(200, 350)  # move a origem para um ponto mais abaixo do que o meio da tela (posição 0)
    
    line(0, 0, 0, -100)  # uma linha de tamanho 100 para cima
    translate(0, -100)   # muda a origem para o final da linha (posição 1)
    
    rotate(radians(45))  # gira o papel 45 graus
    
    line(0, 0, 0, -100)  # uma linha de tamanho 100 para cima
    translate(0, -100)   # muda a origem para o final da linha (posição 2)

    push_matrix()        # guarda o sistema de coordenadas (posição 2)
    
    rotate(radians(45))  # "gira o papel" 45 graus para a esquerda
    
    line(0, 0, 0, -100)  # uma linha de tamanho 100 para cima
    translate(0, -100)   # muda a origem para o final dalinha (posição 3)
    
    pop_matrix()         # vota ao sistema de coordenadas (posição 2)
    
    rotate(radians(-45))  # "gira o papel" 45 graus para a direita
        
    line(0, 0, 0, -100)  # uma linha de tamanho 100 para cima
    translate(0, -100)   # muda a origem para o final dalinha (posição 4)

turtle-move

Um exemplo inicial de L-System

Estudados os pré-requisitos podemos finalmente construir um exemplo de L-System.

Partindo de regras de substição aplicadas à uma frase inicial (axioma) é possível produzir desenhos que se aproximam de plantas e fractais com autosimilaridade em várias escalas. Para isso parte dos símbolos (letras) são interpretados como uma ação de desenho, tal como andar com uma caneta para frente, virar para direita ou para esquerda um certo ângulo, ou ainda, volar a uma posição anterior armazenada em uma pilha de estados do sistema de coordenadas.

image

axioma = "X"
regras = {"X": "F+[[X]-X]-F[-FX]+X",
          "F": "FF"
          }

passo = 10
angulo = 25
iteracoes = 4  # repeticoes (voltas na aplicação das regras)
xo, yo = 300, 500

def setup():
    size(600, 600)
    frase_inicial = axioma
    for i in range(iteracoes):
        frase = ""
        for simbolo in frase_inicial:
            substituicao = regras.get(simbolo, simbolo)
            frase = frase + substituicao
        frase_inicial = frase
    print(len(frase))

    background(240, 240, 200)
    translate(xo, yo)
    for simbolo in frase:
        if simbolo == "F":
            line(0, 0, 0, -passo)  # desenha uma linha
            translate(0, -passo)   # move a origem 
        if simbolo == "+":
            rotate(radians(angulo)) 
        if simbolo == "-":
            rotate(radians(-angulo))  
        if simbolo == "[":
            push_matrix()  # grava o estado (posição e ângulo)
        if simbolo == "]":
            pop_matrix()   # volta ao último estado gravado

Uma versão com interatividade

Esta versão permite controlar com o teclado o tamnaho das linhas (passo), ângulo e número de iterações (cuidado que muitas iterações podem tornar o sketch muito lento)>

axioma = "X"
regras = {"X": "F+[[X]-X]-F[-FX]+X",
          "F": "FF"
          }
passo = 10
angulo = 25
iteracoes = 4  # repeticoes (voltas na aplicação das regras)
xo, yo = 300, 500

def setup():
    global frase
    size(600, 600)
    frase = gerar_sistema(iteracoes, axioma, regras)
    print(len(frase))

def draw():
    background(240, 240, 200)
    translate(xo, yo)
    desenha_sistema(frase)

def gerar_sistema(num, axioma, regras):
    """
    Produz um sistema-L a partir da  frase `axioma`,
    repetindo `num` iterações, as substituições descritas
    nas pelo dicionário `regras`
    """
    frase_inicial = axioma
    for i in range(num):
        frase_nova = ""
        for simbolo in frase_inicial:
            substituicao = regras.get(simbolo, simbolo)
            frase_nova = frase_nova + substituicao
        frase_inicial = frase_nova
    return frase_nova

def desenha_sistema(simbolos):
    """
    Recebe uma frase e desenha de acordo com
    as "regras de desenho".
    """
    for simbolo in simbolos:
        if simbolo == "F":
            line(0, 0, 0, -passo)
            translate(0, -passo)
        if simbolo == "+":
            rotate(radians(angulo))
        if simbolo == "-":
            rotate(radians(-angulo))
        if simbolo == "[":
            push_matrix()
        if simbolo == "]":
            pop_matrix()

def key_pressed():
    global passo, angulo, iteracoes, frase
    if key == 'z':
        passo -= 1  # passo = passo - 1
    if key == 'x':
        passo += 1
    if key == 'a':
        angulo -= 1
    if key == 's':
        angulo += 1
    if key == 'q':
        iteracoes -= 1
        frase = gerar_sistema(iteracoes, axioma, regras)
        print(len(frase))
    if key == 'w':
        iteracoes += 1
        frase = gerar_sistema(iteracoes, axioma, regras)
        print(len(frase))

Exemplo 3D

animacao

Exemplo usando P3D e adicionando rotate_y()

axioma = 'X'
regras = {
    'X': 'F+[[X]-X]-F[-FX]+X',
    'F': 'FF',
    }
passo = 5
angulo = 25  # angulo em graus
iteracoes = 5

def setup():
    global frase_resultado
    size(900, 900, P3D)
    frase_inicial=axioma
    for _ in range(iteracoes):
        frase_resultado = ''
        for simbolo in frase_inicial:
            substituir = regras.get(simbolo, simbolo)
            frase_resultado = frase_resultado + substituir
        # print(frase_inicial, frase_resultado)
        frase_inicial=frase_resultado
    print(len(frase_resultado))

def draw():
    background(210, 210, 150)
    stroke_weight(3)
    angulo = 25
    # angulo = mouse_x / 70.0
    translate(width / 2, height * 0.8)
    rotate_y(frame_count / 100.0)
    for simbolo in frase_resultado:
        if simbolo == 'X':   # se simbolo for igual a 'X'
            pass
        elif simbolo == 'F':   # else if (senão se) o simbolo é F
                line(0, 0, 0, -passo)
                translate(0, -passo)
                rotate_y(radians(-angulo))
        elif simbolo == '+':
            rotate(radians(-angulo))  # + random(-5, 5)))
        elif simbolo == '-':
            rotate(radians(+angulo))  # + random(-5, 5)))
        elif simbolo == '[':
            push_matrix()
        elif simbolo == ']':
            pop_matrix()

Esta última versão com 3D precisa modificações no editor online do pyp5js pois o p5.js tem a origem no centro da tela quando em 3D… esperimente este link com o código modificado