Avançar para o conteúdo
Imagem com logotipo, contendo link para a página inicial
  • United Stated of America flag, representing the option for the English language.
  • Bandeira do Brasil, simbolizando a opção pelo idioma Português do Brasil.

Ideias, Regras, Simulação: Pixels e Primitivas Gráficas (Pontos, Linhas, e Arcos)

Imagens com uso de pixels e primitivas gráficas (pontos, linhas e arcos) criadas neste tópico para JavaScript com HTML Canvas, GDScript com Godot Engine, Python com PyGame, e Lua com LÖVE. A imagem também apresenta um link para este website: <www.francogarcia.com>, assim como a conta francogarciacom, usada para o Twitter e Instagram do autor.

Créditos para a imagem: Imagem criada pelo autor usando o programa Inkscape; ícones de Font Awesome.

Pré-Requisitos

O material de Ideias, Regras, Simulação promove aprendizado baseado em projetos (simulações interativas e jogos digitais) com recursos multimídia. Assim, ele é complementar ao material de Aprenda Programação, que introduz fundamentos e técnicas básicas de programação, com exemplos para JavaScript, Python, Lua e GDScript (para Godot Engine).

Para Ideias, Regras, Simulação, você precisará de um ambiente de desenvolvimento configurado para uma das linguagens anteriores:

JavaScript (para navegadores) e GDScript fornecem suporte para conteúdo multimídia.

Também estou considerando melhorar os editores online para programação dentro do website, em um estilo de curso interativo. Atualmente, a página de ferramentas fornece opções para:

Os editores possuem suporte para imagens em navegador. Contudo, Python e Lua utilizam a sintaxe para JavaScript com canvas. Caso eu criasse uma abstração e adicionasse suporte para áudio, eles tornar-se-iam opções válidas (ao menos para primeiras atividades).

Contudo, convém configurar um Ambiente Integrado de Desenvolvimento (em inglês, Integrated Development Environment ou IDE), ou a combinação de editor de texto com interpretador assim que possível para programar em sua máquina com maior eficiência. Ambientes online são práticos, mas ambientes locais são mais potencialmente mais completos, eficientes, rápidos e personalizáveis. Caso queira tornar-se profissional, você precisará de um ambiente de desenvolvimento configurado. Quanto antes o fizer, melhor.

Versão em Vídeo

Este tópico possui versões compactas em vídeo no canal do autor no YouTube:

Contudo, esta versão em texto é mais completa.

Documentação

Pratique consultar pela documentação:

Praticamente todos os links possuem um campo para buscas. Em Lua, a documentação está em uma única página. Você pode procurar por entradas usando o atalho Ctrl F (busca ou search).

Primitivas Gráficas

No primeiro tópico de Ideias, Regras, Simulação, criamos uma janela e escrevemos texto nela. O desenho de um texto é, na realidade, algo relativamente complexo. As bibliotecas usadas simplificaram a operação.

Caso você quisesse criar sua própria implementação, o processo seria semelhante à criar rasterizador de texto para arquivos de imagem. A diferença é que, ao invés de salvar os dados em um arquivo de imagem, o resultado seria desenhado na tela.

Desenhar na tela. Afinal, o que é necessário para isso?

A menor unidade para desenho na tela é um pixel (px), o nome resultante da combinação dos termos em Inglês picture element (elemento de imagem). Um pixel normalmente é um (minúsculo) quadrado que possui uma cor. Entretanto, a forma quadrada não é obrigatória; por exemplo, existem tecnologias que definem pixels retangulares. De fato, isso era comum em telas mais antigas. Por exemplo, nesta lista de resoluções de tela comuns, todos os valores da coluna Pixel que não possuem proporção 1:1 utilizam pixels retangulares.

Qualquer que seja o caso, uma imagem digital ou um monitor combinam pixels para gerar imagens. Por exemplo, um monitor que possui resolução Full HD possui 1920 linhas e 1080 colunas. O produto de linhas e colunas é o número de pixels que podem ser desenhados na resolução. Ou seja, a resolução Full HD possui pixels. Arrendondando para baixo, seriam 2 milhões de pixels; caso prefira, cerca de 2,07 MP (Megapixels).

Assim, o elemento básico de uma imagem é um pixel. Pode-se pensar um pixel como um ponto na tela. Desenhos são, pois, combinações de pontos seguindo padrões artísticos.

Evidentemente, pensar em padrões de pontos não é algo conveniente. Para facilitar operações com imagens, pode-se combinar pixels para se desenhar padrões mais complexos, como linhas e arcos. Pontos, linhas e arcos são chamados de primitivas gráficas ou primitivas para desenhos.

Primitiva significa que elas servem como elementos básicos para a criação de padrões mais complexos. Por exemplo, combinações de linhas podem gerar retângulos, quadrados, losangos e outros polígonos. Arcos podem gerar elipses, círculos, e outras foram envolvendo curvas. Combinações de linhas e arcos permitem desenhar formas mais complexas.

Imagens Matriciais (Raster) e Imagens Vetoriais

Linhas e arcos são a base de gráficos vetoriais, também chamados de desenhos vetoriais. Gráficos vetoriais são construções matemáticas baseadas em vetores. Eles não usam pixels, mas equações matemáticas. Isso permite, dentre outros e com os devidos cuidados, redimensionar imagens sem perdas de qualidade.

A criação de imagens com pixels, por outro lado, é matricial. Por isso, imagens com pixels são comumente chamadas de raster, matriciais ou bitmap (mapa de bits). A ampliação de imagens raster tende a ser problemática, comumente gerando perda de qualidade.

Uma imagem raster pode aproximar formas vetoriais usando um processo chamado de rasterização. Como pixels costumam ser quadrados ou retangulares, a rasterização é um processo imperfeito. Por exemplo, círculos não serão perfeitamente curvos, mas aproximações desenhadas como combinações de dezenas, centenas ou milhares de segmentos de retas. Com milhões de pixels e o uso de técnicas como anti-aliasing, é possível iludir os olhos com aproximações convincentes. Contudo, elas são aproximações (e as técnicas podem ser interessantes para tópicos futuros).

Para trabalhar com imagens digitais, é possível utilizar imagens matriciais (raster) ou vetoriais. A escolha depende de preferências da (ou do) artista, e/ou da ferramenta (editor de imagem) empregada para a criação da imagem. Exemplos tradicionais de ferramentas que trabalham com imagens raster podem incluir Adobe Photoshop, GIMP e Microsoft Paint; formatos tradicionais incluem BMP, JPEG e PNG. Exemplos tradicionais de ferramentas que trabalham com imagens vetoriais podem incluir Adobe Illustrator, Inkscape e CorelDRAW; um dos formatos mais tradicionais é o SVG.

Neste primeiro momento, os exemplos utilizarão imagens matriciais. A escolha é conveniente, pois permite desenhar imagens diretamente usando pixels. Contudo, equações matemáticas serão implementadas para aproximar valores vetoriais para valores discretos (que serão desenhados como pixels). Essas aproximações resultarão em erros observáveis nas imagens resultantes.

Sistemas de Coordenadas e Plano Cartesiano

Matemática é essencial para trabalhar com gráficos em aplicações minimamente complexas. Em particular, trigonometria é importante. Álgebra vetorial e álgebra linear também são úteis. Entretanto, em muitos casos, Matemática básica é suficiente.

Caso você sobrevivera aos parágrafo anterior, basta saber que você não precisa ser especialista em Matemática. De fato, eu (o autor) apresentarei rapidamente conceitos importantes conforme necessário.

O primeiro deles é o Plano Cartesiano. Na realidade, mais importante são sistemas de coordenadas. Se você conseguiu escrever texto no tópico anterior, você praticamente já sabe tudo que precisará neste primeiro momento.

Um ponto em um plano (de duas dimensões ou 2D) pode ser representado como um par ordenado . O primeiro valor, , é chamado de abscissa e representa o valor no eixo X (o eixo horizontal). O segundo valor, , é chamado de ordenada e representa o valor no eixo Y (o eixo vertical).

Para um espaço tridimensional (3D), o ponto seria representado pelo terno ordenado . O terceiro valor, , é chamado de cota, e representa o valor no eixo Z (eixo transversal).

A Matemática para trabalhar com 2D é mais simples que 3D; um segundo benefício é que parte do conhecimento também será aplicável para 3D. Assim, convém começar com duas dimensões. Dependendo da evolução da série, pode-se considerar a terceira dimensão no futuro.

Em gráficos computacionais (computer graphics) 2D, é comum iniciar a origem do sistema de coordenadas, isto é, o par no canto superior esquerdo da janela. Embora existam exceções, a convenção é comum.

Assim, para um par , valores positivos de aparecem à direita da origem; valores negativos aparecem à esquerda. Similarmente, valores positivos de aparecem à abaixo da origem; valores negativos aparecem acima.

Em outras palavras, as coordenadas "crescem" para a direita e para baixo. Elas "diminuem" para a esquerda e para cima. Ou seja, o eixo Y é invertido em relação ao Plano Cartesiano usado na Matemática escolar.

Se você estiver usando um computador de mesa ou laptop, passe o mouse sobre o retângulo a seguir. O par ordenado será escrito na posição do cursor do mouse no canvas (isso significa que posições próximas dos cantos serão cortadas).

