Ideas, Rules, Simulation: Pixels and Drawing Primitives (Points, Lines, and Arcs)
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:
- GDScript (Godot Engine);
- JavaScript;
- Python. For Python, it is also necessary to install PyGame. Text instructions are available at Libraries: Graphical Interface with Thonny. Video instructions are available in Python on the command line and using Thonny IDE, hosted on YouTube;
- Lua. In Lua, it is also necessary to install LÖVE (Love2D). Video instructions are available in Lua on the command line and using ZeroBraneStudio IDE, hosted on YouTube.
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
This entry has video versions on the author's YouTube channel:
However, this text version is more complete.
Documentation
Practice consulting the documentation:
- GDScript: language documentation;
- JavaScript: language documentation;
- Python: language documentation and documentation for PyGame;
- Lua: language documentation (Lua 5.1) e documentation for LÖVE.
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).
Drawing Primitives
In the first topic of Ideas, Rules, Simulation, we have created a window and wrote text on it. Drawing text is actually relatively complex. The libraries made the operation simple.
If you wanted to create your own implementation, the process would be similar to create a raster that converted text to image files. The difference is that, instead of saving data in a file, the results would be drawn to the screen.
Draw on the screen. After all, what is required to do that?
The smallest unit to draw on the screen is a pixel (px), which is the name of the combination of the terms picture element. However, a pixel is not required to be square; for instance, some technologies define rectangular pixels. In fact, this was common in older screens. For instance, in this list of common resolutions, every value from the Pixel column that does not have a 1:1 proportion use rectangular pixels.
Regardless of the case, a digital image or a monitor combine pixels to generate images. For instance, a monitor with Full HD resolution has 1920 lines and 1080 columns. The product of lines and columns is the number of pixels that can be drawn in the resolution. In other words, the Full HD resolution has pixels. Rounding the value down, this is more than 2 million pixels; if you prefer, about 2.07 MP (Megapixels).
Therefore, the basic element of an image is a pixel. Pixels can also be though as a point in the screen. Drawings are, thus, combinations of points following artistic patterns.
Naturally, it is not convenient to think about patterns of points. To make it easier to operate images, pixels can be combined to draw more complex patterns, such as lines and arcs. Points, lines and arcs are called drawing primitives or graphical primitives.
Primitive means that they serve as the basic elements to create more complex patterns. For instance, combinations of lines can generate rectangles, squares, diamonds, and other polygons. Arcs can generate ellipses, circles, and other forms involving curves. Combinations of lines and arcs allow drawing complex shapes.
Raster Images and Vector Images
Lines and arcs are the base of vector graphics. Vector graphics are mathematical constructs based on vectors. They do not use pixels; they use Math equations. This allows, among other features and with the due care, to resize images without loss of quality.
The creation of images using pixels, on the other hand, uses matrices. Hence, images with pixels are often called raster graphics or bitmaps. It can be problematic to scale raster images up, often resulting in a loss of quality.
A raster graphic can approximate vector shapes using a process called rasterization. As pixels are usually square or rectangular, the rasterization is an imperfect process. For instance, circles will be perfectly curved; they will be approximations drawn as combinations of tens, hundreds or thousands or line segments. With millions of pixels and the use of techniques such as anti-aliasing, it is possible to trick the eyes with convincing approximations. Nevertheless, they are approximations (and the techniques themselves can be interesting for future topics).
To work with digital images, one can use raster or vector graphics. The choice depends on the preferences of the artistic, e/or of the tool (image editor) used to create and image. Traditional examples of tools that work with raster images can include Adobe Photoshop, GIMP and Microsoft Paint; popular formats include BMP, JPEG and PNG. Traditional examples of tools that work with vector graphics can include Adobe Illustrator, Inkscape and CorelDRAW; one of the most traditional formats is SVG.
At first, the examples will use raster images. This choice is convenient, because it allows drawing images directly using pixels. However, Math equations will be implemented to approximate vector values to discrete ones (which will be drawn as pixels). These approximations will result in observable errors in the resulting images.
Coordinate Systems and the Cartesian Coordinate System
Mathematics is essential to work with graphics in minimally complex applications. In particular, trigonometry is important. Vector algebra and linear algebra are also useful. However, in many cases, basic Math suffices.
If you have survived the previous paragraph, it is sufficient knowing that you must not be an expert at Mathematics. In fact, I (the author) will present import concepts briefly as needed.
The first is the Cartesian Plane. In reality, coordinate systems are more important. If you have managed to write text in the previous topic, you practically know everything you will need at this time.
A point in a (two-dimensional or 2D) plane can be represented as ordered pair . The first value, , is called abscissa and represents the value in the X axis (the horizontal axis). The second value, , is called ordinate and represents the value in the Y axis (the vertical axis).
For a tri-dimensional (3D) space, the point would be represented by an ordered triple . The third value, , is called applicate, and represents the value in Z axis (the transversal axis).
The Mathematics to work with 2D is simpler than to work with 3D; a second benefit is that part of the knowledge will also be applicable for 3D. Thus, it is worth starting with two dimensions. Depending on how this series evolves, we could consider the third dimension in the future.
In 2D computer graphics, the origin of the coordinate system, that is, the pair often starts in the top-left corner of the screen. Although there are exceptions, this convention is common.
Therefore, for a pair , positive values for show on the right side of the origin; negative values show on the left side. Similarly, positive values of show below the origin; negative values show above the origin.
In other words, coordinates "grow" to the right and down. They "reduce" to the left and up. This means that the Y axis is inverted in relation to the Cartesian Plane used in school's Math.
If you are using a desktop or a laptop, hove the mouse over the next rectangle.
The ordered pair will be written in the mouse cursor on the canvas
(this means that positions next to the corners will be cut).
If you are using a mobile device (such as a tablet or smartphone), you can touch a position inside the rectangle to write the position inside it.
It is interesting noticing that you can already implement two parts of the previous program: draw the background color and the text.
The previous program monitors the mouse position inside the canvas
, updating the drawing area every time the coordinate change (in other words, whenever the mouse is moved inside the region).
Points and Pixels
The knowledge of how to write an ordered pair is sufficient to start drawing with drawing primitives. It is also worth knowing where the drawing will start; hence the importance of knowing the origin and the direction of the positive values.
Thus, the next step is drawing a pixel on the screen. Some Application Programming Interfaces (APIs) provide subroutines to draw pixels. Others draw a point as a representation of a pixel
If the API does not provide the feature, an alternative is drawing a rectangle. A rectangle with unitary (1) values for width and height generate a square that can serve as a representation for a pixel.
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.
GDScript, JavaScript, Python and Lua
A pixel is small. To make it easier to visualize it, the examples will draw a white pixel on a black background. The contrast will make it easier to identify the pixel on the window.
# Root must be a Node that allows drawing using _draw().
extends Control
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var points = PoolVector2Array([Vector2(10, 20)])
var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
var uvs = PoolVector2Array()
draw_primitive(points, colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "white"
context.fillRect(10, 20, 1, 1)
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
window.set_at((10, 20), (255, 255, 255))
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
love.graphics.setColor(1.0, 1.0, 1.0)
love.graphics.points(10, 20)
end
The message Olá, meu nome é Franco!
is the Portuguese version of Hello, my name is Franco!
.
In all examples, the only requirement is making a subroutine call to draw a pixel in the window.
The simpler implementations are in Python with PyGame and Lua with LÖVE (Love2D).
PyGame provides the method set_at()
to color a pixel at a position.
LÖVE provides love.graphics.points()
to draw one or more pixels at the provided x
and y
positions.
In JavaScript with canvas
, a drawing of a square using fillRect
can simulate drawing a pixel.
In GDScript, draw_primitive()
allows drawing a point, a line, a triangle or a quadrilateral, depending on the number of points provided as the first argument.
The following canvas
shows the result.
The white pixel probably is not a bad pixel in your screen.
If you scroll the page in your browser, the white pixel should move along the black rectangle.
As you can observe in the canvas
, a pixel is small.
If you want to magnify it, operating systems typically provide a program to magnify images on the screen.
The program is an accessibility tool called magnifier or magnifying lens.
For instance, Magnifier for Windows and KMag from KDE.
The magnifier or magnifying lens are useful, for instance, to help people with low vision to see content on the screen.
Anyway, to draw a pixel at another position, the process should be repeated choosing another coordinate for the drawing. If you select the same one, only the last provided value will be considered.
Although it is possible to compose colors using transparency, this usually require using a technique called alpha blending. In fact, alpha blending will be one of the future topics of Ideas, Rules, Simulation.
Lines
A pixel can be imagined as a point. A sequence of continuous points generate a line.
Lines with Points
Strictly speaking, a line must not be a straight line segment. However, when one thinks about lines in drawing APIs, it is common that the definition is a straight line segment.
For instance, drawing a sequence of points (as pixels) such as (10, 20), (11, 20), (12, 20), (13, 40), (14,20), (15,20), (16, 20), (17, 20), (18, 20), (19, 20), and (20, 20) would generate a small horizontal line segment. In other words, a horizontal line.
# Root must be a Node that allows drawing using _draw().
extends Control
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
var uvs = PoolVector2Array()
draw_primitive(PoolVector2Array([Vector2(10, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(11, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(12, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(13, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(14, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(15, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(16, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(17, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(18, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(19, 20)]), colors, uvs)
draw_primitive(PoolVector2Array([Vector2(20, 20)]), colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "white"
context.fillRect(10, 20, 1, 1)
context.fillRect(11, 20, 1, 1)
context.fillRect(12, 20, 1, 1)
context.fillRect(13, 20, 1, 1)
context.fillRect(14, 20, 1, 1)
context.fillRect(15, 20, 1, 1)
context.fillRect(16, 20, 1, 1)
context.fillRect(17, 20, 1, 1)
context.fillRect(18, 20, 1, 1)
context.fillRect(19, 20, 1, 1)
context.fillRect(20, 20, 1, 1)
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
window.set_at((10, 20), (255, 255, 255))
window.set_at((11, 20), (255, 255, 255))
window.set_at((12, 20), (255, 255, 255))
window.set_at((13, 20), (255, 255, 255))
window.set_at((14, 20), (255, 255, 255))
window.set_at((15, 20), (255, 255, 255))
window.set_at((16, 20), (255, 255, 255))
window.set_at((17, 20), (255, 255, 255))
window.set_at((18, 20), (255, 255, 255))
window.set_at((19, 20), (255, 255, 255))
window.set_at((20, 20), (255, 255, 255))
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
love.graphics.setColor(1.0, 1.0, 1.0)
love.graphics.points(10, 20)
love.graphics.points(11, 20)
love.graphics.points(12, 20)
love.graphics.points(13, 20)
love.graphics.points(14, 20)
love.graphics.points(15, 20)
love.graphics.points(16, 20)
love.graphics.points(17, 20)
love.graphics.points(18, 20)
love.graphics.points(19, 20)
love.graphics.points(20, 20)
-- Or:
--[[
love.graphics.points(
10, 20,
11, 20,
12, 20,
13, 20,
14, 20,
15, 20,
16, 20,
17, 20,
18, 20,
19, 20,
20, 20
)
]]--
end
The resulting drawing is displayed in the following canvas
.
To draw a vertical line, it would suffice to keep the values in as constants and choose the values of . For instance, (10, 20), (10, 21), (10, 22), (10, 23), (10, 24), (10, 25), (10, 26), (10, 27), (10, 28), (10, 29), and (10, 30). With a new intermediate horizontal line (for instance, made with the points (10, 25), (11, 25), (12, 25), (13, 25), (14, 25), (15, 25), and (16, 25)), one could make an uppercase F letter.
Finally, changing values of and could make inclined line segments. Depending on the angular coefficient (slope) of the line, the drawing could look "serrated". This is a consequence of the attempt of adapting a mathematical expression using real numbers (a line equation) to a raster image composed of integer numbers.
Before continuing, try to write a letter G or the first letter of your name using points. The task will be repetitive. There is a more convenient way.
Lines with Repetitions of Points
Instead of describing a drawing point by point, a better alternative is letting the computer to perform the repetitive part. To do this, it is sufficient to use a repetition structure (or loops), as previously mentioned in Learn Programming.
For a first example one can use the For statement.
The for
structure has four parts:
- Initialization of a variable used as the condition;
- Stop condition;
- Modification of the conditional variable;
- A block of code to repeat.
To draw a line, the block of code to repeat will be the calls to draw each pixel. The other parts will control the length of the line; each repetition (or iteration) will draw a new pixel on the window.
For the implementation, it should be decided whether the line will include (or not) the last pixel in the final position. This may vary among different drawing APIs. To keep all examples identical, all versions will draw the pixel at the final coordinate.
# Root must be a Node that allows drawing using _draw().
extends Control
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var colors = PoolColorArray([Color(1.0, 1.0, 1.0)])
var uvs = PoolVector2Array()
# F
for i in range(0, 11):
draw_primitive(PoolVector2Array([Vector2(10 + i, 20)]), colors, uvs)
for i in range(0, 11):
draw_primitive(PoolVector2Array([Vector2(10, 20 + i)]), colors, uvs)
for i in range(0, 7):
draw_primitive(PoolVector2Array([Vector2(10 + i, 25)]), colors, uvs)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "white"
// F
for (let i = 0; i <= 10; ++i) {
context.fillRect(10 + i, 20, 1, 1)
}
for (let i = 0; i <= 10; ++i) {
context.fillRect(10, 20 + i, 1, 1)
}
for (let i = 0; i <= 6; ++i) {
context.fillRect(10 + i, 25, 1, 1)
}
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
# F
for i in range(0, 11):
window.set_at((10 + i, 20), (255, 255, 255))
for i in range(0, 11):
window.set_at((10, 20 + i), (255, 255, 255))
for i in range(0, 7):
window.set_at((10 + i, 25), (255, 255, 255))
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
love.graphics.setColor(1.0, 1.0, 1.0)
-- F
for i = 0, 10 do
love.graphics.points(10 + i, 20)
end
for i = 0, 10 do
love.graphics.points(10, 20 + i)
end
for i = 0, 6 do
love.graphics.points(10 + i, 25)
end
end
The new version draws an uppercase F character.
Instead of duplicating calls to draw a pixel, the implementation uses a combination of arithmetic with the i
variable declared for the repetitions to modify the next pixel to paint.
To increase or decrease the size, it would suffice to change the final value defined in the condition of each for
loop.
Try to modify the previous example to draw an uppercase A instead of an F. The A can be rectangular, to make it easier to draw.
_
|_|
| |
The solution should use four loops. It will be similar to the implementation for the F character.
Lines with Subroutines
Besides repetition structures, the solution can be further simplified using a reusable block of code to draw the line. To do this, we will return to Learn Programming: subroutines (functions and procedures).
To draw a horizontal line, an ordinate ( value) could be fixed and vary the abscissa (value of ) using a repetition structure.
For a reusable solution, the created code could be defined as a draw_horizontal_line()
procedure.
To draw a vertical line, the process would be inverted: it keeps a fixed abscissa and vary the ordinate using a repetition structure.
The procedure could be called draw_vertical_line()
.
# Root must be a Node that allows drawing using _draw().
extends Control
func draw_horizontal_line(x0, x1, y, color):
for x in range(x0, x1 + 1):
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func draw_vertical_line(x, y0, y1, color):
for y in range(y0, y1 + 1):
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
# F
draw_horizontal_line(10, 20, 20, Color.white)
draw_vertical_line(10, 20, 30, Color.white)
draw_horizontal_line(10, 16, 25, Color.white)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function draw_horizontal_line(x0, x1, y, color) {
context.fillStyle = color
for (let x = x0; x <= x1; ++x) {
context.fillRect(x, y, 1, 1)
}
}
function draw_vertical_line(x, y0, y1, color) {
context.fillStyle = color
for (let y = y0; y <= y1; ++y) {
context.fillRect(x, y, 1, 1)
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
// F
draw_horizontal_line(10, 20, 20, "white")
draw_vertical_line(10, 20, 30, "white")
draw_horizontal_line(10, 16, 25, "white")
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_horizontal_line(x0, x1, y, color):
for x in range(x0, x1 + 1):
window.set_at((x, y), color)
def draw_vertical_line(x, y0, y1, color):
for y in range(y0, y1 + 1):
window.set_at((x, y), color)
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
# F
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 16, 25, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_horizontal_line(x0, x1, y, color)
love.graphics.setColor(color)
for x = x0, x1 do
love.graphics.points(x, y)
end
end
function draw_vertical_line(x, y0, y1, color)
love.graphics.setColor(color)
for y = y0, y1 do
love.graphics.points(x, y)
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
-- F
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 16, 25, WHITE)
end
The new versions also include variables and constants for colors. The purpose is making the implementations more similar to each other; this allows you identifying how a solution is similar independently of the programming language or library. Once again, fundamentals do not depend on technology (they are technology-agnostic).
With the change, the F character can be written using three lines of code: to call to draw_horizontal_line()
and a call to draw_vertical_line()
.
To draw an A instead of an F, it would be sufficient to add a new call to draw_vertical_line()
in the appropriate position and change the length of the intermediate horizontal line.
In fact, except for the parameter of the color output, every implementation would be virtually equal.
# A
var WHITE = Color.white
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
// A
const WHITE = "white"
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
# A
WHITE = pygame.Color("white")
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
-- A
local WHITE = {1.0, 1.0, 1.0}
draw_horizontal_line(10, 20, 20, WHITE)
draw_vertical_line(10, 20, 30, WHITE)
draw_horizontal_line(10, 20, 25, WHITE)
draw_vertical_line(20, 20, 30, WHITE)
To complete the operations, the next steep would be drawing an inclined line segment.
Instead of a third procedure, all calls could be unified in a single procedure, as a generic draw_line()
.
Unifying the Subroutines to Draw Lines
Mathematically, given two points and , the equation of the line that passes along them can be defined as:
In the equation, is the angular coefficient (slope). is the linear coefficient, which is the value of , or, in other words, the value of for the point with . To calculate , one can choose of the two points and isolate the variable in the line equation.
For the point :
Alternatively, for the point :
Thus, two points and are sufficient to draw a straight line. One can use the provided values to find the equation of the line, and, then, use it to find the intermediate values between and .
For example, a procedure draw_line()
could be defined to draw lines.
It could have as parameters: the initial coordinate (a point), the final coordinate (another point, different from the first), and the color.
For the implementation, it would suffice translating the equation to arithmetic operations.
However, there is a special case to consider. In the case of a vertical line, would be equal to , which would result into a division by zero. To avoid it, a conditional structure can be used, which is explained in Learn Programming.
An if/then/else structure define two possible alternative flows, with one chosen as the result of a logical expression:
- If the expression results
True
, only the code defined for the then-part (then
) is executed; - Otherwise, if the expression results
False
, only the code defined in the else-part (else
) is executed.
The flows are mutually exclusive.
If this is your first contact with a conditional structure, the Lua version is the most illustrative.
Thus, for drawing of a generic line, two scenarios should be considered:
- In the case of a vertical line (that is,
x0 == x1
), the implementation will be identical to the one performed fordraw_vertical_line()
; - For any other case, the line equation will be used. For the drawing, the values of an ascending sequence must be calculated from belonging to the interval (or , depending on the greatest value).
Strictly speaking, there exists a third possible case: if both points are equal, it is not possible to draw a line (the result would be the point itself). You can try implementing it (adding a new conditional structure).
# Root must be a Node that allows drawing using _draw().
extends Control
func franco_draw_line(x0, y0, x1, y1, color):
if (x0 == x1):
for y in range(y0, y1 + 1):
draw_primitive(PoolVector2Array([Vector2(x0, y)]),
PoolColorArray([color]),
PoolVector2Array())
else:
var a = 1.0 * (y1 - y0) / (x1 - x0)
var b = y0 - a * x0
var max_x = max(x0, x1)
var min_x = min(x0, x1)
for x in range(min_x, max_x + 1):
var y = floor(a * x + b)
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var WHITE = Color.white
# F
franco_draw_line(10, 20, 20, 20, WHITE)
franco_draw_line(10, 20, 10, 30, WHITE)
franco_draw_line(10, 25, 16, 25, WHITE)
# Triangle (dashed?)
franco_draw_line(110, 120, 120, 120, WHITE)
franco_draw_line(110, 120, 115, 130, WHITE)
franco_draw_line(115, 130, 120, 120, WHITE)
# Dashed?
franco_draw_line(200, 40, 250, 200, WHITE)
franco_draw_line(250, 40, 260, 200, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function draw_line(x0, y0, x1, y1, color) {
context.fillStyle = color
if (x0 === x1) {
for (let y = y0; y <= y1; ++y) {
context.fillRect(x0, y, 1, 1)
}
} else {
let a = 1.0 * (y1 - y0) / (x1 - x0)
let b = y0 - a * x0
let max_x = Math.max(x0, x1)
let min_x = Math.min(x0, x1)
for (let x = min_x; x <= max_x; ++x) {
let y = Math.floor(a * x + b)
context.fillRect(x, y, 1, 1)
}
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
// Triangle (dashed?)
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
// Dashed?
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_line(x0, y0, x1, y1, color):
if (x0 == x1):
for y in range(y0, y1 + 1):
window.set_at((x0, y), color)
else:
a = 1.0 * (y1 - y0) / (x1 - x0)
b = y0 - a * x0
max_x = max(x0, x1)
min_x = min(x0, x1)
for x in range(min_x, max_x + 1):
y = math.floor(a * x + b)
window.set_at((x, y), color)
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
# Triangle (dashed?)
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
# Dashed?
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
love.graphics.setColor(color)
if (x0 == x1) then
for y = y0, y1 do
love.graphics.points(x0, y)
end
else
local a = 1.0 * (y1 - y0) / (x1 - x0)
local b = y0 - a * x0
local max_x = math.max(x0, x1)
local min_x = math.min(x0, x1)
for x = min_x, max_x do
local y = math.floor(a * x + b)
love.graphics.points(x, y)
end
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
-- F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
-- Triangle (dashed?)
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
-- Dashed?
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
end
The GDScript implementation uses the name franco_draw_line()
because draw_line()
is the name of a method of Control
(inherited from CanvasItem
).
The calls to max()
and min()
in each implementation replaces the use of a conditional structure.
For instance, max_x = max(x0, x1)
would be equivalent to:
max_x = x0
if (x1 > x0):
max_x = x1
The next examples will alternate between ready-to-use function calls or the use of conditional structures (if you need to practice using them).
The result of the JavaScript version for canvas
is displayed as follows.
It can be noted that the inclined lines are not continuous; they are dashed. This happens because an insufficient number of values for where used to approximate a result closer to the real one. In fact, the greater the interval in and the smaller the interval in , the greater will be the distance between the pixels that are drawn.
Furthermore, it is possible to change the solution to make it more computationally efficient. Instead of using the equation to calculate each value of , an accumulator can be applied. For instance, the following Python implementation is equivalent to the previous one, though it uses sums instead of multiplications.
def draw_line(x0, y0, x1, y1, color):
if (x0 == x1):
for y in range(y0, y1 + 1):
window.set_at((x0, y), color)
else:
dx = x1 - x0
dy = y1 - y0
a = 1.0 * dy / dx
repetitions = dx
if (repetitions < 0):
repetitions = -repetitions
x = x0
y = y0
for i in range(repetitions + 1):
window.set_at((int(x), int(y)), color)
x += 1
y += a
In this case, you could substitute the conditional structure to make the value of repetitions
positive by a call to an absolute value function (or modulo; often abs()
).
The calculated values for x
and y
update the previous value with the next expected value.
For instance, x += 1
corresponds to x = x + 1
; in other words, 1 is added to the previous value to update it.
As a multiplication can be calculated as successive sums, the accumulated result of y
corresponds to what would be calculated using the line equation.
Before showing some algorithms to draw lines, think about how you could change the solution to draw a more continuous line. Tip: a simpler way is increasing the number of intermediate points drawn between and when . The drawing will be better the greater the number of points used to approximate the line.
Drawing Continuous Lines
A way to remove the dashes to generate more continuous lines consists on drawing more points when needed. However, there are many ways to do that.
In fact, there are many algorithms to draw lines. Of the simplest is the digital differential analyzer (DDA).
The DDA algorithm is similar to using the line equation when . However, when , it invests the increments: is iterated one by one, and is incremented by . In other words, it is sufficient to add a few conditional structures to the Python's modified version. This way, a more continuous line will result from drawing more intermediate points.
# Root must be a Node that allows drawing using _draw().
extends Control
func franco_draw_line(x0, y0, x1, y1, color):
if (x0 == x1):
for y in range(y0, y1 + 1):
draw_primitive(PoolVector2Array([Vector2(x0, y)]),
PoolColorArray([color]),
PoolVector2Array())
else:
var dx = x1 - x0
var dy = y1 - y0
var a = 1.0 * dy / dx
var increment_x = 1
var increment_y = a
var repetitions = dx
if (a > 0):
increment_x = 1.0 / a
increment_y = 1
repetitions = dy
if (repetitions < 0):
repetitions = -repetitions
var x = x0
var y = y0
for i in range(repetitions + 1):
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
x += increment_x
y += increment_y
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var WHITE = Color.white
# F
franco_draw_line(10, 20, 20, 20, WHITE)
franco_draw_line(10, 20, 10, 30, WHITE)
franco_draw_line(10, 25, 16, 25, WHITE)
# Triangle
franco_draw_line(110, 120, 120, 120, WHITE)
franco_draw_line(110, 120, 115, 130, WHITE)
franco_draw_line(115, 130, 120, 120, WHITE)
# Continuous lines
franco_draw_line(200, 40, 250, 200, WHITE)
franco_draw_line(250, 40, 260, 200, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function draw_line(x0, y0, x1, y1, color) {
context.fillStyle = color
if (x0 === x1) {
for (let y = y0; y <= y1; ++y) {
context.fillRect(x0, y, 1, 1)
}
} else {
let dx = x1 - x0
let dy = y1 - y0
let a = 1.0 * dy / dx
let increment_x = 1
let increment_y = a
let repetitions = dx
if (a > 0) {
increment_x = 1.0 / a
increment_y = 1
repetitions = dy
}
if (repetitions < 0) {
repetitions = -repetitions
}
let x = x0
let y = y0
for (let i = 0; i <= repetitions; ++i) {
context.fillRect(x, y, 1, 1)
x += increment_x
y += increment_y
}
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
// Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
// Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_line(x0, y0, x1, y1, color):
if (x0 == x1):
for y in range(y0, y1 + 1):
window.set_at((x0, y), color)
else:
dx = x1 - x0
dy = y1 - y0
a = 1.0 * dy / dx
increment_x = 1
increment_y = a
repetitions = dx
if (a > 0):
increment_x = 1.0 / a
increment_y = 1
repetitions = dy
if (repetitions < 0):
repetitions = -repetitions
x = x0
y = y0
for i in range(repetitions + 1):
window.set_at((int(x), int(y)), color)
x += increment_x
y += increment_y
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
# Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
# Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
love.graphics.setColor(color)
if (x0 == x1) then
for y = y0, y1 do
love.graphics.points(x0, y)
end
else
local dx = x1 - x0
local dy = y1 - y0
local a = 1.0 * dy / dx
local increment_x = 1
local increment_y = a
local repetitions = dx
if (a > 0) then
increment_x = 1.0 / a
increment_y = 1
repetitions = dy
end
if (repetitions < 0) then
repetitions = -repetitions
end
local x = x0
local y = y0
for i = 0, repetitions do
love.graphics.points(x, y)
x = x + increment_x
y = y + increment_y
end
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
-- F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
-- Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
-- Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
end
The result for the JavaScript version using canvas
is displayed below.
The lines still look "serrated", though they are more continuous, as expected. Techniques such as anti-aliasing could reduce the impression that it is "serrated".
Lines as API Primitives
Drawing APIs typically provide a subroutine to draw lines. Thus, instead of creating one (as it was done in the previous sections), they can be used directly. Besides being more practical, the API implementation should be optimized, and, therefore, it should be faster and more efficient.
Nevertheless, it is important to highlight that some APIs draw the pixel from the last coordinate, whilst others do not. Therefore, it is important consulting the documentation to know the behavior of a given implementation. If the documentation does not mention the behavior, another option is drawing a line with one or two pixels, and watch the result.
# Root must be a Node that allows drawing using _draw().
extends Control
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var WHITE = Color.white
# F
draw_line(Vector2(10, 20), Vector2(20, 20), WHITE)
draw_line(Vector2(10, 20), Vector2(10, 30), WHITE)
draw_line(Vector2(10, 25), Vector2(16, 25), WHITE)
# Triangle
draw_line(Vector2(110, 120), Vector2(120, 120), WHITE)
draw_line(Vector2(110, 120), Vector2(115, 130), WHITE)
draw_line(Vector2(115, 130), Vector2(120, 120), WHITE)
# Continuous lines
draw_line(Vector2(200, 40), Vector2(250, 200), WHITE)
draw_line(Vector2(250, 40), Vector2(260, 200), WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function draw_line(x0, y0, x1, y1, color) {
context.strokeStyle = color
context.fillStyle = color
context.beginPath()
context.moveTo(x0, y0)
context.lineTo(x1, y1)
context.closePath()
context.stroke()
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
const WHITE = "white"
// F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
// Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
// Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_line(x0, y0, x1, y1, color):
pygame.draw.line(window, color, (x0, y0),(x1, y1))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
# F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
# Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
# Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_line(x0, y0, x1, y1, color)
love.graphics.setColor(color)
love.graphics.line(x0, y0, x1, y1)
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
-- F
draw_line(10, 20, 20, 20, WHITE)
draw_line(10, 20, 10, 30, WHITE)
draw_line(10, 25, 16, 25, WHITE)
-- Triangle
draw_line(110, 120, 120, 120, WHITE)
draw_line(110, 120, 115, 130, WHITE)
draw_line(115, 130, 120, 120, WHITE)
-- Continuous lines
draw_line(200, 40, 250, 200, WHITE)
draw_line(250, 40, 260, 200, WHITE)
end
Except GDScript (that uses draw_line()
directly), the implementations in JavaScript, Python and Lua perform a call to the API's subroutine for Canvas, PyGame and LÖVE in draw_line()
.
This allows keeping the previous code; the draw_line()
procedure abstracts the calls of the chosen API.
An additional benefit of using the API's primitive is the possibility of defining a width for the line. This can also be implemented on your own implementation; in this case, the drawing could be repeated many times (above and/or below the original line) to make the line broader.
Arcs
A line does not need to be a straight line. A simple way to add curves in a line consists on inserting an angulation as part of the displacement. To draw an angulation, one can draw an arc.
Circles and Angles (Degrees and Radians)
If the angulation is 360° or rad, the drawing will be a circle. In programming, mathematical APIs normally use radians instead of degrees. It is simple to convert between them:
Thus, if the programming of your choice does not provide a default implementation, you can create a function defining the previous expression.
The conversion from radians to degrees is also simple:
Both equations will be implemented in JavaScript to convert angles between degrees and radians. Therefore, although there are online tools to convert angles, it is simple doing it directly using programming languages. Besides, some problems are easier to solve in degrees than in radians; in these cases, the calculations can be performed in degrees, and the final result can be converted to radians for drawing it.
Polar Coordinates
A simple way to draw arcs consists in drawing sequences of points calculated using polar coordinates. Polar coordinates define a coordinate system that calculates a point using a distance (radius) and an angle. They are defined mathematically by the next equations.
As the equations are simple, the implementation of a procedure draw_arc()
will also be straightforward.
# Root must be a Node that allows drawing using _draw().
extends Control
func franco_draw_arc(center_x, center_y, angle, radius, color):
for alpha in range(0, angle):
var alpha_radians = deg2rad(alpha)
var x = center_x + radius * cos(alpha_radians)
var y = center_y + radius * sin(alpha_radians)
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var WHITE = Color.white
franco_draw_arc(120, 80, 360, 20, WHITE)
franco_draw_arc(200, 80, 360, 20, WHITE)
franco_draw_arc(160, 120, 180, 80, WHITE)
franco_draw_arc(160, 120, 360, 100, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function radians(angle_degrees) {
return angle_degrees * Math.PI / 180.0
}
function draw_arc(center_x, center_y, angle, radius, color) {
context.fillStyle = color
for (let alpha = 0; alpha < angle; ++alpha) {
let alpha_radians = radians(alpha)
let x = center_x + radius * Math.cos(alpha_radians)
let y = center_y + radius * Math.sin(alpha_radians)
context.fillRect(x, y, 1, 1)
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
const WHITE = "white"
draw_arc(120, 80, 360, 20, WHITE)
draw_arc(200, 80, 360, 20, WHITE)
draw_arc(160, 120, 180, 80, WHITE)
draw_arc(160, 120, 360, 100, WHITE)
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_arc(center_x, center_y, angle, radius, color):
for alpha in range(0, angle):
alpha_radians = math.radians(alpha)
x = center_x + radius * math.cos(alpha_radians)
y = center_y + radius * math.sin(alpha_radians)
window.set_at((int(x), int(y)), color)
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
draw_arc(120, 80, 360, 20, WHITE)
draw_arc(200, 80, 360, 20, WHITE)
draw_arc(160, 120, 180, 80, WHITE)
draw_arc(160, 120, 360, 100, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_arc(center_x, center_y, angle, radius, color)
love.graphics.setColor(color)
for alpha = 0, angle - 1 do
local alpha_radians = math.rad(alpha)
local x = center_x + radius * math.cos(alpha_radians)
local y = center_y + radius * math.sin(alpha_radians)
love.graphics.points(x, y)
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
draw_arc(120, 80, 360, 20, WHITE)
draw_arc(200, 80, 360, 20, WHITE)
draw_arc(160, 120, 180, 80, WHITE)
draw_arc(160, 120, 360, 100, WHITE)
end
JavaScript does not provide a subroutine to convert angles from degrees to radians; thus, the example define the function radians()
to perform such conversion.
Once again, this is necessary because drawing APIs (and for Mathematics, in general) typically use angles in radians for operations.
The output of the program shows the (rudimentary or... primitive) artistic talents of the author. To make it better, you could add a line to complete the smile in the sequence of circles and arc forming a happy face.
The implementation from the example does not allow for customization. To improve it, it would be useful to define a direction for the drawing (clockwise or counterclockwise), an initial and a final angle for the arc, and a number of points or segments to approximate the curve.
Polar coordinates use real numbers as floating-point numbers to perform the calculus. In the past, operations using floating-point numbers were expensive. Currently, the performance can be acceptable, depending on the hardware (pre-calculating values for sin and cosine of angles would also serve as an optimization).
Drawing operations in the past used integer numbers instead of floating-point numbers. The goal was avoiding (or minimizing) the use of trigonometric operations, and/or floating-point operations.
For didactic purposes and before introducing the drawing APIs provided by Godot Engine, HTML Canvas, LÖVE and PyGame, the next section shows an alternative way to draw circles and arcs using integer numbers.
Circles with Midpoint Circle Algorithm
Mathematically, every point of a circle satisfy the following equation:
In the equation, and are coordinates of a point , and is the radius.
An algorithm to approximate the equation is the Midpoint circle from Bresenham. For a circle centered in the origin , the algorithm can be described by the following equations:
The equations are recursive (or iterative), meaning that they use previous values to calculate the next ones. and are the coordinates of the point, is the radius, and is the value used to decide the next position of the circle.
It is possible to simplify applying the distributive property to reduce the number of multiplications.
Alternatively, a left bit-shift could be performed to double the value.
As a circle is a symmetrically geometric shape, the number of required operations can be reduced by dividing the drawing in octants (in other words, dividing the circle in eight equal parts), and adjusting the values of the coordinates. Likewise, to draw a circle centered at an arbitrary point , it is sufficient to add the values when painting the pixel.
To implement this algorithm, a While statement can be used.
Unlike a for
, a while
statement defines only the stop condition.
The initialization of the control variable is performed before starting the loop; the adjustments are performed inside the loop.
# Root must be a Node that allows drawing using _draw().
extends Control
func franco_draw_pixel(x, y, color):
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func franco_draw_circle(center_x, center_y, radius, color):
var x = radius
var y = 0
var e = 3 - 2 * radius
while (x >= y):
# 0 - 44
franco_draw_pixel(center_x + x, center_y - y, color)
# 45 - 89
franco_draw_pixel(center_x + y, center_y - x, color)
# 90 - 134
franco_draw_pixel(center_x - y, center_y - x, color)
# 135 - 179
franco_draw_pixel(center_x - x, center_y - y, color)
# 180 - 224
franco_draw_pixel(center_x - x, center_y + y, color)
# 225 - 269
franco_draw_pixel(center_x - y, center_y + x, color)
# 270 - 314
franco_draw_pixel(center_x + y, center_y + x, color)
# 315 - 359
franco_draw_pixel(center_x + x, center_y + y, color)
if (e > 0):
# e = e + 2 * (5 - 2x + 2y)
e += 10 + 4 * (-x + y)
x -= 1
else:
# e = e + 2 * (3 + 2 * y)
e += 6 + 4 * y
y += 1
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
var WHITE = Color.white
franco_draw_circle(0, 0, 30, WHITE)
franco_draw_circle(50, 50, 30, WHITE)
for radius in range(10, 100, 20):
franco_draw_circle(160, 120, radius, WHITE)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function draw_circle(center_x, center_y, radius, color) {
context.fillStyle = color
let x = radius
let y = 0
let e = 3 - 2 * radius
while (x >= y) {
// 0 - 44
context.fillRect(center_x + x, center_y - y, 1, 1)
// 45 - 89
context.fillRect(center_x + y, center_y - x, 1, 1)
// 90 - 134
context.fillRect(center_x - y, center_y - x, 1, 1)
// 135 - 179
context.fillRect(center_x - x, center_y - y, 1, 1)
// 180 - 224
context.fillRect(center_x - x, center_y + y, 1, 1)
// 225 - 269
context.fillRect(center_x - y, center_y + x, 1, 1)
// 270 - 314
context.fillRect(center_x + y, center_y + x, 1, 1)
// 315 - 359
context.fillRect(center_x + x, center_y + y, 1, 1)
if (e > 0) {
// e = e + 2 * (5 - 2x + 2y)
e += 10 + 4 * (-x + y)
--x
} else {
// e = e + 2 * (3 + 2 * y)
e += 6 + 4 * y
}
++y
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
const WHITE = "white"
draw_circle(0, 0, 30, WHITE)
draw_circle(50, 50, 30, WHITE)
for (let radius = 10; radius < 100; radius += 20) {
draw_circle(160, 120, radius, WHITE)
}
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_circle(center_x, center_y, radius, color):
x = radius
y = 0
e = 3 - 2 * radius
while (x >= y):
# 0 - 44
window.set_at((center_x + x, center_y - y), color)
# 45 - 89
window.set_at((center_x + y, center_y - x), color)
# 90 - 134
window.set_at((center_x - y, center_y - x), color)
# 135 - 179
window.set_at((center_x - x, center_y - y), color)
# 180 - 224
window.set_at((center_x - x, center_y + y), color)
# 225 - 269
window.set_at((center_x - y, center_y + x), color)
# 270 - 314
window.set_at((center_x + y, center_y + x), color)
# 315 - 359
window.set_at((center_x + x, center_y + y), color)
if (e > 0):
# e = e + 2 * (5 - 2x + 2y)
e += 10 + 4 * (-x + y)
x -= 1
else:
# e = e + 2 * (3 + 2 * y)
e += 6 + 4 * y
y += 1
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
WHITE = pygame.Color("white")
draw_circle(0, 0, 30, WHITE)
draw_circle(50, 50, 30, WHITE)
for radius in range(10, 100, 20):
draw_circle(160, 120, radius, WHITE)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function draw_circle(center_x, center_y, radius, color)
love.graphics.setColor(color)
local x = radius
local y = 0
local e = 3 - 2 * radius
while (x >= y) do
-- 0 - 44
love.graphics.points(center_x + x, center_y - y)
-- 45 - 89
love.graphics.points(center_x + y, center_y - x)
-- 90 - 134
love.graphics.points(center_x - y, center_y - x)
-- 135 - 179
love.graphics.points(center_x - x, center_y - y)
-- 180 - 224
love.graphics.points(center_x - x, center_y + y)
-- 225 - 269
love.graphics.points(center_x - y, center_y + x)
-- 270 - 314
love.graphics.points(center_x + y, center_y + x)
-- 315 - 359
love.graphics.points(center_x + x, center_y + y)
if (e > 0) then
-- e = e + 2 * (5 - 2x + 2y)
e = e + 10 + 4 * (-x + y)
x = x - 1
else
-- e = e + 2 * (3 + 2 * y)
e = e + 6 + 4 * y
end
y = y + 1
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
local WHITE = {1.0, 1.0, 1.0}
draw_circle(0, 0, 30, WHITE)
draw_circle(50, 50, 30, WHITE)
for radius = 10, 100, 20 do
draw_circle(160, 120, radius, WHITE)
end
end
The example uses a repetition structure to draw some concentric circles, albeit with different sizes for the radius. The result is an image that resembles a target. To increase or reduce the number of concentric circles, it would suffice to change the increment in the repetition structure (or the final value of the comparison).
A possible way to understand the drawing is thinking on a drawing compass (pair of compasses). If the supporting leg is kept at the same position, and the size of the opening of the part with graphite, each circle would have the same center, but different radius.
Arcs Using Midpoint Circle Algorithm
If one wishes to draw only part of an arc instead of the whole circle, it is necessary to draw only a part of the circle. This can be surprisingly complex using Midpoint Circle Algorithm, because the immediate solution is not viable due to the mirroring performed on the even octants. As even octants are drawn from the end to the beginning, arcs that end in one of them would be separated by a blank space.
Show / Hide Example in JavaScript
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function degrees(angle_radians) {
return angle_radians * 180.0 / Math.PI
}
function draw_arc(center_x, center_y, start_angle, end_angle, radius, color) {
context.fillStyle = color
let x = radius
let y = 0
let e = 3 - 2 * radius
let current_angle = degrees(Math.atan2(y, x))
while (x >= y) { // && (current_angle < end_angle)
console.log(current_angle)
// 0 - 44
let angle = current_angle
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + x, center_y - y, 1, 1)
}
// 45 - 89
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + y, center_y - x, 1, 1)
}
// 90 - 134
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - y, center_y - x, 1, 1)
}
// 135 - 179
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - x, center_y - y, 1, 1)
}
// 180 - 224
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - x, center_y + y, 1, 1)
}
// 225 - 269
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - y, center_y + x, 1, 1)
}
// 270 - 314
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + y, center_y + x, 1, 1)
}
// 315 - 359
angle += 45
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + x, center_y + y, 1, 1)
}
if (e > 0) {
// e = e + 2 * (5 - 2x + 2y)
e += 10 + 4 * (-x + y)
--x
} else {
// e = e + 2 * (3 + 2 * y)
e += 6 + 4 * y
}
++y
current_angle = degrees(Math.atan2(y, x))
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
draw_arc(30, 200, 0, 360, 20, "red")
draw_arc(30, 30, 0, 30, 20, "yellow")
draw_arc(200, 80, 0, 45, 20, "cyan")
draw_arc(160, 120, 45, 90, 40, "magenta")
draw_arc(160, 120, 90, 120, 60, "blue")
draw_arc(160, 120, 90, 150, 80, "green")
draw_arc(160, 120, 120, 350, 100, "white")
In the output of the program, it is possible to observe the gaps between lines that should be continuous.
An alternative consists of changing the implementation to use quadrants instead of octants. The next example illustrates a prototype of the idea implemented by the author. It is a quick draft, for purposes of proof of concept. Thus, the resulting drawing is "serrated" and has some wrong pixels.
# Root must be a Node that allows drawing using _draw().
extends Control
func franco_draw_pixel(x, y, color):
draw_primitive(PoolVector2Array([Vector2(x, y)]),
PoolColorArray([color]),
PoolVector2Array())
func franco_draw_arc(center_x, center_y, start_angle, end_angle, radius, color):
var x = radius
var y = 0
var e = 3 - 2 * radius
var current_angle = rad2deg(atan2(y, x))
while (x >= 0):
# 0 - 89
var angle = current_angle
if ((angle >= start_angle) and (angle < end_angle)):
franco_draw_pixel(center_x + x, center_y - y, color)
# 90 - 179
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
franco_draw_pixel(center_x - y, center_y - x, color)
# 180 - 269
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
franco_draw_pixel(center_x - x, center_y + y, color)
# 270 - 359
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
franco_draw_pixel(center_x + y, center_y + x, color)
if (e <= y):
x -= 1
e += 2 * x
if (e > y):
y += 1
e += -2 * y
current_angle = rad2deg(atan2(y, x))
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
franco_draw_arc(30, 200, 0, 360, 20, Color.red)
franco_draw_arc(30, 30, 0, 30, 20, Color.yellow)
franco_draw_arc(200, 80, 0, 45, 20, Color.cyan)
franco_draw_arc(160, 120, 45, 90, 40, Color.magenta)
franco_draw_arc(160, 120, 90, 120, 60, Color.blue)
franco_draw_arc(160, 120, 90, 150, 80, Color.green)
franco_draw_arc(160, 120, 120, 350, 100, Color.white)
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
function degrees(angle_radians) {
return angle_radians * 180.0 / Math.PI
}
function draw_arc(center_x, center_y, start_angle, end_angle, radius, color) {
context.fillStyle = color
let x = radius
let y = 0
let e = 3 - 2 * radius
let current_angle = degrees(Math.atan2(y, x))
while (x >= 0) {
// 0 - 89
let angle = current_angle
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + x, center_y - y, 1, 1)
}
// 90 - 179
angle += 90
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - y, center_y - x, 1, 1)
}
// 180 - 269
angle += 90
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x - x, center_y + y, 1, 1)
}
// 270 - 359
angle += 90
if ((angle >= start_angle) && (angle < end_angle)) {
context.fillRect(center_x + y, center_y + x, 1, 1)
}
if (e <= y) {
--x
e += 2 * x
}
if (e > y) {
++y
e += -2 * y
}
current_angle = degrees(Math.atan2(y, x))
}
}
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
draw_arc(30, 200, 0, 360, 20, "red")
draw_arc(30, 30, 0, 30, 20, "yellow")
draw_arc(200, 80, 0, 45, 20, "cyan")
draw_arc(160, 120, 45, 90, 40, "magenta")
draw_arc(160, 120, 90, 120, 60, "blue")
draw_arc(160, 120, 90, 150, 80, "green")
draw_arc(160, 120, 120, 350, 100, "white")
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
def draw_arc(center_x, center_y, start_angle, end_angle, radius, color):
x = radius
y = 0
e = 3 - 2 * radius
current_angle = math.degrees(math.atan2(y, x))
while (x >= 0):
# 0 - 89
angle = current_angle
if ((angle >= start_angle) and (angle < end_angle)):
window.set_at((center_x + x, center_y - y), color)
# 90 - 179
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
window.set_at((center_x - y, center_y - x), color)
# 180 - 269
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
window.set_at((center_x - x, center_y + y), color)
# 270 - 359
angle += 90
if ((angle >= start_angle) and (angle < end_angle)):
window.set_at((center_x + y, center_y + x), color)
if (e <= y):
x -= 1
e += 2 * x
if (e > y):
y += 1
e += -2 * y
current_angle = math.degrees(math.atan2(y, x))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
draw_arc(30, 200, 0, 360, 20, pygame.Color("red"))
draw_arc(30, 30, 0, 30, 20, pygame.Color("yellow"))
draw_arc(200, 80, 0, 45, 20, pygame.Color("cyan"))
draw_arc(160, 120, 45, 90, 40, pygame.Color("magenta"))
draw_arc(160, 120, 90, 120, 60, pygame.Color("blue"))
draw_arc(160, 120, 90, 150, 80, pygame.Color("green"))
draw_arc(160, 120, 120, 350, 100, pygame.Color("white"))
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
local COLORS = {
RED = {1.0, 0.0, 0.0},
YELLOW = {1.0, 1.0, 0.0},
CYAN = {0.0, 1.0, 1.0},
MAGENTA = {1.0, 0.0, 1.0},
BLUE = {0.0, 0.0, 1.0},
GREEN = {0.0, 1.0, 0.0},
WHITE = {1.0, 1.0, 1.0},
}
function draw_arc(center_x, center_y, start_angle, end_angle, radius, color)
love.graphics.setColor(color)
local x = radius
local y = 0
local e = 3 - 2 * radius
local current_angle = math.deg(math.atan2(y, x))
while (x >= 0) do
-- 0 - 89
local angle = current_angle
if ((angle >= start_angle) and (angle < end_angle)) then
love.graphics.points(center_x + x, center_y - y)
end
-- 90 - 179
angle = angle + 90
if ((angle >= start_angle) and (angle < end_angle)) then
love.graphics.points(center_x - y, center_y - x)
end
-- 180 - 269
angle = angle + 90
if ((angle >= start_angle) and (angle < end_angle)) then
love.graphics.points(center_x - x, center_y + y)
end
-- 270 - 359
angle = angle + 90
if ((angle >= start_angle) and (angle < end_angle)) then
love.graphics.points(center_x + y, center_y + x)
end
if (e <= y) then
x = x - 1
e = e + 2 * x
end
if (e > y) then
y = y + 1
e = e - 2 * y
end
current_angle = math.deg(math.atan2(y, x))
end
end
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
draw_arc(30, 200, 0, 360, 20, COLORS.RED)
draw_arc(30, 30, 0, 30, 20, COLORS.YELLOW)
draw_arc(200, 80, 0, 45, 20, COLORS.CYAN)
draw_arc(160, 120, 45, 90, 40, COLORS.MAGENTA)
draw_arc(160, 120, 90, 120, 60, COLORS.BLUE)
draw_arc(160, 120, 90, 150, 80, COLORS.GREEN)
draw_arc(160, 120, 120, 350, 100, COLORS.WHITE)
end
The implementation uses x >= 0
(ending at 90°) instead of x >= y
(ending at 45°) to iterate values in the first quadrant.
There are imperfections in parts of the curves and at each division of quadrants.
To improve the implementation, it would be necessary better adjustments for the values of the error calculated on e
.
You can change the values used to calculate e
to try drawing circles and arcs with fewer imperfections.
As the next topics will not use midpoint circle to draw arcs, it is worth introducing the primitive from the libraries and engines (instead of focusing on improving the implementation).
Arcs as API Primitives
Drawing APIs usually provide primitives to draw arcs. The signature of the subroutines to draw arcs typically receive the coordinate of the center and the radius (although this can vary). The initial and the final angle are usually measured in radians.
- Godot provides
draw_arc()
; - JavaScript provides
arc()
; - PyGame provides two methods; this example uses
gfxdraw.arc()
. It is important noticing that the subroutine use angles in degrees instead of radians. Furthermore, the maximum angulation is 359°; - LÖVE provides
arc()
.
# Root must be a Node that allows drawing using _draw().
extends Control
func _ready():
OS.set_window_size(Vector2(320, 240))
OS.set_window_title("Olá, meu nome é Franco!")
func _draw():
VisualServer.set_default_clear_color(Color(0.0, 0.0, 0.0))
for i in range(2, 30, 2):
var center_x = i * 7.0
var center_y = i * 7.0
var radius = i * 3.0
var start_angle = PI / i
var end_angle = 2.0 * PI
var point_count = 100
draw_arc(Vector2(center_x, center_y),
radius,
start_angle, end_angle,
point_count,
Color(2.0 / i, 2.0 / i, 2.0 / i))
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
canvas.width = 320
canvas.height = 240
document.title = "Olá, meu nome é Franco!"
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = "black"
context.fillRect(0, 0, canvas.width, canvas.height)
for (let i = 2; i < 30; i += 2) {
let center_x = i * 7.0
let center_y = i * 7.0
let radius = i * 3.0
let start_angle = Math.PI / i
let end_angle = 2.0 * Math.PI
context.strokeStyle = `rgb(${510 / i}, ${510 / i}, ${510 / i})`
context.beginPath()
context.arc(center_x, center_y,
radius,
start_angle, end_angle)
context.stroke()
context.closePath()
}
import math
import pygame
import sys
from pygame import gfxdraw
pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
for i in range(2, 30, 2):
center_x = i * 7
center_y = i * 7
radius = i * 3
start_angle = math.degrees(math.pi / i)
end_angle = math.degrees(2.0 * math.pi)
pygame.gfxdraw.arc(window,
center_x, center_y,
radius,
math.floor(start_angle), math.floor(end_angle),
(510 // i, 510 // i, 510 // i))
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
function love.load()
love.window.setMode(320, 240)
love.window.setTitle("Olá, meu nome é Franco!")
end
function love.draw()
love.graphics.setBackgroundColor(0.0, 0.0, 0.0)
for i = 2, 29, 2 do
local center_x = i * 7.0
local center_y = i * 7.0
local radius = i * 3.0
local start_angle = math.pi / i
local end_angle = 2.0 * math.pi
local point_count = 100
love.graphics.setColor(2.0 / i, 2.0 / i, 2.0 / i)
love.graphics.arc("line", "open",
center_x, center_y,
radius,
start_angle, end_angle,
point_count)
end
end
The variables describe the parameters.
Implementations defining point_count
use a predefined number of point or line segments to draw the arc.
The greater the number, the better the result; however, it will also be more computationally expensive to perform the operation.
The value 510 that appears in JavaScript is the double of 255, which is the number of possible integer values for each primary color. As the counter of the repetition structure starts at 2, all values for primary colors will belong to 0.0 to 1.0 (or 0 to 255).
APIs typically provide specialized subroutines to draw circles instead of arcs. This will be address on a future topic, exploring more geometric shapes.
Concepts Covered From Learn Programming
Concepts from Learn Programming covered in this topic:
- Entry point;
- Output;
- Data types;
- Variables and constants;
- Arithmetic;
- Comparisons;
- Logic operations;
- Conditional structures;
- Subroutines (functions and procedures);
- Repetition structures (or loops);
- Libraries;
It is worth noticing that even drawing primitives can use almost every basic programming concept.
Furthermore, the values for colors in Lua used tables (dictionaries), described in Collections.
New Items for Your Inventory
Computational Thinking Abilities:
- Comparisons;
- Logic operations;
- Conditional structures;
- Subroutines (functions and procedures);
- Repetition structures (or loops);
Tools:
- Image editors;
- Magnifier or magnifying lens;
- Angle converters.
Skills:
- Drawing points or pixels
- Drawing lines;
- Drawing arc;
- Implementing some drawing primitives: line, arc, circle.
Concepts:
- Pixel;
- Resolution;
- Drawing primitive;
- Vector graphics;
- Raster graphics;
- Coordinate systems;
- Cartesian plane (Cartesian coordinate system);
- Point;
- Line;
- Arc.
Programming resources:
- Drawing graphical primitives.
Practice
To learn programming, deliberate practice must follow the concepts. Try doing the next exercises to practice.
Draw a character or a number using pixels;
Create graphics using drawing primitives.
At this time, only points, lines and arcs have been described. A future topic will introduce polygons to make it easier to create graphics.
How would you create a rectangle or a square using lines or pixels?
How would you color a rectangle or a square using pixels?
This will be address in future topics; however, you already can create a solution if you think about it.
Can a line be curved? Can a straight line segment be curved?
Create a program that converts angles from degrees to radians;
Create a program that converts angles from radians to degrees;
Change values to calculate the error stored in the variable
e
for the Midpoint Circle algorithm. After each change, re-run the program and view the results.Why should you use drawing primitives provided by APIs instead of creating your own?
Write a subroutine that draws a dashed line using repetition and conditional structures.
Deepening
In Ideas, Rules, Simulation, this section provides complementary content.
PyGame: Desenhando Arcos com draw.arc()
It is also possible to draw arcs in PyGame using pygame.draw.arc()
.
However, the method receives a rectangle instead of the center coordinate and the radius.
Thus, to use the function, the top-left corner must be defined as center_x - radius
, the right one as center_y - radius
, and the width and height as 2 * radius
.
import math
import pygame
import sys
pygame.init()
window = pygame.display.set_mode((320, 240))
pygame.display.set_caption("Olá, meu nome é Franco!")
window.fill((0, 0, 0))
for i in range(2, 30, 2):
center_x = i * 7.0
center_y = i * 7.0
radius = i * 3.0
start_angle = math.pi / i
end_angle = 2.0 * math.pi
pygame.draw.arc(window,
(510.0 / i, 510.0 / i, 510.0 / i),
[center_x - radius, center_y - radius, 2 * radius, 2 * radius],
start_angle, end_angle)
pygame.display.flip()
while (True):
for event in pygame.event.get():
if (event.type == pygame.QUIT):
pygame.quit()
sys.exit(0)
It is worth noticing that the arc will be drawn differently from the other implementations.
Next Steps
Pixels, points, lines, arcs. With the previous drawing primitives, it is possible to start exploring more complex graphics in Ideas, Rules, Simulation.
In the next topics, the examples will use the primitives provided by the APIs of each library. Nevertheless, you now have an idea of how they work and are implemented.
Now it is also possible to start creating the first simulations using (simple) graphics. To make them more interesting, a possibility is exploring pseudorandom numbers. If the name sounds familiar, they have been previously introduced in Repetition structures (or loops) in Learn Programming.
Furthermore, if you have created a creative or interesting illustration, consider sharing it. Alternatively, if you have found this material useful, you can also share it. If possible, use the hashtags #IdeasRulesSimulation and #FrancoGarciaCom.
I thank you for your attention. See you soon!
Ideas, Rules, Simulation
- Motivation;
- Introduction: Window and Hello World;
- Pixels and drawing primitives (points, lines, and arcs);
- Randomness and noise;
- Coins and dice, rectangles and squares;
- Drawing with drawing primitives (strokes and fillings for circles, ellipses and polygons);
- Saving and loading image files;
- ...
This material is a work in progress; therefore, if you have arrived early and the previous items do not have links, please return to this page to check out updates.
If you wish to contact me or have any questions, you can chat with me by:
- E-mail: francogarcia@protonmail.com
- GitHub: francogarcia
- GitLab: francogarcia
- Bitbucket: francogarcia
- YouTube: channel UCxbFFDZ4BmnT-Mhm8z1JsOA
- Instagram: @francogarciacom
- Twitter: @francogarciacom
Information about contact and social networks are also available at the footer of every page.
Your opinion about the series will be fundamental to enable me to create a material that is more accessible and simpler for more people.