"""2026-04-18
Oscar Schmidt
Uma singela homenagem a um ídolo que se foi cedo demais.
ericof.com
png
Sketch,py5,CreativeCoding
"""
from dataclasses import dataclass
from sketches.utils.draw import canvas
from sketches.utils.draw.basquete import BolaSpec
from sketches.utils.draw.basquete import calcula_bola_basquete
from sketches.utils.draw.basquete import DadosBola
from sketches.utils.draw.basquete import desenha_bola_basquete
from sketches.utils.helpers import images
from sketches.utils.helpers import sketches as helpers
import py5
sketch = helpers.info_for_sketch(__file__, __doc__)
cor_fundo = py5.color(0)
cor_fundo_interna = py5.color(47, 42, 65) # fundo da imagem (RGB)
raio_bola = 350
resolucao = 50
@dataclass
class Rotacao:
x: float = -102
y: float = -69
z: float = 0
dist_z: float = -380
espec_bola = BolaSpec(
raio=raio_bola,
resolucao=resolucao,
texto_extensao=(0.15, 0.12),
texto_theta=1.75, # abaixo do equador
texto_phi=-0.16, # longitude
texto_rotacao_uv=True, # corrige rotação 90°
texto="#14",
texto_dim=(200, 200),
texto_fonte=80,
)
backdrop_pos = [-500.0, -500.0] # x, y do backdrop
backdrop_tam = (0, 0) # calculado no setup com base na imagem
backdrop_crop = 0.35 # fração central da largura a exibir (0.2 = 20%)
rotacao = Rotacao()
dados_bola: DadosBola | None = None
textura_numero: py5.Py5Graphics | None = None
imagem = None
def _cria_textura_numero(spec: BolaSpec) -> py5.Py5Graphics:
"""Renderiza '#14' em uma textura com fundo transparente."""
centro = tuple(dim / 2 for dim in spec.texto_dim)
# Usar JAVA2D para alpha confiável, depois copiar para P3D
pg = py5.create_graphics(*spec.texto_dim)
with pg.begin_draw():
pg.background(0, 0) # transparente
pg.fill(30) # quase preto
pg.no_stroke()
pg.text_align(py5.CENTER, py5.CENTER)
pg.text_size(spec.texto_fonte)
pg.text(spec.texto, *centro)
return pg
def desenha_fundo():
tam = max(helpers.DIMENSOES.internal) * 2
meio = tam / 2
with py5.push():
py5.translate(0, 0, -raio_bola * 1.4)
py5.no_stroke()
py5.fill(cor_fundo_interna)
with py5.begin_shape(py5.QUADS):
py5.normal(0, 0, 1)
py5.vertex(-meio, -meio, 0)
py5.vertex(meio, -meio, 0)
py5.vertex(meio, meio, 0)
py5.vertex(-meio, meio, 0)
def desenha_backdrop(escala: float = 1.0):
if imagem is None:
return
# Dimensões da região visível, escaladas
vis_w = backdrop_tam[0] * backdrop_crop * escala
vis_h = backdrop_tam[1] * escala
# Canto inferior direito da área interna
# internal = (800, 800), centrado em (500, 500) → dir-inf = (900, 900)
_, (iw, ih) = helpers.DIMENSOES.external, helpers.DIMENSOES.internal
cx, cy = helpers.DIMENSOES.centro
# Borda direita/inferior da área interna
dir_x = cx + iw / 2
inf_y = cy + ih / 2
# Posicionar o quad com canto inferior direito nesse ponto
x1 = dir_x
y1 = inf_y
x0 = x1 - vis_w
y0 = y1 - vis_h
# UV centralizado
u0 = 0.5 - backdrop_crop / 2
u1 = 0.5 + backdrop_crop / 2
with py5.push():
py5.translate(0, 0, -1)
py5.texture_mode(py5.NORMAL)
py5.no_stroke()
py5.no_lights()
with py5.begin_shape(py5.QUADS):
py5.texture(imagem)
py5.vertex(x0, y0, 0, u0, 0)
py5.vertex(x1, y0, 0, u1, 0)
py5.vertex(x1, y1, 0, u1, 1)
py5.vertex(x0, y1, 0, u0, 1)
def aplica_rotacao_iluminacao():
py5.rotate_y(py5.radians(rotacao.y))
py5.rotate_x(py5.radians(rotacao.x))
py5.rotate_z(py5.radians(rotacao.z))
# Iluminação
py5.point_light(360, 0, 100, 400, -300, 400)
py5.point_light(360, 0, 60, -300, 200, -200)
py5.ambient_light(0, 0, 30)
def setup():
global dados_bola, textura_numero, imagem, backdrop_tam
py5.size(*helpers.DIMENSOES.external, py5.P3D)
py5.color_mode(py5.HSB, 360, 100, 100)
dados_bola = calcula_bola_basquete(spec=espec_bola)
textura_numero = _cria_textura_numero(spec=espec_bola)
img_array = images.resource_image_as_array("oscar.jpg")
# Adicionar canal alpha (255 = opaco) se a imagem for RGB
if img_array.shape[2] == 3:
import numpy as np
alpha = np.full((*img_array.shape[:2], 1), 255, dtype=img_array.dtype)
img_array = np.concatenate([alpha, img_array], axis=2)
backdrop_tam = (img_array.shape[1], img_array.shape[0]) # (largura, altura)
imagem = py5.create_image_from_numpy(img_array)
def draw():
py5.background(cor_fundo)
# Backdrop atrás da bola
desenha_backdrop(escala=0.5)
# Fundo com iluminação mas sem rotação
with py5.push():
py5.translate(*helpers.DIMENSOES.centro, rotacao.dist_z)
py5.point_light(360, 0, 100, 400, -300, 400)
py5.point_light(360, 0, 60, -300, 200, -200)
py5.ambient_light(0, 0, 30)
desenha_fundo()
# Resetar luzes do fundo antes da bola
py5.no_lights()
# Bola com rotação e iluminação
with py5.push():
py5.translate(*helpers.DIMENSOES.centro, rotacao.dist_z)
aplica_rotacao_iluminacao()
if dados_bola:
desenha_bola_basquete(
dados_bola,
espessura_costura=5.5,
textura_texto=textura_numero,
)
msg = (
f"R: ({rotacao.x:.1f}, {rotacao.y:.1f}, "
f"{rotacao.z:.1f}) | "
f"Z: {rotacao.dist_z:.1f}"
)
py5.window_title(msg)
py5.no_lights()
canvas.sketch_frame(
sketch,
cor_fundo,
"large_transparent_white",
"transparent_white",
version=2,
msg=msg,
)
def key_pressed():
match py5.key:
case " ":
save_and_close()
case "-":
rotacao.dist_z -= 20
case "+":
rotacao.dist_z += 20
match py5.key_code:
case py5.UP:
rotacao.x += 3
case py5.DOWN:
rotacao.x -= 3
case py5.LEFT:
rotacao.y += 3
case py5.RIGHT:
rotacao.y -= 3
def save_and_close():
py5.no_loop()
canvas.save_sketch_image(sketch)
py5.exit_sketch()
if __name__ == "__main__":
py5.run_sketch()