"""As três transformações geométricas 2D, escritas à mão. Aula 04 — Instanciamento e Transformações Geométricas 2D. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python transformacoes2d.py Um triângulo fixo (cinza, tracejado por cor mais clara) é sempre desenhado na mesma posição. Um segundo triângulo (azul, sólido) é o resultado de aplicar UMA das três transformações a cada vértice do primeiro, calculada pelas funções ``transladar``, ``escalar`` e ``rotacionar`` — sem nenhuma chamada ``gl*`` de transformação ainda. É a mesma disciplina do mapeamento2d.py da Aula 03: antes de deixar o OpenGL fazer a conta, é preciso ver a conta. Repare que escala e rotação são calculadas em torno da ORIGEM do mundo, não do centro do triângulo — por isso, ao girar ou escalar, o triângulo (que não está na origem) se desloca bastante, além de mudar de forma ou orientação. Teclas: T E R escolhe qual transformação está ativa setas muda os parâmetros dela (Tx/Ty, Ex/Ey ou o ângulo) 0 zera todos os parâmetros P imprime no terminal os três vértices, antes e depois Esc fecha O título da janela mostra o modo corrente e os valores dos parâmetros. """ from __future__ import annotations import math import sys from PySide6.QtCore import Qt from PySide6.QtGui import QSurfaceFormat from PySide6.QtWidgets import QApplication from PySide6.QtOpenGLWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import gluOrtho2D MUNDO = 15.0 # semilargura da window, em unidades do mundo PASSO_T = 1.0 PASSO_E = 0.15 PASSO_ANGULO = 5.0 # Um triângulo escaleno, deliberadamente longe da origem: é o que torna # visível que escala e rotação acontecem em torno de (0, 0), não do # centro do próprio triângulo. TRIANGULO_BASE = [(2.0, 1.0), (9.0, 1.0), (3.5, 7.0)] def transladar(p, tx, ty): x, y = p return (x + tx, y + ty) def escalar(p, ex, ey): x, y = p return (ex * x, ey * y) def rotacionar(p, angulo_graus): x, y = p a = math.radians(angulo_graus) cos_a, sin_a = math.cos(a), math.sin(a) return (x * cos_a - y * sin_a, x * sin_a + y * cos_a) class Transformacoes2D(QOpenGLWidget): """Um triângulo fixo e um transformado, lado a lado no mesmo mundo.""" def __init__(self, parent=None): super().__init__(parent) self.modo = "T" self.tx, self.ty = 0.0, 0.0 self.ex, self.ey = 1.0, 1.0 self.angulo = 0.0 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) def resizeGL(self, w: int, h: int) -> None: w, h = max(1, w), max(1, h) glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) glLoadIdentity() if w <= h: gluOrtho2D(-MUNDO, MUNDO, -MUNDO * h / w, MUNDO * h / w) else: gluOrtho2D(-MUNDO * w / h, MUNDO * w / h, -MUNDO, MUNDO) glMatrixMode(GL_MODELVIEW) glLoadIdentity() def _pontos_transformados(self): if self.modo == "T": return [transladar(p, self.tx, self.ty) for p in TRIANGULO_BASE] if self.modo == "E": return [escalar(p, self.ex, self.ey) for p in TRIANGULO_BASE] return [rotacionar(p, self.angulo) for p in TRIANGULO_BASE] @staticmethod def _desenhar_eixos() -> None: glColor3f(0.30, 0.32, 0.38) glLineWidth(1.0) glBegin(GL_LINES) glVertex2f(-MUNDO, 0.0) glVertex2f(MUNDO, 0.0) glVertex2f(0.0, -MUNDO) glVertex2f(0.0, MUNDO) glEnd() def paintGL(self) -> None: glClear(GL_COLOR_BUFFER_BIT) self._desenhar_eixos() # original: sempre no mesmo lugar, mais claro e mais fino glColor3f(0.55, 0.58, 0.65) glLineWidth(1.5) glBegin(GL_LINE_LOOP) for x, y in TRIANGULO_BASE: glVertex2f(x, y) glEnd() # transformado: recalculado a cada quadro pelas funções acima glColor3f(0.35, 0.55, 0.85) glLineWidth(2.5) glBegin(GL_LINE_LOOP) for x, y in self._pontos_transformados(): glVertex2f(x, y) glEnd() self._atualizar_titulo() def _atualizar_titulo(self) -> None: if self.modo == "T": info = f"T=({self.tx:.1f}, {self.ty:.1f})" elif self.modo == "E": info = f"Ex={self.ex:.2f} Ey={self.ey:.2f}" else: info = f"ângulo={self.angulo:.0f}°" self.setWindowTitle(f"Transformações 2D — modo {self.modo} — {info}") def _imprimir_vertices(self) -> None: print(f"modo = {self.modo}") for original, novo in zip(TRIANGULO_BASE, self._pontos_transformados()): ox, oy = original nx, ny = novo print(f" ({ox:6.2f}, {oy:6.2f}) -> ({nx:6.2f}, {ny:6.2f})") def keyPressEvent(self, event) -> None: tecla = event.key() if tecla == Qt.Key.Key_Escape: self.close() return elif tecla == Qt.Key.Key_T: self.modo = "T" elif tecla == Qt.Key.Key_E: self.modo = "E" elif tecla == Qt.Key.Key_R: self.modo = "R" elif tecla == Qt.Key.Key_0: self.tx = self.ty = 0.0 self.ex = self.ey = 1.0 self.angulo = 0.0 elif tecla == Qt.Key.Key_P: self._imprimir_vertices() return elif tecla in (Qt.Key.Key_Left, Qt.Key.Key_Right): sinal = -1.0 if tecla == Qt.Key.Key_Left else 1.0 if self.modo == "T": self.tx += sinal * PASSO_T elif self.modo == "E": self.ex += sinal * PASSO_E else: self.angulo += sinal * PASSO_ANGULO elif tecla in (Qt.Key.Key_Up, Qt.Key.Key_Down): sinal = 1.0 if tecla == Qt.Key.Key_Up else -1.0 if self.modo == "T": self.ty += sinal * PASSO_T elif self.modo == "E": self.ey += sinal * PASSO_E else: self.angulo += sinal * PASSO_ANGULO else: return self.update() def main() -> int: fmt = QSurfaceFormat() fmt.setVersion(2, 1) fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) fmt.setDepthBufferSize(0) QSurfaceFormat.setDefaultFormat(fmt) app = QApplication(sys.argv) janela = Transformacoes2D() janela.setWindowTitle("Transformações 2D") janela.resize(760, 560) janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())