Skip to content
Image with logo, providing a link to the home page
  • 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.

Ideas, Rules, Simulation: Drawing with Drawing Primitives (Circles, Ellipses and Polygons)

Illustrations that were created using drawing primitives presented over Ideas, Rules, Simulation: filled shapes; a woman; a house; a chick; a cat; a dog; a pig; and a cow. The images are outputs presented on a window, resulting from the programs created for JavaScript with HTML Canvas, GDScript with Godot Engine, Python with PyGame, and Lua with LÖVE. The image also provides a link to this website: <www.francogarcia.com>, as well as the account francogarciacom, used for the Twitter and Instagram of the author.

Image credits: Image created by the author using the program Inkscape; icons by Font Awesome.

Requirements

The material for Ideas, Rules, Simulations promotes project based learning (interactive simulations and digital games) using multimedia resources. As such, it is complimentary to the material of Learn Programming, which introduces fundamentals and basic programming techniques for programming, with examples for JavaScript, Python, Lua and GDScript (for Godot Engine).

For Ideas, Rules, Simulations you will need a configured development environment for one of the previous languages:

JavaScript (for browsers) and GDScript provide built-in support for multimedia content.

I am also considering improving the online programming editors provided in this website, to support an interactive course. Currently, the page with tools provide options for:

The editors support images in the browser. However, Python and Lua use the JavaScript syntax for the canvas. If I create an abstraction and add support for audio, they could become valid tools (at least for the first activities).

However, it is worth configuring an Integrated Development Environment (IDE), or a combination of text editor with interpreter as soon as possible, to enable you to program using your machine with greater efficiency. Online environments are practical, though local environments are potentially more complete, efficient, faster and customizable. If you want to become a professional, you will need a configured development environment. The sooner you do it, the better.

Version in Video (In Progress)

Video versions are coming soon to the author's YouTube channel.

Documentation

Practice consulting the documentation:

Most links have a search field. In Lua, the documentation is in a single page. You can search for entries using the shortcut Ctrl F (search).

Outlines and Fills

The representation for the simulation of throwing coins and dice used lines, arcs, and rectangles to represent results. Although simple, the graphics have fulfilled their purpose of representing the sides of a coin, or the faces of a die.

However, the results would be more aesthetically pleasing if the drawings had colorful fills instead of being mere outlines. For instance, on graphic editors such as GIMP and Microsoft Paint provide a tool with an icon of a bucket (bucket fill) to fill regions delimited by contours.

Have you ever thought about how they are created? To implement a bucket fill, it would be necessary to add data input features to the window. This is slightly different from what has been done in Learn Programming: Console (Terminal) Input; thus, for a simpler topic, a bucket fill feature is implemented as a Deepening.

Nevertheless, there are other algorithms to fill shapes. Thus, for this topic on, polygon with outlines and fills will be part of the drawing resources of Ideas, Rules, Simulation. In fact, one the sections will create simple drawings using the drawing primitives.

That is the final destination of this topic. To reach it, it will be necessary learning how to fill circles and polygons. To complement the drawing primitives studied so far, we can also consider how to draw ellipses.

HTML Canvas for JavaScript

As in the Introduction of Ideas, Rules, Simulation, JavaScript requires an auxiliary HTML file to declare the canvas. You can choose the name of the HTML file (for instance, index.html). The follow example assumes that the JavaScript fill is named script.js and the canvas will have the identifier (id) canvas. If you change the values, remember to modify them in the files as needed.

<!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;">
      >
          Accessibility: alternative text for graphical content.
      </canvas>
    </div>

    <!-- NOTE Updated with the name of the JavaScript file. -->
    <script src="./script.js"></script>
  </body>
</html>

The file also keeps the reminder about accessibility. In this topic, the content will become even more inaccessible for people with certain vision disabilities, due to the addition of graphical content without alternatives to convey the contents.

In the future, the intention is discussing ways to make simulations more accessible. At this time, this reminder is only informative, to raise awareness of the importance of accessibility.

Circles

Circles have been briefly commented in Pixels and Drawing Primitives (Points, Lines, and Arcs). At the occasion, circles were created using arcs. Thus, the results were outlines of circles.

The next subsections describe how to fill circles.

Filling Circles with Midpoint Circle Algorithm

A simple way of filling circles consists of modifying the circles which have been implemented using the Midpoint Circle Algorithm implemented in Pixels and Drawing Primitives (Points, Lines, and Arcs). Instead of drawing the points (pixels) of the extremities, it suffices to draw a line (straight line segment) connecting the points with a same ordinate (y value).

Thus, in franco_draw_circle(), one can swap the original implementation, that draws a single pixel at a time using franco_draw_pixel() as:

# 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)

To the drawing of a line connecting the pairs with the same y value using franco_draw_line():

franco_draw_line(center_x + x, center_y - y, center_x - x, center_y - y, color)
franco_draw_line(center_x - x, center_y + y, center_x + x, center_y + y, color)
franco_draw_line(center_x + y, center_y - x, center_x - y, center_y - x, color)
franco_draw_line(center_x - y, center_y + x, center_x + y, center_y + x, color)

The next snippets perform such changes.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white


func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func franco_draw_circle(center_x, center_y, radius, color):
    var x = radius
    var y = 0
    var e = 3 - 2 * radius
    while (x >= y):
        franco_draw_line(center_x + x, center_y - y, center_x - x, center_y - y, color)
        franco_draw_line(center_x - x, center_y + y, center_x + x, center_y + y, color)
        franco_draw_line(center_x + y, center_y - x, center_x - y, center_y - x, color)
        franco_draw_line(center_x - y, center_y + x, center_x + y, center_y + x, 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(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_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()
}


function franco_draw_circle(center_x, center_y, radius, color) {
    let x = radius
    let y = 0
    let e = 3 - 2 * radius
    while (x >= y) {
        franco_draw_line(center_x + x, center_y - y, center_x - x, center_y - y, color)
        franco_draw_line(center_x - x, center_y + y, center_x + x, center_y + y, color)
        franco_draw_line(center_x + y, center_y - x, center_x - y, center_y - x, color)
        franco_draw_line(center_x - y, center_y + x, center_x + y, center_y + x, color)

        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
    }
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


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


def franco_draw_circle(center_x, center_y, radius, color):
    x = radius
    y = 0
    e = 3 - 2 * radius
    while (x >= y):
        franco_draw_line(center_x + x, center_y - y, center_x - x, center_y - y, color)
        franco_draw_line(center_x - x, center_y + y, center_x + x, center_y + y, color)
        franco_draw_line(center_x + y, center_y - x, center_x - y, center_y - x, color)
        franco_draw_line(center_x - y, center_y + x, center_x + y, center_y + x, 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


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}


function franco_draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


function franco_draw_circle(center_x, center_y, radius, color)
    local x = radius
    local y = 0
    local e = 3 - 2 * radius
    while (x >= y) do
        franco_draw_line(center_x + x, center_y - y, center_x - x, center_y - y, color)
        franco_draw_line(center_x - x, center_y + y, center_x + x, center_y + y, color)
        franco_draw_line(center_x + y, center_y - x, center_x - y, center_y - x, color)
        franco_draw_line(center_x - y, center_y + x, center_x + y, center_y + x, color)

        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(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
end

The following canvas shows the result.

Drawing of a white circle on a white background using the described modification on the Midpoint Circle Algorithm.

In the JavaScript versions with canvas and Lua with LÖVE (Love2D), one can notice that some lines are darker than others. This happens because they have been drawn more than once.

Filling Circles with Drawing Primitives

Graphical Application Programming Interfaces (APIs) typically provide a primitive to draw circles. In fact, GDScript provides draw_circle(), JavaScript with canvas allows filling an arc, Python with PyGame has pygame.draw.circle(), and Lua with LÖVE provides love.graphics.circle(). Thus, they will be used in the next examples instead of the author's implementation.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white


func franco_draw_circle(center_x, center_y, radius, color):
    draw_circle(Vector2(center_x, center_y), radius, color)


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


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

    context.beginPath()
    context.arc(center_x, center_y,
                radius,
                0, 2 * Math.PI)
    context.fill()
    context.closePath()
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


def franco_draw_circle(center_x, center_y, radius, color):
    pygame.draw.circle(window, color, (center_x, center_y), radius)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

        window.fill(BLACK)

        franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)

        pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}


function franco_draw_circle(center_x, center_y, radius, color)
    love.graphics.setColor(color)
    love.graphics.circle("fill", center_x, center_y, radius)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    franco_draw_circle(0.5 * WIDTH, 0.5 * HEIGHT, 0.45 * HEIGHT, WHITE)
end

The following canvas shows the result.

Drawing of a white circle on a black background using drawing primitives.

It is worth noticing that the drawing primitive provided by PyGame for Python draws the entire circle. In other words, it does not have the limitation for drawing arcs that pygame.gfxdraw.arc() had.

Ellipses

For a generalization of circles, ellipses can be drawn. Though, actually, a circle is a particular case of an ellipse.

The Wikipedia entry provides equations for the Cartesian Coordinates Systems and for polar coordinates. However, it is time to introduce a more scientific approach.

Drawing Outlines of Ellipses

Academics produce scientific knowledge, which is usually published as papers in means such as articles, journals, or personal websites. A good option to find solutions for problems is searching for works of professors and researchers.

To illustrate the potential of the approach, the implementation for drawing the outline of ellipses will follow the algorithm by John Kennedy. The document illustrates an example of text in the format of a paper; when a paper has an algorithm, it is easy to follow it even if one is not a researcher herself/himself.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const BLUE = Color.blue


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


func plot_4_ellipse_points(center_x, center_y, x, y, color):
    franco_draw_pixel(center_x + x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y - y, color)
    franco_draw_pixel(center_x + x, center_y - y, color)


func franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color):
    var two_a_square = 2 * x_radius * x_radius
    var two_b_square = 2 * y_radius * y_radius

    var x = x_radius
    var y = 0
    var x_change = y_radius * y_radius * (1 - 2 * x_radius)
    var y_change = x_radius * x_radius
    var ellipse_error = 0
    var stopping_x = two_b_square * x_radius
    var stopping_y = 0
    while (stopping_x >= stopping_y):
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        y += 1
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0):
            x -= 1
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y):
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        x += 1
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0):
            y -= 1
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const BLUE = "blue"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_draw_pixel(x, y, color) {
    context.fillStyle = color
    context.fillRect(x, y, 1, 1)
}


function plot_4_ellipse_points(center_x, center_y, x, y, color) {
    franco_draw_pixel(center_x + x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y - y, color)
    franco_draw_pixel(center_x + x, center_y - y, color)
}


function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color) {
    let two_a_square = 2 * x_radius * x_radius
    let two_b_square = 2 * y_radius * y_radius

    let x = x_radius
    let y = 0
    let x_change = y_radius * y_radius * (1 - 2 * x_radius)
    let y_change = x_radius * x_radius
    let ellipse_error = 0
    let stopping_x = two_b_square * x_radius
    let stopping_y = 0
    while (stopping_x >= stopping_y) {
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        ++y
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0) {
            --x
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square
        }
    }

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y) {
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        ++x
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0) {
            --y
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square
        }
    }
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
BLUE: Final = pygame.Color("blue")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


def franco_draw_pixel(x, y, color):
    window.set_at((x, y), color)


def plot_4_ellipse_points(center_x, center_y, x, y, color):
    franco_draw_pixel(center_x + x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y - y, color)
    franco_draw_pixel(center_x + x, center_y - y, color)


def franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color):
    two_a_square = 2 * x_radius * x_radius
    two_b_square = 2 * y_radius * y_radius

    x = x_radius
    y = 0
    x_change = y_radius * y_radius * (1 - 2 * x_radius)
    y_change = x_radius * x_radius
    ellipse_error = 0
    stopping_x = two_b_square * x_radius
    stopping_y = 0
    while (stopping_x >= stopping_y):
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        y += 1
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0):
            x -= 1
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y):
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        x += 1
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0):
            y -= 1
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            franco_draw_ellipse(160, 120, 120, 80, BLUE)
            franco_draw_ellipse(160, 120, 60, 40, RED)
            franco_draw_ellipse(160, 120, 30, 30, WHITE)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}


function franco_draw_pixel(x, y, color)
    love.graphics.setColor(color)
    love.graphics.points(x, y)
end


function plot_4_ellipse_points(center_x, center_y, x, y, color)
    franco_draw_pixel(center_x + x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y - y, color)
    franco_draw_pixel(center_x + x, center_y - y, color)
end


function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color)
    local two_a_square = 2 * x_radius * x_radius
    local two_b_square = 2 * y_radius * y_radius

    local x = x_radius
    local y = 0
    local x_change = y_radius * y_radius * (1 - 2 * x_radius)
    local y_change = x_radius * x_radius
    local ellipse_error = 0
    local stopping_x = two_b_square * x_radius
    local stopping_y = 0
    while (stopping_x >= stopping_y) do
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        y = y + 1
        stopping_y = stopping_y + two_a_square
        ellipse_error = ellipse_error + y_change
        y_change = y_change + two_a_square

        if ((2 * ellipse_error + x_change) > 0) then
            x = x - 1
            stopping_x = stopping_x - two_b_square
            ellipse_error = ellipse_error + x_change
            x_change = x_change + two_b_square
        end
    end

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y) do
        plot_4_ellipse_points(center_x, center_y, x, y, color)

        x = x + 1
        stopping_x = stopping_x + two_b_square
        ellipse_error = ellipse_error + x_change
        x_change = x_change + two_b_square

        if ((2 * ellipse_error + y_change) > 0) then
            y = y - 1
            stopping_y = stopping_y - two_a_square
            ellipse_error = ellipse_error + y_change
            y_change = y_change + two_a_square
        end
    end
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
end

The following canvas shows the result.

Drawing of three concentric ellipses, following the algorithm by John Kennedy.

The algorithm is similar to Bresenham's to draw circles.

It is worth noticing that an ellipse on which the two radii (x_radius and y_radius) have the same measure form a circle. In fact, a circle is a particular case of an ellipse.

Filling Ellipses

As Kennedy's algorithm is similar to Bresenham's, the implementation for filling an ellipse can follow the same strategy used for circles: it is sufficient to draw straight lines for points with the same ordinate (y value).

In other words, it suffices to modify plot_4_ellipse_lines() that used franco_draw_pixel():

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


func plot_4_ellipse_points(center_x, center_y, x, y, color):
    franco_draw_pixel(center_x + x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y + y, color)
    franco_draw_pixel(center_x - x, center_y - y, color)
    franco_draw_pixel(center_x + x, center_y - y, color)

For a new plot_2_ellipse_lines() that will use franco_draw_line() to finish the implementation.

func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func plot_2_ellipse_lines(center_x, center_y, x, y, color):
    franco_draw_line(center_x + x, center_y + y, center_x - x, center_y + y, color)
    franco_draw_line(center_x - x, center_y - y, center_x + x, center_y - y, color)

Next, all that remains is updating the two calls in franco_draw_ellipse().

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const BLUE = Color.blue


func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func plot_2_ellipse_lines(center_x, center_y, x, y, color):
    franco_draw_line(center_x + x, center_y + y, center_x - x, center_y + y, color)
    franco_draw_line(center_x - x, center_y - y, center_x + x, center_y - y, color)


func franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color):
    var two_a_square = 2 * x_radius * x_radius
    var two_b_square = 2 * y_radius * y_radius

    var x = x_radius
    var y = 0
    var x_change = y_radius * y_radius * (1 - 2 * x_radius)
    var y_change = x_radius * x_radius
    var ellipse_error = 0
    var stopping_x = two_b_square * x_radius
    var stopping_y = 0
    while (stopping_x >= stopping_y):
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        y += 1
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0):
            x -= 1
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y):
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        x += 1
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0):
            y -= 1
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const BLUE = "blue"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_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()
}


function plot_2_ellipse_lines(center_x, center_y, x, y, color) {
    franco_draw_line(center_x + x, center_y + y, center_x - x, center_y + y, color)
    franco_draw_line(center_x - x, center_y - y, center_x + x, center_y - y, color)
}