Se você estiver usando um dispositivo móvel (como tablet ou smartphone), você pode tocar uma posição dentro do retângulo para escrever a posição dentro dele.

Visualizador de posição de mouse dentro do canvas. As posições são exibidas como um par ordenado (x,y ).

É interessante notar que você já sabe fazer duas partes do programa anterior: o desenho da cor do fundo e do texto. O programa anterior monitora a posição do mouse dentro do canvas, atualizando a área de desenho a cada atualização de coordenada (isto é, sempre que se move o mouse dentro da região).

Pontos e Pixels

Saber como escrever um par ordenado é suficiente para começar desenhos com primitivas gráficas. Ter uma noção de ondem o desenho começará também é útil; por isso a importância de conhecer a origem e a direção de valores positivos.

Assim, o próximo passo é desenhar um pixel na tela. Algumas Interfaces de Programação de Aplicações (Application Programming Interfaces ou APIs) fornecem subrotinas para desenhar pixels. Outras desenham um ponto como representação de um pixel.

Caso a API não forneça o recurso, uma alternativa é desenhar um retângulo. Um retângulo com valores unitários (1) para altura e largura formam um quadrado que pode servir como representação de um pixel.

Canvas em HTML para JavaScript

Como na Introdução de Ideias, Regras, Simulação, JavaScript requer um arquivo HTML auxiliar para a declaração do canvas. O nome do arquivo HTML é de sua escolha (por exemplo, index.html). O exemplo a seguir assume que o arquivo JavaScript terá nome script.js e que o canvas terá identificador (id) canvas. Caso você altere os valores, lembre-se de modificá-los nos arquivos conforme necessário.

<!DOCTYPE html>
<html lang="pt-BR">
  <head>
    <meta charset="utf-8">
    <title>www.FrancoGarcia.com</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <div style="text-align: center;">
      <canvas id="canvas"
              width="1"
              height="1"
              style="border: 1px solid #000000;">
      >
          Acessibilidade: texto alternativo para conteúdo gráfico.
      </canvas>
    </div>

    <!-- NOTA Atualizar com nome do arquivo JavaScript. -->
    <script src="./script.js"></script>
  </body>
</html>

O arquivo também mantém o lembrete sobre acessibilidade. Neste tópico, o conteúdo tornar-se-á ainda mais inacessível para pessoas com certos tipos de deficiência visual, devido a adição de conteúdo gráfico sem opções alternativas para comunicação do conteúdo.

No futuro, a intenção é abordar formas de tornar simulações mais acessíveis. Neste momento, este lembrete é apenas informativo, para conscientizar você da importância de acessibilidade.

GDScript, JavaScript, Python e Lua

