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