function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color) {
    let two_a_square = 2 * x_radius * x_radius
    let two_b_square = 2 * y_radius * y_radius

    let x = x_radius
    let y = 0
    let x_change = y_radius * y_radius * (1 - 2 * x_radius)
    let y_change = x_radius * x_radius
    let ellipse_error = 0
    let stopping_x = two_b_square * x_radius
    let stopping_y = 0
    while (stopping_x >= stopping_y) {
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        ++y
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0) {
            --x
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square
        }
    }

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y) {
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        ++x
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0) {
            --y
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square
        }
    }
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
BLUE: Final = pygame.Color("blue")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


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


def plot_2_ellipse_lines(center_x, center_y, x, y, color):
    franco_draw_line(center_x + x, center_y + y, center_x - x, center_y + y, color)
    franco_draw_line(center_x - x, center_y - y, center_x + x, center_y - y, color)


def franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color):
    two_a_square = 2 * x_radius * x_radius
    two_b_square = 2 * y_radius * y_radius

    x = x_radius
    y = 0
    x_change = y_radius * y_radius * (1 - 2 * x_radius)
    y_change = x_radius * x_radius
    ellipse_error = 0
    stopping_x = two_b_square * x_radius
    stopping_y = 0
    while (stopping_x >= stopping_y):
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        y += 1
        stopping_y += two_a_square
        ellipse_error += y_change
        y_change += two_a_square

        if ((2 * ellipse_error + x_change) > 0):
            x -= 1
            stopping_x -= two_b_square
            ellipse_error += x_change
            x_change += two_b_square

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y):
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        x += 1
        stopping_x += two_b_square
        ellipse_error += x_change
        x_change += two_b_square

        if ((2 * ellipse_error + y_change) > 0):
            y -= 1
            stopping_y -= two_a_square
            ellipse_error += y_change
            y_change += two_a_square


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            franco_draw_ellipse(160, 120, 120, 80, BLUE)
            franco_draw_ellipse(160, 120, 60, 40, RED)
            franco_draw_ellipse(160, 120, 30, 30, WHITE)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}


function franco_draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


function plot_2_ellipse_lines(center_x, center_y, x, y, color)
    franco_draw_line(center_x + x, center_y + y, center_x - x, center_y + y, color)
    franco_draw_line(center_x - x, center_y - y, center_x + x, center_y - y, color)
end


function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color)
    local two_a_square = 2 * x_radius * x_radius
    local two_b_square = 2 * y_radius * y_radius

    local x = x_radius
    local y = 0
    local x_change = y_radius * y_radius * (1 - 2 * x_radius)
    local y_change = x_radius * x_radius
    local ellipse_error = 0
    local stopping_x = two_b_square * x_radius
    local stopping_y = 0
    while (stopping_x >= stopping_y) do
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        y = y + 1
        stopping_y = stopping_y + two_a_square
        ellipse_error = ellipse_error + y_change
        y_change = y_change + two_a_square

        if ((2 * ellipse_error + x_change) > 0) then
            x = x - 1
            stopping_x = stopping_x - two_b_square
            ellipse_error = ellipse_error + x_change
            x_change = x_change + two_b_square
        end
    end

    x = 0
    y = y_radius
    x_change = y_radius * y_radius
    y_change = x_radius * x_radius * (1 - 2 * y_radius)
    ellipse_error = 0
    stopping_x = 0
    stopping_y = two_a_square * y_radius
    while (stopping_x <= stopping_y) do
        plot_2_ellipse_lines(center_x, center_y, x, y, color)

        x = x + 1
        stopping_x = stopping_x + two_b_square
        ellipse_error = ellipse_error + x_change
        x_change = x_change + two_b_square

        if ((2 * ellipse_error + y_change) > 0) then
            y = y - 1
            stopping_y = stopping_y - two_a_square
            ellipse_error = ellipse_error + y_change
            y_change = y_change + two_a_square
        end
    end
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE)
    franco_draw_ellipse(160, 120, 60, 40, RED)
    franco_draw_ellipse(160, 120, 30, 30, WHITE)
end

The following canvas shows the result.

Drawing of three filled concentric ellipses, following the algorithm by John Kennedy.

The approach illustrates how it is possible to reuse the knowledge acquired from previous solutions to solve new problems. The larger your repertory of solved problems, the greater will be the knowledge that you will possess to solve new problems. In other words, solving increasingly complex problems is an excellent way to become better at problem-solving, and, consequently, at programming. In sum, solve problems to solve problems.

Drawing Primitives for Ellipses

JavaScript with canvas provides ellipse() to draw ellipses. Python with PyGame has pygame.draw.ellipse() for an ellipse contained in a rectangle, or pygame.gfxdraw.ellipse() and pygame.gfxdraw.filled_ellipse() for an ellipse defined by radii (radiuses). For simplicity, the implementation will choose the version with radii. Lua with LÖVE provides love.graphics.ellipse().

GDScript does not provide a primitive for drawing ellipses. One alternative using only existing resources consists of applying a transform matrix to modify a circle, as suggested on this answer. The example provides a generalization of the answer, adapting the equation of the circle to an equation of an ellipse.

With some adjustments to the circle equation, it is possible to note that a circle is an ellipse with .

Thus, the idea is drawing a circle with a unity radius centered at the origin. The transform will apply an operation of scale (to set the sizes of the radius) and of translation (to the position of the desired center of the ellipse), creating the desired ellipse. In other words, the ellipse will be created as a deformation of the original circle.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const BLUE = Color.blue


const POINT_COUNT = 30
func franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill = false):
    draw_set_transform(Vector2(center_x, center_y), 0, Vector2(x_radius, y_radius))
    if (fill):
        draw_circle(Vector2(0.0, 0.0), 1.0, color)
    else:
        draw_arc(Vector2(0.0, 0.0), 1.0, 0.0, 2.0 * PI, POINT_COUNT, color)

    draw_set_transform(Vector2(0.0, 0.0), 0, Vector2(1.0, 1.0))


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE, true)
    franco_draw_ellipse(160, 120, 60, 40, RED, true)
    franco_draw_ellipse(160, 120, 30, 30, WHITE, true)

    franco_draw_ellipse(160, 120, 30, 20, BLUE)
    franco_draw_ellipse(160, 120, 15, 10, RED)
    franco_draw_ellipse(160, 120, 7, 7, BLACK)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const BLUE = "blue"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill = false) {
    if (fill) {
        context.fillStyle = color
    } else {
        context.strokeStyle = color
    }

    context.beginPath()
    context.ellipse(center_x, center_y, x_radius, y_radius, 0.0, 0.0, 2.0 * Math.PI)
    context.closePath()

    if (fill) {
        context.fill()
    } else {
        context.stroke()
    }
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    franco_draw_ellipse(160, 120, 120, 80, BLUE, true)
    franco_draw_ellipse(160, 120, 60, 40, RED, true)
    franco_draw_ellipse(160, 120, 30, 30, WHITE, true)

    franco_draw_ellipse(160, 120, 30, 20, BLUE)
    franco_draw_ellipse(160, 120, 15, 10, RED)
    franco_draw_ellipse(160, 120, 7, 7, BLACK)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import pygame.gfxdraw
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
BLUE: Final = pygame.Color("blue")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


def franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill = False):
    if (fill):
        pygame.gfxdraw.filled_ellipse(window, int(center_x), int(center_y), int(x_radius), int(y_radius), color)
    else:
        pygame.gfxdraw.ellipse(window, int(center_x), int(center_y), int(x_radius), int(y_radius), color)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            franco_draw_ellipse(160, 120, 120, 80, BLUE, True)
            franco_draw_ellipse(160, 120, 60, 40, RED, True)
            franco_draw_ellipse(160, 120, 30, 30, WHITE, True)

            franco_draw_ellipse(160, 120, 30, 20, BLUE)
            franco_draw_ellipse(160, 120, 15, 10, RED)
            franco_draw_ellipse(160, 120, 7, 7, BLACK)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}


local ELLIPSE_SEGMENTS = nil -- 30
function franco_draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill)
    local fill_string = "line"
    if (fill) then
        fill_string = "fill"
    end

    love.graphics.setColor(color)

    love.graphics.ellipse(fill_string, center_x, center_y, x_radius, y_radius, ELLIPSE_SEGMENTS)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    franco_draw_ellipse(160, 120, 120, 80, BLUE, true)
    franco_draw_ellipse(160, 120, 60, 40, RED, true)
    franco_draw_ellipse(160, 120, 30, 30, WHITE, true)

    franco_draw_ellipse(160, 120, 30, 20, BLUE)
    franco_draw_ellipse(160, 120, 15, 10, RED)
    franco_draw_ellipse(160, 120, 7, 7, BLACK)
end

The following canvas shows the result.

Drawing of three filled concentric ellipses, with drawing primitives.

The implementation defines a fill parameter as a logic (boolean) value to provide an option to draw only the outline (if false) or fill the ellipse (if true). This avoids duplicating implementations.

If one would rather have two procedures, one of the could be called franco_draw_ellipse(), franco_stroke_ellipse() or franco_outline_ellipse() to create the contour; the other could be called franco_fill_ellipse() to fill the shape.

Polygons

Polygons can be drawn as sequences of lines, as it has been done for drawing the tails on the simulation of throwing coins and dice.

Drawing Outlines of Polygons

To generalize the process, Lists or Vectors can store many values of points, which will be connected as straight lines to form the polygon. The use of arrays and other collections will be detailed in future topics of Ideas, Rules, Simulation; at this time, it is sufficient to know that a variable that instances an array can store multiple values, and each of these values can be accessed using an index.

In GDScript, Python and JavaScript, an array with size elements has the first element at the position 0 and the last element at the position size - 1. In Lua, the first element is at the position 1 and the last element is on the position size. Thus, the way the implementations manipulate indices can be slightly different.

In both cases, the access to an element at the position index of the array is performed using array_name[index]. Thus, for instance, array_name[2] would access the third value stored in GDScript, Python and JavaScript. In Lua, array_name[2] would access the second value stored in the array.

Finally, GDScript, Python e JavaScript use square brackets to declare arrays (or lists, depending on the language). Lua use curly brackets (more specifically, Lua defines an optimized table for the array).

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    var length = points.size()
    for index in range(0, length - 1):
        var current_point = points[index]
        var next_point = points[index + 1]

        franco_draw_line(current_point[0], current_point[1],
                         next_point[0], next_point[1],
                         color)

    franco_draw_line(points[0][0], points[0][1],
                     points[length - 1][0], points[length - 1][1],
                     color)


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    # Triangle
    franco_draw_polygon([[10, 40], [40, 40], [40, 10]], WHITE)

    # Rectangle
    franco_draw_polygon([[50, 10], [50, 80], [140, 80], [140, 10]], RED)

    # Diamond
    franco_draw_polygon([[130, 100], [150, 160], [100, 160], [80, 100]], GREEN)

    # Polygon with assorted points
    franco_draw_polygon([
        [160, 10], [150, 70], [170, 90], [140, 230],
        [180, 200], [210, 210], [250, 190], [300, 120],
        [260, 80], [260, 10]
     ], BLUE)

    # F
    franco_draw_polygon([
        [10, 105], [10, 220], [30, 220],
        [30, 170], [60, 170], [60, 150],
        [30, 150], [30, 130], [70, 130],
        [70, 105],
    ], YELLOW)

    # Star
    franco_draw_polygon([
        [80, 180], [50, 230], [120, 200],
        [40, 200], [110, 230],
    ], MAGENTA)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const YELLOW = "yellow"
const MAGENTA = "magenta"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_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()
}