Um pixel é pequeno. Para facilitar a visualização, os exemplos desenharão um pixel branco sobre um fundo preto. O contraste facilitará a identificação do pixel desenhado na janela.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var points = PoolVector2Array([Vector2(10, 20)])
    var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
    var uvs =  PoolVector2Array()
    draw_primitive(points, colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

context.fillStyle = "white"
context.fillRect(10, 20, 1, 1)
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")

window.fill((0, 0, 0))

window.set_at((10, 20), (255, 255, 255))

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    love.graphics.setColor(1.0, 1.0, 1.0)
    love.graphics.points(10, 20)
end

Em todos os exemplos, basta uma chamada de subrotina para desenhar um pixel na janela.

As implementações mais simples são em Python com PyGame e Lua com LÖVE (Love2D). PyGame fornece o método set_at() para colorir um pixel em uma posição. LÖVE fornece love.graphics.points() para desenhar um ou mais pixels em pares de posições x e y fornecidas.

Em JavaScript com canvas, pode-se desenhar um quadrado usando fillRect() para simular o desenho de um pixel. Em GDScript, draw_primitive() permite desenhar um ponto, uma linha, um triângulo ou um quadrilátero, dependendo do número de pontos passados para o primeiro argumento.

O canvas a seguir apresenta o resultado. O pixel branco provavelmente não é um bad pixel em seu monitor. Caso você role a página em seu navegador, o pixel branco deve acompanhar o retângulo preto.

Desenho de um pixel branco na posição (10, 20) de um canvas com fundo preto.

Como se pode observar no canvas, um pixel é pequeno. Caso queira ampliá-lo, sistemas operacionais comumente fornecem um programa para ampliação de imagens na tela. O programa é uma ferramenta de acessibilidade chamada lupa ou lente de aumento. Por exemplo, Lupa para Windows e KMag do KDE. A lupa ou lente de aumento é útil, por exemplo, para ajudar pessoas com baixa visão a enxergarem conteúdo na tela.

De qualquer forma, para desenhar um pixel em outra posição, basta repetir o processo escolhendo outra coordenada para o desenho. Caso se escolha a mesma, apenas o último valor fornecido será considerado.

Embora seja possível sobrepor cores usando transparência, isso costuma requerer o uso de uma técnica chamada alpha blending (que significa misturar alpha). Por sinal, alpha blending será um dos futuros tópicos de Ideias, Regras, Simulação.

Linhas

Um pixel pode ser imaginado como um ponto. Uma seqüência de pontos contínuos forma uma linha.

Linhas com Pontos

A rigor, uma linha não precisa ser um segmento de reta (ou seja, uma linha pode ter curvas). Entretanto, quando se pensa em linhas em APIs de desenho, é comum que a definição seja um segmento de reta.

Por exemplo, o desenho de uma seqüência de pontos (como pixels) como (10, 20), (11, 20), (12, 20), (13, 40), (14,20), (15,20), (16, 20), (17, 20), (18, 20), (19, 20), e (20, 20) formaria um pequeno segmento de reta horizontal. Ou seja, uma linha horizontal.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
    var uvs =  PoolVector2Array()
    draw_primitive(PoolVector2Array([Vector2(10, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(11, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(12, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(13, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(14, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(15, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(16, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(17, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(18, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(19, 20)]), colors, uvs)
    draw_primitive(PoolVector2Array([Vector2(20, 20)]), colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

context.fillStyle = "white"
context.fillRect(10, 20, 1, 1)
context.fillRect(11, 20, 1, 1)
context.fillRect(12, 20, 1, 1)
context.fillRect(13, 20, 1, 1)
context.fillRect(14, 20, 1, 1)
context.fillRect(15, 20, 1, 1)
context.fillRect(16, 20, 1, 1)
context.fillRect(17, 20, 1, 1)
context.fillRect(18, 20, 1, 1)
context.fillRect(19, 20, 1, 1)
context.fillRect(20, 20, 1, 1)
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")

window.fill((0, 0, 0))

window.set_at((10, 20), (255, 255, 255))
window.set_at((11, 20), (255, 255, 255))
window.set_at((12, 20), (255, 255, 255))
window.set_at((13, 20), (255, 255, 255))
window.set_at((14, 20), (255, 255, 255))
window.set_at((15, 20), (255, 255, 255))
window.set_at((16, 20), (255, 255, 255))
window.set_at((17, 20), (255, 255, 255))
window.set_at((18, 20), (255, 255, 255))
window.set_at((19, 20), (255, 255, 255))
window.set_at((20, 20), (255, 255, 255))

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    love.graphics.setColor(1.0, 1.0, 1.0)
    love.graphics.points(10, 20)
    love.graphics.points(11, 20)
    love.graphics.points(12, 20)
    love.graphics.points(13, 20)
    love.graphics.points(14, 20)
    love.graphics.points(15, 20)
    love.graphics.points(16, 20)
    love.graphics.points(17, 20)
    love.graphics.points(18, 20)
    love.graphics.points(19, 20)
    love.graphics.points(20, 20)

    -- Ou:
--[[
    love.graphics.points(
        10, 20,
        11, 20,
        12, 20,
        13, 20,
        14, 20,
        15, 20,
        16, 20,
        17, 20,
        18, 20,
        19, 20,
        20, 20
    )
]]--
end

O resultado do desenho é exibido no próximo canvas.

Um segmento de reta formado pelos pontos (10, 20), (11, 20), (12, 20), (13, 40), (14,20), (15,20), (16, 20), (17, 20), (18, 20), (19, 20) e (20, 20).

Para desenhar uma linha vertical, bastaria manter os valores em constantes e alterar os valores de . Por exemplo, (10, 20), (10, 21), (10, 22), (10, 23), (10, 24), (10, 25), (10, 26), (10, 27), (10, 28), (10, 29), e (10, 30). Com uma nova linha horizontal intermediária (por exemplo, com os pontos (10, 25), (11, 25), (12, 25), (13, 25), (14, 25), (15, 25), e (16, 25)), poder-se-ia formar uma letra F maiúscula.

Uma letra F desenhada ponto a ponto.

Finalmente, alterando-se valores de e , poder-se-ia criar segmentos de retas inclinados. Dependendo do coeficiente angular da reta, o desenho poderia ser aparecer "serrilhado". Essa é uma conseqüência da tentativa de adaptar uma expressão matemática com números reais (uma equação de reta) em uma imagem raster formada por números inteiros.

Antes de continuar, tente escrever a letra G ou a primeira letra de seu nome usando pontos. A tarefa será repetitiva. Existe uma forma mais conveniente.

Linhas com Repetições de Pontos

Ao invés de descrever o desenho ponto a ponto, uma alternativa melhor é deixar a parte repetitiva a cargo de um computador. Para isso, basta usar uma estrutura de repetição (laço ou loop), como apresentado em Aprenda Programação.

Para um primeiro exemplo, pode-se usar o comando Para (For). Uma estrutura for possui quatro partes:

  1. Inicialização de uma variável usada para a condição;
  2. Condição de parada;
  3. Alteração da variável de condição;
  4. Um bloco de código para repetir.

Para o desenho de uma linha, o bloco de código para se repetir será as chamadas para desenhar cada um dos pixels. As demais partes controlarão o tamanho da linha; cada repetição (ou iteração) desenhará um novo pixel na janela.

Para a implementação, deve-se escolher se o desenho na linha incluirá (ou não) o último pixel na posição final. Isso é algo que pode variar entre diferentes APIs gráficas. Para manter todos os exemplos idênticos, todas as versões desenharão o pixel na coordenada final.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
    var uvs =  PoolVector2Array()
    # F
    for i in range(0, 11):
        draw_primitive(PoolVector2Array([Vector2(10 + i, 20)]), colors, uvs)

    for i in range(0, 11):
        draw_primitive(PoolVector2Array([Vector2(10, 20 + i)]), colors, uvs)

    for i in range(0, 7):
        draw_primitive(PoolVector2Array([Vector2(10 + i, 25)]), colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

context.fillStyle = "white"
// F
for (let i = 0; i <= 10; ++i) {
    context.fillRect(10 + i, 20, 1, 1)
}

for (let i = 0; i <= 10; ++i) {
    context.fillRect(10, 20 + i, 1, 1)
}

for (let i = 0; i <= 6; ++i) {
    context.fillRect(10 + i, 25, 1, 1)
}
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")

window.fill((0, 0, 0))

# F
for i in range(0, 11):
    window.set_at((10 + i, 20), (255, 255, 255))

for i in range(0, 11):
    window.set_at((10, 20 + i), (255, 255, 255))

for i in range(0, 7):
    window.set_at((10 + i, 25), (255, 255, 255))

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    love.graphics.setColor(1.0, 1.0, 1.0)
    -- F
    for i = 0, 10 do
        love.graphics.points(10 + i, 20)
    end

    for i = 0, 10 do
        love.graphics.points(10, 20 + i)
    end

    for i = 0, 6 do
        love.graphics.points(10 + i, 25)
    end
end

A nova versão desenha uma letra F maiúscula. Ao invés de duplicar chamadas para desenhar um pixel, usa-se uma combinação de aritmética com a variável i declarada para as repetições para modificar o próximo pixel a ser colorido.

Uma letra F desenhada ponto a ponto usando um laço.

Para aumentar ou diminuir o tamanho, bastaria alterar o valor final definido na condição de cada laço for.

Tente modificar o exemplo anterior para desenhar um A maiúsculo ao invés de um F. A letra A pode ser retangular, para facilitar.

 _
|_|
| |

A solução deve usar quatro laços. Ela será semelhante à feita para a letra F.

Linhas com Subrotinas

Além de estruturas de repetição, é possível simplificar ainda mais a solução definindo um bloco de código reusável para desenhar a linha. Para isso, novamente retomamos Aprenda Programação: subrotinas (funções e procedimentos).

Para desenhar uma linha horizontal, bastaria manter uma ordenada (valor de ) fixa e variar a abscissa (valor de ) usando uma estrutura de repetição. Para uma solução reusável, o código criado poderia ser definido como um procedimento draw_horizontal_line() (ou desenhe_linha_horizontal()).

Para desenhar uma linha vertical, bastaria inverter o processo: mantém-se uma abscissa fixa e varia-se a ordenada usando uma estrutura de repetição. O procedimento poderia chamar draw_vertical_line() (ou desenhe_linha_vertical()).

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func draw_horizontal_line(x0, x1, y, color):
    for x in range(x0, x1 + 1):
        draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                        PoolColorArray([color]),
                                        PoolVector2Array())

func draw_vertical_line(x, y0, y1, color):
    for y in range(y0, y1 + 1):
        draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                        PoolColorArray([color]),
                                        PoolVector2Array())

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    # F
    draw_horizontal_line(10, 20, 20, Color.white)
    draw_vertical_line(10, 20, 30, Color.white)
    draw_horizontal_line(10, 16, 25, Color.white)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function draw_horizontal_line(x0, x1, y, color) {
    context.fillStyle = color
    for (let x = x0; x <= x1; ++x) {
        context.fillRect(x, y, 1, 1)
    }
}

function draw_vertical_line(x, y0, y1, color) {
    context.fillStyle = color
    for (let y = y0; y <= y1; ++y) {
        context.fillRect(x, y, 1, 1)
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

// F
draw_horizontal_line(10, 20, 20, "white")
draw_vertical_line(10, 20, 30, "white")
draw_horizontal_line(10, 16, 25, "white")
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_horizontal_line(x0, x1, y, color):
    for x in range(x0, x1 + 1):
        window.set_at((x, y), color)

def draw_vertical_line(x, y0, y1, color):
    for y in range(y0, y1 + 1):
        window.set_at((x, y), color)

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
# F
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 16, 25, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_horizontal_line(x0, x1, y, color)
    love.graphics.setColor(color)
    for x = x0, x1 do
        love.graphics.points(x, y)
    end
end

function draw_vertical_line(x, y0, y1, color)
    love.graphics.setColor(color)
    for y = y0, y1 do
        love.graphics.points(x, y)
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    -- F
    draw_horizontal_line(10, 20, 20, WHITE)
    draw_vertical_line(10, 20, 30, WHITE)
    draw_horizontal_line(10, 16, 25, WHITE)
end

As novas versões também incluem variáveis ou constantes para cores. O intuito é tornar as implementações mais similares, para que você possa identificar como a solução é semelhante independentemente da linguagem de programação ou biblioteca. Novamente, fundamentos independem de tecnologia (eles são agnósticos de tecnologia).

Com a mudança, a escrita da letra F pode ser feita usando três linhas de código: duas chamadas a draw_horizontal_line() e uma chamada a draw_vertical_line(). Para desenhar um A ao invés de um F, bastaria fazer uma nova chamada a draw_vertical_line() na posição apropriada e mudar o tamanho da linha horizontal intermediária.

De fato, excetuando-se o parâmetro de saída cor, todas as implementações ficariam praticamente iguais.

# A
var WHITE = Color.white
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
// A
const WHITE = "white"
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
# A
WHITE = pygame.Color("white")
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
-- A
local WHITE = {1.0, 1.0, 1.0}
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)

Para completar as operações, o próximo passo seria desenhar um segmento de reta inclinado. Ao invés de um terceiro procedimento, convém unificar todas as chamadas em um único procedimento, como um draw_line() genérico.

Unificando as Subrotinas de Desenho de Linhas

Matematicamente, dados dois pontos e , pode-se definir a equação da reta que passa por eles como:

Na equação, é o coeficiente angular. é o coeficiente linear, que é o valor de , ou seja, o valor de quando para o ponto com . Para calcular , basta escolher um dos dois pontos e isolar a variável na equação da reta.

Para o ponto :

Alternativamente, para o ponto :

Assim, bastam dois pontos e para desenhar uma reta. Pode-se usar os valores fornecidos para se obter a equação da reta, e, em seguida, utilizá-la para obter os valores intermediários entre e .

Por exemplo, poder-se-ia criar um procedimento chamado draw_line() para o desenho de linhas. Ele poderia ter como parâmetros: a coordenada inicial (um ponto), a coordenada final (outro ponto diferente do primeiro), e a cor. Para a implementação, bastaria traduzir a equação para operações aritméticas.

Contudo, existe um caso especial a ser considerar. No caso de uma reta vertical, seria igual a , o que geraria uma divisão por zero. Para evitá-la, pode-se usar uma estrutura de condição, cuja explicação está em Aprenda Programação.

Uma estrutura se/então/senão define dois possíveis fluxos alternativos, sendo um o escolhido conforme o resultado de uma expressão lógica:

  1. Se a expressão resultar em True (Verdadeiro), executa-se apenas o código definido para parte então (then);
  2. Caso contrário, se a expressão resultar em False (Falso), executa-se apenas o código definido para parte senão (else).

Os fluxos são mutuamente exclusivos.

Se este é seu primeiro contato com uma estrutura de condição, a versão em Lua é a mais ilustrativa.

Assim, para o desenho de uma linha qualquer, pode-se considerar dois cenários:

  1. No caso de uma reta vertical (isto é, x0 == x1), a implementação será idêntica à feita para draw_vertical_line();
  2. Em qualquer outro caso, usa a equação da reta . Para o desenho, deve-se calcular os valores para uma seqüência crescente de valores de pertencentes ao intervalo (ou , dependendo do maior valor).

A rigor, existe um terceiro caso possível: se os dois pontos forem iguais, não é possível desenhar a reta (o resultado seria o próprio ponto). Você pode tentar implementá-lo (adicionando uma nova estrutura condicional).

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func franco_draw_line(x0, y0, x1, y1, color):
    if (x0 == x1):
        for y in range(y0, y1 + 1):
            draw_primitive(PoolVector2Array([Vector2(x0, y)]),
                                            PoolColorArray([color]),
                                            PoolVector2Array())
    else:
        var a = 1.0 * (y1 - y0) / (x1 - x0)
        var b = y0 - a * x0

        var max_x = max(x0, x1)
        var min_x = min(x0, x1)
        for x in range(min_x, max_x + 1):
            var y = floor(a * x + b)
            draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                            PoolColorArray([color]),
                                            PoolVector2Array())

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var WHITE = Color.white
    # F
    franco_draw_line(10, 20, 20, 20, WHITE)
    franco_draw_line(10, 20, 10, 30, WHITE)
    franco_draw_line(10, 25, 16, 25, WHITE)

    # Triângulo (pontilhado?)
    franco_draw_line(110, 120, 120, 120, WHITE)
    franco_draw_line(110, 120, 115, 130, WHITE)
    franco_draw_line(115, 130, 120, 120, WHITE)

    # Pontilhado?
    franco_draw_line(200, 40, 250, 200, WHITE)
    franco_draw_line(250, 40, 260, 200, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function draw_line(x0, y0, x1, y1, color) {
    context.fillStyle = color
    if (x0 === x1) {
        for (let y = y0; y <= y1; ++y) {
            context.fillRect(x0, y, 1, 1)
        }
    } else {
        let a = 1.0 * (y1 - y0) / (x1 - x0)
        let b = y0 - a * x0

        let max_x = Math.max(x0, x1)
        let min_x = Math.min(x0, x1)
        for (let x = min_x; x <= max_x; ++x) {
            let y = Math.floor(a * x + b)
            context.fillRect(x, y, 1, 1)
        }
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

// Triângulo (pontilhado?)
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

// Pontilhado?
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_line(x0, y0, x1, y1, color):
    if (x0 == x1):
        for y in range(y0, y1 + 1):
            window.set_at((x0, y), color)
    else:
        a = 1.0 * (y1 - y0) / (x1 - x0)
        b = y0 - a * x0

        max_x = max(x0, x1)
        min_x = min(x0, x1)
        for x in range(min_x, max_x + 1):
            y = math.floor(a * x + b)
            window.set_at((x, y), color)

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

# Triângulo (pontilhado?)
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

# Pontilhado?
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)

    if (x0 == x1) then
        for y = y0, y1 do
            love.graphics.points(x0, y)
        end
    else
        local a = 1.0 * (y1 - y0) / (x1 - x0)
        local b = y0 - a * x0

        local max_x = math.max(x0, x1)
        local min_x = math.min(x0, x1)
        for x = min_x, max_x do
            local y = math.floor(a * x + b)
            love.graphics.points(x, y)
        end
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    -- F
    draw_line(10, 20, 20, 20, WHITE)
    draw_line(10, 20, 10, 30, WHITE)
    draw_line(10, 25, 16, 25, WHITE)

    -- Triângulo (pontilhado?)
    draw_line(110, 120, 120, 120, WHITE)
    draw_line(110, 120, 115, 130, WHITE)
    draw_line(115, 130, 120, 120, WHITE)

    -- Pontilhado?
    draw_line(200, 40, 250, 200, WHITE)
    draw_line(250, 40, 260, 200, WHITE)
end

A implementação em GDScript utiliza o nome franco_draw_line() porque draw_line() é o nome de um método de Control (herdado de CanvasItem).

As chamadas de max() e min() em cada implementação substituem o uso de uma estrutura condicional. Por exemplo max_x = max(x0, x1) equivaleria a:

max_x = x0
if (x1 > x0):
    max_x = x1

Os próximos exemplos alternarão entre chamadas de funções prontas ou o uso de estruturas condicionais (caso você precise praticar).

O resultado da versão em JavaScript para canvas aparece a seguir.

Desenho de um F, um triângulo e duas retas inclinadas. As retas inclinadas e os lados inclinados do triângulo estão pontilhados.

Pode-se perceber que as retas inclinadas não são contínuas, mas pontilhadas. Isso ocorre porque se usou um número insuficiente de valores para para aproximar um resultado mais próximo do real. De fato, quanto maior o intervalo em e menor o intervalo em , maior será a distância entre os pixels desenhados.

Além disso, é possível modificar a solução para torná-la mais eficiente computacionalmente. Ao invés de utilizar a equação para calcular cada valor de , pode-se usar um acumulador. Por exemplo, a implementação em Python a seguir é equivalente à anterior, mas realiza somas ao invés de multiplicações.

def draw_line(x0, y0, x1, y1, color):
    if (x0 == x1):
        for y in range(y0, y1 + 1):
            window.set_at((x0, y), color)
    else:
        dx = x1 - x0
        dy = y1 - y0
        a = 1.0 * dy / dx

        repetitions = dx
        if (repetitions < 0):
            repetitions = -repetitions

        x = x0
        y = y0
        for i in range(repetitions + 1):
            window.set_at((int(x), int(y)), color)
            x += 1
            y += a

Neste caso, poder-se-ia substituir a estrutura condicional para tornar o valor de repetitions positivo por uma chamada de uma função valor absoluto (ou módulo; normalmente abs()).

Os valores calculados para x e y atualizam o valor anterior com o próximo valor esperado. Por exemplo, x += 1 corresponde a x = x + 1, ou seja, adiciona-se 1 ao valor anterior para atualizá-lo. Como uma multiplicação pode ser calculada como somas sucessivas, o resultado acumulado de y corresponde ao que seria obtido usando-se a equação da reta.

Antes de apresentar algoritmos para desenhar linhas, pense em como você poderia alterar a solução para desenhar uma linha mais contínua. Dica: uma forma simples é aumentar o número de pontos intermediários desenhados entre e quando . Quanto mais pontos para aproximar a linha, melhor será o desenho.

Desenhando Linhas Contínuas

Uma forma de remover o pontilhado para gerar linhas mais contínuas consiste em desenhar mais pontos quando necessário. Entretanto, é possível fazê-lo de diferentes formas.

De fato, existem vários algoritmos para desenhar linhas. Um dos mais simples chama-se analisador diferencial digital (do inglês, digital differential analyzer ou DDA).

O algoritmo DDA é similar ao uso da equação da reta quando . Entretanto, quando , ele inverte os incrementos: muda-se de um em um, e incrementa-se de por . Ou seja, basta incluir algumas estruturas de condição na versão modificada para Python. Dessa forma, gera-se mais pontos intermediários para desenhar a reta, resultando em uma linha contínua.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func franco_draw_line(x0, y0, x1, y1, color):
    if (x0 == x1):
        for y in range(y0, y1 + 1):
            draw_primitive(PoolVector2Array([Vector2(x0, y)]),
                                            PoolColorArray([color]),
                                            PoolVector2Array())
    else:
        var dx = x1 - x0
        var dy = y1 - y0
        var a = 1.0 * dy / dx

        var increment_x = 1
        var increment_y = a
        var repetitions = dx
        if (a > 0):
            increment_x = 1.0 / a
            increment_y = 1
            repetitions = dy

        if (repetitions < 0):
            repetitions = -repetitions

        var x = x0
        var y = y0
        for i in range(repetitions + 1):
            draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                            PoolColorArray([color]),
                                            PoolVector2Array())
            x += increment_x
            y += increment_y

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var WHITE = Color.white
    # F
    franco_draw_line(10, 20, 20, 20, WHITE)
    franco_draw_line(10, 20, 10, 30, WHITE)
    franco_draw_line(10, 25, 16, 25, WHITE)

    # Triângulo
    franco_draw_line(110, 120, 120, 120, WHITE)
    franco_draw_line(110, 120, 115, 130, WHITE)
    franco_draw_line(115, 130, 120, 120, WHITE)

    # Linhas contínuas
    franco_draw_line(200, 40, 250, 200, WHITE)
    franco_draw_line(250, 40, 260, 200, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function draw_line(x0, y0, x1, y1, color) {
    context.fillStyle = color
    if (x0 === x1) {
        for (let y = y0; y <= y1; ++y) {
            context.fillRect(x0, y, 1, 1)
        }
    } else {
        let dx = x1 - x0
        let dy = y1 - y0
        let a = 1.0 * dy / dx

        let increment_x = 1
        let increment_y = a
        let repetitions = dx
        if (a > 0) {
            increment_x = 1.0 / a
            increment_y = 1
            repetitions = dy
        }

        if (repetitions < 0) {
            repetitions = -repetitions
        }

        let x = x0
        let y = y0
        for (let i = 0; i <= repetitions; ++i) {
            context.fillRect(x, y, 1, 1)
            x += increment_x
            y += increment_y
        }
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

// Triângulo
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

// Linhas contínuas
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_line(x0, y0, x1, y1, color):
    if (x0 == x1):
        for y in range(y0, y1 + 1):
            window.set_at((x0, y), color)
    else:
        dx = x1 - x0
        dy = y1 - y0
        a = 1.0 * dy / dx

        increment_x = 1
        increment_y = a
        repetitions = dx
        if (a > 0):
            increment_x = 1.0 / a
            increment_y = 1
            repetitions = dy

        if (repetitions < 0):
            repetitions = -repetitions

        x = x0
        y = y0
        for i in range(repetitions + 1):
            window.set_at((int(x), int(y)), color)
            x += increment_x
            y += increment_y

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

# Triângulo
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

# Linhas contínuas
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)

    if (x0 == x1) then
        for y = y0, y1 do
            love.graphics.points(x0, y)
        end
    else
        local dx = x1 - x0
        local dy = y1 - y0
        local a = 1.0 * dy / dx

        local increment_x = 1
        local increment_y = a
        local repetitions = dx
        if (a > 0) then
            increment_x = 1.0 / a
            increment_y = 1
            repetitions = dy
        end

        if (repetitions < 0) then
            repetitions = -repetitions
        end

        local x = x0
        local y = y0
        for i = 0, repetitions do
            love.graphics.points(x, y)
            x = x + increment_x
            y = y + increment_y
        end
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    -- F
    draw_line(10, 20, 20, 20, WHITE)
    draw_line(10, 20, 10, 30, WHITE)
    draw_line(10, 25, 16, 25, WHITE)

    -- Triângulo
    draw_line(110, 120, 120, 120, WHITE)
    draw_line(110, 120, 115, 130, WHITE)
    draw_line(115, 130, 120, 120, WHITE)

    -- Linhas contínuas
    draw_line(200, 40, 250, 200, WHITE)
    draw_line(250, 40, 260, 200, WHITE)
end

O resultado da versão em JavaScript para canvas aparece a seguir.

Desenho de um F, um triângulo e duas retas inclinadas usando o algoritmo DDA.

As linhas parecem estar "serrilhadas", mas são mais contínuas, conforme o esperado. Técnicas como anti-aliasing poderiam amenizar a impressão do "serrilhado".

Linhas como Primitivas de APIs

APIs de desenho comumente fornecem uma subrotina para o desenho de linhas. Assim, ao invés de criar-se uma (como feito nas subseções anteriores), é possível usá-las diretamente. Além de mais prático, a implementação da API é comumente otimizada, e, portanto, mais rápida e eficiente.

Entretanto, deve-se destacar, novamente, que algumas APIs desenham o pixel da última coordenada, enquanto outras não desenham. Logo, é importante consultar a documentação para conhecer o comportamento de uma determinada implementação. Caso a documentação especifique o comportamento, outra opção é desenhar uma linha com um ou dois pixels e observar o resultado.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var WHITE = Color.white
    # F
    draw_line(Vector2(10, 20), Vector2(20, 20), WHITE)
    draw_line(Vector2(10, 20), Vector2(10, 30), WHITE)
    draw_line(Vector2(10, 25), Vector2(16, 25), WHITE)

    # Triângulo
    draw_line(Vector2(110, 120), Vector2(120, 120), WHITE)
    draw_line(Vector2(110, 120), Vector2(115, 130), WHITE)
    draw_line(Vector2(115, 130), Vector2(120, 120), WHITE)

    # Linhas contínuas
    draw_line(Vector2(200, 40), Vector2(250, 200), WHITE)
    draw_line(Vector2(250, 40), Vector2(260, 200), WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function draw_line(x0, y0, x1, y1, color) {
    context.strokeStyle = color
    context.fillStyle = color
    context.beginPath()
    context.moveTo(x0, y0)
    context.lineTo(x1, y1)
    context.closePath()
    context.stroke()
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

// Triângulo
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

// Linhas contínuas
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_line(x0, y0, x1, y1, color):
    pygame.draw.line(window, color, (x0, y0),(x1, y1))

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)

# Triângulo
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)

# Linhas contínuas
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    -- F
    draw_line(10, 20, 20, 20, WHITE)
    draw_line(10, 20, 10, 30, WHITE)
    draw_line(10, 25, 16, 25, WHITE)

    -- Triângulo
    draw_line(110, 120, 120, 120, WHITE)
    draw_line(110, 120, 115, 130, WHITE)
    draw_line(115, 130, 120, 120, WHITE)

    -- Linhas contínuas
    draw_line(200, 40, 250, 200, WHITE)
    draw_line(250, 40, 260, 200, WHITE)
end

Exceto em GDScript (que usa draw_line() diretamente), as implementações em JavaScript, Python e Lua realizam a chamada da subrotina da API de Canvas, PyGame e LÖVE em draw_line(). Isso permite manter o código anterior; o procedimento draw_line() proposto abstraí as chamadas da API usada.

Desenho de um F, um triângulo e duas retas inclinadas usando primitivas do HTML canvas.

Um benefício adicional de usar a primitiva da API é a possibilidade de definir a largura (width) da linha. Isso também pode ser feito na implementação própria; no caso, bastaria repetir o desenho da linha múltiplas vezes (acima e/ou abaixo da posição original) para torná-la mais grossa.

Arcos

Uma linha não precisa ser reta. Uma forma simples de adicionar curvas em uma linha consiste em adicionar uma angulação como parte do deslocamento. Para o desenho de uma angulação, pode-se desenhar um arco.

Círculos e Ângulos (Graus e Radianos)

Caso a angulação seja de 360° ou rad, o desenho será um círculo. Em programação, APIs matemáticas normalmente utilizam radianos ao invés de graus. A conversão é simples:

Assim, caso não exista uma implementação padrão na linguagem de programação de sua escolha, basta criar uma função que defina a expressão anterior.

A conversão de radianos para graus também é simples:

As duas equações serão implementadas em JavaScript para conversões de ângulos entre graus e radianos. Assim, embora existam ferramentas online para conversões de ângulos, é simples fazê-lo diretamente usando linguagens de programação. Além disso, alguns problemas são mais simples de resolver em graus que em radianos; in casos assim, pode-se realizar os cálculos em graus, e converter-se o resultado final para radianos para desenhá-lo.

Coordenadas Polares

Uma forma simples de desenhar um arco consiste em desenhar seqüências de pontos calculados usando coordenadas polares. Coordenadas polares definem um sistema de coordenadas que calcula um ponto por meio de uma distância (um raio) e um ângulo. Elas são definidas matematicamente pelas próximas equações.

Como as equações são simples, a implementação de um procedimento draw_arc() (ou franco_draw_arc() em GDScript) também será.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func franco_draw_arc(center_x, center_y, angle, radius, color):
    for alpha in range(0, angle):
        var alpha_radians = deg2rad(alpha)
        var x = center_x + radius * cos(alpha_radians)
        var y = center_y + radius * sin(alpha_radians)
        draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                        PoolColorArray([color]),
                                        PoolVector2Array())

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var WHITE = Color.white
    franco_draw_arc(120, 80, 360, 20, WHITE)
    franco_draw_arc(200, 80, 360, 20, WHITE)
    franco_draw_arc(160, 120, 180, 80, WHITE)
    franco_draw_arc(160, 120, 360, 100, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function radians(angle_degrees) {
    return angle_degrees * Math.PI / 180.0
}

function draw_arc(center_x, center_y, angle, radius, color) {
    context.fillStyle = color

    for (let alpha = 0; alpha < angle; ++alpha) {
        let alpha_radians = radians(alpha)
        let x = center_x + radius * Math.cos(alpha_radians)
        let y = center_y + radius * Math.sin(alpha_radians)
        context.fillRect(x, y, 1, 1)
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

const WHITE = "white"
draw_arc(120, 80, 360, 20, WHITE)
draw_arc(200, 80, 360, 20, WHITE)
draw_arc(160, 120, 180, 80, WHITE)
draw_arc(160, 120, 360, 100, WHITE)
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_arc(center_x, center_y, angle, radius, color):
    for alpha in range(0, angle):
        alpha_radians = math.radians(alpha)
        x = center_x + radius * math.cos(alpha_radians)
        y = center_y + radius * math.sin(alpha_radians)
        window.set_at((int(x), int(y)), color)

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
draw_arc(120, 80, 360, 20, WHITE)
draw_arc(200, 80, 360, 20, WHITE)
draw_arc(160, 120, 180, 80, WHITE)
draw_arc(160, 120, 360, 100, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_arc(center_x, center_y, angle, radius, color)
    love.graphics.setColor(color)

    for alpha = 0, angle - 1 do
        local alpha_radians = math.rad(alpha)
        local x = center_x + radius * math.cos(alpha_radians)
        local y = center_y + radius * math.sin(alpha_radians)
        love.graphics.points(x, y)
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    draw_arc(120, 80, 360, 20, WHITE)
    draw_arc(200, 80, 360, 20, WHITE)
    draw_arc(160, 120, 180, 80, WHITE)
    draw_arc(160, 120, 360, 100, WHITE)
end

JavaScript não fornece uma subrotina para conversão de ângulos de graus para radianos; assim, o exemplo define a função radians() para realizar a conversão. Novamente, isso é necessário porque APIs de desenho (e matemáticas, em geral) normalmente utilizam ângulos em radianos para operações.

A saída do programa releva os (rudimentares ou... primitivos) talentos artísticos do autor. Para melhorá-lo, você poderia adicionar uma linha para completar o sorriso da seqüência de círculos e arco formando um rosto feliz.

Desenho de círculos e arcos usando coordenadas polares. A seqüência de imagens forma um rosto feliz.

A implementação do exemplo não fornece muito controle para personalização. Para melhorá-la, seria útil definir o sentido para desenho (horário ou anti-horário), um ângulo inicial e um ângulo final para o arco, e um número de pontos ou segmentos para a aproximação.

Coordenadas polares utilizam números reais em ponto flutuante para cálculos. Antigamente, operações envolvendo números em ponto flutuante eram caras. Atualmente, o desempenho pode ser aceitável, dependendo do hardware (pré-calcular valores de senos e cossenos também serviria como otimização).

Operações de desenho no passado eram escritas usando números inteiros ao invés de números reais. O intuito era evitar (ou minimizar) o uso de operações trigonométricas, e/ou operações de ponto flutuante.

Para fins didáticos e antes de introduzir as APIs fornecidas por Godot Engine, HTML Canvas, LÖVE e PyGame, a próxima seção ilustra uma abordagem alternativa para desenho de círculos e arcos usando números inteiros.

Círculos com Midpoint Circle Algorithm

Matematicamente, todos os pontos de um círculo satisfazem a seguinte equação:

Na equação, e são coordenadas de um ponto , e é o raio.

Um algoritmo para aproximar a equação é o Midpoint circle de Bresenham. Para um círculo com centro na origem , o algoritmo pode ser descrito pelas seguintes equações:

As equações são recursivas (ou iterativas), ou seja, elas utilizam valores calculados anteriormente para o cáculo dos próximos valores. e são as coordenadas do ponto, é o raio, e é o valor usado para decidir pela próxima posição de no círculo.

É possível simplificar aplicando a propriedade distributiva para reduzir o número de multiplicações.

Alternativamente, poder-se-ia realizar um deslocamento de bits para a esquerda para dobrar o valor.

Como um círculo é uma forma geométrica simétrica, pode-se reduzir o número de operações necessários dividindo o desenho em octantes (ou seja, dividindo o círculo em oito partes iguais), e ajustado os valores das coordenadas. Da mesma forma, para desenhar um círculo com centro em um ponto arbitrário , basta somar os valores no momento de colorir o pixel.

Para a implementação do algoritmo, pode-se usar o comando Enquanto (While). Diferentemente de um for, o comando while contém apenas a condição de parada. A inicialização da variável de controle é feita antes do início do laço; os ajustes são feitos dentro do laço.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func franco_draw_pixel(x, y, color):
    draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                    PoolColorArray([color]),
                                    PoolVector2Array())

func franco_draw_circle(center_x, center_y, radius, color):
    var x = radius
    var y = 0
    var e = 3 - 2 * radius
    while (x >= y):
        # 0 - 44
        franco_draw_pixel(center_x + x, center_y - y, color)
        # 45 - 89
        franco_draw_pixel(center_x + y, center_y - x, color)
        # 90 - 134
        franco_draw_pixel(center_x - y, center_y - x, color)
        # 135 - 179
        franco_draw_pixel(center_x - x, center_y - y, color)
        # 180 - 224
        franco_draw_pixel(center_x - x, center_y + y, color)
        # 225 - 269
        franco_draw_pixel(center_x - y, center_y + x, color)
        # 270 - 314
        franco_draw_pixel(center_x + y, center_y + x, color)
        # 315 - 359
        franco_draw_pixel(center_x + x, center_y + y, color)

        if (e > 0):
            # e = e + 2 * (5 - 2x + 2y)
            e += 10 + 4 * (-x + y)
            x -= 1
        else:
            # e = e + 2 * (3 + 2 * y)
            e += 6 + 4 * y

        y += 1

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    var WHITE = Color.white
    franco_draw_circle(0, 0, 30, WHITE)
    franco_draw_circle(50, 50, 30, WHITE)

    for radius in range(10, 100, 20):
        franco_draw_circle(160, 120, radius, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function draw_circle(center_x, center_y, radius, color) {
    context.fillStyle = color

    let x = radius
    let y = 0
    let e = 3 - 2 * radius
    while (x >= y) {
        // 0 - 44
        context.fillRect(center_x + x, center_y - y, 1, 1)
        // 45 - 89
        context.fillRect(center_x + y, center_y - x, 1, 1)
        // 90 - 134
        context.fillRect(center_x - y, center_y - x, 1, 1)
        // 135 - 179
        context.fillRect(center_x - x, center_y - y, 1, 1)
        // 180 - 224
        context.fillRect(center_x - x, center_y + y, 1, 1)
        // 225 - 269
        context.fillRect(center_x - y, center_y + x, 1, 1)
        // 270 - 314
        context.fillRect(center_x + y, center_y + x, 1, 1)
        // 315 - 359
        context.fillRect(center_x + x, center_y + y, 1, 1)

        if (e > 0) {
            // e = e + 2 * (5 - 2x + 2y)
            e += 10 + 4 * (-x + y)
            --x
        } else {
            // e = e + 2 * (3 + 2 * y)
            e += 6 + 4 * y
        }

        ++y
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

const WHITE = "white"
draw_circle(0, 0, 30, WHITE)
draw_circle(50, 50, 30, WHITE)

for (let radius = 10; radius < 100; radius += 20) {
    draw_circle(160, 120, radius, WHITE)
}
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_circle(center_x, center_y, radius, color):
    x = radius
    y = 0
    e = 3 - 2 * radius
    while (x >= y):
        # 0 - 44
        window.set_at((center_x + x, center_y - y), color)
        # 45 - 89
        window.set_at((center_x + y, center_y - x), color)
        # 90 - 134
        window.set_at((center_x - y, center_y - x), color)
        # 135 - 179
        window.set_at((center_x - x, center_y - y), color)
        # 180 - 224
        window.set_at((center_x - x, center_y + y), color)
        # 225 - 269
        window.set_at((center_x - y, center_y + x), color)
        # 270 - 314
        window.set_at((center_x + y, center_y + x), color)
        # 315 - 359
        window.set_at((center_x + x, center_y + y), color)

        if (e > 0):
            # e = e + 2 * (5 - 2x + 2y)
            e += 10 + 4 * (-x + y)
            x -= 1
        else:
            # e = e + 2 * (3 + 2 * y)
            e += 6 + 4 * y

        y += 1

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

WHITE = pygame.Color("white")
draw_circle(0, 0, 30, WHITE)
draw_circle(50, 50, 30, WHITE)

for radius in range(10, 100, 20):
    draw_circle(160, 120, radius, WHITE)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function draw_circle(center_x, center_y, radius, color)
    love.graphics.setColor(color)

    local x = radius
    local y = 0
    local e = 3 - 2 * radius
    while (x >= y) do
        -- 0 - 44
        love.graphics.points(center_x + x, center_y - y)
        -- 45 - 89
        love.graphics.points(center_x + y, center_y - x)
        -- 90 - 134
        love.graphics.points(center_x - y, center_y - x)
        -- 135 - 179
        love.graphics.points(center_x - x, center_y - y)
        -- 180 - 224
        love.graphics.points(center_x - x, center_y + y)
        -- 225 - 269
        love.graphics.points(center_x - y, center_y + x)
        -- 270 - 314
        love.graphics.points(center_x + y, center_y + x)
        -- 315 - 359
        love.graphics.points(center_x + x, center_y + y)

        if (e > 0) then
            -- e = e + 2 * (5 - 2x + 2y)
            e = e + 10 + 4 * (-x + y)
            x = x - 1
        else
            -- e = e + 2 * (3 + 2 * y)
            e = e + 6 + 4 * y
        end

        y = y + 1
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    local WHITE = {1.0, 1.0, 1.0}
    draw_circle(0, 0, 30, WHITE)
    draw_circle(50, 50, 30, WHITE)

    for radius = 10, 100, 20 do
        draw_circle(160, 120, radius, WHITE)
    end
end

O exemplo utiliza uma estrutura de repetição para desenhar uma série de círculos concêntricos, mas com diferentes tamanhos para o raio. O resultado é uma imagem que parece um alvo. Para aumentar ou diminuir o número de círculos concêntricos, bastaria alterar o incremento da estrutura de repetição (ou o valor final da comparação).

Desenho de círculos usando midpoint circle. Uma das ilustrações apresenta uma seqüência de círculos concêntricos gerados no laço.

Uma forma de imaginar o desenho seria pensar no uso de um compasso. Mantendo-se o apoio do compasso na mesma posição e alterando-se o tamanho da abertura da parte com grafite, cada círculo desenhado teria o mesmo centro, mas diferentes raios.

Arcos Usando Midpoint Circle Algorithm

Caso se deseje desenhar apenas um arco ao invés do círculo inteiro, é necessário desenhar apenas parte do círculo. Isso pode ser surpreendimento complexo usando o Midpoint Circle Algorithm, pois a solução que seria imediata é inviável pelo espelhamento feito para octantes pares. Como octantes pares são desenhados do final para o começo, arcos que terminem em um deles são encorrentante separados por espaço vazio.

Exibir / Ocultar Exemplo em JavaScript

let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function degrees(angle_radians) {
    return angle_radians * 180.0 / Math.PI
}

function draw_arc(center_x, center_y, start_angle, end_angle, radius, color) {
    context.fillStyle = color

    let x = radius
    let y = 0
    let e = 3 - 2 * radius
    let current_angle = degrees(Math.atan2(y, x))
    while (x >= y) { //  && (current_angle < end_angle)
        console.log(current_angle)

        // 0 - 44
        let angle = current_angle
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + x, center_y - y, 1, 1)
        }

        // 45 - 89
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + y, center_y - x, 1, 1)
        }

        // 90 - 134
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - y, center_y - x, 1, 1)
        }

        // 135 - 179
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - x, center_y - y, 1, 1)
        }

        // 180 - 224
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - x, center_y + y, 1, 1)
        }

        // 225 - 269
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - y, center_y + x, 1, 1)
        }

        // 270 - 314
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + y, center_y + x, 1, 1)
        }

        // 315 - 359
        angle += 45
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + x, center_y + y, 1, 1)
        }

        if (e > 0) {
            // e = e + 2 * (5 - 2x + 2y)
            e += 10 + 4 * (-x + y)
            --x
        } else {
            // e = e + 2 * (3 + 2 * y)
            e += 6 + 4 * y
        }

        ++y
        current_angle = degrees(Math.atan2(y, x))
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

draw_arc(30, 200, 0, 360, 20, "red")
draw_arc(30, 30, 0, 30, 20, "yellow")
draw_arc(200, 80, 0, 45, 20, "cyan")
draw_arc(160, 120, 45, 90, 40, "magenta")
draw_arc(160, 120, 90, 120, 60, "blue")
draw_arc(160, 120, 90, 150, 80, "green")
draw_arc(160, 120, 120, 350, 100, "white")

Na saída do programa, é possível observar as lacunas entre partes das linhas que deveriam ser contínuas.

Desenho de arcos usando uma adaptação criada pelo autor de midpoint circle. Os arcos não são contínuos.

Uma alternativa é alterar a implementação para usar quadrantes ao invés de octantes. O próximo exemplo ilustra um protótipo da idéia implementada pelo autor. Trata-se de um esboço rápido, a título de prova de conceito. Assim, o desenho resultante é "serrilhado" e possui alguns pixels incorretos.

# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func franco_draw_pixel(x, y, color):
    draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                    PoolColorArray([color]),
                                    PoolVector2Array())

func franco_draw_arc(center_x, center_y, start_angle, end_angle, radius, color):
    var x = radius
    var y = 0
    var e = 3 - 2 * radius
    var current_angle = rad2deg(atan2(y, x))
    while (x >= 0):
        # 0 - 89
        var angle = current_angle
        if ((angle >= start_angle) and (angle < end_angle)):
            franco_draw_pixel(center_x + x, center_y - y, color)

        # 90 - 179
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            franco_draw_pixel(center_x - y, center_y - x, color)

        # 180 - 269
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            franco_draw_pixel(center_x - x, center_y + y, color)

        # 270 - 359
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            franco_draw_pixel(center_x + y, center_y + x, color)

        if (e <= y):
            x -= 1
            e += 2 * x

        if (e > y):
            y += 1
            e += -2 * y

        current_angle = rad2deg(atan2(y, x))

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    franco_draw_arc(30, 200, 0, 360, 20, Color.red)
    franco_draw_arc(30, 30, 0, 30, 20, Color.yellow)
    franco_draw_arc(200, 80, 0, 45, 20, Color.cyan)
    franco_draw_arc(160, 120, 45, 90, 40, Color.magenta)
    franco_draw_arc(160, 120, 90, 120, 60, Color.blue)
    franco_draw_arc(160, 120, 90, 150, 80, Color.green)
    franco_draw_arc(160, 120, 120, 350, 100, Color.white)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

function degrees(angle_radians) {
    return angle_radians * 180.0 / Math.PI
}

function draw_arc(center_x, center_y, start_angle, end_angle, radius, color) {
    context.fillStyle = color

    let x = radius
    let y = 0
    let e = 3 - 2 * radius
    let current_angle = degrees(Math.atan2(y, x))
    while (x >= 0) {
        // 0 - 89
        let angle = current_angle
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + x, center_y - y, 1, 1)
        }

        // 90 - 179
        angle += 90
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - y, center_y - x, 1, 1)
        }

        // 180 - 269
        angle += 90
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x - x, center_y + y, 1, 1)
        }

        // 270 - 359
        angle += 90
        if ((angle >= start_angle) && (angle < end_angle)) {
            context.fillRect(center_x + y, center_y + x, 1, 1)
        }

        if (e <= y) {
            --x
            e += 2 * x
        }

        if (e > y) {
            ++y
            e += -2 * y
        }

        current_angle = degrees(Math.atan2(y, x))
    }
}

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

