Experimentos em Programação Criativa - 3

Essa é mais uma tentativa de reproduzir uma imagem utilizando computação criativa, puramente para aprender coisas novas e passar o tempo.

Inspiração

A imagem que tentei replicar através de código desta vez é a seguinte:

Várias linhas conectadas se movendo, gerando um desenho ao fundo

E o meu resultado é o seguinte:

Um parecido canvas com várias linhas conectadas se movendo, gerando um desenho ao fundo

Um pouco longe do resultado desejado, mas já foi muito bom para utilizar algoritmos e estruturas de dados diferentes e praticar um pouquinho com o loop de desenho do p5.

Implementação

Usarei novamento o editor do p5 para essa implementação.

Passo 1

Começando por um passo que parece simples mas já precisará de um pouco e matemática, vamos tentar primeiro desenhar a linha central que fica estática. Basicamente temos um ponto no centro da imagem, com uma barra em 45º.

Como todas as barras serão movimentadas com base no seu ponto central, e terão um ângulo e comprimento diferentes, podemos criar uma função que receba a coordenada central, um ângulo e um comprimento para definir quais são as coordenadas das extremidades da linha. Assim só precisamos desenhar a linha entre os dois pontos.

Conseguimos fazer isso com a seguinte função:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function calculateLinePoints(center, angle, lineSize) {
// Converter o ângulo de graus para radianos
const angleInRadians = (angle * Math.PI) / 180

// Calcular a metade da linha
const halfLineSize = lineSize / 2

// Calcular os componentes x e y da linha
const xComponent = Math.cos(angleInRadians) * halfLineSize
const yComponent = Math.sin(angleInRadians) * halfLineSize

// Calcular os outros dois pontos da linha
const point1 = {
x: center.x - xComponent,
y: center.y - yComponent
}

const point2 = {
x: center.x + xComponent,
y: center.y + yComponent
}

return [point1, point2]
}

E desenhar na tela com o método draw do p5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const SIZE = 500

function setup() {
createCanvas(SIZE, SIZE)
}

function draw() {
background(220)

stroke('green')
strokeWeight(10)

const coord = { x: width / 2, y: height / 2, }

point(coord.x, coord.y)

const [point1, point2] = this.calculateLinePoints(coord, 45, 300)

strokeWeight(2)
line(point1.x, point1.y, point2.x, point2.y)
}

Assim temos nossa primeira linha

Canvas com linha central em 45 graus

Passo 2

Como as linhas terão conexão umas com as outras decidi usar classes para esse projeto. Para isso criarei uma classe para desenhar as linhas, para facilitar o trabalho.
Com um pouco de refatoração, podemos atualizar o código anterior para:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function draw() {
background(220);

const r = new Regua({ x: width / 2, y: height / 2, }, 45, 300)
r.draw()
}

class Regua {
constructor(coord, angle, length) {
this.coord = coord
this.angle = angle
this.length = length
}

draw() {
stroke('green')
strokeWeight(10)

point(this.coord.x, this.coord.y)

const [point1, point2] = this.calculateLinePoints(this.coord, this.angle, this.length)

strokeWeight(2)
line(point1.x, point1.y, point2.x, point2.y)
}

calculateLinePoints(center, angle, lineSize) {
// Converter o angulo de graus para radianos
const angleInRadians = (angle * Math.PI) / 180

// Calcular a metade da linha
const halfLineSize = lineSize / 2

// Calcular os componentes x e y da linha
const xComponent = Math.cos(angleInRadians) * halfLineSize
const yComponent = Math.sin(angleInRadians) * halfLineSize

// Calcular os outros dois pontos da linha
const point1 = {
x: center.x - xComponent,
y: center.y - yComponent
}

const point2 = {
x: center.x + xComponent,
y: center.y + yComponent
}

return [point1, point2]
}
}

Passo 3

Podemos então adicionar um novo parâmetros para a nossa classe, para desenharmos as linhas seguintes: a linha pai.

1
2
3
4
5
6
7
8
9
10
11
12
13
function draw() {
...

const r2 = new Regua({ x: width / 2, y: height / 2, }, 0, 300, r)
r2.draw()
}

class Regua {
constructor(coord, angle, length, parent) {
...
this.parent = parent
}
}

Como vamos precisar que a linha ande, não podemos sempre redesenhá-la na mesma coordenada. Para isso decidi mudar nosso código para criar as réguas no método setup (que só é executado uma vez), e somente desenhá-los no método draw.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const RULERS = []

function setup() {
createCanvas(SIZE, SIZE)

const r = new Regua({ x: width / 2, y: height / 2, }, 45, 300)
const r2 = new Regua({ x: width / 2, y: height / 2, }, 0, 300, r)

RULERS.push(r)
RULERS.push(r2)
}