function franco_draw_polygon(points, color) {
    console.assert(points.length >= 2)

    let length = points.length
    for (let index = 0; index < length - 1; ++index) {
        let current_point = points[index]
        let next_point = points[index + 1]

        franco_draw_line(current_point[0], current_point[1],
                         next_point[0], next_point[1],
                         color)
    }

    franco_draw_line(points[0][0], points[0][1],
                     points[length - 1][0], points[length - 1][1],
                     color)
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Triangle
    franco_draw_polygon([[10, 40], [40, 40], [40, 10]], WHITE)

    // Rectangle
    franco_draw_polygon([[50, 10], [50, 80], [140, 80], [140, 10]], RED)

    // Diamond
    franco_draw_polygon([[130, 100], [150, 160], [100, 160], [80, 100]], GREEN)

    // Polygon with assorted points
    franco_draw_polygon([
        [160, 10], [150, 70], [170, 90], [140, 230],
        [180, 200], [210, 210], [250, 190], [300, 120],
        [260, 80], [260, 10]
    ], BLUE)

    // F
    franco_draw_polygon([
        [10, 105], [10, 220], [30, 220],
        [30, 170], [60, 170], [60, 150],
        [30, 150], [30, 130], [70, 130],
        [70, 105],
    ], YELLOW)

    // Star
    franco_draw_polygon([
        [80, 180], [50, 230], [120, 200],
        [40, 200], [110, 230],
    ], MAGENTA)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


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


def franco_draw_polygon(points, color):
    assert(len(points) >= 2)

    length = len(points)
    for index in range(0, length - 1):
        current_point = points[index]
        next_point = points[index + 1]

        franco_draw_line(current_point[0], current_point[1],
                         next_point[0], next_point[1],
                         color)

    franco_draw_line(points[0][0], points[0][1],
                     points[length - 1][0], points[length - 1][1],
                     color)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            # Triangle
            franco_draw_polygon([[10, 40], [40, 40], [40, 10]], WHITE)

            # Rectangle
            franco_draw_polygon([[50, 10], [50, 80], [140, 80], [140, 10]], RED)

            # Diamond
            franco_draw_polygon([[130, 100], [150, 160], [100, 160], [80, 100]], GREEN)

            # Polygon with assorted points
            franco_draw_polygon([
                [160, 10], [150, 70], [170, 90], [140, 230],
                [180, 200], [210, 210], [250, 190], [300, 120],
                [260, 80], [260, 10]
            ], BLUE)

            # F
            franco_draw_polygon([
                [10, 105], [10, 220], [30, 220],
                [30, 170], [60, 170], [60, 150],
                [30, 150], [30, 130], [70, 130],
                [70, 105],
            ], YELLOW)

            # Star
            franco_draw_polygon([
                [80, 180], [50, 230], [120, 200],
                [40, 200], [110, 230],
            ], MAGENTA)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


function franco_draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


function franco_draw_polygon(points, color)
    assert(#points >= 2)

    local length = #points
    for index = 1, length - 1 do
        local current_point = points[index]
        local next_point = points[index + 1]

        franco_draw_line(current_point[1], current_point[2],
                         next_point[1], next_point[2],
                         color)
    end

    franco_draw_line(points[1][1], points[1][2],
                     points[length][1], points[length][2],
                     color)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    -- Triangle
    franco_draw_polygon({{10, 40}, {40, 40}, {40, 10}}, WHITE)

    -- Rectangle
    franco_draw_polygon({{50, 10}, {50, 80}, {140, 80}, {140, 10}}, RED)

    -- Diamond
    franco_draw_polygon({{130, 100}, {150, 160}, {100, 160}, {80, 100}}, GREEN)

    -- Polygon with assorted points
    franco_draw_polygon({
        {160, 10}, {150, 70}, {170, 90}, {140, 230},
        {180, 200}, {210, 210}, {250, 190}, {300, 120},
        {260, 80}, {260, 10}
    }, BLUE)

    -- F
    franco_draw_polygon({
        {10, 105}, {10, 220}, {30, 220},
        {30, 170}, {60, 170}, {60, 150},
        {30, 150}, {30, 130}, {70, 130},
        {70, 105},
    }, YELLOW)

    -- Star
    franco_draw_polygon({
        {80, 180}, {50, 230}, {120, 200},
        {40, 200}, {110, 230},
    }, MAGENTA)
end

The following canvas shows the result.

Drawings of polygons using lines connected by a common point. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a polygon with assorted points in blue, and a magenta star. The background is black.

In simple words, the repetition structure in franco_draw_polygon() draw lines connecting pairs of points in contiguous positions (next to each other). The first point is connected to the second, the second to the third, and do son. The loop ends at the penultimate point, which is connected to the last one.

The call to franco_draw_line() outside the loop connects the first point to the last one, to connect the drawing.

Thus, a correct usage of the procedures requires, at least, two points at the call (preferably distinct, to draw a line). The use of assert() checks the size; if the array has less than two values, the call fails and shows an error during development. This is called an assertion; assertions have been previously commented on Learn Programming: Subroutines (Functions and Procedures) and Learn Programming: Records. An assertion is not appropriate to handle errors in programs provided for end-users; it serves to inform the programmer that she/he has made an implementation error (which should be fixed).

It is worth noticing that a line can cross the drawing, depending on the chosen points. Strictly speaking, this would not form a convex polygon. In a convex polygon, all internal angles are less than 180°. Besides context, polygons can be concave or complex. A concave polygon may have internal angles larger than 180°. The complex polygon category includes the other two. Edges that cross the polygon itself makes a complex polygon (more specifically, a self-intersecting polygon).

In the created example, the triangle, the rectangle and the diamond are convex polygons. The yellow letter F and the blue polygon are concave. The magenta star is a complex polygon.

To use franco_draw_polygon(), the call define a series of arrays. Each point (ordered pair) is passed as an array.

Refactoring for Data Abstraction: Creating a Point Data Type

For a cleaner solution, it is also possible to define a Records (Structs) to define a point (Point) data type. Although GDScript, JavaScript and Python allow creating classes from the Object-Oriented Programming (OOP) paradigm, this topic will assume the use of records, as defined in Records (Structs): composite types to hold heterogeneous data. OOP will be addressed in the future, once data types require complex processing.

A record is a type created by the programmer of the application. In other words, besides types provides the programming language, now you will be able to create your very own types.

The goal is creating a new data type that store two values: an integer or real value for x, and an integer or real value for y to form a bi-dimensional (2D) point. At a high level, a Point record could be defined as follows in pseudocode:

record Point
    x: real
    y: real
end

var point_a: Point
point_a.x = 10
point_a.y = 20

var point_b: Point
point_b.x = 20
point_b.y = 10

var delta_x: real
delta_x = point_a.x - point_b.x

Thus, the declaration of a variable of the type Point would create an instance of the record with two variables (x and y) for the coordinates. The next examples refactor the code from the previous section to add a record. Therefore, instead of arrays with pairs of values for each coordinate of the polygon, one will be able to create a polygon as an array of variables of the Point type. For instance, instead of [10, 40], she/he could create a Point with x equal to 10 and y equal to 40, and use it as an equivalent alternative.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


class Point:
    var x
    var y


    func _init(_x, _y):
        self.x = _x
        self.y = _y


func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    var length = points.size()
    for index in range(0, length - 1):
        var current_point = points[index]
        var next_point = points[index + 1]

        franco_draw_line(current_point.x, current_point.y,
                         next_point.x, next_point.y,
                         color)

    franco_draw_line(points[0].x, points[0].y,
                     points[length - 1].x, points[length - 1].y,
                     color)


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    # Triangle
    franco_draw_polygon([
        Point.new(10, 40),
        Point.new(40, 40),
        Point.new(40, 10)
    ], WHITE)

    # Rectangle
    franco_draw_polygon([
        Point.new(50, 10),
        Point.new(50, 80),
        Point.new(140, 80),
        Point.new(140, 10),
        Point.new(50, 10)
    ], RED)

    # Diamond
    franco_draw_polygon([
        Point.new(130, 100),
        Point.new(150, 160),
        Point.new(100, 160),
        Point.new(80, 100)
    ], GREEN)

    # Polygon with assorted points
    franco_draw_polygon([
        Point.new(160, 10), Point.new(150, 70), Point.new(170, 90), Point.new(140, 230),
        Point.new(180, 200), Point.new(210, 210), Point.new(250, 190), Point.new(300, 120),
        Point.new(260, 80), Point.new(260, 10), Point.new(160, 10)
    ], BLUE)

    # F
    franco_draw_polygon([
        Point.new(10, 105), Point.new(10, 220), Point.new(30, 220),
        Point.new(30, 170), Point.new(60, 170), Point.new(60, 150),
        Point.new(30, 150), Point.new(30, 130), Point.new(70, 130),
        Point.new(70, 105), Point.new(10, 105)
    ], YELLOW)

    # Star
    franco_draw_polygon([
        Point.new(80, 180), Point.new(50, 230), Point.new(120, 200),
        Point.new(40, 200), Point.new(110, 230), Point.new(80, 180)
    ], MAGENTA)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const YELLOW = "yellow"
const MAGENTA = "magenta"


class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
}


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_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()
}