draw_arc(30, 200, 0, 360, 20, "red")
draw_arc(30, 30, 0, 30, 20, "yellow")
draw_arc(200, 80, 0, 45, 20, "cyan")
draw_arc(160, 120, 45, 90, 40, "magenta")
draw_arc(160, 120, 90, 120, 60, "blue")
draw_arc(160, 120, 90, 150, 80, "green")
draw_arc(160, 120, 120, 350, 100, "white")
import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

def draw_arc(center_x, center_y, start_angle, end_angle, radius, color):
    x = radius
    y = 0
    e = 3 - 2 * radius
    current_angle = math.degrees(math.atan2(y, x))
    while (x >= 0):
        # 0 - 89
        angle = current_angle
        if ((angle >= start_angle) and (angle < end_angle)):
            window.set_at((center_x + x, center_y - y), color)

        # 90 - 179
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            window.set_at((center_x - y, center_y - x), color)

        # 180 - 269
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            window.set_at((center_x - x, center_y + y), color)

        # 270 - 359
        angle += 90
        if ((angle >= start_angle) and (angle < end_angle)):
            window.set_at((center_x + y, center_y + x), color)

        if (e <= y):
            x -= 1
            e += 2 * x

        if (e > y):
            y += 1
            e += -2 * y

        current_angle = math.degrees(math.atan2(y, x))

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

