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


Desenhando Curvas - I

Agora que já sabemos desenhar um polígonos com begin_shape() e end_shape() ou end_shape(CLOSE) podemos experimentar formas curvas no py5, primeiro curvas Bézier cúbicas, com as funções bezier_vertex(), em seguida curvas Bézier quadráticas usando quadratic_vertex() e por fim uma implementação de Catmull-Rom splines com curve_vertex().

As curvas Bézier levam o nome do engenheiro francês Pierre Bézier, que as desenvolveu a partir dos algorítimos do matemático e físico francês Paul de Casteljau, em seus trabalhos na década de 1960 na indústria automotiva. As curvas Bézier descrevem formas a partir das coordenadas de pontos, ou âncoras, que delimitam o início e o fim de um trecho de curva, mas também precisam de coordenadas de “pontos de controle” que em geral ficam fora da curva, alterando o seu comportamento. Essas curvas polinomiais podem ser expressas como a interpolação linear entre alguns pontos como descrito e ilustrado com animações na Wikipedia.

Curvas Bézier cúbicas com bezier_vertex()

animação arrastando pontos da curva

Podemos criar uma forma curva aberta com uma ou mais chamadas a bezier_vertex() entre o begin_shape() e o end_shape(). A curva pode ser fechada se usarmos end_shape(CLOSE) ao final.

Note que antes de cada bezier_vertex() é preciso que haja algum vértice, um ponto âncora, então, antes da primeira chamada a bezier_vertex() em geral é usada uma chamada da função vertex(), como neste exemplo a seguir. Este primeiro tipo de curva Bézier que veremos requer dois pontos de controle para cada novo vértice, sem levar em conta o primeiro vértice-âncora, por isso, na função bezier_vertex() os quatro primeiros argumentos são as cordenadas de dois pontos de controle e os últimos dois são as coordenadas do vértice. Cada vértice Bézier por sua vez pode servir de âncora para um próximo vértice Bézier, permitindo o encadeamento de trechos curvos.

begin_shape()
vertex(100, 50)           # 0: âncora inicial
bezier_vertex(150, 150,   # 1: primeiro ponto de controle do primeiro vértice
              250, 150,   # 2: segundo ponto de controle do primeiro vértice
              200, 200),  # 3: vértice final da primeira curva, âncora da segunda
bezier_vertex(150, 250,   # 4: primeiro ponto de controle do segundo vértice
              50, 200,    # 5: segundo ponto de controle do segundo vértice
              50, 100)    # 6: segundo vértice bezier (final)
end_shape()

bezier