function franco_draw_polygon(points, color) {
    console.assert(points.length >= 2)

    let length = points.length
    for (let index = 0; index < length - 1; ++index) {
        let current_point = points[index]
        let next_point = points[index + 1]

        franco_draw_line(current_point.x, current_point.y,
                         next_point.x, next_point.y,
                         color)
    }

    franco_draw_line(points[0].x, points[0].y,
                     points[length - 1].x, points[length - 1].y,
                     color)
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Triangle
    franco_draw_polygon([
        new Point(10, 40),
        new Point(40, 40),
        new Point(40, 10)
    ], WHITE)

    // Rectangle
    franco_draw_polygon([
        new Point(50, 10),
        new Point(50, 80),
        new Point(140, 80),
        new Point(140, 10)
    ], RED)

    // Diamond
    franco_draw_polygon([
        new Point(130, 100),
        new Point(150, 160),
        new Point(100, 160),
        new Point(80, 100)
    ], GREEN)

    // Polygon with assorted points
    franco_draw_polygon([
        new Point(160, 10), new Point(150, 70), new Point(170, 90), new Point(140, 230),
        new Point(180, 200), new Point(210, 210), new Point(250, 190), new Point(300, 120),
        new Point(260, 80), new Point(260, 10)
    ], BLUE)

    // F
    franco_draw_polygon([
        new Point(10, 105), new Point(10, 220), new Point(30, 220),
        new Point(30, 170), new Point(60, 170), new Point(60, 150),
        new Point(30, 150), new Point(30, 130), new Point(70, 130),
        new Point(70, 105),
    ], YELLOW)

    // Star
    franco_draw_polygon([
        new Point(80, 180), new Point(50, 230), new Point(120, 200),
        new Point(40, 200), new Point(110, 230),
    ], MAGENTA)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


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


def franco_draw_polygon(points, color):
    assert(len(points) >= 2)

    length = len(points)
    for index in range(0, length - 1):
        current_point = points[index]
        next_point = points[index + 1]

        franco_draw_line(current_point.x, current_point.y,
                         next_point.x, next_point.y,
                         color)

    franco_draw_line(points[0].x, points[0].y,
                     points[length - 1].x, points[length - 1].y,
                     color)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            # Triangle
            franco_draw_polygon([
                Point(10, 40),
                Point(40, 40),
                Point(40, 10)
            ], WHITE)

            # Rectangle
            franco_draw_polygon([
                Point(50, 10),
                Point(50, 80),
                Point(140, 80),
                Point(140, 10),
                Point(50, 10)
            ], RED)

            # Diamond
            franco_draw_polygon([
                Point(130, 100),
                Point(150, 160),
                Point(100, 160),
                Point(80, 100)
            ], GREEN)

            # Polygon with assorted points
            franco_draw_polygon([
                Point(160, 10), Point(150, 70), Point(170, 90), Point(140, 230),
                Point(180, 200), Point(210, 210), Point(250, 190), Point(300, 120),
                Point(260, 80), Point(260, 10), Point(160, 10)
            ], BLUE)

            # F
            franco_draw_polygon([
                Point(10, 105), Point(10, 220), Point(30, 220),
                Point(30, 170), Point(60, 170), Point(60, 150),
                Point(30, 150), Point(30, 130), Point(70, 130),
                Point(70, 105), Point(10, 105)
            ], YELLOW)

            # Star
            franco_draw_polygon([
                Point(80, 180), Point(50, 230), Point(120, 200),
                Point(40, 200), Point(110, 230), Point(80, 180)
            ], MAGENTA)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


function Point(x, y)
    return {
        x = x,
        y = y
    }
end


function franco_draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


function franco_draw_polygon(points, color)
    assert(#points >= 2)

    local length = #points
    for index = 1, length - 1 do
        local current_point = points[index]
        local next_point = points[index + 1]

        franco_draw_line(current_point.x, current_point.y,
                         next_point.x, next_point.y,
                         color)
    end

    franco_draw_line(points[1].x, points[1].y,
                     points[length].x, points[length].y,
                     color)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    -- Triangle
    franco_draw_polygon({
        Point(10, 40),
        Point(40, 40),
        Point(40, 10)
    }, WHITE)

    -- Rectangle
    franco_draw_polygon({
        Point(50, 10),
        Point(50, 80),
        Point(140, 80),
        Point(140, 10),
        Point(50, 10)
    }, RED)

    -- Diamond
    franco_draw_polygon({
        Point(130, 100),
        Point(150, 160),
        Point(100, 160),
        Point(80, 100)
    }, GREEN)

    -- Polygon with assorted points
    franco_draw_polygon({
        Point(160, 10), Point(150, 70), Point(170, 90), Point(140, 230),
        Point(180, 200), Point(210, 210), Point(250, 190), Point(300, 120),
        Point(260, 80), Point(260, 10), Point(160, 10)
    }, BLUE)

    -- F
    franco_draw_polygon({
        Point(10, 105), Point(10, 220), Point(30, 220),
        Point(30, 170), Point(60, 170), Point(60, 150),
        Point(30, 150), Point(30, 130), Point(70, 130),
        Point(70, 105), Point(10, 105)
    }, YELLOW)

    -- Star
    franco_draw_polygon({
        Point(80, 180), Point(50, 230), Point(120, 200),
        Point(40, 200), Point(110, 230), Point(80, 180)
    }, MAGENTA)
end

The following canvas shows the result.

Drawings of polygons using lines connected by a common point. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a polygon with assorted points in blue, and a magenta star. The background is black.

The GDScript version uses a inner class. The Lua version uses a table to avoid introducing an arbitrary OOP model, as the languages does not provide constructions for records or classes. The same could be done for JavaScript, using JavaScript Objects.

If one wishes, she/he could also refactor franco_draw_line() to receive a Point instead of two numbers as the coordinate.

Suitable alternatives can enrich an API, making it more expressive. The counterpoint is that all versions must be kept up-to-date and functional. An elegant way of minimizing the problem consists of defining a lower level subroutine, and call it by similar subroutines (for a higher-level abstraction).

In this approach, a second procedure could be defined as a variation using points. The next example illustrates the approach in GDScript.

func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func franco_draw_line_from_points(point0, point1, color):
    franco_draw_line(point0.x, point0.y, point1.x, point1.y, color)


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    var length = points.size()
    for index in range(0, length - 1):
        var current_point = points[index]
        var next_point = points[index + 1]

        franco_draw_line_from_points(current_point, next_point, color)

    franco_draw_line_from_points(points[0], points[length - 1], color)

franco_draw_line_from_points() receives two points parameters, and call the original franco_draw_line() to draw the line. The modified version of franco_draw_polygon() calls franco_draw_line_from_points() to draw each line of the polygon.

Provided the parameters are kept unchanged, the version with points would be automatically updated whenever the original version was modified, for all it does it call the original.

Drawing Primitives for Outlines of Polygons

Drawing APIs typically provide a subroutine to create polygons using a sequence of points. GDScript provides draw_polyline(), Python with PyGame has pygame.draw.polygon(), and Lua with LÖVE provides love.graphics.polygon(). JavaScript does not provide a specific subroutine; in this case, the lines should be drawn individually.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    draw_polyline(PoolVector2Array(points), color)


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    # Triangle
    franco_draw_polygon([Vector2(10, 40), Vector2(40, 40), Vector2(40, 10), Vector2(10, 40)], WHITE)

    # Rectangle
    franco_draw_polygon([Vector2(50, 10), Vector2(50, 80), Vector2(140, 80), Vector2(140, 10), Vector2(50, 10)], RED)

    # Diamond
    franco_draw_polygon([Vector2(130, 100), Vector2(150, 160), Vector2(100, 160), Vector2(80, 100), Vector2(130, 100)], GREEN)

    # Polygon with assorted points
    franco_draw_polygon([
        Vector2(160, 10), Vector2(150, 70), Vector2(170, 90), Vector2(140, 230),
        Vector2(180, 200), Vector2(210, 210), Vector2(250, 190), Vector2(300, 120),
        Vector2(260, 80), Vector2(260, 10), Vector2(160, 10)
    ], BLUE)

    # F
    franco_draw_polygon([
        Vector2(10, 105), Vector2(10, 220), Vector2(30, 220),
        Vector2(30, 170), Vector2(60, 170), Vector2(60, 150),
        Vector2(30, 150), Vector2(30, 130), Vector2(70, 130),
        Vector2(70, 105), Vector2(10, 105)
    ], YELLOW)

    # Star
    franco_draw_polygon([
        Vector2(80, 180), Vector2(50, 230), Vector2(120, 200),
        Vector2(40, 200), Vector2(110, 230), Vector2(80, 180)
    ], MAGENTA)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const YELLOW = "yellow"
const MAGENTA = "magenta"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_draw_polygon(points, color) {
    console.assert(points.length >= 2)

    context.strokeStyle = color
    // context.fillStyle = color

    context.beginPath()
    context.moveTo(points[0][0], points[0][1])

    let length = points.length
    for (let index = 1; index < length; ++index) {
        let next_point = points[index]
        context.lineTo(next_point[0], next_point[1])
    }

    context.closePath()
    context.stroke()
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Triangle
    franco_draw_polygon([[10, 40], [40, 40], [40, 10]], WHITE)

    // Rectangle
    franco_draw_polygon([[50, 10], [50, 80], [140, 80], [140, 10]], RED)

    // Diamond
    franco_draw_polygon([[130, 100], [150, 160], [100, 160], [80, 100]], GREEN)

    // Polygon with assorted points
    franco_draw_polygon([
        [160, 10], [150, 70], [170, 90], [140, 230],
        [180, 200], [210, 210], [250, 190], [300, 120],
        [260, 80], [260, 10]
    ], BLUE)

    // F
    franco_draw_polygon([
        [10, 105], [10, 220], [30, 220],
        [30, 170], [60, 170], [60, 150],
        [30, 150], [30, 130], [70, 130],
        [70, 105],
    ], YELLOW)

    // Star
    franco_draw_polygon([
        [80, 180], [50, 230], [120, 200],
        [40, 200], [110, 230], [80, 180]
    ], MAGENTA)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


def franco_draw_polygon(points, color):
    assert(len(points) >= 2)

    # 0: fill polygon; > 0: line width.
    pygame.draw.polygon(window, color, points, 1)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            # Triangle
            franco_draw_polygon([(10, 40), (40, 40), (40, 10)], WHITE)

            # Rectangle
            franco_draw_polygon([(50, 10), (50, 80), (140, 80), (140, 10)], RED)

            # Diamond
            franco_draw_polygon([(130, 100), (150, 160), (100, 160), (80, 100)], GREEN)

            # Polygon with assorted points
            franco_draw_polygon([
                (160, 10), (150, 70), (170, 90), (140, 230),
                (180, 200), (210, 210), (250, 190), (300, 120),
                (260, 80), (260, 10)
            ], BLUE)

            # F
            franco_draw_polygon([
                (10, 105), (10, 220), (30, 220),
                (30, 170), (60, 170), (60, 150),
                (30, 150), (30, 130), (70, 130),
                (70, 105),
            ], YELLOW)

            # Star
            franco_draw_polygon([
                (80, 180), (50, 230), (120, 200),
                (40, 200), (110, 230), (80, 180)
            ], MAGENTA)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


function franco_draw_polygon(points, color)
    assert(#points >= 2)

    love.graphics.setColor(color)
    love.graphics.polygon("line", points)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    -- Triangle
    franco_draw_polygon({10, 40, 40, 40, 40, 10}, WHITE)

    -- Rectangle
    franco_draw_polygon({50, 10, 50, 80, 140, 80, 140, 10}, RED)

    -- Diamond
    franco_draw_polygon({130, 100, 150, 160, 100, 160, 80, 100}, GREEN)

    -- Polygon with assorted points
    franco_draw_polygon({
        160, 10, 150, 70, 170, 90, 140, 230,
        180, 200, 210, 210, 250, 190, 300, 120,
        260, 80, 260, 10
    }, BLUE)

    -- F
    franco_draw_polygon({
        10, 105, 10, 220, 30, 220,
        30, 170, 60, 170, 60, 150,
        30, 150, 30, 130, 70, 130,
        70, 105,
    }, YELLOW)

    -- Star
    -- NOTE LÖVE does not draw it.
    franco_draw_polygon({
        80, 180, 50, 230, 120, 200,
        40, 200, 110, 230, 80, 180
    }, MAGENTA)
end

The following canvas shows the result.

Drawings of polygons using drawing primitives. Drawings of polygons using lines connected by a common point. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a polygon with assorted points in blue, and a magenta star. The background is black.

As expected, the result is the very same. After all, this is a refactor (often called a class extraction).

In GDScript, the first point must be repeated at the end of array as a parameter to make draw_polyline() complete the outline of the polygon.

In JavaScript, the previous implementation has been almost entirely reused. The code of franco_draw_line() has been merged to franco_draw_polygon().

In Lua with LÖVE, points can be passed directly to the call as alternate coordinates, or in an array (as a 'table'). It also should be noted that love.graphics.polygon() does not draw complex polygons.

Filling Polygons

After defining the outlines, it is time to fill them to create colored polygons. The algorithm to fill polygons possibly will the most complex one hitherto in Ideas, Rules, Simulation. To implement it, it will be necessary to use operations Lists or Arrays, such as insertion of new values and sorting.

Thus, if you are just starting your programming activities, it can be interesting to explore the predefined primitives. Later, once you have more practice, you can return to the polygon filling algorithm.

Filling Polygons with Scanline Rendering (Scan-Line Algorithm)

One of the simplest algorithms to fill polygons is called scanline rendering, also known as scan-line algorithm. A good description for it can be found in this page.

The implementation of this topic will be slightly simpler and inefficient, keeping all points stored in an array. It performs the following steps:

  1. Determination of the largest and smallest values of y ordinate of the polygon. The values will be used to fill the polygon line by line;
  2. Identification of the x abscissa for the minimum value of y. It will be used to follow the lines during the filling;
  3. Calculus of the angular coefficient for the straight lines. The coefficient will be used to determine inclined points inside the polygons;
  4. Determination of straight line segments for each y of the drawing. Points will be paired two by two. The lines will be drawn between alternate intervals of x pairs. This allows coloring the relevant parts, as well as ignoring gaps between them.

The next canvas provides an animation of how the algorithm performs the filling. To start it, you can use the button Start Animation. The speed of the reproduction can be set.




Animation of how the Scanline Algorithm fills polygons to draw them. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a blue polygon with assorted points, and a magenta star. The background of the image is black.

In simple terms, the implementation of algorithm maps the straight line segments that make the polygon (by selecting pairs of consecutive points). Next, it fills an instance of an edge 'Edge' to store the minimum and maximum value of y, the inverse (reciprocal) of the angular coefficient, and the value of x for which y is minimum (to start drawing the line segment). Each value is stored in an array called edges.

After processing all pairs, the implementation starts filling the drawing. The code starts in the while repetition structure.

The implementation maps each edge that must be drawn for the current (y) line, that is, those on which the minimum value of the ordinate (minimum_y) is larger or equal to y, and that (at the same time) have the maximum value for the ordinate (maximum_y) less than y. Each initial value of x is stored in an array starting_x.

For the next step, the initial value of x for each edge is calculated using the inverse of the angular coefficient. This is similar to what has been previously done to draw inclined lines in Pixels and Drawing Primitives (Points, Lines, and Arcs).

The values are sorted to handle the case of lines crossing over the drawing (which can happen for complex polygons). Finally, all that remains is drawing alternate line segments. The first is drawn; the second is ignored; the third is drawn; the fourth is ignored; and so it continues. This allows ignoring gaps between lines of the polygon.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


class Point:
    var x
    var y


    func _init(_x, _y):
        self.x = _x
        self.y = _y


class Edge:
    var maximum_y
    var minimum_y
    var x
    # m (angular coefficient)
    var inverted_slope


func franco_draw_line(x0, y0, x1, y1, color):
    draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    var length = points.size()

    var edges = []
    var minimum_y = points[0].y
    var maximum_y = points[0].y
    for index in range(0, length):
        var current_point = points[index]
        var next_point
        if (index < (length - 1)):
            next_point = points[index + 1]
        else:
            next_point = points[0]

        var x0 = current_point.x
        var y0 = current_point.y
        var x1 = next_point.x
        var y1 = next_point.y
        if (current_point.y <= next_point.y):
            x0 = next_point.x
            y0 = next_point.y
            x1 = current_point.x
            y1 = current_point.y

        var edge = Edge.new()
        if (y1 < y0):
            edge.maximum_y = y0
            edge.minimum_y = y1
            edge.x = x1
        else:
            edge.maximum_y = y1
            edge.minimum_y = y0
            edge.x = x0

        if (y0 != y1):
            edge.inverted_slope = 1.0 * (x0 - x1) / (y0 - y1)
        else:
            edge.inverted_slope = 0.0

        edges.append(edge)

        if (edge.minimum_y < minimum_y):
            minimum_y = edge.minimum_y

        if (edge.maximum_y > maximum_y):
            maximum_y = edge.maximum_y

    var PAINT_LAST_PIXEL = 1
    var y = minimum_y
    while (y < maximum_y):
        var starting_x = []
        for edge in edges:
            if ((y >= edge.minimum_y) and (y < edge.maximum_y)):
                starting_x.append(edge.x)

                edge.x += edge.inverted_slope

        starting_x.sort()
        for index in range(0, starting_x.size() - 1, 2):
            var x0 = starting_x[index] - PAINT_LAST_PIXEL
            var x1 = starting_x[index + 1] + PAINT_LAST_PIXEL
            franco_draw_line(x0, y, x1, y, color)

        y += 1


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    # Triangle
    franco_draw_polygon([
        Point.new(10, 40),
        Point.new(40, 40),
        Point.new(40, 10)
    ], WHITE)

    # Rectangle
    franco_draw_polygon([
        Point.new(50, 10),
        Point.new(50, 80),
        Point.new(140, 80),
        Point.new(140, 10),
        Point.new(50, 10)
    ], RED)

    # Diamond
    franco_draw_polygon([
        Point.new(130, 100),
        Point.new(150, 160),
        Point.new(100, 160),
        Point.new(80, 100)
    ], GREEN)

    # Polygon with assorted points
    franco_draw_polygon([
        Point.new(160, 10), Point.new(150, 70), Point.new(170, 90), Point.new(140, 230),
        Point.new(180, 200), Point.new(210, 210), Point.new(250, 190), Point.new(300, 120),
        Point.new(260, 80), Point.new(260, 10), Point.new(160, 10)
    ], BLUE)

    # F
    franco_draw_polygon([
        Point.new(10, 105), Point.new(10, 220), Point.new(30, 220),
        Point.new(30, 170), Point.new(60, 170), Point.new(60, 150),
        Point.new(30, 150), Point.new(30, 130), Point.new(70, 130),
        Point.new(70, 105), Point.new(10, 105)
    ], YELLOW)

    # Star
    franco_draw_polygon([
        Point.new(80, 180), Point.new(50, 230), Point.new(120, 200),
        Point.new(40, 200), Point.new(110, 230), Point.new(80, 180)
    ], MAGENTA)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const YELLOW = "yellow"
const MAGENTA = "magenta"


class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
}


class Edge {
    constructor() {
        this.maximum_y = null
        this.minimum_y = null
        this.x = null
        // m (angular coefficient)
        this.inverted_slope = null
    }
}


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_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()
}


function franco_draw_polygon(points, color) {
    console.assert(points.length >= 2)

    var length = points.length

    let edges = []
    let minimum_y = points[0].y
    let maximum_y = points[0].y
    for (let index = 0; index < length; ++index) {
        let current_point = points[index]
        let next_point
        if (index < (length - 1)) {
            next_point = points[index + 1]
        } else {
            next_point = points[0]
        }

        let x0 = current_point.x
        let y0 = current_point.y
        let x1 = next_point.x
        let y1 = next_point.y
        if (current_point.y <= next_point.y) {
            x0 = next_point.x
            y0 = next_point.y
            x1 = current_point.x
            y1 = current_point.y
        }

        let edge = new Edge()
        if (y1 < y0) {
            edge.maximum_y = y0
            edge.minimum_y = y1
            edge.x = x1
        } else {
            edge.maximum_y = y1
            edge.minimum_y = y0
            edge.x = x0
        }

        if (y0 !== y1) {
            edge.inverted_slope = 1.0 * (x0 - x1) / (y0 - y1)
        } else {
            edge.inverted_slope = 0.0
        }

        edges.push(edge)

        if (edge.minimum_y < minimum_y) {
            minimum_y = edge.minimum_y
        }

        if (edge.maximum_y > maximum_y) {
            maximum_y = edge.maximum_y
        }
    }

    let PAINT_LAST_PIXEL = 1
    let y = minimum_y
    while (y < maximum_y) {
        let starting_x = []
        for (let edge of edges) {
            if ((y >= edge.minimum_y) && (y < edge.maximum_y)) {
                starting_x.push(edge.x)

                edge.x += edge.inverted_slope
            }
        }

        starting_x.sort(function(x, y) {
            return x - y
        })
        for (let index = 0, end = starting_x.length - 1;
             index <= end;
             index += 2) {
            let x0 = starting_x[index] - PAINT_LAST_PIXEL
            let x1 = starting_x[index + 1] + PAINT_LAST_PIXEL
            franco_draw_line(x0, y, x1, y, color)
        }

        y += 1
    }
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Triangle
    franco_draw_polygon([
        new Point(10, 40),
        new Point(40, 40),
        new Point(40, 10)
    ], WHITE)

    // Rectangle
    franco_draw_polygon([
        new Point(50, 10),
        new Point(50, 80),
        new Point(140, 80),
        new Point(140, 10)
    ], RED)

    // Diamond
    franco_draw_polygon([
        new Point(130, 100),
        new Point(150, 160),
        new Point(100, 160),
        new Point(80, 100)
    ], GREEN)

    // Polygon with assorted points
    franco_draw_polygon([
        new Point(160, 10), new Point(150, 70), new Point(170, 90), new Point(140, 230),
        new Point(180, 200), new Point(210, 210), new Point(250, 190), new Point(300, 120),
        new Point(260, 80), new Point(260, 10)
    ], BLUE)

    // F
    franco_draw_polygon([
        new Point(10, 105), new Point(10, 220), new Point(30, 220),
        new Point(30, 170), new Point(60, 170), new Point(60, 150),
        new Point(30, 150), new Point(30, 130), new Point(70, 130),
        new Point(70, 105),
    ], YELLOW)

    // Star
    franco_draw_polygon([
        new Point(80, 180), new Point(50, 230), new Point(120, 200),
        new Point(40, 200), new Point(110, 230),
    ], MAGENTA)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y



class Edge:
    def __init__(self):
        self.maximum_y = None
        self.minimum_y = None
        self.x = None
        # m (angular coefficient)
        self.inverted_slope = None



pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


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


def franco_draw_polygon(points, color):
    assert(len(points) >= 2)

    length = len(points)
    edges = []
    minimum_y = points[0].y
    maximum_y = points[0].y
    for index in range(0, length):
        current_point = points[index]
        next_point = None
        if (index < (length - 1)):
            next_point = points[index + 1]
        else:
            next_point = points[0]

        x0 = current_point.x
        y0 = current_point.y
        x1 = next_point.x
        y1 = next_point.y
        if (current_point.y <= next_point.y):
            x0 = next_point.x
            y0 = next_point.y
            x1 = current_point.x
            y1 = current_point.y

        edge = Edge()
        if (y1 < y0):
            edge.maximum_y = y0
            edge.minimum_y = y1
            edge.x = x1
        else:
            edge.maximum_y = y1
            edge.minimum_y = y0
            edge.x = x0

        if (y0 != y1):
            edge.inverted_slope = 1.0 * (x0 - x1) / (y0 - y1)
        else:
            edge.inverted_slope = 0.0

        edges.append(edge)

        if (edge.minimum_y < minimum_y):
            minimum_y = edge.minimum_y

        if (edge.maximum_y > maximum_y):
            maximum_y = edge.maximum_y

    PAINT_LAST_PIXEL = 1
    y = minimum_y
    while (y < maximum_y):
        starting_x = []
        for edge in edges:
            if ((y >= edge.minimum_y) and (y < edge.maximum_y)):
                starting_x.append(edge.x)

                edge.x += edge.inverted_slope

        starting_x.sort()
        for index in range(0, len(starting_x) - 1, 2):
            x0 = starting_x[index] - PAINT_LAST_PIXEL
            x1 = starting_x[index + 1] + PAINT_LAST_PIXEL
            franco_draw_line(x0, y, x1, y, color)

        y += 1


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            # Triangle
            franco_draw_polygon([
                Point(10, 40),
                Point(40, 40),
                Point(40, 10)
            ], WHITE)

            # Rectangle
            franco_draw_polygon([
                Point(50, 10),
                Point(50, 80),
                Point(140, 80),
                Point(140, 10),
                Point(50, 10)
            ], RED)

            # Diamond
            franco_draw_polygon([
                Point(130, 100),
                Point(150, 160),
                Point(100, 160),
                Point(80, 100)
            ], GREEN)

            # Polygon with assorted points
            franco_draw_polygon([
                Point(160, 10), Point(150, 70), Point(170, 90), Point(140, 230),
                Point(180, 200), Point(210, 210), Point(250, 190), Point(300, 120),
                Point(260, 80), Point(260, 10), Point(160, 10)
            ], BLUE)

            # F
            franco_draw_polygon([
                Point(10, 105), Point(10, 220), Point(30, 220),
                Point(30, 170), Point(60, 170), Point(60, 150),
                Point(30, 150), Point(30, 130), Point(70, 130),
                Point(70, 105), Point(10, 105)
            ], YELLOW)

            # Star
            franco_draw_polygon([
                Point(80, 180), Point(50, 230), Point(120, 200),
                Point(40, 200), Point(110, 230), Point(80, 180)
            ], MAGENTA)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


function Point(x, y)
    return {
        x = x,
        y = y
    }
end


function Edge()
    return {
        maximum_y = nil,
        minimum_y = nil,
        x = nil,
        -- m (angular coefficient)
        inverted_slope = nil
    }
end


function franco_draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


function franco_draw_polygon(points, color)
    assert(#points >= 2)

    local length = #points

    local edges = {}
    local minimum_y = points[1].y
    local maximum_y = points[1].y
    for index = 1, length do
        local current_point = points[index]
        local next_point
        if (index < length) then
            next_point = points[index + 1]
        else
            next_point = points[1]
        end

        local x0 = current_point.x
        local y0 = current_point.y
        local x1 = next_point.x
        local y1 = next_point.y
        if (current_point.y <= next_point.y) then
            x0 = next_point.x
            y0 = next_point.y
            x1 = current_point.x
            y1 = current_point.y
        end

        local edge = Edge()
        if (y1 < y0) then
            edge.maximum_y = y0
            edge.minimum_y = y1
            edge.x = x1
        else
            edge.maximum_y = y1
            edge.minimum_y = y0
            edge.x = x0
        end

        if (y0 ~= y1) then
            edge.inverted_slope = 1.0 * (x0 - x1) / (y0 - y1)
        else
            edge.inverted_slope = 0.0
        end

        table.insert(edges, edge)

        if (edge.minimum_y < minimum_y) then
            minimum_y = edge.minimum_y
        end

        if (edge.maximum_y > maximum_y) then
            maximum_y = edge.maximum_y
        end
    end

    local PAINT_LAST_PIXEL = 1
    local y = minimum_y
    while (y < maximum_y) do
        local starting_x = {}
        for _, edge in ipairs(edges) do
            if ((y >= edge.minimum_y) and (y < edge.maximum_y)) then
                table.insert(starting_x, edge.x)

                edge.x = edge.x + edge.inverted_slope
            end
        end

        table.sort(starting_x)
        for index = 1, #starting_x - 1, 2 do
            local x0 = starting_x[index] - PAINT_LAST_PIXEL
            local x1 = starting_x[index + 1] + PAINT_LAST_PIXEL
            franco_draw_line(x0, y, x1, y, color)
        end

        y = y + 1
    end
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    -- Triangle
    franco_draw_polygon({
        Point(10, 40),
        Point(40, 40),
        Point(40, 10)
    }, WHITE)

    -- Rectangle
    franco_draw_polygon({
        Point(50, 10),
        Point(50, 80),
        Point(140, 80),
        Point(140, 10),
        Point(50, 10)
    }, RED)

    -- Diamond
    franco_draw_polygon({
        Point(130, 100),
        Point(150, 160),
        Point(100, 160),
        Point(80, 100)
    }, GREEN)

    -- Polygon with assorted points
    franco_draw_polygon({
        Point(160, 10), Point(150, 70), Point(170, 90), Point(140, 230),
        Point(180, 200), Point(210, 210), Point(250, 190), Point(300, 120),
        Point(260, 80), Point(260, 10), Point(160, 10)
    }, BLUE)

    -- F
    franco_draw_polygon({
        Point(10, 105), Point(10, 220), Point(30, 220),
        Point(30, 170), Point(60, 170), Point(60, 150),
        Point(30, 150), Point(30, 130), Point(70, 130),
        Point(70, 105), Point(10, 105)
    }, YELLOW)

    -- Star
    franco_draw_polygon({
        Point(80, 180), Point(50, 230), Point(120, 200),
        Point(40, 200), Point(110, 230), Point(80, 180)
    }, MAGENTA)
end

The following canvas shows the result.

Drawing filling polygons using the Scanline Algorithm. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a polygon with assorted points in blue, and a magenta star. The background is black.

For a more efficient implementation, one could remove the values in edges on which the maximum values of maximum_y were greater than the current y value (because they would not be drawn anymore). Likewise, she/he could start searching for new values only for compatible values of minimum_y (because smaller values would not be drawn).

Filling Polygons with Drawing Primitives

Although drawing primitives allow filing polygons, many API implementations are restricted to convex polygons (because they are faster and simpler to draw). In fact, filling the magenta star will fail in some examples (GDScript, JavaScript with canvas and Lua with LÖVE). Thus, only the Python with PyGame implementation will fill the start correctly (with a gap at the central part).

Considering this note, the relevant code is provided in franco_draw_polygon(). GDScript provides draw_colored_polygon() to draw colored polygons. JavaScript with canvas uses a combination of beginPath(), moveTo(), lineTo(), closePath(), and fill(). Python with PyGame has pygame.draw.polygon(). Lua with LÖVE provides love.graphics.polygon().

In some cases, all that is required is changing the drawing mode.

# Root must be a Node that allows drawing using _draw().
extends Control


const WIDTH = 320
const HEIGHT = 240
const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


func franco_draw_polygon(points, color):
    assert(points.size() >= 2)

    draw_colored_polygon(PoolVector2Array(points), color)


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title("Hello, my name is Franco!")


func _draw():
    VisualServer.set_default_clear_color(BLACK)

    # Triangle
    franco_draw_polygon([Vector2(10, 40), Vector2(40, 40), Vector2(40, 10), Vector2(10, 40)], WHITE)

    # Rectangle
    franco_draw_polygon([Vector2(50, 10), Vector2(50, 80), Vector2(140, 80), Vector2(140, 10), Vector2(50, 10)], RED)

    # Diamond
    franco_draw_polygon([Vector2(130, 100), Vector2(150, 160), Vector2(100, 160), Vector2(80, 100), Vector2(130, 100)], GREEN)

    # Polygon with assorted points
    franco_draw_polygon([
        Vector2(160, 10), Vector2(150, 70), Vector2(170, 90), Vector2(140, 230),
        Vector2(180, 200), Vector2(210, 210), Vector2(250, 190), Vector2(300, 120),
        Vector2(260, 80), Vector2(260, 10), Vector2(160, 10)
    ], BLUE)

    # F
    franco_draw_polygon([
        Vector2(10, 105), Vector2(10, 220), Vector2(30, 220),
        Vector2(30, 170), Vector2(60, 170), Vector2(60, 150),
        Vector2(30, 150), Vector2(30, 130), Vector2(70, 130),
        Vector2(70, 105), Vector2(10, 105)
    ], YELLOW)

    # Star
    # NOTE Godot does not draw it.
    franco_draw_polygon([
        Vector2(80, 180), Vector2(50, 230), Vector2(120, 200),
        Vector2(40, 200), Vector2(110, 230), Vector2(80, 180)
    ], MAGENTA)
const WIDTH = 320
const HEIGHT = 240
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const YELLOW = "yellow"
const MAGENTA = "magenta"


let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
document.title = "Hello, my name is Franco!"


function franco_draw_polygon(points, color) {
    console.assert(points.length >= 2)

    // context.strokeStyle = color
    context.fillStyle = color

    context.beginPath()
    context.moveTo(points[0][0], points[0][1])

    let length = points.length
    for (let index = 1; index < length; ++index) {
        let next_point = points[index]
        context.lineTo(next_point[0], next_point[1])
    }

    context.closePath()
    context.fill()
}


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Triangle
    franco_draw_polygon([[10, 40], [40, 40], [40, 10]], WHITE)

    // Rectangle
    franco_draw_polygon([[50, 10], [50, 80], [140, 80], [140, 10]], RED)

    // Diamond
    franco_draw_polygon([[130, 100], [150, 160], [100, 160], [80, 100]], GREEN)

    // Polygon with assorted points
    franco_draw_polygon([
        [160, 10], [150, 70], [170, 90], [140, 230],
        [180, 200], [210, 210], [250, 190], [300, 120],
        [260, 80], [260, 10]
    ], BLUE)

    // F
    franco_draw_polygon([
        [10, 105], [10, 220], [30, 220],
        [30, 170], [60, 170], [60, 150],
        [30, 150], [30, 130], [70, 130],
        [70, 105],
    ], YELLOW)

    // Star
    // NOTE Canvas does not fill it correctly.
    franco_draw_polygon([
        [80, 180], [50, 230], [120, 200],
        [40, 200], [110, 230], [80, 180]
    ], MAGENTA)
}


function main() {
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys

from typing import Final


WIDTH: Final = 320
HEIGHT: Final = 240
BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))


def franco_draw_polygon(points, color):
    assert(len(points) >= 2)

    # 0: fill polygon; > 0: line width.
    pygame.draw.polygon(window, color, points, 0)


def main():
    pygame.display.set_caption("Hello, my name is Franco!")

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

            window.fill(BLACK)

            # Triangle
            franco_draw_polygon([(10, 40), (40, 40), (40, 10)], WHITE)

            # Rectangle
            franco_draw_polygon([(50, 10), (50, 80), (140, 80), (140, 10)], RED)

            # Diamond
            franco_draw_polygon([(130, 100), (150, 160), (100, 160), (80, 100)], GREEN)

            # Polygon with assorted points
            franco_draw_polygon([
                (160, 10), (150, 70), (170, 90), (140, 230),
                (180, 200), (210, 210), (250, 190), (300, 120),
                (260, 80), (260, 10)
            ], BLUE)

            # F
            franco_draw_polygon([
                (10, 105), (10, 220), (30, 220),
                (30, 170), (60, 170), (60, 150),
                (30, 150), (30, 130), (70, 130),
                (70, 105),
            ], YELLOW)

            # Star
            franco_draw_polygon([
                (80, 180), (50, 230), (120, 200),
                (40, 200), (110, 230), (80, 180)
            ], MAGENTA)

            pygame.display.flip()


if (__name__ == "__main__"):
    main()
io.stdout:setvbuf('no')


local WIDTH = 320
local HEIGHT = 240
local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


function franco_draw_polygon(points, color)
    assert(#points >= 2)

    love.graphics.setColor(color)
    love.graphics.polygon("fill", points)
end


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle("Hello, my name is Franco!")
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    -- Triangle
    franco_draw_polygon({10, 40, 40, 40, 40, 10}, WHITE)

    -- Rectangle
    franco_draw_polygon({50, 10, 50, 80, 140, 80, 140, 10}, RED)

    -- Diamond
    franco_draw_polygon({130, 100, 150, 160, 100, 160, 80, 100}, GREEN)

    -- Polygon with assorted points
    franco_draw_polygon({
        160, 10, 150, 70, 170, 90, 140, 230,
        180, 200, 210, 210, 250, 190, 300, 120,
        260, 80, 260, 10
    }, BLUE)

    -- F
    franco_draw_polygon({
        10, 105, 10, 220, 30, 220,
        30, 170, 60, 170, 60, 150,
        30, 150, 30, 130, 70, 130,
        70, 105,
    }, YELLOW)

    -- Star
    -- NOTE LÖVE fills it incorrectly.
    franco_draw_polygon({
        80, 180, 50, 230, 120, 200,
        40, 200, 110, 230, 80, 180
    }, MAGENTA)
end

The following canvas shows the result.

Drawings of polygons filled by drawing primitives. Drawings of polygons using lines connected by a common point. The figure has a white triangle, a red rectangle, a green diamond, a yellow letter F, a polygon with assorted points in blue, and a magenta star. The background is black.

Although the restriction to fill convex polygons is a restriction, it is possible to divide complex shapes into triangles, which can allow the shape to be drawn correctly. This technique will be used in LÖVE during the creation of a library of drawing primitives.

Drawing with Drawing Primitives

At this point, the drawing primitives include resources to create outlines and filled points, lines, polygons (rectangles or defined by points), and ellipses (arbitrary ellipses, circles and arcs). The drawing primitives have been defined at this topic and in Pixels and Drawing Primitives (Points, Lines, and Arcs). Furthermore, it is also possible to write text, as it has been done in Introduction (Window and Hello, world!).

Therefore, now it is possible to draw illustrations that are similar to those that could be created with simple raster graphic editors (such as Microsoft Paint). Thus, with careful observations, some thinking and with creativity, it is already possible to understand how a simple image editor works, as well as to think how to create your own.

In fact, one can combine existing drawing primitives to implement additional features that are provided by Microsoft Paint. For instance:

  1. By alternating continuous lines and spaces, one can create dashed (or dotted) lines;
  2. By drawing small filled circles or points, one can create the spray tool;
  3. By overlapping the drawing of figures (or by drawing an outside contour), it would be possible to create outlines;
  4. By coloring parts of the drawing with the background color, one could implement an eraser;
  5. To write text, one could use the subroutines described in previous topics;
  6. Drawing pixels over a mouse movement, one could create a pencil or painting brush...

From the mentioned items, only the last one requires features that have not yet been discussed in Ideas, Rules, Simulation. Such drawing would require using real time input via a mouse, besides handling clicks.

The moment of exploring input is getting closer; nevertheless, to avoid introducing too many concepts in a single topic, the remainder of this section provides some illustrations created using the drawing primitives explored hitherto. Before that, it is worth defining our own libraries to group subroutines for the drawings.

Creating a Graphics Library with Drawing Primitives

A programming library can combine definitions for data types, values, variables, and subroutines to perform predefined arbitrary processing. Both for the continuity of this topic and for future topics, it would be convenient to group all subroutines created for drawing in a library. This would allow importing the defined code whenever one needed to use it, instead of duplicating it among projects. This promotes good programming practices such as code reuse; you can learn more in Learn Programming: Libraries.

There are two ways to create a library for this topic:

  1. Use the drawing primitives provided by the APIs of each library, framework, or engine;
  2. Use the author's definitions created over the topics.

For a simpler, concise and efficient code, the library will follow the first possibility. However, the definition of a common API for the library (meaning that there is a standard for names and signatures of subroutines, besides behaviors, side effects and expected results), one could alternate between them. In fact, this technique has been presented in Libraries as an Abstraction by Interfaces.

As a project with library can use multiple files, the names for each file of the following example will be defined as franco_graphics.{EXTENSION}. Thus:

  • GDScript: franco_graphics.gd;
  • JavaScript: franco_graphics.js or franco_graphics.mjs. The choice of the name depends on the chosen approach for the library (global or module). Some interpreters require the use of .mjs as an extension for modules, yet others do not;
  • Python: franco_graphics.py;
  • Lua: franco_graphics.lua.

As the GDScript version can be created using a conventional Node, it no longer will be necessary to prefix subroutines with franco_. Thus, for instance, franco_draw_pixel() can be renamed to draw_pixel().

To avoid duplicating code, each drawing subroutine has a parameter fill for filling. The default value will be true to create a filled drawing. To draw only the outline, it is sufficient to pass the false during the call.

extends Node
class_name FrancoGraphics


const POINT_COUNT = 100

const BLACK = Color.black
const WHITE = Color.white
const RED = Color.red
const GREEN = Color.green
const BLUE = Color.blue
const CYAN = Color.cyan
const YELLOW = Color.yellow
const MAGENTA = Color.magenta


static func draw_pixel(drawable, x, y, color):
    drawable.draw_primitive(PoolVector2Array([Vector2(x, y)]),
                                    PoolColorArray([color]),
                                    PoolVector2Array())


static func draw_line(drawable, x0, y0, x1, y1, color):
    drawable.draw_line(Vector2(x0, y0), Vector2(x1, y1), color)


static func draw_rectangle(drawable, x, y, width, height, color, fill = true):
    drawable.draw_rect(Rect2(x, y, width, height), color, fill)


static func draw_square(drawable, x, y, side, color, fill = true):
    draw_rectangle(drawable, x, y, side, side, color, fill)


static func draw_polygon(drawable, points, color, fill = true):
    assert(points.size() >= 2)

    if (fill):
        drawable.draw_colored_polygon(PoolVector2Array(points), color)
    else:
        drawable.draw_polyline(PoolVector2Array(points), color)


static func draw_ellipse(drawable, center_x, center_y, x_radius, y_radius, color, fill = true):
    drawable.draw_set_transform(Vector2(center_x, center_y), 0, Vector2(x_radius, y_radius))
    if (fill):
        drawable.draw_circle(Vector2(0.0, 0.0), 1.0, color)
    else:
        drawable.draw_arc(Vector2(0.0, 0.0), 1.0, 0.0, 2.0 * PI, POINT_COUNT, color)

    drawable.draw_set_transform(Vector2(0.0, 0.0), 0, Vector2(1.0, 1.0))


static func draw_arc(drawable, center_x, center_y, radius, start_angle, end_angle, color, fill = true):
    if (fill):
        var center = Vector2(center_x, center_y)
        # <https://docs.godotengine.org/en/latest/tutorials/2d/custom_drawing_in_2d.html>
        var points_arc = PoolVector2Array()
        points_arc.push_back(center)
        for i in range(POINT_COUNT + 1):
            var angle_point = start_angle + i * (end_angle - start_angle) / POINT_COUNT
            points_arc.push_back(center + radius * Vector2(cos(angle_point), sin(angle_point)))

        drawable.draw_colored_polygon(points_arc, color)
    else:
        drawable.draw_arc(Vector2(center_x, center_y),
                radius,
                start_angle, end_angle,
                POINT_COUNT,
                color)


static func draw_circle(drawable, center_x, center_y, radius, color, fill = true):
    if (fill):
        drawable.draw_circle(Vector2(center_x, center_y), radius, color)
    else:
        draw_arc(drawable, center_x, center_y, radius, 0.0, 2.0 * PI, color, false)
const BLACK = "black"
const WHITE = "white"
const RED = "red"
const GREEN = "green"
const BLUE = "blue"
const CYAN = "cyan"
const YELLOW = "yellow"
const MAGENTA = "magenta"


class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
}


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


function fill_or_line_set_color(fill, color) {
    if (fill) {
        context.fillStyle = color
    } else {
        context.strokeStyle = color
    }
}


function fill_or_line_draw(fill) {
    if (fill) {
        context.fill()
    } else {
        context.stroke()
    }
}


function draw_pixel(x, y, color) {
    context.fillStyle = color
    context.fillRect(x, y, 1, 1)
}


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()
}


function draw_rectangle(x, y, width, height, color, fill = true) {
    if (fill) {
        context.fillStyle = color
        context.fillRect(x, y, width, height)
    } else {
        context.strokeStyle = color
        context.strokeRect(x, y, width, height)
    }
}


function draw_square(x, y, side, color, fill = true) {
    draw_rectangle(x, y, side, side, color, fill)
}


function draw_polygon(points, color, fill = true) {
    console.assert(points.length >= 2)

    fill_or_line_set_color(fill, color)

    context.beginPath()
    context.moveTo(points[0].x, points[0].y)

    let length = points.length
    for (let index = 1; index < length; ++index) {
        let next_point = points[index]
        context.lineTo(next_point.x, next_point.y)
    }

    context.closePath()

    fill_or_line_draw(fill)
}


function draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill = true) {
    fill_or_line_set_color(fill, color)

    context.beginPath()
    context.ellipse(center_x, center_y, x_radius, y_radius, 0.0, 0.0, 2.0 * Math.PI)
    context.closePath()

    fill_or_line_draw(fill)
}