draw_arc(30, 200, 0, 360, 20, pygame.Color("red"))
draw_arc(30, 30, 0, 30, 20, pygame.Color("yellow"))
draw_arc(200, 80, 0, 45, 20, pygame.Color("cyan"))
draw_arc(160, 120, 45, 90, 40, pygame.Color("magenta"))
draw_arc(160, 120, 90, 120, 60, pygame.Color("blue"))
draw_arc(160, 120, 90, 150, 80, pygame.Color("green"))
draw_arc(160, 120, 120, 350, 100, pygame.Color("white"))

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
local COLORS = {
    RED = {1.0, 0.0, 0.0},
    YELLOW = {1.0, 1.0, 0.0},
    CYAN = {0.0, 1.0, 1.0},
    MAGENTA = {1.0, 0.0, 1.0},
    BLUE = {0.0, 0.0, 1.0},
    GREEN = {0.0, 1.0, 0.0},
    WHITE = {1.0, 1.0, 1.0},
}

function draw_arc(center_x, center_y, start_angle, end_angle, radius, color)
    love.graphics.setColor(color)

    local x = radius
    local y = 0
    local e = 3 - 2 * radius
    local current_angle = math.deg(math.atan2(y, x))
    while (x >= 0) do
        -- 0 - 89
        local angle = current_angle
        if ((angle >= start_angle) and (angle < end_angle)) then
            love.graphics.points(center_x + x, center_y - y)
        end

        -- 90 - 179
        angle = angle + 90
        if ((angle >= start_angle) and (angle < end_angle)) then
            love.graphics.points(center_x - y, center_y - x)
        end

        -- 180 - 269
        angle = angle + 90
        if ((angle >= start_angle) and (angle < end_angle)) then
            love.graphics.points(center_x - x, center_y + y)
        end

        -- 270 - 359
        angle = angle + 90
        if ((angle >= start_angle) and (angle < end_angle)) then
            love.graphics.points(center_x + y, center_y + x)
        end

        if (e <= y) then
            x = x - 1
            e = e + 2 * x
        end

        if (e > y) then
            y = y + 1
            e = e - 2 * y
        end

        current_angle = math.deg(math.atan2(y, x))
    end
