-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path__main__.py
More file actions
216 lines (182 loc) · 7.56 KB
/
Copy path__main__.py
File metadata and controls
216 lines (182 loc) · 7.56 KB
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""2026-05-03
Vitral 09
Inspirado em vitrais, sketch com hexágonos e octágonos.
ericof.com|https://ericof.com/en/sketches/2023-06-01
png
Sketch,py5,CreativeCoding,Mosaico,Vitral
"""
from sketches.utils.draw import canvas
from sketches.utils.draw.formas import gera_hexagono
from sketches.utils.draw.formas import gera_octagono
from sketches.utils.draw.grade import cria_grade_ex
from sketches.utils.helpers import sketches as helpers
import py5
sketch = helpers.info_for_sketch(__file__, __doc__)
cor_fundo = py5.color(0)
hexagono: py5.Py5Shape | None = None
octagono: py5.Py5Shape | None = None
tamanho = 30
h_base = 200
h_jitter = 15
grade: list[tuple[int, float, int, float]] | None = None
celula_x = 0
celula_y = 0
celulas = 1
banda = 20
def pos(pos: float) -> float:
"""Aplica jitter aleatório uniforme a uma coordenada.
Soma um inteiro sorteado em ``[-banda, banda]`` à coordenada
recebida, quebrando o alinhamento perfeito da grade e dando um ar
artesanal — como peças de vitral cortadas à mão — ao mosaico.
:param pos: coordenada original (``x`` ou ``y``) em pixels.
:returns: coordenada deslocada em até ``±banda`` pixels.
"""
return pos + py5.random_int(-banda, banda)
def calcula_grade() -> list[tuple[int, float, int, float]]:
"""Calcula a grade escalonada de células para posicionar os elementos.
Sorteia novas dimensões de célula (``celula_x`` e ``celula_y``) num
intervalo entre ``1.2x`` e ``1.5x`` ``tamanho``, atualiza
``celulas`` (número aproximado de colunas, usado no cálculo do
brilho radial em :func:`cores_elemento`) e gera a grade alternada
via :func:`cria_grade_ex`. Em seguida aplica :func:`pos` a cada
coordenada, introduzindo jitter de ``±banda`` pixels para suavizar
o alinhamento. Cada item devolvido é a tupla ``(idx, x, idy, y)``
com índices e coordenadas já deslocadas.
:returns: lista de tuplas ``(idx, x, idy, y)``.
"""
global grade, celula_x, celula_y, celulas
celula_x = int(tamanho * py5.random(1.2, 1.5))
celula_y = int(tamanho * py5.random(1.2, 1.5))
celulas = helpers.DIMENSOES.external[0] // celula_x + 1
grade = cria_grade_ex(
largura=helpers.DIMENSOES.external[0],
altura=helpers.DIMENSOES.external[1],
margem_x=0,
margem_y=0,
celula_x=celula_x,
celula_y=celula_y,
alternada=True,
)
grade = [(idx, pos(x), idy, pos(y)) for idx, x, idy, y in grade]
return grade
def cores_elemento(idx: int, idy: int, angulo: float) -> tuple[int, int]:
"""Deriva as cores de traço e preenchimento de um elemento da grade.
A paleta é **analógica em torno de** ``h_base``: combina os
índices de coluna (``idx``) e linha (``idy``) com pesos coprimos e
aplica passo quasi-dourado (``122.508`` por unidade) para gerar
uma distribuição 2D quasi-aleatória, mas em vez de cobrir todo o
círculo cromático o resultado é mapeado para
``[-h_jitter, +h_jitter]`` e somado a ``h_base``, mantendo todas
as peças como variações de uma mesma cor. O ``angulo`` adiciona
um pequeno deslocamento extra de matiz (``angulo · 0.2``), o
suficiente para diferenciar as duas camadas rotacionadas sem sair
da família cromática.
Para compensar a faixa estreita de matiz, a saturação foi
alargada para ``60..100`` via ``(idx + idy · 11) % 41``,
distribuindo quasi-aleatoriamente peças mais saturadas e mais
"lavadas" pela grade (sem padrão espacial). O brilho mantém
a vinheta em distância de Manhattan
(``100 - (|cx - idx| + |cy - idy|)``, com ``cx = cy = celulas // 2``),
escurecendo os elementos longe do centro. A transparência é
modulada por ``t = angulo / 6 + 70`` e o traço permanece escuro
(``saturação 30``, ``brilho 0``), reforçando o efeito de vitral.
:param idx: índice da coluna na grade.
:param idy: índice da linha na grade.
:param angulo: ângulo, em graus, usado como offset adicional do
matiz e para modular a transparência do preenchimento.
:returns: par ``(traco, cor)`` com os valores ``py5.color`` para
stroke e fill.
"""
delta_h = ((idx + idy * 34) * 122.508) % (2 * h_jitter) - h_jitter
h = (h_base + delta_h + angulo * 0.2) % 360
s = 60 + ((idx + idy * 11) % 41)
b = 100 - (abs(celulas // 2 - idx) + abs(celulas // 2 - idy))
t = angulo / 6 + 70
traco = py5.color(h, 30, 0)
cor = py5.color(h, s, b, t)
return traco, cor
def setup():
"""Inicializa o sketch.
Define o tamanho do canvas, ativa P3D e o modo de cor HSB, cria os
shapes-base (hexágono e octágono) e calcula a grade inicial.
"""
global hexagono, octagono
py5.size(*helpers.DIMENSOES.external, py5.P3D)
py5.shape_mode(py5.CENTER)
py5.color_mode(py5.HSB, 360, 100, 100)
hexagono = gera_hexagono()
octagono = gera_octagono()
calcula_grade()
def draw():
"""Desenha o quadro atual com duas camadas rotacionadas.
A uma profundidade ``z=-20``, empilha duas camadas rotacionadas em
torno do eixo Y (0° e 45°). Em cada camada, percorre toda a grade
escolhendo hexágono ou octágono pela regra ``(idx + idy) % 4 == 0``
(um hexágono a cada quatro células). As cores vêm de
:func:`cores_elemento` recebendo o ``angulo`` da camada — o que
desloca matiz e transparência entre as duas — e cada forma é
desenhada em escala ``celula * 1.4`` com stroke weight 8, gerando o
contorno espesso típico de vitral.
Ao final, renderiza a moldura de créditos do sketch.
"""
py5.background(cor_fundo)
if grade and hexagono and octagono:
with py5.push():
py5.translate(0, 0, -20)
for angulo in range(0, 90, 45):
with py5.push():
py5.rotate_y(py5.radians(angulo))
for idx, x, idy, y in grade:
traco, cor = cores_elemento(idx, idy, angulo)
forma = hexagono if (idx + idy) % 4 == 0 else octagono
forma.set_stroke(traco)
forma.set_stroke_weight(8)
forma.set_fill(cor)
mult = 1.4
py5.shape(forma, x, y, celula_x * mult, celula_y * mult)
msg = f"T: {tamanho}, H: {h_base}"
# Credits and go
canvas.sketch_frame(
sketch,
cor_fundo,
"large_transparent_white",
"transparent_white",
version=2,
msg=msg,
)
def key_pressed():
"""Trata atalhos de teclado.
- ``space``: salva a imagem e encerra o sketch.
- ``r``: recalcula a grade (sorteia novas dimensões de célula).
- ``+`` / ``-``: incrementa ou decrementa ``tamanho`` (base usada
para o sorteio das células) em 2px e recalcula a grade.
- ``>`` / ``<``: incrementa ou decrementa ``h_base`` (matiz central
da paleta analógica em :func:`cores_elemento`) em 10 graus e
recalcula a grade.
"""
global tamanho, h_base
key = py5.key
match key:
case " ":
save_and_close()
case "r":
calcula_grade()
case "+":
tamanho += 2
calcula_grade()
case "-":
tamanho -= 2
calcula_grade()
case ">":
h_base = (h_base + 10) % 360
calcula_grade()
case "<":
h_base = (h_base - 10) % 360
calcula_grade()
def save_and_close():
"""Pausa o loop, salva o PNG do sketch e encerra a execução."""
py5.no_loop()
canvas.save_sketch_image(sketch)
py5.exit_sketch()
if __name__ == "__main__":
py5.run_sketch()