function draw_arc(center_x, center_y, radius, start_angle, end_angle, color, fill = true) {
    fill_or_line_set_color(fill, color)

    context.beginPath()
    context.arc(center_x, center_y,
                radius,
                start_angle, end_angle)

    if (!fill) {
        context.stroke()
    }

    context.closePath()

    if (fill) {
        context.fill()
    }
}


function draw_circle(center_x, center_y, radius, color, fill = true) {
    draw_arc(center_x, center_y, radius, 0, 2 * Math.PI, color, fill)
}


export {
    BLACK,
    WHITE,
    RED,
    GREEN,
    BLUE,
    CYAN,
    YELLOW,
    MAGENTA,
    Point,
    canvas,
    context,
    draw_pixel,
    draw_line,
    draw_rectangle,
    draw_square,
    draw_polygon,
    draw_ellipse,
    draw_arc,
    draw_circle,
}
import math
import pygame
import pygame.gfxdraw

from typing import Final


POINT_COUNT: Final = 100

BLACK: Final = pygame.Color("black")
WHITE: Final = pygame.Color("white")
RED: Final = pygame.Color("red")
GREEN: Final = pygame.Color("green")
BLUE: Final = pygame.Color("blue")
CYAN: Final = pygame.Color("cyan")
YELLOW: Final = pygame.Color("yellow")
MAGENTA: Final = pygame.Color("magenta")