end

function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    draw_arc(30, 200, 0, 360, 20, COLORS.RED)
    draw_arc(30, 30, 0, 30, 20, COLORS.YELLOW)
    draw_arc(200, 80, 0, 45, 20, COLORS.CYAN)
    draw_arc(160, 120, 45, 90, 40, COLORS.MAGENTA)
    draw_arc(160, 120, 90, 120, 60, COLORS.BLUE)
    draw_arc(160, 120, 90, 150, 80, COLORS.GREEN)
    draw_arc(160, 120, 120, 350, 100, COLORS.WHITE)
end

A implementação usa x >= 0 (término em 90°) ao invés de x >= y (término em 45°) para iterar por valores do primeiro quadrante. É possível notar imperfeições em partes das curvas e em cada divisão dos quadrantes.

Desenho de arcos usando uma adaptação criada pelo autor de midpoint circle.

Para melhorar a implementação, seria necessário ajustar melhor os valores para o erro calculado em e. Você pode alterar os valores do cálculo de e para tentar desenhar círculos e arcos com menos imperfeições.

Como os próximos tópicos não usaram midpoint circle para o desenho de arcos, convém introduzir as primitivas das bibliotecas e motores usados (ao invés de melhorar a implementação).

Arcos como Primitivas de APIs

APIs de desenho costumam fornecer primitivas para o desenho de arcos. A assinatura de subrotinas para desenho de arcos costumam receber a coordenada do centro e o raio (embora isso possa variar). O ângulo inicial e o ângulo final costumam ser medidos em radianos.

  • Godot fornece draw_arc();
  • JavaScript fornece arc();
  • PyGame fornece dois métodos; este exemplo utiliza gfxdraw.arc(). É importante notar que a subrotina usa ângulo em graus ao invés de radianos. Além disso, a angulação máxima é de 359°;
  • LÖVE fornece arc().
