import os
import sys
import numpy as np
import pygame as pg
# ---
# Utilidades 2D homogéneo
# ---
def to_homogeneous(P):
return np.vstack([P, np.ones((1, P.shape[1]))])
def from_homogeneous(PH):
M = PH[2, :]
return PH[:2, :] / M
def T(dx, dy):
return np.array([[1, 0, dx],
[0, 1, dy],
[0, 0, 1]], dtype=float)
def S(sx, sy):
return np.array([[sx, 0, 0],
[0, sy, 0],
[0, 0, 1]], dtype=float)
def R(theta_deg):
th = np.deg2rad(theta_deg)
c, s = np.cos(th), np.sin(th)
return np.array([[c, -s, 0],
[s, c, 0],
[0, 0, 1]], dtype=float)
def transform_around(point, M_local):
cx, cy = point
return T(cx, cy) @ M_local @ T(-cx, -cy)
# ---
# Inicialización Pygame
# ---
pg.init()
W, H = 900, 600 # Corregido: M cambió a W para coincidir con línea 69
screen = pg.display.set_mode((W, H))
pg.display.set_caption("Sprite + Matrix homogénea (S=R=T) – Controles
FIX")
clock = pg.time.Clock()
font = pg.font.SysFont("consolas", 18)
# ---
# Carga de sprite (fallback)
# ---
IMAGE_PATH = "sprite.png"
if os.path.isfile(IMAGE_PATH):
sprite = pg.image.load(IMAGE_PATH).convert_alpha()
else:
# Crear un sprite de ejemplo si no existe el archivo
sprite = pg.Surface((120, 80), pg.SRCALPHA)
sprite.fill((220, 235, 255, 255))
pg.draw.rect(sprite, (50, 120, 230), sprite.get_rect(), 4) #
Corregido: get_pect() -> get_rect()
pg.draw.line(sprite, (230, 80, 80), (0, 0), (120, 80), 3)
pg.draw.line(sprite, (80, 180, 80), (120, 0), (0, 80), 3)
spr_w, spr_h = sprite.get_width(), sprite.get_height()
square_local = np.array([[0, spr_w, spr_w, 0, 0],
[0, 0, spr_h, spr_h, 0]], dtype=float)
# ---
# Estado transformaciones
# ---
sx, sy = 1.0, 1.0
theta = 0.0
dx, dy = W/2, H/2
pivot = (spr_w/2, spr_h/2)
# Velocidades (más altas para que se note bien)
SPEED_T = 450.0 # px/s (corregido: SPEED_I no definido, cambiado a
SPEED_T)
SPEED_R = 180.0 # deg/s
SPEED_S = 2.5 # unidades de escala por segundo (+/-)
last_input = "-"
def draw_text_lines(lines, x, y):
yy = y
for ln in lines:
surf = font.render(ln, True, (30, 30, 30))
screen.blit(surf, (x, yy))
yy += surf.get_height() + 2
running = True
while running:
dt = clock.tick(60) / 1000.0 # segundos
# Asegura que la ventana procese eventos (necesario para get_pressed)
for e in pg.event.get():
if e.type == pg.QUIT:
running = False
elif e.type == pg.KEYDOWN:
if e.key == pg.K_ESCAPE:
running = False
if e.key == pg.K_r:
sx, sy = 1.0, 1.0
theta = 0.0
dx, dy = W/2, H/2
last_input = "Reset (R)"
keys = pg.key.get_pressed()
# Traslación: Flechas o WASD
if keys[pg.K_LEFT] or keys[pg.K_a]:
dx -= SPEED_T * dt # Corregido: := no es válido en Python,
cambiado a -=
last_input = "Traslación ← (Left/A)"
if keys[pg.K_RIGHT] or keys[pg.K_d]:
dx += SPEED_T * dt
last_input = "Traslación → (Right/D)"
if keys[pg.K_UP] or keys[pg.K_w]:
dy -= SPEED_T * dt # Corregido: := no es válido en Python,
cambiado a -=
last_input = "Traslación ↑ (Up/W)"
if keys[pg.K_DOWN] or keys[pg.K_s]:
dy += SPEED_T * dt
last_input = "Traslación ↓ (Down/S)"
# Rotación: Q/E
if keys[pg.K_q] or keys[pg.K_COMMA]: # coma por si cambia layout
theta += SPEED_R * dt
last_input = "Rotación + (Q)"
if keys[pg.K_e] or keys[pg.K_PERIOD]: # punto
theta -= SPEED_R * dt
last_input = "Rotación - (E)"
# Escala X: Z/X o '-'/'=' (teclado principal)
if keys[pg.K_z] or keys[pg.K_MINUS]:
sx -= SPEED_S * dt
last_input = "Escala X - (Z/-)"
if keys[pg.K_x] or keys[pg.K_EQUALS]:
sx += SPEED_S * dt
last_input = "Escala X + (X/=)"
# Escala Y: V/C o '['/']'
if keys[pg.K_v] or keys[pg.K_LEFTBRACKET]:
sy -= SPEED_S * dt
last_input = "Escala Y - (V/[)"
if keys[pg.K_c] or keys[pg.K_RIGHTBRACKET]: # Corregido:
RIGHTBROCKET -> RIGHTBRACKET
sy += SPEED_S * dt
last_input = "Escala Y + (C/])"
# Clamp de escala
sx = max(0.05, sx)
sy = max(0.05, sy)
# Matrix compuesta: escalar+rotar alrededor del centro local, luego
trasladar
M_local = R(theta) @ S(sx, sy) # Corregido: 5(sx, sy) -> S(sx, sy)
M = T(dx, dy) @ transform_around(pivot, M_local)
# Transformar esquinas
square_h = to_homogeneous(square_local)
square_world = from_homogeneous(M @ square_h)
# Render
screen.fill((248, 248, 248))
# Ejes
pg.draw.line(screen, (180, 180, 180), (0, dy), (W, dy), 1) #
Corregido: M -> W
pg.draw.line(screen, (180, 180, 180), (dx, 0), (dx, H), 1)
# Contorno transformado (exacto según matriz)
pts = list(map(tuple, square_world.T)) # Corregido: .1 -> .T
(transpuesta)
pg.draw.lines(screen, (40, 90, 200), False, pts, 3)
# Imagen (aprox con rotozoom escala uniforme)
scale_uniform = (sx + sy) * 0.5
spr_rot = pg.transform.rotozoom(sprite, -theta, scale_uniform)
spr_rect = spr_rot.get_rect(center=(dx, dy))
screen.blit(spr_rot, spr_rect)
info = [
"Controles (duplicados para evitar layouts):",
" Traslación: Flechas o WASD",
" Rotación: Q/E (alterno: , . )",
" Escala X: Z/X (alterno: - = )",
" Escala Y: V/C (alterno: [ ] )",
" Reset: R | Salir: ESC",
f"Estado -> sx={sx:.2f} sy={sy:.2f} θ={theta:.1f}°
pos=({dx:.1f},{dy:.1f})",
f"Último input: {last_input}",
"TIP: haz clic dentro de la ventana si no responde (foco)."
]
# Recuadro UI
pg.draw.rect(screen, (235, 235, 235), pg.Rect(10, 10, 560, 150))
pg.draw.rect(screen, (210, 210, 210), pg.Rect(10, 10, 560, 150), 1)
yy = 16
for line in info:
surf = font.render(line, True, (30, 30, 30))
screen.blit(surf, (18, yy))
yy += surf.get_height() + 2
pg.display.flip()
pg.quit()
sys.exit()