function draw() {
background(220)

RULERS.forEach(ruler => {
ruler.draw()
})
}

Canvas com duas linhas

Passo 4

Agora podemos começar com a parte de movimentação. Podemos criar uma função walk em nossa classe de linha, para movimentá-la dentro do loop de desenho. Para encontrar o próximo ponto da linha

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Regua {
walk() {
if (!this.parent) return

// Selecionar as extremidades da linha pai
const [point1, point2] = this.calculateLinePoints(this.parent.coord, this.parent.angle, this.parent.length)

// Calcular qual percentual da linha pai será o próximo ponto a seguir. Utilizamos o resto da divisão entre o número de loops e o dobro do tamanho do pai, e mapeamos para que a linha ande nas duas direções
const percent = map(frameCount % this.parent.length * 2, 0, this.parent.length, -100, 0)

// Atualizar a coordenada
this.coord = this.getPointOnLine(point1, point2, Math.abs(percent))
}

getPointOnLine(coordinate1, coordinate2, percent) {
// Calcular diferença entre coordenadas x e y
const dx = coordinate2.x - coordinate1.x;
const dy = coordinate2.y - coordinate1.y;

// Calcular o ponto na linha com base no percentual
const point = {
x: coordinate1.x + (dx * percent) / 100,
y: coordinate1.y + (dy * percent) / 100
};

return point;
}
}

E por fim chamamos o método de desenho dentro do loop:

1
2
3
4
5
6
7
8
function draw() {
background(220)

RULERS.forEach(ruler => {
ruler.draw()
ruler.walk()
})
}

Linha em movimento

Passo 5

Um novo incremento que podemos fazer em nossa linha é configurar também a velocidade em que ela se move, já que cada linha terá uma velocidade diferente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Regua {
constructor(coord, angle, length, parent, velocity = 1) {
...
this.velocity = velocity
}

walk() {
...

const v = frameCount * this.velocity

// Calcular qual percentual da linha pai será o próximo ponto a seguir. Utilizamos o resto da divisão entre o número de loops e o dobro do tamanho do pai, e mapeamos para que a linha ande nas duas direções
const percent = map(v % this.parent.length * 2, 0, this.parent.length, -100, 0)

...
}
}

E então podemos atualizar o o método setup para criar várias linhas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const RULERS = []
const RULER_COUNT = 6

function setup() {
createCanvas(SIZE, SIZE)

const base = new Regua({ x: width / 2, y: height / 2, }, 45, 300)
RULERS.push(base)

for (let i = 0; i < RULER_COUNT - 1; i++) {
const parent = RULERS[RULERS.length - 1]
const angle = random(0, 180)
const length = random(50, SIZE / 2)
const velocity = random(0.3, 1.5)

const r = new Regua(base.coord, angle, length, parent, velocity)

RULERS.push(r)
}
}

Várias linhas em movimento

Passo 6

A última linha será a responsável por adicionar o desenho na tela. Podemos ter uma propriedade para indentificarmos a linha que ela deve também desenhar a curva:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function setup() {
...

for (let i = 0; i < RULER_COUNT - 1; i++) {
...
const shouldDrawCurve = i === RULER_COUNT - 2

const r = new Regua(base.coord, angle, length, parent, velocity, shouldDrawCurve)
...
}
}

class Regua {
constructor(coord, angle, length, parent, velocity = 1, shouldDrawCurve = false) {
...
this.shouldDrawCurve = shouldDrawCurve
}

...
}

Na sequência criamos uma lista para os pontos da curva em que preenchemos a cada passo da régua, e chamamos uma função para desenhar a curva após desenharmos a linha:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

class Regua {
constructor(coord, angle, length, parent, velocity = 1, shouldDrawCurve = false) {
...
this.curvePath = []
}

draw() {
...

if (this.shouldDrawCurve) {
this.drawCurve()
}
}

walk() {
...

if (this.shouldDrawCurve) {
const [point1] = this.calculateLinePoints(this.coord, this.angle, this.length)

this.curvePath.push(point1)
}
}

drawCurve() {
noFill()
stroke('black')
strokeWeight(2)

beginShape()
this.curvePath.forEach((coord) => {
curveVertex(coord.x, coord.y)
})
endShape()
}
}

Quase lá. A curva não parece tanto com uma curva.

Várias linhas em movimento desenhando uma "curva"

Passo 7

Assistindo a esse vídeo de Dave Pagurek, uma forma que é apresentada para resolver esse problema é não adicionar em nossa lista de pontos da curva a extremidade atual da linha, e sim um percentual de distância entre a extremidade e o ponto anterior.