# Raíz deve ser um Node que permita desenhar usando _draw().
extends Control

func _ready():
    OS.set_window_size(Vector2(320, 240))
    OS.set_window_title("Olá, meu nome é Franco!")

func _draw():
    VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))

    for i in range(2, 30, 2):
        var center_x = i * 7.0
        var center_y = i * 7.0
        var radius = i * 3.0
        var start_angle = PI / i
        var end_angle = 2.0 * PI
        var point_count = 100

        draw_arc(Vector2(center_x, center_y),
                 radius,
                 start_angle, end_angle,
                 point_count,
                 Color(2.0 / i, 2.0 / i, 2.0 / i))
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")

canvas.width = 320
canvas.height = 240

document.title = "Olá, meu nome é Franco!"

context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)

for (let i = 2; i < 30; i += 2) {
    let center_x = i * 7.0
    let center_y = i * 7.0
    let radius = i * 3.0
    let start_angle = Math.PI / i
    let end_angle = 2.0 * Math.PI

    context.strokeStyle = `rgb(${510 / i}, ${510 / i}, ${510 / i})`
    context.beginPath()
    context.arc(center_x, center_y,
                radius,
                start_angle, end_angle)
    context.stroke()
    context.closePath()
}
import math
import pygame
import sys

from pygame import gfxdraw