Código completo para reproduzir a imagem acima
def setup():
    size(400, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    vertex(100, 50)            # 0: vértice âncora
    bezier_vertex(150, 150,    # 1: ponto de controle
                  250, 150,    # 2: ponto de controle
                  200, 200),   # 3: vértice
    bezier_vertex(150, 250,    # 4: ponto de controle
                  50, 200,     # 5: ponto de controle
                  50, 100)     # 6: vértice
    end_shape()

    # anotações
    pts = [
        (100, 50),   # 0 
        (150, 150),  # 1
        (250, 150),  # 2
        (200, 200),  # 3
        (150, 250),  # 4
        (50, 200),   # 5
        (50, 100),   # 6
        ]
    stroke_weight(1)
    for i, (x, y) in enumerate(pts):
        fill(255)
        circle(x, y, 5)
        text(f"{i}: {x}, {y}", x+5, y-5)

Repare neste exemplo que quando há o alinhamento entre o segundo ponto de controle de um vértice (2), o próprio vértice (3), e o primeiro ponto de controle (4), pertencente ao próximo vértice em uma sequência de vértices, haverá continuidade na curva de um trecho para outro.

Curvas Bézier quadráticas com quadratic_vertex()

animação arrastando pontos da curva

Estas curvas também são construídas dentro de um contexto begin_shape()/end_shape() e também precisam de um vértice-âncora. comummente obtido usando uma chamada da função vertex(), em seguinda, cada chamada a quadratic_vertex() inclui nos argumentos as coordenades de um ponto de controle seguidas das coordenadas do novo vértice (que por sua vez pode servir de âncora para vértices Bézier subsequentes).

begin_shape()
vertex(100, 50)              # 0: vertex inicial
quadratic_vertex(150, 100,   # 1: ponto de controle
                 250, 100)   # 2: vértice-âncora
quadratic_vertex(250, 200,   # 3: ponto de controle
                 150, 200)   # 4: vértice-âncora
quadratic_vertex(50, 200,    # 5: ponto de controle
                 50, 100)    # 6: vértice-âncora final
end_shape()

exemplo de curva com vértices quadráticos

Código completo para reproduzir a imagem acima
def setup():
    size(400, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    vertex(100, 50)              # 0: vertex âncora inicial
    quadratic_vertex(150, 100,   # 1: ponto de controle
                     250, 100)   # 2: vértice
    quadratic_vertex(250, 200,   # 3: ponto de controle
                     150, 200)   # 4: vértice
    quadratic_vertex(50, 200,    # 5: ponto de controle
                     50, 100)    # 6: vértice
    end_shape()

    pontos = [
        (100, 50),
        (150, 150),
        (250, 100),
        (250, 200),
        (150, 250),
        (50, 200),
        (50, 100),
        ]
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        fill(255)
        circle(x, y, 5)
        t = f'{i}: {"vertex" if i == 0 else "control" if i % 2 else "quadratic"}'
        text(t, x+5, y-5)

Note como neste exemplo, na sequência final de trechos, há o alinhamento entre o ponto de controle de um vértice (3), o próprio vértice (4), e o ponto de controle (5) do próximo vértice, produzindo continuidade na curva de um trecho para outro.

Curvas Catmull-Rom com curve_vertex()

Vejamos agora as Catmull-Rom splines, uma forma de descrever curvas que não tem os pontos de controle independentes como as curvas Bézier, a curvatura em seus vértices é influenciada pelos vértices que vem antes e depois deles: é como se cada vértice fosse ao mesmo tempo sua própria âncora e ponto de controle de outros vértices anteriores e posteriores.

Vamos iterar por uma lista de coordenadas em forma de tuplas, da mesma forma que fizemos para desenhar um polígono, só que desta vez vamos experimentar usar curve_vertex() que acabamos de mencionar. Considere esta lista de pontos:

pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

Exemplo 1: Comportamento inesperado

Se chamarmos uma vez curve_vertex() para cada vértice dentro de um contexto de begin_shape() e end_shape(CLOSE)obteremos o seguinte resultado, esquisito (estou aqui omitindo parte do código que controla os atributos gráficos e mostra os texto com os índices dos pontos):

begin_shape()
for x, y in pontos:
    curve_vertex(x, y)
end_shape(CLOSE)

errada

Código completo para reproduzir a imagem acima
 pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

def setup():
    size(300, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    for x, y in pontos:
        curve_vertex(x, y)
    end_shape(CLOSE)
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        fill(255)
        circle(x, y, 5)
        text(i, x+5, y-5)

Exemplo 2: Fechando a curva corretamente

Para obter o resultado esperado (ou, caro leitor, pelo menos o que eu esperava) temos que acrescentar uma chamada com as coordenadas do último vértice antes do primeiro, e do primeiro e segundo vértices depois do último! Diga lá se não é estranho isso!

curve_vertex(pontos[-1][0], pontos[-1][1])
for x, y in pontos:
    curve_vertex(x, y)
curve_vertex(pontos[0][0], pontos[0][1])
curve_vertex(pontos[1][0], pontos[1][1])
end_shape(CLOSE)

fechada

Código completo para reproduzir a imagem acima
pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

def setup():
    size(300, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    curve_vertex(pontos[-1][0], pontos[-1][1])
    for x, y in pontos:
        curve_vertex(x, y)
    curve_vertex(pontos[0][0], pontos[0][1])
    curve_vertex(pontos[1][0], pontos[1][1])
    end_shape(CLOSE)
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y=ponto
        fill(255)
        circle(x, y, 5)
        text(i, x + 5, y - 5)

Exemplo 3: Curva aberta

É possível fazer uma curva aberta com os mesmo pontos e a mesma influência do último ponto no primeiro, e do primeiro no último, omitindo o CLOSE:

curve_vertex(pontos[-1][0], pontos[-1][1])
for x, y in pontos:
    curve_vertex(x, y)
curve_vertex(pontos[0][0], pontos[0][1])
end_shape()

aberta com a forma da fechada

Código completo para reproduzir a imagem acima
pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

def setup():
    size(300, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    curve_vertex(pontos[-1][0], pontos[-1][1])
    for x, y in pontos:
        curve_vertex(x, y)
    curve_vertex(pontos[0][0], pontos[0][1])
    curve_vertex(pontos[1][0], pontos[1][1])
    pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]


Exemplo 4: Curva aberta usando diferentes pontos

Agora se não queremos essa influência da curva fechada, é preciso repetir o primeiro e o último vértice.

begin_shape()
curve_vertex(pontos[0][0], pontos[0][1])
for x, y in pontos:
    curve_vertex(x, y)
curve_vertex(pontos[-1][0], pontos[-1][1])
end_shape()

curva aberta sem influência dos extremos

Código completo para reproduzir a imagem acima
pontos=[
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

def setup():
    size(300, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    curve_vertex(pontos[0][0], pontos[0][1])
    for x, y in pontos:
        curve_vertex(x, y)
    curve_vertex(pontos[-1][0], pontos[-1][1])
    end_shape()
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        fill(255)
        circle(x, y, 5)
        text(i, x+5, y-5)

Exemplo 5: Usando end_shape(CLOSE)

Veja como ficaria acrescentando-se o CLOSE em end_shape(CLOSE). Fica um tanto estranha.

curva fechada sem influência dos extremos

Código completo para reproduzir a imagem acima
pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100),
    ]

def setup():
    size(300, 300)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    curve_vertex(pontos[0][0], pontos[0][1])
    for x, y in pontos:
        curve_vertex(x, y)
    curve_vertex(pontos[-1][0], pontos[-1][1])
    end_shape(CLOSE)
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y=ponto
        fill(255)
        circle(x, y, 5)
        text(i, x+5, y-5)

Extra: Um testador de curvas interativo

Desafio: Você conseguiria escrever o código que permite testar as curvas arrastando os pontos com o mouse, usando a estratégia do exemplo “arrastando vários círculos”?

animação arrastando pontos da curva

Resposta: Testador para bezier_vertex() com pontos arrastáveis.
arrastando = None

pontos = [
    (100, 50),   # 0: vertex ponto âncora inicial 
    (150, 150),  # 1: primeiro ponto de controle
    (250, 150),  # 2: segundo ponto de controle
    (200, 200),  # 3: vértice bezier
    (150, 250),  # 4: primeiro ponto de controle
    (50, 200),   # 5: segundo ponto de controle
    (50, 100),   # 6: vértice bezier
    ]

s = 2 # scale factor

def setup():
    size(800, 600)

def draw():
    scale(s)
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    for i, (x, y) in enumerate(pontos):
        if i == 0:
            vertex(x, y)
        elif i % 3 == 0:  # elementos divisíveis por 3 da lista
            c1x, c1y = pontos[i - 2]
            c2x, c2y = pontos[i - 1]
            bezier_vertex(c1x, c1y,  #  primeiro ponto de controle
                          c2x, c2y,  #  segundo ponto de controle
                          x, y),     #  vértice
    end_shape()
    
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if i == arrastando:
            fill(200, 0, 0)
        elif dist(mouse_x / s, mouse_y / s, x, y) < 10:
            fill(255, 255, 0)
        else:
            fill(255)
        ellipse(x, y, 5, 5)
        t = f'{i}: {"vertex" if i == 0 else f"control-{i%3}" if i % 3 else "bezier"}'
        text(t, x + 5, y - 5)

def mouse_pressed():
    global arrastando
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if dist(mouse_x / s, mouse_y / s, x, y) < 10:
            arrastando = i
            break 

def mouse_released():
    global arrastando
    arrastando = None

def mouse_dragged():
    global pontos
    global arrastando
    if arrastando is not None:
        x, y = pontos[arrastando]
        x += (mouse_x - pmouse_x) / s
        y += (mouse_y - pmouse_y) / s
        pontos[arrastando] = x, y 
Resposta: Testador para quadratic_vertex() com pontos arrastáveis.
arrastando = None

pontos = [
    (100, 50),   # 0: vertex() âncora inicial 
    (150, 100),  # 1: ponto de controle
    (250, 100),  # 2: vértice e âncora do próximo
    (250, 200),  # 3: ponto de controle
    (150, 200),  # 4: vértice e âncora do próximo
    (50, 200),   # 5: ponto de controle
    (50, 100),   # 6: vértice final
]

def setup():
    size(400, 300)

def draw():
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    with begin_shape():
        vertex(pontos[0][0], pontos[0][1])  # primeiro ponto (índice 0)
        for (px, py), (x, y) in zip(pontos[1::2], pontos[2::2]):  
            # do segundo e terceiro pontos (índices 1 e 2) em diante 
            quadratic_vertex(px, py, x, y)
    
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if i == arrastando:
            fill(200, 0, 0)
        elif dist(mouse_x, mouse_y, x, y) < 10:
            fill(255, 255, 0)
        else:
            fill(255)
        ellipse(x, y, 5, 5)
        t = f'{i}: {"vertex" if i == 0 else "control" if i % 2 else "quadratic"}'
        text(t, x + 5, y - 5)

def mouse_pressed():
    global arrastando
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if dist(mouse_x, mouse_y, x, y) < 10:
            arrastando = i
            break 

def mouse_released():
    global arrastando
    arrastando = None

def mouse_dragged():
    global pontos
    global arrastando
    if arrastando is not None:
        x, y = pontos[arrastando]
        x += mouse_x - pmouse_x
        y += mouse_y - pmouse_y
        pontos[arrastando] = x, y
Resposta: Testador para curve_vertex() com pontos arrastáveis.
arrastando = None

pontos = [
    (100, 50),
    (150, 100),
    (250, 100),
    (250, 200),
    (150, 200),
    (50, 200),
    (50, 100)]

def setup():
    size(300, 300)

def draw():
    background(100)
    stroke_weight(3)
    stroke(0)
    no_fill()

    begin_shape()
    curve_vertex(pontos[-1][0], pontos[-1][1])
    for x, y in pontos:
        curve_vertex(x, y)
    curve_vertex(pontos[0][0], pontos[0][1])
    curve_vertex(pontos[1][0], pontos[1][1])
    end_shape(CLOSE)
    stroke_weight(1)
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if i == arrastando:
            fill(200, 0, 0)
        elif dist(mouse_x, mouse_y, x, y) < 10:
            fill(255, 255, 0)
        else:
            fill(255)
        circle(x, y, 5)
        t = '{}: {:03}, {:03}'.format(i, x, y)
        text(t, x + 5, y - 5)

def mouse_pressed():
    # quando um botão do mouse é apertado
    global arrastando
    for i, ponto in enumerate(pontos):
        x, y = ponto
        if dist(mouse_x, mouse_y, x, y) < 10:
            arrastando = i
            break  # encerra o laço

def mouse_released():
    # quando um botão do mouse é solto
    global arrastando
    arrastando = None

def mouse_dragged():
     # quando o mouse é movido apertado
     global pontos
     global arrastando
     if arrastando is not None:
        x, y = pontos[arrastando]
        x += mouse_x - pmouse_x
        y += mouse_y - pmouse_y
        pontos[arrastando] = x, y

Assuntos relacionados