O vídeo faz um papel muito melhor em explicar esse algoritmo, mas o código simples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Regua {
walk() {
...

if (this.shouldDrawCurve) {
const [point1] = this.calculateLinePoints(this.coord, this.angle, this.length)
const lastPoint = this.curvePath[this.curvePath.length - 1]

if (!lastPoint) {
this.curvePath.push(point1)
return
}

const point = createVector(lastPoint.x, lastPoint.y).lerp(createVector(point1.x, point1.y), 0.1)
this.curvePath.push({ x: point.x, y: point.y })
}
}
}

Várias linhas em movimento desenhando uma curva de verdade

Próximos passos

Algumas outras ideias para evoluir essa brincadeira seriam:

  • Alterar a grossura da curva, quando a velocidade de desenho diminui
  • Conectar a curva com a extremidade da linha, para que o desenho seja mais suave (sugestões de como implementar ambas ideias são também no vídeo mencionado acima)
Código completo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const SIZE = 500
const RULERS = []
const RULER_COUNT = 6

function setup() {
createCanvas(SIZE, SIZE)

const base = new Regua({ x: width / 2, y: height / 2, }, 45, 300)
RULERS.push(base)

for (let i = 0; i < RULER_COUNT - 1; i++) {
const parent = RULERS[RULERS.length - 1]
const angle = random(0,180)
const length = random(50, SIZE/2)
const velocity = random(0.3, 1.5)
const shouldDrawCurve = i === RULER_COUNT - 2

const r = new Regua(base.coord, angle, length, parent, velocity, shouldDrawCurve)

RULERS.push(r)
}
}

function draw() {
background(220)

RULERS.forEach(ruler => {
ruler.draw()
ruler.walk()
})
}

class Regua {
constructor(coord, angle, length, parent, velocity = 1, shouldDrawCurve = false) {
this.coord = coord
this.angle = angle
this.length = length
this.parent = parent
this.velocity = velocity
this.shouldDrawCurve = shouldDrawCurve
this.curvePath = []
}

draw() {
stroke('green')
strokeWeight(10)

point(this.coord.x, this.coord.y)

const [point1, point2] = this.calculateLinePoints(this.coord, this.angle, this.length)

strokeWeight(2)
line(point1.x, point1.y, point2.x, point2.y)

if (this.shouldDrawCurve) {
this.drawCurve()
}
}

walk() {
if (!this.parent) return

// Selecionar as extremidades da linha pai
const [point1, point2] = this.calculateLinePoints(this.parent.coord, this.parent.angle, this.parent.length)

const v = frameCount * this.velocity
// Calcular qual percentual da linha pai será o próximo ponto a seguir. Utilizamos o resto da divisão entre o número de loops e o dobro do tamanho do pai, e mapeamos para que a linha ande nas duas direções
const percent = map(v % this.parent.length * 2, 0, this.parent.length, -100, 0)

// Atualizar a coordenada
this.coord = this.getPointOnLine(point1, point2, Math.abs(percent))

if (this.shouldDrawCurve) {
const [point1] = this.calculateLinePoints(this.coord, this.angle, this.length)
const lastPoint = this.curvePath[this.curvePath.length - 1]

if (!lastPoint) {
this.curvePath.push(point1)
return
}

const point = createVector(lastPoint.x, lastPoint.y).lerp(createVector(point1.x, point1.y), 0.1)
this.curvePath.push({ x: point.x, y: point.y })
}
}

drawCurve() {
noFill()
stroke('black')
strokeWeight(2)

beginShape()
this.curvePath.forEach((coord) => {
curveVertex(coord.x, coord.y)
})
endShape()
}

calculateLinePoints(center, angle, lineSize) {
// Converter o angulo de graus para radianos
const angleInRadians = (angle * Math.PI) / 180

// Calcular a metade da linha
const halfLineSize = lineSize / 2

// Calcular os componentes x e y da linha
const xComponent = Math.cos(angleInRadians) * halfLineSize
const yComponent = Math.sin(angleInRadians) * halfLineSize

// Calcular os outros dois pontos da linha
const point1 = {
x: center.x - xComponent,
y: center.y - yComponent
}

const point2 = {
x: center.x + xComponent,
y: center.y + yComponent
}

return [point1, point2]
}

getPointOnLine(coordinate1, coordinate2, percent) {
// Calcular diferença entre coordenadas x e y
const dx = coordinate2.x - coordinate1.x;
const dy = coordinate2.y - coordinate1.y;

// Calcular o ponto na linha com base no percentual
const point = {
x: coordinate1.x + (dx * percent) / 100,
y: coordinate1.y + (dy * percent) / 100
};

return point;
}
}