pygame.init()
window = pygame.display.set_mode((320, 240))

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

for i in range(2, 30, 2):
    center_x = i * 7
    center_y = i * 7
    radius = i * 3
    start_angle = math.degrees(math.pi / i)
    end_angle = math.degrees(2.0 * math.pi)

    pygame.gfxdraw.arc(window,
                       center_x, center_y,
                       radius,
                       math.floor(start_angle), math.floor(end_angle),
                       (510 // i, 510 // i, 510 // i))


pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)
function love.load()
    love.window.setMode(320, 240)
    love.window.setTitle("Olá, meu nome é Franco!")
end

function love.draw()
    love.graphics.setBackgroundColor(0.0, 0.0, 0.0)

    for i = 2, 29, 2 do
        local center_x = i * 7.0
        local center_y = i * 7.0
        local radius = i * 3.0
        local start_angle = math.pi / i
        local end_angle = 2.0 * math.pi
        local point_count = 100

        love.graphics.setColor(2.0 / i, 2.0 / i, 2.0 / i)
        love.graphics.arc("line", "open",
                          center_x, center_y,
                          radius,
                          start_angle, end_angle,
                          point_count)
    end
end

As variáveis descrevem os parâmetros. Implementações que definem point_count utilizam um número pré-determinado de pontos ou segmentos de reta para o desenho. Quanto maior o número, melhor o resultado; porém, mais computacionalmente caro será a operação.

O valor 510 que aparece em JavaScript e Python é o dobro de 255, que é o número de possíveis valores inteiros para cada primária. Como o contador da estrutura de repetição começa em 2, todos os valores para cores primárias estarão entre 0.0 a 1.0 (ou 0 a 255).

Desenho de arcos com valores incrementais para centros, raios, e ângulos iniciais e finais.

Caso se queira desenhar um círculo inteiro ao invés de um arco, é comum que as APIs forneçam subrotinas especializadas para círculos. Isso será abordado em um tópico futuro, explorando formas geométricas.

Conceitos Abordados de Aprenda Programação

Conceitos de Aprenda Programação abordados neste tópico:

  1. Ponto de entrada;
  2. Saída;
  3. Tipos de dados;
  4. Variáveis e constantes;
  5. Aritmética;
  6. Comparações;
  7. Operações lógicas;
  8. Estruturas de condição;
  9. Subrotinas (funções e procedimentos);
  10. Estruturas de repetição (laços ou loops);
  11. Bibliotecas.

É interessante notar que mesmo primitivas para desenhos podem utilizar quase todos os conceitos básicos de programação.

Além disso, os valores para cores em Lua usaram tabelas (dicionários), descritos em Coleções.

Novos Itens para Seu Inventário

Habilidades de Pensamento Computacional:

Ferramentas:

  • Editores de imagens;
  • Lupa ou lente de aumento;
  • Conversor de ângulos.

Habilidades:

  • Desenho de pontos ou pixels;
  • Desenho de linhas;
  • Desenho de arcos;
  • Implementação de algumas primitivas gráficas: linha, arco, círculo.

Conceitos:

  • Pixel;
  • Resolução;
  • Primitiva gráfica;
  • Gráficos vetoriais;
  • Gráficos matriciais ou raster;
  • Sistema de coordenadas;
  • Plano cartesiano;
  • Ponto;
  • Linha;
  • Arco.

Recursos de programação:

  • Desenho de primitivas gráficas.

Pratique

Para aprender programação, prática deliberada deve acompanhar conceitos. Tente fazer os próximos exercícios para praticar.

  1. Escreva uma letra ou número usando pixels;

  2. Crie desenhos usando primitivas gráficas.

    Neste momento, apenas descreveu-se pontos, linhas e arcos. Um tópico futuro introduzirá polígonos para facilitar desenhos.

  3. Como você criaria um retângulo ou um quadrado usando linhas ou pixels?

  4. Como você coloriria um retângulo ou quadrado usando pixels?

    Isso será abordado em tópicos futuros; entretanto, você já pode criar uma solução caso você pense sobre o problema.

  5. Uma linha pode ser curva? Uma reta (ou um segmento de reta) pode ser curva?

  6. Crie um programa que converta ângulos de graus para radianos;

  7. Crie um programa que converta ângulos de radianos para graus;

  8. Altere valores para cálculo de erro armazenado na variável e do algoritmo Midpoint Circle. A cada modificação, re-execute o programa e verifique os resultados.

  9. Por que usar primitivas gráficas fornecidas em APIs ao invés de criar as próprias?

  10. Escreva uma subrotina que desenhe uma linha pontilhada usando estruturas de repetição e de condição.

Aprofundamentos

Em Ideias, Regras, Simulação, esta seção apresenta conteúdo complementar.

PyGame: Desenhando Arcos com draw.arc()

Também é possível desenhar arcos em PyGame usando pygame.draw.arc(). Contudo, o método recebe um retângulo ao invés da coordenada do centro e um raio.

Assim, para usar a função, é necessário definir o canto superior esquerdo como centro_x - raio, o direito como centro_y - raio, e a altura e a largura como 2 * raio.

import math
import pygame
import sys

pygame.init()
window = pygame.display.set_mode((320, 240))

pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))

for i in range(2, 30, 2):
    center_x = i * 7.0
    center_y = i * 7.0
    radius = i * 3.0
    start_angle = math.pi / i
    end_angle = 2.0 * math.pi

    pygame.draw.arc(window,
                    (510.0 / i, 510.0 / i, 510.0 / i),
                    [center_x - radius, center_y - radius, 2 * radius, 2 * radius],
                    start_angle, end_angle)

pygame.display.flip()

while (True):
    for event in pygame.event.get():
        if (event.type == pygame.QUIT):
            pygame.quit()
            sys.exit(0)

O uso do programa também demonstra que o arco será desenhado de forma diferente das demais implementações.

Próximos Passos

Pixels, pontos, linhas, arcos. Com as primitivas gráficas anteriores, é possível começar a explorar desenhos mais complexos em Ideias, Regras, Simulação.

Nos próximos tópicos, os exemplos usarão as primitivas fornecidas pelas APIs de cada biblioteca. Contudo, agora você possui uma idéia de como elas funcionam e de como implementar uma.

Também já é possível criar as primeiras simulações com gráficos (simples). Para torná-las mais interessantes, uma forma é explorar números pseudoaleatórios. Se o nome for familiar, eles foram introduzidos em estruturas de repetição em Aprenda Programação.

No mais, se você criou uma ilustração criativa ou interessante, considere compartilhá-la. Alternativamente, se você considerou este material útil, você também pode compartilhá-lo. Se possível, use as hashtags #IdeiasRegrasSimulação e #FrancoGarciaCom.

Agradeço a atenção. Até a próxima!

Ideias, Regras, Simulação

  1. Motivação;
  2. Introdução: Janela e Olá Mundo;
  3. Pixels e primitivas gráficas (pontos, linhas, e arcos);
  4. Aleatoriedade e ruído;
  5. Moedas e dados, retângulos e quadrados;
  6. Desenhando com primitivas gráficas (contornos e preenchimento para círculos, elipses e polígonos);
  7. Salvando e carregando arquivos de imagens;
  8. ...

A escrita do material está em progresso; portanto, se você chegou muito cedo e os itens anteriores não possuem links, por favor, retorne a esta página para conferir as atualizações.

Caso queira entrar em contato ou tenha alguma dúvida, você pode conversar comigo das seguintes maneiras:

Contatos e redes sociais também estão no rodapé das páginas.

Suas opiniões sobre a série serão fundamentais para que eu possa tornar o material mais acessível e simples para mais pessoas.

  • Vídeo
  • Informática
  • Programação
  • Iniciante
  • Pensamento Computacional
  • Ideias, Regras, Simulação
  • Python
  • PyGame
  • Lua
  • LÖVE (Love2D)
  • Javascript
  • HTML Canvas
  • Godot
  • Gdscript