"""2026-05-02
STXP
Pixelado com o logo da Starfleet
ericof.com|https://ericof.com/en/sketches/2023-11-01
png
Sketch,py5,CreativeCoding
"""
from sketches.utils.draw import canvas
from sketches.utils.helpers import sketches as helpers
from sketches.utils.helpers.images import resource_image_as_array
import numpy as np
import py5
sketch = helpers.info_for_sketch(__file__, __doc__)
cor_fundo = py5.color(0)
STROKE_MOD = 0.75
total_chunks = 100
tam_min = 4.0
tam_max = 12.0
img_array: np.ndarray | None = None
pontos: list[tuple[int, int, int, np.ndarray, list[float]]] = []
altura_chunk = helpers.DIMENSOES.internal[1] / total_chunks
def _chunks_v_exponencial(
largura: int,
n: int = 80,
c_min: float = 4.0,
c_max: float = 16.0,
) -> np.ndarray:
"""Gera ``n`` chunks em V exponencial cuja soma é exatamente ``largura``.
Os tamanhos de bloco seguem ``c(t) = c_min · 2^(alpha·|2t - 1|)`` com
``t ∈ [0, 1]`` discretizado em ``n`` pontos e
``alpha = log2(c_max / c_min)``, de modo que ``c(0) = c(1) = c_max``
(bordas) e ``c(0.5) = c_min`` (centro). Em seguida o array é
reescalado para que a soma bata exatamente ``largura``,
arredondado para inteiros, e o resíduo do arredondamento é
absorvido no chunk central (o menor) — preservando o perfil em V
e evitando lacuna ou estouro no eixo X.
:param largura: total de pixels a cobrir no eixo X.
:param n: número de chunks na sequência.
:param c_min: tamanho do bloco no centro do V (pixels).
:param c_max: tamanho do bloco nas bordas do V (pixels).
:returns: array de inteiros (``≥ 1``) com soma igual a ``largura``.
"""
alpha = np.log2(c_max / c_min)
t = np.linspace(0, 1, n)
raw = c_min * np.power(2.0, alpha * np.abs(2 * t - 1))
raw = raw * (largura / raw.sum())
chunks = np.maximum(1, np.round(raw).astype(int))
diff = largura - int(chunks.sum())
if diff != 0:
meio = n // 2
chunks[meio] = max(1, chunks[meio] + diff)
return chunks
def _pixelar(
img_array: np.ndarray,
altura: int,
x: int,
chunks: np.ndarray,
limite: int,
) -> list[tuple[int, int, int, np.ndarray, list[float]]]:
"""Amostra a imagem em colunas com tamanho de bloco variável.
Percorre ``chunks`` da esquerda para a direita, avançando ``x`` em
cada iteração. Para cada tamanho de bloco varre uma coluna inteira
no eixo Y em blocos quadrados de lado ``pixel_chunk``. Cada bloco
devolve a cor mais intensa por canal via :func:`np.max` (e não a
média, apesar do nome ``avg_color``), preservando bordas luminosas
do logo da Starfleet sobre o fundo preto. O traço de cada elipse é
uma versão atenuada da própria cor (``STROKE_MOD = 0.75``),
criando contorno sutil sem desviar da paleta.
Espera-se que ``chunks`` siga um perfil em V (tamanhos maiores
nas bordas, menores no centro) — ver :func:`_chunks_v_exponencial`
— concentrando detalhe na região focal. Encerra quando ``x``
ultrapassa ``limite``.
:param img_array: array NumPy da imagem ``(H, W, C)``.
:param altura: altura útil em pixels para a varredura vertical.
:param x: coordenada inicial do eixo X em pixels.
:param chunks: sequência de tamanhos de bloco (em pixels).
:param limite: largura máxima em pixels; corta a iteração ao ultrapassar.
:returns: lista de tuplas ``(x, y, pixel_chunk, cor_max, stroke)``.
"""
data = []
for pixel_chunk in chunks:
pixel_chunk = int(pixel_chunk)
for y in range(0, altura, pixel_chunk):
block = img_array[y : y + pixel_chunk, x : x + pixel_chunk]
avg_color = np.max(block, axis=(0, 1))
stroke = [c * STROKE_MOD for c in avg_color]
data.append((x, y, pixel_chunk, avg_color, stroke))
x += pixel_chunk
if x >= limite:
return data
return data
def setup() -> None:
"""Configura janela P3D, carrega ``st-mask.png`` e pré-calcula a pixelação.
O perfil ``pixel_chunks`` é uma curva em V exponencial gerada por
:func:`_chunks_v_exponencial` — blocos de ``c_max`` pixels nas
bordas decrescendo até ``c_min`` no centro segundo
``c(t) = c_min · 2^(alpha·|2t-1|)`` — concentrando detalhe na
região do logo da Starfleet.
"""
global img_array, pontos
py5.size(*helpers.DIMENSOES.external, py5.P3D)
py5.ellipse_mode(py5.CENTER)
img_array = resource_image_as_array("st-mask.png")
largura, altura = helpers.DIMENSOES.internal
pixel_chunks = _chunks_v_exponencial(
largura, n=total_chunks, c_min=tam_min, c_max=tam_max
)
pontos = _pixelar(img_array, altura, 0, pixel_chunks, largura)
def draw() -> None:
"""Renderiza o frame: fundo preto, elipses pixeladas e créditos."""
py5.background(cor_fundo)
with py5.push():
py5.translate(*helpers.DIMENSOES.pos_interno, -10)
for x, y, pixel_chunk, cor_media, stroke in pontos:
py5.stroke(*stroke)
py5.fill(*cor_media)
py5.ellipse(
x + pixel_chunk / 2, y + altura_chunk / 2, pixel_chunk, altura_chunk
)
# Credits and go
canvas.sketch_frame(
sketch,
cor_fundo,
"large_transparent_white",
"transparent_white",
version=2,
)
def key_pressed() -> None:
"""Captura teclas: ``espaço`` salva a imagem e encerra."""
key = py5.key
if key == " ":
save_and_close()
def save_and_close() -> None:
"""Para o loop, salva a imagem do sketch e encerra."""
py5.no_loop()
canvas.save_sketch_image(sketch)
py5.exit_sketch()
if __name__ == "__main__":
py5.run_sketch()