window = None


def init(width, height):
    global window

    pygame.init()
    window = pygame.display.set_mode((width, height))

    return window


# 0: fill polygon; > 0: line width.
def fill_or_line(fill):
    if (fill):
        return 0

    return 1


def draw_pixel(x, y, color):
    window.set_at((x, y), color)


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


def draw_rectangle(x, y, width, height, color, fill = True):
    line_width = fill_or_line(fill)
    pygame.draw.rect(window, color, (x, y, width, height), line_width)


def draw_square(x, y, side, color, fill = True):
    draw_rectangle(x, y, side, side, color, fill)


def draw_polygon(points, color, fill = True):
    assert(len(points) >= 2)

    line_width = fill_or_line(fill)
    pygame.draw.polygon(window, color, points, line_width)


def draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill = True):
    if (fill):
        pygame.gfxdraw.filled_ellipse(window, int(center_x), int(center_y), int(x_radius), int(y_radius), color)
    else:
        pygame.gfxdraw.ellipse(window, int(center_x), int(center_y), int(x_radius), int(y_radius), color)


def draw_arc(center_x, center_y, radius, start_angle, end_angle, color, fill = True):
    if (fill):
        # pygame.gfxdraw.pie
        # pygame.gfxdraw.filled_arc
        center = (center_x, center_y)
        points_arc = []
        points_arc.append(center)
        for i in range(POINT_COUNT + 1):
            angle_point = start_angle + i * (end_angle - start_angle) / POINT_COUNT
            points_arc.append((center_x + radius * math.cos(angle_point),
                               center_y + radius * math.sin(angle_point)))

        draw_polygon(points_arc, color, True)
    else:
        pygame.gfxdraw.arc(window,
                           int(center_x), int(center_y),
                           int(radius),
                           int(math.degrees(start_angle)), int(math.degrees(end_angle)),
                           color)


def draw_circle(center_x, center_y, radius, color, fill = True):
    line_width = fill_or_line(fill)
    pygame.draw.circle(window, color, (center_x, center_y), radius, line_width)
local POINT_COUNT = 100

local BLACK = {0.0, 0.0, 0.0}
local WHITE = {1.0, 1.0, 1.0}
local RED = {1.0, 0.0, 0.0}
local GREEN = {0.0, 1.0, 0.0}
local BLUE = {0.0, 0.0, 1.0}
local CYAN = {0.0, 1.0, 1.0}
local YELLOW = {1.0, 1.0, 0.0}
local MAGENTA = {1.0, 0.0, 1.0}


local function Point(x, y)
    return {
        x = x,
        y = y
    }
end


local function fill_or_line(fill)
    if (not fill) then
        return "line"
    end

    return "fill"
end


local function draw_pixel(x, y, color)
    love.graphics.setColor(color)
    love.graphics.points(x, y)
end


local function draw_line(x0, y0, x1, y1, color)
    love.graphics.setColor(color)
    love.graphics.line(x0, y0, x1, y1)
end


-- NOTE It is not possible to use fill = fill or true, because fill == false always
-- would be initialized as true.


local function draw_rectangle(x, y, width, height, color, fill)
    if (fill == nil) then
        fill = true
    end

    love.graphics.setColor(color)
    love.graphics.rectangle(fill_or_line(fill), x, y, width, height)
end


local function draw_square(x, y, side, color, fill)
    draw_rectangle(x, y, side, side, color, fill)
end


local function draw_polygon(points, color, fill)
    assert(#points >= 2)

    if (fill == nil) then
        fill = true
    end

    local points_array = {}
    for _, point in ipairs(points) do
        table.insert(points_array, point.x)
        table.insert(points_array, point.y)
    end

    love.graphics.setColor(color)
    if (not fill) then
        love.graphics.polygon(fill_or_line(fill), points_array)
    elseif (love.math.isConvex(points_array)) then
        love.graphics.polygon("fill", points_array)
    else
        -- NOTE Polygon cannot intersect itself.
        triangles = love.math.triangulate(points_array)
        for _, triangle in ipairs(triangles) do
            love.graphics.polygon("fill", triangle)
        end
    end
end


local function draw_ellipse(center_x, center_y, x_radius, y_radius, color, fill)
    if (fill == nil) then
        fill = true
    end

    love.graphics.setColor(color)
    love.graphics.ellipse(fill_or_line(fill), center_x, center_y, x_radius, y_radius, POINT_COUNT)
end


local function draw_arc(center_x, center_y, radius, start_angle, end_angle, color, fill)
    if (fill == nil) then
        fill = true
    end

    love.graphics.setColor(color)
    love.graphics.arc(fill_or_line(fill), "open",
                      center_x, center_y,
                      radius,
                      start_angle, end_angle,
                      POINT_COUNT)
end


local function draw_circle(center_x, center_y, radius, color, fill)
    if (fill == nil) then
        fill = true
    end

    love.graphics.setColor(color)
    love.graphics.circle(fill_or_line(fill), center_x, center_y, radius)
end


return {
    BLACK = BLACK,
    WHITE = WHITE,
    RED = RED,
    GREEN = GREEN,
    BLUE = BLUE,
    CYAN = CYAN,
    YELLOW = YELLOW,
    MAGENTA = MAGENTA,
    Point = Point,
    canvas = canvas,
    context = context,
    draw_pixel = draw_pixel,
    draw_line = draw_line,
    draw_rectangle = draw_rectangle,
    draw_square = draw_square,
    draw_polygon = draw_polygon,
    draw_ellipse = draw_ellipse,
    draw_arc = draw_arc,
    draw_circle = draw_circle
}

It should be noted that no implementation defines an entry point. The library only provides the definitions and the code do perform some tasks. The code of the program will be defined by the file that imports the library; thus, new files will define the application.

This is not the only possible approach. Higher-level structures, such as frameworks or engines can define the entry point in the definition of the code of the library. In this case, the code of the application under development must follow the conventions of the framework or engine to create the program. This happens in Lua with LÖVE and GDScript with Godot.

Furthermore, most of the code reuses implementations from previous sections and topics, performing adjustments to switch between drawing an outline or filling the shape.

Some implementations define an internal function to set the outline of filling, to avoid duplicating code. Although this is a good practice in general, perhaps the choice may make the code more complex in the examples. Regardless, as the examples have educational purposes, refactoring the common code illustrates the (good) practice.

Besides, each programming language has its own conventions for libraries. In some cases, they may exist several alternatives. You can learn more about them in Learn Programming: Libraries.

The next paragraphs describe particularities of each implementation. To make the libraries easier to use, one window is assumed per program. Thus, the variable used to handle the window can be global. For a more generic implementation, the window (or the abstraction for drawing on the screen, such as the context) could be passed as a parameter -- as done in GDScript.

In the GDScript version, the use of class_name allows defining a name of the generated class (the file). This name can be used to import the code. To avoid the necessity of instancing an object of the class, all subroutines have been declared as static. In OOP, a static method is a class method, that is, one that does not depend on an instance (an object) to use it. Moreover, all subroutines include a drawable parameter, because drawing operations require a Node that allows using _draw() (for instance, Control). This Node will need to be declared in the file that calls the code; to use it, self can be passed as parameter (this will be performed in the next section).

Alternatively, one could create a drawable variable and a init() procedure, as it has been done in Python. In this case, the subroutines could not be static, and it would be required to operate with an instance of FrancoGraphics, created with FrancoGraphics.new().

In Python, a function init() (from initialize or set-up) to start the window. The function uses global window to inform the interpreter that window is a variable that has been declared outside the subroutine, with global scope (for the library, called a module). init() will be called in the application code to create the PyGame window (which requires the size). Alternatively, one could do as in GDScript: each subroutine could have a window parameter, provided by the code that used the library.

In the JavaScript version, it is important noticing the use of export at the end of the file to export the declarations of variables, constants, data types and subroutines that will be provided by the library. Only features marked with export will be provided for the application code. Thus, internal definitions (such as fill_or_line_set_color() and fill_or_line_draw()) do not need to be exported; they will a mere implementation detail. Once again, an alternative implementation could do as in GDScript: each subroutine could have a window parameter, provided by the code that used the library.

Something similar happens in Lua: all definitions are marked as local. The features that should be exported are returned (return) as a table at the end of the file. LÖVE does not require a parameter for the window because it assumes that a single window exists per application.

Furthermore, the implementation in Lua with LÖVE shows the use of triangulation to decompose the drawing of the letter F in triangles. This is done using love.math.triangulate(). After decomposing the shape, each triangle is drawn individually using love.graphics.polygon() using a loop.

Thus, from a computational perspective, the basic features are implemented in all languages, and available in a Public API. The API can be used for the creation of drawings. In the future, it could be improved. For instance, for more sophisticated drawings, it would be interesting to rotate the primitives (for instance, to draw inclined ellipses); at this time, this can only be approximated by drawing hand-made polygons.

From an artistic perspective, the quality will depend on the abilities, creativity, and aesthetic sense of the person who creates the drawings.

Example of How to Use the Libraries

After defined, one can import or load libraries whenever she/he wishes to use the created code. In other words, it will not be necessary to retype the code (or copy and paste implementations); hereafter, it will be enough to load the implementation from the files defining the libraries.

The next code blocks load libraries to use them. Thus, except for Lua with LÖVE (main.lua), you can choose the names of the files according to your own preference. For instance:

  • GDScript: main.gd, script.gd, or other name;
  • JavaScript: main.js, script.js, or other name;
  • Python: main.js, script.js, or other name;
  • Lua: main.js (required for the entry point).

To use the library, the implementation for each language should use its respective franco_graphics.{EXTENSION}. Thus, for instance, the Lua version will use franco_graphics.lua.

In GDScript and JavaScript, the use of the libraries will be slightly more complex than in Python and Lua. Thus, if you prefer and find it hard to use the libraries correctly at this time, you can copy and paste the code of the files (GDScript) franco_graphics.gd in main.gd, and (JavaScript) franco_graphics.js in main.js. To use them correctly, keep reading the text in this section.

To illustrate how to use each subroutine, drawings of outlines are made inside (or above) the filled version.

# Root must be a Node that allows drawing using _draw().
extends Control


const TITLE = "Hello, my name is Franco!"
const WIDTH = 320
const HEIGHT = 240


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title(TITLE)


func _draw():
    VisualServer.set_default_clear_color(FrancoGraphics.BLACK)

    for x in range(0, 20, 2):
        FrancoGraphics.draw_pixel(self, x, 10, FrancoGraphics.WHITE)

    FrancoGraphics.draw_line(self, 30, 10, 60, 10, FrancoGraphics.BLUE)

    FrancoGraphics.draw_rectangle(self, 10, 20, 100, 20, FrancoGraphics.RED)
    FrancoGraphics.draw_rectangle(self, 12, 22, 96, 16, FrancoGraphics.WHITE, false)

    FrancoGraphics.draw_square(self, 20, 50, 22, FrancoGraphics.GREEN)
    FrancoGraphics.draw_square(self, 22, 52, 18, FrancoGraphics.BLACK, false)

    var polygon = [
        Vector2(10, 105), Vector2(10, 220), Vector2(30, 220),
        Vector2(30, 170), Vector2(60, 170), Vector2(60, 150),
        Vector2(30, 150), Vector2(30, 130), Vector2(70, 130),
        Vector2(70, 105), Vector2(10, 105)
    ]
    FrancoGraphics.draw_polygon(self, polygon, FrancoGraphics.BLUE)
    FrancoGraphics.draw_polygon(self, polygon, FrancoGraphics.YELLOW, false)

    FrancoGraphics.draw_ellipse(self, 160, 25, 15, 20, FrancoGraphics.BLUE)
    FrancoGraphics.draw_ellipse(self, 160, 25, 10, 15, FrancoGraphics.GREEN, false)

    FrancoGraphics.draw_arc(self, 160, 120, 20, 0.0, PI, FrancoGraphics.BLUE)
    FrancoGraphics.draw_arc(self, 160, 120, 10, 0.0, PI, FrancoGraphics.CYAN, false)

    FrancoGraphics.draw_circle(self, 160, 80, 20, FrancoGraphics.WHITE)
    FrancoGraphics.draw_circle(self, 160, 80, 16, FrancoGraphics.RED, false)
import {
    BLACK,
    WHITE,
    RED,
    GREEN,
    BLUE,
    CYAN,
    YELLOW,
    MAGENTA,
    Point,
    canvas,
    context,
    draw_pixel,
    draw_line,
    draw_rectangle,
    draw_square,
    draw_polygon,
    draw_ellipse,
    draw_arc,
    draw_circle,
} from "./franco_graphics.js"


const TITLE = "Hello, my name is Franco!"
const WIDTH = 320
const HEIGHT = 240


function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = BLACK
    context.fillRect(0, 0, WIDTH, HEIGHT)

    for (let x = 0; x < 20; x += 2) {
        draw_pixel(x, 10, WHITE)
    }

    draw_line(30, 10, 60, 10, BLUE)

    draw_rectangle(10, 20, 100, 20, RED)
    draw_rectangle(12, 22, 96, 16, WHITE, false)

    draw_square(20, 50, 22, GREEN)
    draw_square(22, 52, 18, BLACK, false)

    let polygon = [
        new Point(10, 105), new Point(10, 220), new Point(30, 220),
        new Point(30, 170), new Point(60, 170), new Point(60, 150),
        new Point(30, 150), new Point(30, 130), new Point(70, 130),
        new Point(70, 105), new Point(10, 105)
    ]
    draw_polygon(polygon, BLUE)
    draw_polygon(polygon, YELLOW, false)

    draw_ellipse(160, 25, 15, 20, BLUE)
    draw_ellipse(160, 25, 10, 15, GREEN, false)

    draw_arc(160, 120, 20, 0.0, Math.PI, BLUE)
    draw_arc(160, 120, 10, 0.0, Math.PI, CYAN, false)

    draw_circle(160, 80, 20, WHITE)
    draw_circle(160, 80, 16, RED, false)
}


