"""A *window* como câmera 2D: zoom e pan. Aula 03 — Visualização 2D: mapeamento. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python zoom_pan.py O mundo é maior do que a área visível. Mexendo apenas nos limites passados a ``gluOrtho2D`` — sem tocar em um único vértice da cena — obtêm-se as duas operações de navegação 2D: * **zoom**: encolher ou ampliar a window (mostra menos ou mais mundo); * **pan**: deslocar a window (mostra outra região do mundo). Teclas: + - zoom setas pan G mostra ou esconde a grade R volta à window inicial Esc fecha O título da janela mostra os limites correntes da window. Ideias para experimentar: dê zoom até o retângulo tracejado (que marca a window inicial) sair da tela; compare o efeito de dobrar ``ESCALA`` com o de dobrar as coordenadas de todos os vértices da cena. """ 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 ESCALA = 50.0 # semialtura inicial da window, em unidades do mundo FATOR_ZOOM = 1.25 # quanto cada tecla +/- multiplica ou divide a escala FRACAO_PAN = 0.10 # deslocamento por tecla, como fração da escala PASSO_GRADE = 10.0 class ZoomPan(QOpenGLWidget): """Cena fixa, window móvel.""" def __init__(self, parent=None): super().__init__(parent) self.escala = ESCALA self.centro = [0.0, 0.0] self.grade = True self._largura, self._altura = 1, 1 self.window_sru = (-ESCALA, ESCALA, -ESCALA, ESCALA) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # -- ciclo de vida ----------------------------------------------------- def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) def resizeGL(self, w: int, h: int) -> None: self._largura, self._altura = max(1, w), max(1, h) glViewport(0, 0, self._largura, self._altura) def _calcular_window(self) -> None: """Deduz os limites da window a partir de centro, escala e aspecto. Só faz contas e atualiza o título — nenhuma chamada ``gl*``. Assim este método pode ser chamado de qualquer lugar, inclusive de um tratador de teclado. """ w, h = self._largura, self._altura cx, cy = self.centro if w <= h: meia_x = self.escala meia_y = self.escala * h / w else: meia_x = self.escala * w / h meia_y = self.escala self.window_sru = (cx - meia_x, cx + meia_x, cy - meia_y, cy + meia_y) xu_min, xu_max, yu_min, yu_max = self.window_sru self.setWindowTitle( f"Zoom e pan — window = ({xu_min:.1f}, {xu_max:.1f}, " f"{yu_min:.1f}, {yu_max:.1f})" ) # -- desenho ----------------------------------------------------------- def paintGL(self) -> None: # A projeção depende do estado que o usuário controla, e não só do # tamanho da janela: por isso ela é refeita aqui, e não no resizeGL # como nos exemplos da Aula 02. É a exceção que confirma a regra — # quem manda na projeção é quem a faz mudar. self._calcular_window() glMatrixMode(GL_PROJECTION) glLoadIdentity() gluOrtho2D(*self.window_sru) glMatrixMode(GL_MODELVIEW) glLoadIdentity() glClear(GL_COLOR_BUFFER_BIT) if self.grade: self._desenhar_grade() self._desenhar_window_inicial() self._desenhar_cena() def _desenhar_grade(self) -> None: xu_min, xu_max, yu_min, yu_max = self.window_sru glColor3f(0.19, 0.21, 0.26) glLineWidth(1.0) glBegin(GL_LINES) x = PASSO_GRADE * math.floor(xu_min / PASSO_GRADE) while x <= xu_max: glVertex2f(x, yu_min) glVertex2f(x, yu_max) x += PASSO_GRADE y = PASSO_GRADE * math.floor(yu_min / PASSO_GRADE) while y <= yu_max: glVertex2f(xu_min, y) glVertex2f(xu_max, y) y += PASSO_GRADE glEnd() def _desenhar_window_inicial(self) -> None: """Marca, no mundo, o retângulo que a window ocupava no início. Ele é um objeto da cena como outro qualquer — não acompanha a câmera. Serve de referência para perceber o quanto se afastou do ponto de partida. """ glColor3f(0.35, 0.40, 0.50) glLineWidth(1.0) glEnable(GL_LINE_STIPPLE) glLineStipple(2, 0xAAAA) glBegin(GL_LINE_LOOP) glVertex2f(-ESCALA, -ESCALA) glVertex2f(ESCALA, -ESCALA) glVertex2f(ESCALA, ESCALA) glVertex2f(-ESCALA, ESCALA) glEnd() glDisable(GL_LINE_STIPPLE) # o estado vaza se não for desfeito @staticmethod def _desenhar_cena() -> None: # Um anel de círculos de raios diferentes, espalhados bem além da # window inicial: com zoom para fora aparecem novos objetos, e com # zoom para dentro percebe-se que o traço não engrossa junto. objetos = [ (0.0, 0.0, 18.0, (0.92, 0.92, 0.95)), (-70.0, 40.0, 12.0, (0.95, 0.35, 0.35)), (80.0, -30.0, 22.0, (0.35, 0.55, 0.95)), (55.0, 75.0, 9.0, (0.35, 0.85, 0.45)), (-95.0, -80.0, 16.0, (0.95, 0.70, 0.25)), (5.0, -110.0, 14.0, (0.75, 0.50, 0.90)), ] glLineWidth(2.5) for cx, cy, raio, cor in objetos: glColor3f(*cor) glBegin(GL_LINE_LOOP) ang = 0.0 while ang < 2 * math.pi: glVertex2f(cx + raio * math.cos(ang), cy + raio * math.sin(ang)) ang += math.pi / 16.0 glEnd() # Eixos do mundo. glColor3f(0.45, 0.50, 0.60) glLineWidth(1.5) glBegin(GL_LINES) glVertex2f(-200.0, 0.0) glVertex2f(200.0, 0.0) glVertex2f(0.0, -200.0) glVertex2f(0.0, 200.0) glEnd() # -- interação --------------------------------------------------------- def keyPressEvent(self, event) -> None: tecla = event.key() passo = self.escala * FRACAO_PAN if tecla == Qt.Key.Key_Escape: self.close() return elif tecla in (Qt.Key.Key_Plus, Qt.Key.Key_Equal): self.escala /= FATOR_ZOOM # window menor = mais perto elif tecla == Qt.Key.Key_Minus: self.escala *= FATOR_ZOOM elif tecla == Qt.Key.Key_Left: self.centro[0] -= passo elif tecla == Qt.Key.Key_Right: self.centro[0] += passo elif tecla == Qt.Key.Key_Up: self.centro[1] += passo elif tecla == Qt.Key.Key_Down: self.centro[1] -= passo elif tecla == Qt.Key.Key_G: self.grade = not self.grade elif tecla == Qt.Key.Key_R: self.escala, self.centro = ESCALA, [0.0, 0.0] else: return # Mexer na projeção é uma chamada gl*, e portanto não pode acontecer # aqui: keyPressEvent roda fora do ciclo de vida do contexto. O que # este tratador faz é alterar o estado (escala, centro) e pedir um # repintar; quem chama gluOrtho2D é o paintGL. self._calcular_window() # só atualiza o título, sem tocar no OpenGL 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 = ZoomPan() janela.setWindowTitle("Zoom e pan") janela.resize(760, 560) janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())