function main() {
    document.title = TITLE
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}


main()
import math
import pygame
import sys
from typing import Final

import franco_graphics


TITLE: Final = "Hello, my name is Franco!"
WIDTH: Final = 320
HEIGHT: Final = 240


def draw():
    franco_graphics.window.fill(franco_graphics.BLACK)

    for x in range(0, 20, 2):
        franco_graphics.draw_pixel(x, 10, franco_graphics.WHITE)

    franco_graphics.draw_line(30, 10, 60, 10, franco_graphics.BLUE)

    franco_graphics.draw_rectangle(10, 20, 100, 20, franco_graphics.RED)
    franco_graphics.draw_rectangle(12, 22, 96, 16, franco_graphics.WHITE, False)

    franco_graphics.draw_square(20, 50, 22, franco_graphics.GREEN)
    franco_graphics.draw_square(22, 52, 18, franco_graphics.BLACK, False)

    polygon = [
        (10, 105), (10, 220), (30, 220),
        (30, 170), (60, 170), (60, 150),
        (30, 150), (30, 130), (70, 130),
        (70, 105),
    ]
    franco_graphics.draw_polygon(polygon, franco_graphics.BLUE)
    franco_graphics.draw_polygon(polygon, franco_graphics.YELLOW, False)

    franco_graphics.draw_ellipse(160, 25, 15, 20, franco_graphics.BLUE)
    franco_graphics.draw_ellipse(160, 25, 10, 15, franco_graphics.GREEN, False)

    franco_graphics.draw_arc(160, 120, 20, 0.0, math.pi, franco_graphics.BLUE)
    franco_graphics.draw_arc(160, 120, 10, 0.0, math.pi, franco_graphics.CYAN, False)

    franco_graphics.draw_circle(160, 80, 20, franco_graphics.WHITE)
    franco_graphics.draw_circle(160, 80, 16, franco_graphics.RED, False)

    pygame.display.flip()


def main():
    franco_graphics.init(WIDTH, HEIGHT)

    pygame.display.set_caption(TITLE)

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

            draw()


if (__name__ == "__main__"):
    main()
local franco_graphics = require("franco_graphics")
local BLACK = franco_graphics.BLACK
local WHITE = franco_graphics.WHITE
local RED = franco_graphics.RED
local GREEN = franco_graphics.GREEN
local BLUE = franco_graphics.BLUE
local CYAN = franco_graphics.CYAN
local YELLOW = franco_graphics.YELLOW
local MAGENTA = franco_graphics.MAGENTA
local Point = franco_graphics.Point
local draw_pixel = franco_graphics.draw_pixel
local draw_line = franco_graphics.draw_line
local draw_rectangle = franco_graphics.draw_rectangle
local draw_square = franco_graphics.draw_square
local draw_polygon = franco_graphics.draw_polygon
local draw_ellipse = franco_graphics.draw_ellipse
local draw_arc = franco_graphics.draw_arc
local draw_circle = franco_graphics.draw_circle


io.stdout:setvbuf('no')


local TITLE = "Hello, my name is Franco!"
local WIDTH = 320
local HEIGHT = 240


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle(TITLE)
end


function love.draw()
    love.graphics.setBackgroundColor(BLACK)

    for x = 0, 19, 2 do
        draw_pixel(x, 10, WHITE)
    end

    draw_line(30, 10, 60, 10, BLUE)

    draw_rectangle(10, 20, 100, 20, RED)
    draw_rectangle(12, 22, 96, 16, WHITE, false)

    draw_square(20, 50, 22, GREEN)
    draw_square(22, 52, 18, BLACK, false)

    local polygon = {
        Point(10, 105), Point(10, 220), Point(30, 220),
        Point(30, 170), Point(60, 170), Point(60, 150),
        Point(30, 150), Point(30, 130), Point(70, 130),
        Point(70, 105)--, Point(10, 105)
    }
    draw_polygon(polygon, BLUE)
    draw_polygon(polygon, YELLOW, false)

    draw_ellipse(160, 25, 15, 20, BLUE)
    draw_ellipse(160, 25, 10, 15, GREEN, false)

    draw_arc(160, 120, 20, 0.0, math.pi, BLUE)
    draw_arc(160, 120, 10, 0.0, math.pi, CYAN, false)

    draw_circle(160, 80, 20, WHITE)
    draw_circle(160, 80, 16, RED, false)
end

The following canvas shows the result.

Drawings of the drawing primitives that have been created to test the library. The image contains lines, rectangles, squares, a polygon forming the letter F, arcs, circles, and ellipses. The shapes draw an outline above the filled version.

Some implementations store references to features exported from the library in variables. This reduces the need to type the name used when importing the library. Therefore, for instance, Lua could use franco_graphics.draw_pixel(), as it has been done in Python.

The choice of alternating strategies among the different implementations serves to highlight different possibilities; indeed, it takes significant writing efforts to write franco_graphics before each use of the library. The adoption of a name such as fg or fgg (or as an alias during the import) could be shorter, easier, and faster to write. The use of an alias during import is explained in Learn Programming: Libraries.

Nevertheless, the drawings of individual pixels illustrate how to create a dashed line. In this case, only the even indices have been drawn to alternate between the background color and the chosen color for the pixel. For a dotted line, the same strategy could be explored using circles.

Additional Configuration for JavaScript

The JavaScript version will require an HTML file that loads the code of the library. It is important noticing the paths for the files and the use of type=module.

<!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;">
      >
          Accessibility: alternative text for graphical content.
      </canvas>
    </div>

    <!-- NOTE Updated with the name of the JavaScript files. -->
    <script src="/franco_graphics.js" type="module"></script>
    <script src="./script.js" type="module"></script>
  </body>
</html>

Still, that will not be sufficient. To use the program, it will be necessary to start a local Web server (or host the content in a remote server). This is detailed in Learn Programming: Libraries.

If you have the Python interpreter installed in your machine, a simple way to start a local Web server consists of browsing to the directory of the files, open a command line interpreter, and use the following command:

python -m http.server 8080 --bind 127.0.0.1 --directory ./

In general, an IP address has the format host:port. localhost corresponds to IPv4 address 127.0.0.1. It is known as the local server of the machine -- instead of being on the Internet or in an external network, the network is the machine itself. The value 8080 can be modified; it is called port (port).

Now you will be able to use the following address in your browser to access the created page: http://localhost:8080/ (if your page is called index.html) or http://localhost:8080/file_name.html (for any other name).

Creating Simple Illustrations

As the drawing library is now ready for use, the next topics can import it whenever it becomes necessary to draw with drawing primitives. Thus, the next examples omit the code of the library and focus on the code of the application. This is similar to what has always been done with libraries for Math, for instance.

Furthermore, the creation of a library of your own has an additional benefit. As the library is your, you can continue improving it: all you have to do is adding new features or improving existing ones. In fact, each new subroutine added to the library will be it more complete and versatile. For instance, the creation of a new subroutine such as draw_background_color() or clear_screen() could fill the background with a color to clear it.

Regardless, now it is time to let your creativity flow.

# Root must be a Node that allows drawing using _draw().
extends Control


const TITLE = "www.FrancoGarcia.com"
const WIDTH = 320
const HEIGHT = 240


func _ready():
    OS.set_window_size(Vector2(WIDTH, HEIGHT))
    OS.set_window_title(TITLE)


func _draw():
    var BROWN = Color.brown
    var WEB_MAROON = Color.webmaroon
    var SKY_BLUE = Color.skyblue
    var SUNRISE = Color("#F7CD5D")

    VisualServer.set_default_clear_color(SKY_BLUE)

    # Sun
    FrancoGraphics.draw_circle(self, 0.95 * WIDTH, 0.1 * HEIGHT, 0.15 * WIDTH, SUNRISE)

    # Hair
    FrancoGraphics.draw_rectangle(self, 0.2 * WIDTH, 0.3 * HEIGHT, 0.6 * WIDTH, 0.9 * HEIGHT, FrancoGraphics.BLACK)

    # Neck
    FrancoGraphics.draw_rectangle(self, 0.4 * WIDTH, 0.8 * HEIGHT, 0.2 * WIDTH, 0.2 * HEIGHT, BROWN)

    # Head
    FrancoGraphics.draw_ellipse(self, 0.5 * WIDTH, 0.5 * HEIGHT, 0.26 * WIDTH, 0.35 * WIDTH, BROWN)

    # Left eyebrown
    FrancoGraphics.draw_rectangle(self, 0.3 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, FrancoGraphics.BLACK)
    # Left eye
    FrancoGraphics.draw_ellipse(self, 0.37 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, FrancoGraphics.WHITE)
    FrancoGraphics.draw_circle(self, 0.38 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, FrancoGraphics.BLACK)

    # Right eyebrown
    FrancoGraphics.draw_rectangle(self, 0.55 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, FrancoGraphics.BLACK)
    # Right eye
    FrancoGraphics.draw_ellipse(self, 0.63 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, FrancoGraphics.WHITE)
    FrancoGraphics.draw_circle(self, 0.62 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, FrancoGraphics.BLACK)

    # Nose
    FrancoGraphics.draw_polygon(self, [
        Vector2(0.5 * WIDTH, 0.5 * HEIGHT),
        Vector2(0.4 * WIDTH, 0.65 * HEIGHT),
        Vector2(0.6 * WIDTH, 0.65 * HEIGHT),
        Vector2(0.5 * WIDTH, 0.5 * HEIGHT),
    ], WEB_MAROON)

    # Mouth
    FrancoGraphics.draw_arc(self, 0.5 * WIDTH, 0.7 * HEIGHT, 0.1 * WIDTH, 0.0, PI, FrancoGraphics.RED)
    FrancoGraphics.draw_arc(self, 0.5 * WIDTH, 0.72 * HEIGHT, 0.07 * WIDTH, 0.0, PI, FrancoGraphics.WHITE)

    # Chin
    FrancoGraphics.draw_arc(self, 0.5 * WIDTH, 0.82 * HEIGHT, 0.1 * WIDTH, 0.25 * PI, 0.75 * PI, WEB_MAROON, false)

    # Hair (over the head)
    FrancoGraphics.draw_arc(self, 0.5 * WIDTH, 0.3 * HEIGHT, 0.3 * WIDTH, PI, 2.0 * PI, FrancoGraphics.BLACK)
import {
    BLACK,
    WHITE,
    RED,
    GREEN,
    BLUE,
    CYAN,
    YELLOW,
    MAGENTA,
    Point,
    canvas,
    context,
    draw_pixel,
    draw_line,
    draw_rectangle,
    draw_square,
    draw_polygon,
    draw_ellipse,
    draw_arc,
    draw_circle,
} from "./franco_graphics.js"


const TITLE = "www.FrancoGarcia.com"
const WIDTH = 320
const HEIGHT = 240


function draw() {
    var BROWN = "brown"
    var WEB_MAROON = "maroon"
    var SKY_BLUE = "skyblue"
    var SUNRISE = "#F7CD5D"

    context.clearRect(0, 0, WIDTH, HEIGHT)
    context.fillStyle = SKY_BLUE
    context.fillRect(0, 0, WIDTH, HEIGHT)

    // Sun
    draw_circle(0.95 * WIDTH, 0.1 * HEIGHT, 0.15 * WIDTH, SUNRISE)

    // Hair
    draw_rectangle(0.2 * WIDTH, 0.3 * HEIGHT, 0.6 * WIDTH, 0.9 * HEIGHT, BLACK)

    // Neck
    draw_rectangle(0.4 * WIDTH, 0.8 * HEIGHT, 0.2 * WIDTH, 0.2 * HEIGHT, BROWN)

    // Head
    draw_ellipse(0.5 * WIDTH, 0.5 * HEIGHT, 0.26 * WIDTH, 0.35 * WIDTH, BROWN)

    // Left eyebrown
    draw_rectangle(0.3 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, BLACK)
    // Left eye
    draw_ellipse(0.37 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, WHITE)
    draw_circle(0.38 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, BLACK)

    // Right eyebrown
    draw_rectangle(0.55 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, BLACK)
    // Right eye
    draw_ellipse(0.63 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, WHITE)
    draw_circle(0.62 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, BLACK)

    // Nose
    draw_polygon([
        new Point(0.5 * WIDTH, 0.5 * HEIGHT),
        new Point(0.4 * WIDTH, 0.65 * HEIGHT),
        new Point(0.6 * WIDTH, 0.65 * HEIGHT),
        new Point(0.5 * WIDTH, 0.5 * HEIGHT),
    ], WEB_MAROON)

    // Mouth
    draw_arc(0.5 * WIDTH, 0.7 * HEIGHT, 0.1 * WIDTH, 0.0, Math.PI, RED)
    draw_arc(0.5 * WIDTH, 0.72 * HEIGHT, 0.07 * WIDTH, 0.0, Math.PI, WHITE)

    // Chin
    draw_arc(0.5 * WIDTH, 0.82 * HEIGHT, 0.1 * WIDTH, 0.25 * Math.PI, 0.75 * Math.PI, WEB_MAROON, false)

    // Hair (over the head)
    draw_arc(0.5 * WIDTH, 0.3 * HEIGHT, 0.3 * WIDTH, Math.PI, 2.0 * Math.PI, BLACK)
}


function main() {
    document.title = TITLE
    canvas.width = WIDTH
    canvas.height = HEIGHT

    draw()
}

main()
import math
import pygame
import sys
from typing import Final

import franco_graphics


TITLE: Final = "www.FrancoGarcia.com"
WIDTH: Final = 320
HEIGHT: Final = 240


def draw():
    BROWN = pygame.Color("brown")
    WEB_MAROON = pygame.Color("maroon")
    SKY_BLUE = pygame.Color("skyblue")
    SUNRISE = pygame.Color("#F7CD5D")

    franco_graphics.window.fill(SKY_BLUE)

    # Sun
    franco_graphics.draw_circle(0.95 * WIDTH, 0.1 * HEIGHT, 0.15 * WIDTH, SUNRISE)

    # Hair
    franco_graphics.draw_rectangle(0.2 * WIDTH, 0.3 * HEIGHT, 0.6 * WIDTH, 0.9 * HEIGHT, franco_graphics.BLACK)

    # Neck
    franco_graphics.draw_rectangle(0.4 * WIDTH, 0.8 * HEIGHT, 0.2 * WIDTH, 0.2 * HEIGHT, BROWN)

    # Head
    franco_graphics.draw_ellipse(0.5 * WIDTH, 0.5 * HEIGHT, 0.26 * WIDTH, 0.35 * WIDTH, BROWN)

    # Left eyebrown
    franco_graphics.draw_rectangle(0.3 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, franco_graphics.BLACK)
    # Left eye
    franco_graphics.draw_ellipse(0.37 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, franco_graphics.WHITE)
    franco_graphics.draw_circle(0.38 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, franco_graphics.BLACK)

    # Right eyebrown
    franco_graphics.draw_rectangle(0.55 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, franco_graphics.BLACK)
    # Right eye
    franco_graphics.draw_ellipse(0.63 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, franco_graphics.WHITE)
    franco_graphics.draw_circle(0.62 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, franco_graphics.BLACK)

    # Nose
    franco_graphics.draw_polygon([
        (0.5 * WIDTH, 0.5 * HEIGHT),
        (0.4 * WIDTH, 0.65 * HEIGHT),
        (0.6 * WIDTH, 0.65 * HEIGHT),
        (0.5 * WIDTH, 0.5 * HEIGHT),
    ], WEB_MAROON)

    # Mouth
    franco_graphics.draw_arc(0.5 * WIDTH, 0.7 * HEIGHT, 0.1 * WIDTH, 0.0, math.pi, franco_graphics.RED)
    franco_graphics.draw_arc(0.5 * WIDTH, 0.72 * HEIGHT, 0.07 * WIDTH, 0.0, math.pi, franco_graphics.WHITE)

    # Chin
    franco_graphics.draw_arc(0.5 * WIDTH, 0.82 * HEIGHT, 0.1 * WIDTH, 0.25 * math.pi, 0.75 * math.pi, WEB_MAROON, False)

    # Hair (over the head)
    franco_graphics.draw_arc(0.5 * WIDTH, 0.3 * HEIGHT, 0.3 * WIDTH, math.pi, 2.0 * math.pi, franco_graphics.BLACK)

    pygame.display.flip()


def main():
    franco_graphics.init(WIDTH, HEIGHT)

    pygame.display.set_caption(TITLE)

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

            draw()


if (__name__ == "__main__"):
    main()
local franco_graphics = require("franco_graphics")


io.stdout:setvbuf('no')


local TITLE = "www.FrancoGarcia.com"
local WIDTH = 320
local HEIGHT = 240


function love.load()
    love.window.setMode(WIDTH, HEIGHT)
    love.window.setTitle(TITLE)
end


function love.draw()
    local BROWN = {165 / 255.0, 42 / 255.0, 42 / 255.0}
    local WEB_MAROON = {128 / 255.0, 0.0, 0.0}
    local SKY_BLUE = {135 / 255.0, 206 / 255.0, 235 / 255.0}
    local SUNRISE = {247 / 255.0, 205 / 255.0, 93 / 255.0}

    love.graphics.setBackgroundColor(SKY_BLUE)

    -- Sun
    franco_graphics.draw_circle(0.95 * WIDTH, 0.1 * HEIGHT, 0.15 * WIDTH, SUNRISE)

    -- Hair
    franco_graphics.draw_rectangle(0.2 * WIDTH, 0.3 * HEIGHT, 0.6 * WIDTH, 0.9 * HEIGHT, franco_graphics.BLACK)

    -- Neck
    franco_graphics.draw_rectangle(0.4 * WIDTH, 0.8 * HEIGHT, 0.2 * WIDTH, 0.2 * HEIGHT, BROWN)

    -- Head
    franco_graphics.draw_ellipse(0.5 * WIDTH, 0.5 * HEIGHT, 0.26 * WIDTH, 0.35 * WIDTH, BROWN)

    -- Left eyebrown
    franco_graphics.draw_rectangle(0.3 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, franco_graphics.BLACK)
    -- Left eye
    franco_graphics.draw_ellipse(0.37 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, franco_graphics.WHITE)
    franco_graphics.draw_circle(0.38 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, franco_graphics.BLACK)

    -- Right eyebrown
    franco_graphics.draw_rectangle(0.55 * WIDTH, 0.4 * HEIGHT, 0.15 * WIDTH, 0.02 * HEIGHT, franco_graphics.BLACK)
    -- Right eye
    franco_graphics.draw_ellipse(0.63 * WIDTH, 0.5 * HEIGHT, 0.065 * WIDTH, 0.05 * WIDTH, franco_graphics.WHITE)
    franco_graphics.draw_circle(0.62 * WIDTH, 0.5 * HEIGHT, 0.03 * WIDTH, franco_graphics.BLACK)

    -- Nose
    franco_graphics.draw_polygon({
        franco_graphics.Point(0.5 * WIDTH, 0.5 * HEIGHT),
        franco_graphics.Point(0.4 * WIDTH, 0.65 * HEIGHT),
        franco_graphics.Point(0.6 * WIDTH, 0.65 * HEIGHT),
        franco_graphics.Point(0.5 * WIDTH, 0.5 * HEIGHT),
    }, WEB_MAROON)

    -- Mouth
    franco_graphics.draw_arc(0.5 * WIDTH, 0.7 * HEIGHT, 0.1 * WIDTH, 0.0, math.pi, franco_graphics.RED)
    franco_graphics.draw_arc(0.5 * WIDTH, 0.72 * HEIGHT, 0.07 * WIDTH, 0.0, math.pi, franco_graphics.WHITE)

    -- Chin
    franco_graphics.draw_arc(0.5 * WIDTH, 0.82 * HEIGHT, 0.1 * WIDTH, 0.25 * math.pi, 0.75 * math.pi, WEB_MAROON, false)

    -- Hair (over the head)
    franco_graphics.draw_arc(0.5 * WIDTH, 0.3 * HEIGHT, 0.3 * WIDTH, math.pi, 2.0 * math.pi, franco_graphics.BLACK)
end

The following canvas shows the result.

A face drawn using drawing primitives. The drawing illustrates a woman with brown skin, dark hair, dark eyes. The image background has a blue sky with a yellow Sun. The image is defined by circles, ellipses, rectangles, arcs and polygons (triangles).

The result includes the link to this website drawn as text A subroutine to write text could be included as part of the drawing library -- and, in fact, it will be soon.

It is important noticing that images that must appear behind other images must be drawn first. To define arbitrary order for relative positioning of images (in other words, to define which images should appear in front or behind others), it would be possible to implement algorithms to sort drawings (for instance, Z-buffer).

Moreover, the created image is probably one of the best (manual, or not procedural) graphical works of the author. Thus, it is not hard to do better.

You can certainly create more beautiful illustrations with drawing primitives. If you find it hard to work with percentages of width (WIDTH) and height (HEIGHT), you can use specific values for pixels (for instance, (0, 0), (12, 34), and so on). Use your creativity, program your very first artistic image, and share your results with the author and/or with hashtags #IdeasRulesSimulation and #FrancoGarciaCom.

For color codes, you can check this list of colors for the Internet. It has names of colors used by JavaScript, as well as RGB combinations. Another possibility is using tools such as color wheels or color pickers, which have been mentioned in previous topics.

Furthermore, there are computer tools that allow using the mouse to find the RGB values for a cor displayed and selected on the screen. For instance, KDE provides one call KColorChooser; Windows has a similar tool in PowerToys, albeit you will need to install it. In browsers, it is also possible to inspect element (accessible by right-clicking the mouse), and use the dropper icon (color picker tool) to choose a color from the opened page.

For some inspirations, you can check the following illustrations. Try to identify the drawing primitives that have been used (thus, practice your pattern matching skills), and create similar images. This way, you will train your pattern recognition, computational thinking, and programming skills.

A house drawn with drawing primitives. The drawing illustrates a light blue house, with a brown door, two windows with brown frames, red roof, and a tree in the front (brown trunk, green leaves). The image background has a blue sky with a yellow Sun. The image is defined by circles, rectangles, polygons (triangles), and line segments.
A chick drawn with drawing primitives. The drawing illustrates yellow chick, with red beak and dark eyes. The image background is blue. The image is defined by circles, and polygons (triangles).
A cat drawn with drawing primitives. The drawing illustrates light gray cat, with dark eyes, pink nose, pink mouse, and darker gray ears, with details in pink. The image background is blue. The image is defined by circles, ellipses, arcs, polygons (triangles) and line segments.
A dog drawn using drawing primitives. The drawing illustrates a dog in light brown shades, with dark eyes, dark nose, pink tongue, and ears in a darker brown shade. The background of the image is blue. The image is defined by circles, ellipses, rectangles, arcs and line segments.
A pig drawn using drawing primitives. The drawing illustrates a pink pig, with dark pink nose and ears. The image background is blue. The image is defined by circles and ellipses.
A cow drawn using drawing primitives. The image illustrates a light yellow cow, with pink mouth and a red smile. The image is defined by circles, ellipses, and arcs.

The examples shown that, although it is not very convenient to work with drawing primitives directly in the code to create illustrations, it certainly is possible. With minimalist styles, it is possible to create aesthetically simple art -- even by people without much talent and artistic skills, such as the author of this material.

Concepts Covered From Learn Programming

Concepts from Learn Programming covered in this topic:

  1. Entry point;
  2. Output;
  3. Data types;
  4. Variables and constants;
  5. Arithmetic;
  6. Relational operations and comparisons;
  7. Logic operations;
  8. Conditional structures;
  9. Subroutines (functions and procedures);
  10. Repetition structures (or loops);
  11. Collectons: arrays;
  12. Records (Structs);
  13. Libraries.

New Items for Your Inventory

Computational Thinking Abilities:

Tools:

  • Color tables;
  • Color pickers to identify a color on the screen.

Skills:

  • Drawing ellipses;
  • Drawing polygons;
  • Filling circles, ellipses, and polygons;
  • Simple drawings with drawing primitives.

Concepts:

  • Drawing primitives: ellipses;
  • Drawing primitives: polygons;
  • Outlines (contours) and filling.

Programming resources:

  • Drawing (outlines and filling) polygons, circles, and ellipses;
  • Creating simple drawings using drawing primitives.

Practice

To learn programming, deliberate practice must follow the concepts. Try doing the next exercises to practice.

  1. Add a clear_background() subroutine that fills the entire background with a solid color to the graphics library. This will effectively clear the window for next drawings;

  2. Add a draw_text() subroutine that draws the text of a string to the window to the graphics library. Drawing text using APIs ha been previously commented.

    The subroutine must have as parameters: the text to be written, the initial point (x and y), and the desired color. If you need more parameters, you may add them.

    Use the subroutine to sign your artistic creations.

  3. Create your own drawings using the graphics library. Examples of simple drawings (in the style of those created by young children and by the author of this material) include:

    • A building;
    • Car;
    • Forest with some trees (use a repetition structure to draw them);
    • Snowman or snow-woman;
    • Bird (as two arcs);
    • Penguin;
    • Bunny;
    • Panda;
    • Lion;
    • Koala;
    • Face of a teddy bear;
    • Butterfly;
    • Ladybug;
    • Flowers.

    If possible, share your creations with the hashtags #IdeasRulesSimulation and #FrancoGarciaCom to promote them (as well as this material) and also show the author. Contact information are at the end of this page.

  4. Return to the simulation of coins and dice from the previous topic. Create more sophisticated graphics;

  5. Draw a polygon that alternates the colors of the lines using colors that have been predefined in an array. Tip: each line of the polygon can be a pair or points with a color. The second point of the pair will be the first point of the next line (with another color). Use a loop to draw the polygon.

  6. Draw polygons using franco_draw_polygon() or draw_polygon() to practice using arrays. Try to draw letters and geometric shapes. Also try to implement draw_rectangle() using draw_polygon() (to do this, you will need to calculate the other point based on the provided point, and the values for the height and width).

  7. Refactor the previous exercise. Create a record Polygon that stores an array (or a list) of points and an array of colors. Use these values to draw the provided polygon;

  8. What are the advantages of using a library? What are the disadvantages of using one?

    The topic Learn Programming: Libraries can be useful as reference.

Deepening

In Ideas, Rules, Simulation, this section provides complementary content.

Bucket Fill Using Flood Fill Algorithm

The Flood Fill (or Seed Fill) algorithm provides a different approach to fill polygons. Unlike scan-line, flood fill works better in interactive programs, with end-user input. This is because the algorithm requires a point inside a region that one wants to fill. Thus, it is a possible way to implement a bucket fill in graphics editors.

The next canvas provides an implementation of the flood fill algorithm. The tool allows running the algorithm at once, or enable an animation. Warning: the implementation is slow and that is intentional (as it will be explained).

To start filling a shape, you can choose a color. Next, you must click on a pixel of the canvas. The larger the delimited region, the longer will be the time required to fill it. In other words, even if it takes a while, the implementation is working correctly.

Thus, it is interesting to start by clicking on a small region (for instance, the black pupil of an eye or a light pink nostril)

Click on a pixel and wait. The filling will finish... Eventually.


Example of filling polygons using the flood fill algorithm.

The implemented version is the one described in span filling. As the implementation of the tool has not been optimized, it can be noticed that it is slow; the time intervals that seemingly do nothing are checks that do not result on fills. In the version without animation, the filling will be completed after a few seconds. In the animated version, the program may take a few minutes to finish, depending on the complexity and size of the region to be filled. The program seems to start, stop, continue, stop, continue... Until it finishes filling the region.

If you do not like waiting, you can be certain that users of your programs would also like more immediate results. It is worth understanding the problem.

Large Arrays and Algorithmic Complexity (Computational Complexity)

The lack of optimizations is intentional. It has been left as an example of the importance of being careful when working with large data structures and arrays (with thousands of positions). To understand why the algorithm takes a long time to complete, one must learn about algorithmic complexity.

In small parts of the image, such as the pupils, the number of pixels that are verified is small. Thus, even an unoptimized version takes little time to finish.

However, the canvas has dimensions of 320x240 pixels, which result in 76800 pixels. Large regions can have some tens of thousands of pixels, significantly increasing the run-time of the program.

In the specific case of the author's implementation defined for flood fill, as it has not been checked if a point was already inserted or verified, the implementation performs multiple unnecessary checks. The run-time is aggravated in the animated version, that adds small pauses after one of the loops.

Next Steps

Computing is not magic, although it may seem so. As you learn the fundamentals, you will become able to create systems that do the magic. Science for you; magic for the others.

In fact, now you know how simple graphics are created since the very first pixel. We have started from a pixel, and now we have some images... That are pleasant, why not? Perhaps adorable or cute?

Perhaps they impress a child, perhaps that they receive a smile from some readers. Certainly they will not be masterpieces that will dazzle the humanity. At this time, the only prize will be a Cute tag for this topic. Perhaps that adorable was a better term for the tag, yet cute seems to be more casual and appropriate. Definitely cute was not a tag that the author have anticipated as a category for this website. Such is life, full of surprises.

Anyway, as the tag now exists, it has also been applied to Command Line Input. Perhaps that the program bunnysay created in that topic could also be considered cute.

There will come the time when the author reveals his artistic talents. Not by drawing directly, though by writing a computer program to create illustrations. In fact, procedural content generation is approaching after each topic of Ideas, Rules, Simulation.

In the meantime, artistic beauty is your responsibility. Perhaps you are able to create an impressive illustration. Perhaps that your artistic skills are better and more developed than the author's. Thus, create your illustrations to practice using subroutines and libraries. Great artists can perform magic with simple resources. Indeed, there are even people who consider that restrictions and minimalism can enhance the creativity.

Regardless of the case, computation is part science, part art. It is worth practicing both, especially because the creation of images will train your abstraction, representation, modeling, and pattern recognition skills, that are all important for the computational thinking.

Still, perhaps that you create something that you are really proud of; perhaps that you wish to save your creation in a file to share it. Alternatively, perhaps that you wish to create images in advanced graphic editors (or get them from friends or from the Internet), and use them in your program.

All this is possible; in fact, you will become able to do it soon: the next topic introduces matrices and files, using image files as examples.

Furthermore, if you have created a creative or interesting illustration, consider sharing it. Alternatively, if you have found this material useful, you can also share it. If possible, use the hashtags #IdeasRulesSimulation and #FrancoGarciaCom.

I thank you for your attention. See you soon!

Ideas, Rules, Simulation

  1. Motivation;
  2. Introduction: Window and Hello World;
  3. Pixels and drawing primitives (points, lines, and arcs);
  4. Randomness and noise;
  5. Coins and dice, rectangles and squares;
  6. Drawing with drawing primitives (strokes and fillings for circles, ellipses and polygons);
  7. Saving and loading image files;
  8. ...

This material is a work in progress; therefore, if you have arrived early and the previous items do not have links, please return to this page to check out updates.

If you wish to contact me or have any questions, you can chat with me by:

Information about contact and social networks are also available at the footer of every page.

Your opinion about the series will be fundamental to enable me to create a material that is more accessible and simpler for more people.

  • Video
  • Informatics
  • Programming
  • Beginner
  • Computational Thinking
  • Ideas, Rules, Simulation
  • Python
  • PyGame
  • Lua
  • LÖVE (Love2D)
  • Javascript
  • HTML Canvas
  • Godot
  • Gdscript
  • Cute

This is the newest post